Exercises — Socket programming — TCP server - client, UDP server - client in Python
Level 1 — Recognition
You should be able to name the pieces and read a socket call.
Exercise 1.1
For each line, say TCP or UDP, and why:
- (a)
socket.socket(socket.AF_INET, socket.SOCK_STREAM) - (b)
socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - (c)
s.recvfrom(1024) - (d)
s.listen(5)
Recall Solution 1.1
- (a) TCP.
SOCK_STREAM= a reliable, ordered byte stream. Picture a continuous pipe of bytes. - (b) UDP.
SOCK_DGRAM= independent datagrams (postcards). Eachsendtois one self-contained packet. - (c) UDP.
recvfromreturns(data, addr)— you learn who sent it because there is no fixed connection. TCP uses plainrecv(the peer is already known). - (d) TCP.
listenmakes a socket passive (willing to queue incoming calls). UDP has no calls to queue, so it never useslisten.
Exercise 1.2
What does the tuple ('127.0.0.1', 9000) mean in s.bind(('127.0.0.1', 9000))? Name each part.
Recall Solution 1.2
'127.0.0.1'= the IP address (here the loopback address = "this same machine").9000= the port = which door on that machine this program answers. Together they form the local address the socket claims. See Ports and IP addressing. A client must target exactly this(IP, port)to reach the server.
Exercise 1.3
accept() returns two things: conn, addr = s.accept(). What is each, and why is conn a new socket?
Recall Solution 1.3
conn= a brand-new socket dedicated to this one client.addr= the client's(IP, port). The original listening socketsmust stay free to keep accepting other clients. So the OS hands you a separate socket for the actual conversation. This split is exactly why one server can serve many clients: one listener, manyconns.
Level 2 — Application
Write the correct call, in the correct order, with correct types.
Exercise 2.1
Fill the blanks to make a minimal TCP server that accepts one client and reads up to 512 bytes:
import socket
s = socket.socket(socket.AF_INET, ____A____)
s.bind(('127.0.0.1', 5000))
s.____B____(1)
conn, addr = s.____C____()
data = conn.____D____(512)Recall Solution 2.1
- A =
socket.SOCK_STREAM(TCP). - B =
listen— go passive. The1is the backlog (the waiting-bench size defined at the top of the page): here at most 1 completed-but-unaccepted connection may wait. - C =
accept— blocks until a client dials, returns the per-client socket. - D =
recv— read up to 512 bytes from the stream. Order matters: you cannotacceptbeforelisten, and you cannotlisten/acceptbeforebind(you need an address first).
Exercise 2.2
A student wrote c.send("hello") and got TypeError: a bytes-like object is required, not 'str'. Fix it, and show how the server should turn the received bytes back into text.
Recall Solution 2.2
Sockets carry bytes, not str. Fix the client:
c.send("hello".encode()) # or c.send(b"hello")On the server, decode back to text:
text = conn.recv(1024).decode() # bytes -> str"hello".encode() produces 5 bytes (UTF-8); .decode() reverses it. This is the single most common beginner error.
Exercise 2.3
Write a UDP client that sends b"ping" to 127.0.0.1:6000 and prints the reply as text.
Recall Solution 2.3
import socket
c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
c.sendto(b"ping", ('127.0.0.1', 6000)) # address travels WITH the packet
reply, addr = c.recvfrom(1024)
print(reply.decode())
c.close()No connect, no bind on the client — the OS auto-picks an ephemeral source port, and the destination lives in the sendto call itself. Contrast with UDP vs TCP tradeoffs.
Level 3 — Analysis
Predict runtime behaviour — the "TCP is a stream" family.
Exercise 3.1 (the classic)
A TCP client does three sends in a row:
c.sendall(b"A"); c.sendall(b"B"); c.sendall(b"C")The server does one conn.recv(1024). What might it receive? List the possibilities.
Recall Solution 3.1
TCP is a byte stream — it does not preserve message boundaries. A single recv(1024) may return:
b"ABC"(all three coalesced — most common on localhost), orb"A", then laterb"BC", orb"AB", thenb"C", orb"A",b"B",b"C"across threerecvcalls. What you will never see is bytes out of order or missing (TCP guarantees order + delivery). The bytes always arrive asA B Cin sequence — only the chunking varies. This is why you need Message framing / length-prefix protocol.
Exercise 3.2
Same three sends, but UDP with three sendto(b"A"), sendto(b"B"), sendto(b"C"). The server loops recvfrom(1024). What might it read, and in what order?
Recall Solution 3.2
UDP preserves message boundaries but guarantees nothing about delivery or order:
- Each
recvfromreturns exactly one whole datagram: you getb"A",b"B",b"C"individually — neverb"ABC"from one call. - But any of them may be lost, duplicated, or reordered (e.g. you might read
C,A,B, or onlyA,C). Boundary-preserving but unreliable — the exact opposite trade of TCP. See UDP vs TCP tradeoffs.
Exercise 3.3
In a TCP loop, conn.recv(1024) suddenly returns b'' (empty bytes). Your loop keeps running forever, pegging the CPU. Diagnose and fix.
Recall Solution 3.3
In TCP, recv returning b'' is not "empty data" — it is EOF: the peer has closed the connection (orderly shutdown). Looping without checking spins forever on zero-length reads.
Fix: treat b'' as end-of-stream and break:
while True:
data = conn.recv(1024)
if not data: # b'' -> peer closed
break
handle(data)
conn.close()Level 4 — Synthesis
Build a small protocol from parts.
Exercise 4.1 — Length-prefix framing
Because TCP has no message boundaries (Exercise 3.1), design a framing scheme and write a recv_msg(conn) that reads exactly one full message. Use a fixed 4-byte big-endian length prefix.
The figure below shows the idea: every message on the wire is one orange header followed by its teal payload. Read the figure left-to-right as the receiver sees the bytes arrive.

