Step 77. Socket Communication 1 — A TCP echo Server and Client
Level 1 — Programming and the Computer’s Insides | Difficulty ★★★★☆ | Estimated time: 4 hours
Prerequisites: you know the HTTP concepts of Step 73. You can write Python functions and while loops. You can have two terminals open at the same time.
- What you need: Python, two terminals. If you have Linux/WSL, you can also do the netcat experiment in 3-5 (no problem proceeding without it).
- Caution: all of today’s experiments are safe practice happening only inside my own computer (127.0.0.1). That said, the fact that today’s code is a root of attack techniques is something section 7 addresses honestly.
The programs we’ve made so far worked alone. But a computer’s real power comes from connection — the web, games, messengers are all "two programs talking over a network." The telephone of that conversation is the socket. If the requests of past chapters was a convenient remote control, a socket is the circuit board inside the remote. Today we touch that board directly and build the simplest network program of all: an echo server.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Explain in code that a socket is "a communication endpoint between program and program"
- Write the server’s four steps (socket/bind/listen/accept) and the client’s connect in order
- Exchange bytes with sendall/recv, and state why encode/decode is needed
- Know what a Connection refused error means, and that it is the principle of port scanning
- Check "open ports" at the operating-system level with netstat
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Python 3 standard library socket (no installation), two terminals |
| Today’s functions | socket(), bind(), listen(), accept(), connect(), sendall(), recv(), encode()/decode() |
| Concepts needed | IP addresses and ports, 127.0.0.1 (localhost), TCP, bytes vs str |
| Today’s output | server.py + client.py — your first network program, answering with an echo |
2-1. Addresses and Ports — The Building and the Room Number
To find a program on a network, you need two things: an IP address (which computer — the building address) and a port number (which program inside it — the room number). Ports run from 0 to 65535, and famous services use conventionally fixed numbers, like 80 (HTTP) and 22 (SSH). Today we borrow number 9999, which nobody is using.
127.0.0.1 is a promised address pointing to "this computer itself," and it also carries the name localhost. It is the stage of today’s experiment — server and client talk inside my own computer, a complete lab that touches nothing outside.
2-2. The Server’s Four Steps, the Client’s One Step
In TCP (the reliable connection protocol), the two sides’ actions split like this.
The server (the waiting side)
socket()— make the telephonebind(("0.0.0.0", 9999))— give the telephone a number ("0.0.0.0" means "I’ll accept on every address of this computer")listen(1)— enter the state of waiting for incoming callsaccept()— stop and wait until a call comes; when one comes, receive the call telephone (conn)
The client (the calling side)
- After
socket(),connect(("127.0.0.1", 9999))— place a call to that number
Once connected, both sides talk with sendall (send) and recv (receive). What matters is that accept is a function that "stops while waiting." The server program halts here as if asleep, and wakes the moment a client connects.
2-3. bytes and str — The Network’s Language
What travels across a socket is not characters (str) but bytes. When sending, you wrap with .encode(); when receiving, you unwrap with .decode(). The encoding learned in Step 50 becomes real combat here. Most of today’s errors will come from forgetting this wrapping/unwrapping.
3. Follow Along
3-1. Building the echo Server
Create server.py.
Input (server.py)
import socket
s = socket.socket()
s.bind(("0.0.0.0", 9999))
s.listen(1)
print("Waiting... (waiting for a client)", flush=True)
conn, addr = s.accept()
print("Connected:", addr, flush=True)
while True:
data = conn.recv(1024)
if not data:
break
print("Received:", data.decode(), flush=True)
conn.sendall(b"echo: " + data)
conn.close()
s.close()
print("Connection closed, server shutting down")
Input (terminal 1)
python server.py
Output (measured 2026-09-09):
Waiting... (waiting for a client)
How to read it: if the screen looks frozen here, that’s normal. accept is in the middle of waiting for a call. recv(1024) means "receive up to 1024 bytes," and when the other side disconnects, empty data arrives and the loop ends. (flush=True is a device that pushes output to the screen immediately — a habit for watching logs without lag.)
Why: we’ve put a server’s whole life (make → assign a number → wait → converse → finish) into one program. This skeleton is the ancestor of every server you’ll build.
3-2. Placing a Call with the Client
Create client.py.
Input (client.py)
import socket
s = socket.socket()
s.connect(("127.0.0.1", 9999))
msg = input("> ")
s.sendall(msg.encode())
answer = s.recv(1024)
print("Server's answer:", answer.decode())
s.close()
Input (terminal 2 — leave the server running)
python client.py
> Hello, server!
Output (measured 2026-09-09):
Server's answer: echo: Hello, server!
And in the server screen of terminal 1, this is printed (measured 2026-09-09):
Connected: ('127.0.0.1', 52378)
Received: Hello, server!
Connection closed, server shutting down
How to read it: the moment the client connected, the server’s accept woke and printed "Connected," and the sent words came back as an echo (echo:). A number like 52378 in the connection info is the client side’s temporary port. A conversation needs numbers on both ends, so the operating system attached a random free number to the client — it changes with every run. When the client disconnected, the server too received empty data, left the while loop, and exited.
Why: you just made two programs talk over a network. Although it’s inside the same computer, structurally this code is completely identical to code that talks to the far side of the internet. Only the address needs changing.
3-3. Predict — What If You Leave the Server On with No Client?
Time to predict. If you leave only the server on and never run the client, what happens to the server program? ① exits with an error ② goes down from lack of memory ③ just keeps waiting.
Check yourself (measured 2026-09-09): while the server stayed on and we did other work, it remained at "Waiting…" the whole time. accept waits indefinitely.
Why: this is an experiment that engraves the sense that "waiting is a server’s job." A server is a being that sleeps until a guest arrives and wakes when one comes. This "always on and waiting" nature is also why servers become targets of attack.
3-4. Predict — What If You Start the Client with No Server?
The opposite experiment. The server is off and you run the client — what happens? Predict, then stop the server with Ctrl+C and run client.py.
Output (measured 2026-09-09):
ConnectionRefusedError: [WinError 10061] No connection could be made because the target machine actively refused it
(On Linux/WSL it appears in English as [Errno 111] Connection refused. The same event in a different language.)
How to read it: it’s the error "the connection was refused." You placed a call, but there’s no telephone on the receiving side. The operating system immediately answered "there is nobody on that port."
Why: this refusal signal becomes the key ingredient of the Step 79 port scanner. "If it connects, it’s open; if it’s refused, it’s closed" — the principle of port scanning is born from this error message.
3-5. netcat — Talking Without Code
netcat (nc) is the Swiss Army knife of socket communication; it does what our client does with just an install. If you have a Linux/WSL terminal, try it (if you only have Windows, reading past is fine).
Input (leave the server running; in a WSL terminal)
nc 127.0.0.1 9999
It looks like nothing happened, but you’re connected. Type some letters and press Enter.
Output (measured 2026-09-09, typed "hello nc"):
echo: hello nc
The server-side log also printed [Received: hello nc] (measured 2026-09-09).
How to read it: to the server, the client we built in Python and nc are the exact same guest. The server can’t tell whether its counterpart is Python or nc. Anything that speaks to it through a socket is the same customer.
Why: this is the stage of feeling that "as long as the protocol (conversation rules) matches, any tool will do." It’s also why penetration testers love nc — with it you can speak by hand to any service.
3-6. Observation — Seeing Open Ports with Different Eyes
While the server is running, check from a third terminal.
Input (Windows)
netstat -ano | findstr :9999
Output (measured 2026-09-09):
TCP 0.0.0.0:9999 0.0.0.0:0 LISTENING 21420
How to read it: netstat is a command that shows the list of sockets. Our server is sitting on port 9999 in the LISTENING (waiting) state, and the last number, 21420, is the PID (process number) of the process that opened this port. On Linux/WSL you see the same thing with ss -tlnp | grep 9999.
Why: this confirms how "a program opens a port" looks at the operating-system level. Later, the incident investigation that asks "does this server have any suspicious open ports?" starts with exactly this command.
4. Missions & Exercises
Mission — Upgrading the echo Server
- Change the server so that after seeing off one guest it doesn’t exit, but keeps receiving the next guest (hint: put accept inside a while loop)
- Make the server return the received words with a number attached ("echo #1: …")
- Change the client too, so it keeps conversing until you type "quit"
- Make the server record the entire conversation in chat_log.txt with timestamps
- Find the lines of code proving that "the difference between server and client is exactly one thing — waiting vs calling," and write them in a README
Exercises
Q1. Explain each of the server’s four steps (socket/bind/listen/accept) with a telephone analogy.
Q2. State why s.sendall("hello") raises an error and how to fix it.
Q3. In the connection info ('127.0.0.1', 52378), what is the second number, and why does it change with every run?
Q4. Name two things to check when a ConnectionRefusedError appears.
5. Model Answers & Completion Criteria
Mission Model Answer
The skeleton of the upgraded server:
import socket
from datetime import datetime
s = socket.socket()
s.bind(("0.0.0.0", 9999))
s.listen(1)
print("Waiting...", flush=True)
count = 0
while True: # keeps receiving guests
conn, addr = s.accept()
print("Connected:", addr, flush=True)
while True:
data = conn.recv(1024)
if not data:
break
count += 1
text = data.decode().strip()
with open("chat_log.txt", "a", encoding="utf-8") as fp:
fp.write(f"{datetime.now()} {addr} {text}n")
conn.sendall(f"echo #{count}: {text}".encode())
conn.close()
The client’s conversation loop:
import socket
s = socket.socket()
s.connect(("127.0.0.1", 9999))
while True:
msg = input("> ")
if msg == "quit":
break
s.sendall(msg.encode())
print("Server's answer:", s.recv(1024).decode())
s.close()
The answer to item 5: two lines prove it — conn, addr = s.accept() (server, waiting) and s.connect(...) (client, calling). The rest of the code — sendall, recv, encode, decode — is used identically by server and client.
How to verify: ① Does the server survive running the client twice in a row? ② Are the echoes numbered 1, 2, 3…? ③ Does typing quit end only the client? ④ Do time, address, and content pile up in chat_log.txt? If all four are "yes," it’s complete.
Exercise Solutions
Q1 solution. socket() is buying a telephone, bind() is assigning that telephone a phone number (port), listen() is leaving it in a state of waiting for the bell to ring, and accept() is picking up the receiver when the bell rings. The conn that accept returns is a dedicated call line to that guest.
Q2 solution. Because only bytes travel across a socket, while "hello" is characters (str) (measured 2026-09-09: TypeError: a bytes-like object is required, not 'str'). You must send it wrapped as bytes: s.sendall("hello".encode()).
Q3 solution. It’s the client side’s temporary port. A conversation needs two endpoints, "who-to-whom," and since the client didn’t specify a port, the operating system picks an arbitrary free number and attaches it. That’s why a different number appears with every run.
Q4 solution. First, is the server program really running (check LISTENING with netstat)? Second, do the address and port the client dialed match what the server bound — especially the case where the server is bound only to 127.0.0.1 but the client dials an external address.
Completion Criteria Checklist
- [ ] I can recite the server’s four steps (socket/bind/listen/accept) in order
- [ ] I built an echo server and client and made them talk
- [ ] I can explain why encode/decode is needed
- [ ] I can explain what ConnectionRefusedError means
- [ ] I can find a LISTENING port with netstat
- [ ] Mission: I completed the upgraded server and conversational client
6. Common Pitfalls & Fixes
Wall 1. I get a str vs bytes error
Symptom (measured 2026-09-09):
TypeError: a bytes-like object is required, not 'str'
Cause: only bytes travel across a socket. You sent a string as-is, or used bytes as if they were a string.
Fix: .encode() when sending, .decode() when receiving. Remember it as "wrapping and unwrapping." Most of today’s errors come from here.
Wall 2. I get an "Address already in use" error
Symptom (measured 2026-09-09):
OSError: [WinError 10048] Only one usage of each socket address (protocol/network address/port) is normally permitted
Cause: a server you started earlier is still holding port 9999. A window you never finished with Ctrl+C is alive, or the OS is still cleaning up the port right after shutdown.
Fix: make sure the previous server is off, wait a moment, or switch to another port. You can check with netstat who (which PID) is holding it.
Wall 3. recv sits frozen, receiving nothing
Symptom: you sent something, but the receiving side stays silent.
Cause: recv, like accept, is a function that "waits until something comes." Suspect first whether the other side really sent. A deadlock where both sides only wait to receive is a classic beginner trap.
Fix: draw by hand the conversation order of both codebases (the sequence of give and take). If the client enters recv before the server does sendall, both wait forever.
Wall 4. ConnectionRefusedError keeps happening
Symptom (measured 2026-09-09):
ConnectionRefusedError: [WinError 10061] No connection could be made because the target machine actively refused it
Cause: there is no server waiting on that port. You didn’t start the server, the port numbers differ between the two, or the server exited first.
Fix: check that "Waiting…" is showing in the server window, and that the port numbers in both codebases match. Always start the server first.
Wall 5. Connection from another device fails
Symptom: connecting from another computer on the same router times out.
Cause: a firewall is blocking that port, or the address the server bound is "127.0.0.1" (only itself allowed).
Fix: check that the bind address is "0.0.0.0", and allow that port in the operating system’s firewall. And remember — this experiment too is only between devices inside my own lab.
7. Summary
Today’s Concepts
| Concept | One-line description |
|---|---|
| Socket | A communication endpoint between programs — the network’s telephone |
| IP address / port | Which computer (the building) / which program inside it (the room) |
| 127.0.0.1 (localhost) | The promised lab address pointing to "this computer itself" |
| TCP | The connection protocol that checks receipt and keeps things in order |
| LISTENING | The state of a server with a port open, waiting for connections |
| Temporary (ephemeral) port | A number the OS arbitrarily attaches to a client |
Today’s Functions and Commands
| Function/command | What it does |
|---|---|
socket() |
Make a communication endpoint |
bind((address, port)) / listen(n) |
Assign a number / wait for incoming calls |
accept() |
Wait until a call comes, then receive the call socket |
connect((address, port)) |
Place a call to that number |
sendall(bytes) / recv(size) |
Send / receive |
netstat -ano (Windows) / ss -tlnp (Linux) |
See open ports and their owner programs |
The Instinct That Matters More Than Commands
Twist today’s server just a little and it becomes a dangerous object. Build a server that, instead of echoing, "executes received words as commands and returns the result," and that is the prototype of a bind shell (remote control where a door is opened on the victim’s computer and the attacker connects); conversely, make the victim side connect to the attacker and it’s a reverse shell. Both are merely combinations of the five functions learned today. The reason to know this principle is not to attack, but to recognize such code in the traces of a breach. All attack practice belongs only in your own lab and legal platforms — every connection today was made only inside 127.0.0.1.
One more thing. From today, get friendly with the well-known port numbers one by one (21 FTP, 22 SSH, 80 HTTP, 443 HTTPS). The moment a service name springs to mind automatically when you see these numbers, your speed of reading results in the port-scanning chapter changes. And today’s server.py and client.py are not disposable scratch paper — the next chapter’s chat server grows from this same seed.
Once every box is checked, Step 77 is complete.