Trace the two arrows under the blocks: the receiver first reads the 4 orange header bytes to learn the length N (here N = 300), then reads exactly N teal payload bytes. Because it knows N in advance, it always stops at the correct byte even if TCP split or merged the underlying recvs.
Recall Solution 4.1
Scheme: every message = [4-byte length N][N bytes of payload]. The receiver reads 4 bytes first, learns N, then reads exactly N more bytes (looping, since each recv may return less). This matches the two-step trace in the figure above.
Sender:
import struct
def send_msg(conn, payload: bytes):
header = struct.pack('>I', len(payload)) # '>I' = big-endian unsigned 4-byte int
conn.sendall(header + payload)Receiver — a robust "read exactly n bytes" helper first:
def recv_exact(conn, n):
buf = b''
while len(buf) < n:
chunk = conn.recv(n - len(buf))
if not chunk: # b'' -> peer closed early
raise ConnectionError("peer closed mid-message")
buf += chunk
return buf
def recv_msg(conn):
header = recv_exact(conn, 4)
(length,) = struct.unpack('>I', header) # 4 bytes -> int
return recv_exact(conn, length)Why it works: the length tells us exactly how many payload bytes belong to this message, so coalescing/splitting no longer confuses us — we always stop at the right byte. See Message framing / length-prefix protocol.
Numeric check (in decimal and hex — hex was introduced at the top of the page): the header for a 300-byte payload is struct.pack('>I', 300), which is the 4 bytes shown in the figure. In plain decimal, : so the third byte is 1 and the fourth byte is 44. Writing those four byte-values in hexadecimal (two hex digits per byte) gives 00 00 01 2C, because 44 in base 16 is 0x2C (). Verified below.
Exercise 4.2 — Reuse the port after a crash
Your server crashes and you restart it immediately; bind throws OSError: [Errno 98] Address already in use. Explain why, and give the one-line fix.
Recall Solution 4.2
After a TCP connection closes, the OS keeps the local (IP, port) in the TIME_WAIT state for ~1–2 minutes to catch stray late packets from the old connection. A fresh bind on that exact port is refused during this window.
Fix — set SO_REUSEADDR before bind:
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1', 9000))This tells the OS "it's fine to reuse an address stuck in TIME_WAIT." Details: TIME_WAIT and SO_REUSEADDR.
Exercise 4.3 — DNS instead of a raw IP
Rewrite c.connect(('127.0.0.1', 9000)) so the client connects to the host name "example.com" on port 80. Which layer turns the name into an IP?
Recall Solution 4.3
c.connect(('example.com', 80))connect internally resolves the name via DNS (or you can do it explicitly with socket.gethostbyname('example.com'), which returns an IPv4 string). DNS is the phone book mapping human names → IP addresses; see DNS. Port 80 is the well-known port for HTTP.
Level 5 — Mastery
Edge cases, concurrency, and reasoning about the whole system.
Exercise 5.1 — Why does a single-accept server serve only one client?
The Worked Example 1 server calls accept() once, handles the client, then close()s and exits. How would you serve many clients, one after another? And how would you serve them at the same time without threads?
Recall Solution 5.1
Serve many sequentially: wrap accept + handle in a loop, and do not close the listening socket:
while True:
conn, addr = s.accept() # blocks for each new client
data = conn.recv(1024)
conn.sendall(b"echo: " + data)
conn.close() # close the per-client socket, keep s openServe many concurrently without threads: use an event loop that watches many sockets and wakes only on the ready ones — select or asyncio. One process, many live conns, no blocking accept monopolizing the show. See select / asyncio for concurrent servers.
Key insight: the listening socket s and each connection socket conn are different objects — that separation is what lets one server multiplex clients.
Exercise 5.2 — Zero-length datagram
A UDP client runs c.sendto(b"", ('127.0.0.1', 6000)). Does the server's recvfrom fire? What does it return?
Recall Solution 5.2
Yes — a zero-length UDP datagram is a real, valid packet. The server's recvfrom(1024) unblocks and returns (b'', addr): empty payload, but a real sender address.
Contrast with TCP, where recv returning b'' means the peer closed the connection. In UDP b'' just means "an empty postcard arrived" — there is no connection to close. Same-looking value, opposite meaning: this is a favourite exam trap.
Exercise 5.3 — The three-way handshake cost
TCP connect is not free: before your first byte moves, packets fly. Name the exchange, count its messages, and explain why UDP can start sending immediately.
Recall Solution 5.3
TCP sets up a connection with the three-way handshake: SYN → SYN-ACK → ACK = 3 messages before any data. This is what makes TCP a "phone call" (dial, ring, pick up). See TCP three-way handshake.
UDP is connectionless: no handshake, so a client's very first sendto already carries data — 0 setup round-trips. That saved latency is exactly why DNS, gaming, and voice use UDP (UDP vs TCP tradeoffs).
Exercise 5.4 — Backlog reasoning
In s.listen(5), what does 5 bound, and what happens to the 6th simultaneous pending connection if the server hasn't called accept yet?
Recall Solution 5.4
Recall the backlog definition from the top of the page: it is the size of the short "waiting bench" of connections that have finished connecting but have not yet been accepted. So 5 bounds that queue — at most 5 completed-but-unaccepted connections may wait.
What happens to the 6th? If a 6th client completes its connection while the queue is already full and the server has not drained it with accept, the OS may refuse or drop that connection. The client then sees a connection error (or its TCP stack silently retries).
Crucial: the backlog does not limit total or concurrent clients — it is only a buffer for bursts. As long as your server keeps calling accept, the bench keeps emptying and new clients keep succeeding. To actually serve many clients you fix your accept-loop / concurrency model (Exercise 5.1), not this number.
Where to go next
You have now climbed the full ladder: naming calls (L1) → writing them with bytes not str (L2) → predicting TCP-stream vs UDP-datagram behaviour (L3) → building framing + port reuse + DNS (L4) → reasoning about concurrency, EOF, handshakes, and backlog (L5).
Natural follow-ups from here:
- Turn the framing of Exercise 4.1 into a full request/response protocol → Message framing / length-prefix protocol.
- Serve many clients at once without blocking → select / asyncio for concurrent servers.
- Understand the handshake and shutdown states behind
connect/close→ TCP three-way handshake and TIME_WAIT and SO_REUSEADDR. - Zoom out on when to pick each protocol → UDP vs TCP tradeoffs.
Recall One-line summary of the whole ladder
Name the calls (L1) → write them in order with bytes not str (L2) → predict that TCP streams / UDP datagrams behave differently (L3) → build framing + port reuse + DNS (L4) → reason about concurrency, EOF, handshakes, backlog (L5).