4.3.28 · D3Computer Networks
Worked examples — Socket programming — TCP server - client, UDP server - client in Python
You already met the calls in the parent note. Now we stress-test them: every quadrant of "what could actually happen" when two programs talk over a socket. Loss, reordering, partial reads, closed peers, wrong protocol — each gets its own worked example so you never hit a case you haven't seen.
The scenario matrix
Every worked example below is tagged with the cell it covers. Read the table first — it is the whole test surface.
| # | Cell (case class) | Protocol | What's different here | Example |
|---|---|---|---|---|
| A | Happy path, one send = one recv | TCP | Baseline, nothing weird | Ex 1 |
| B | Coalescing — many sends, one recv | TCP | Stream has no message boundaries | Ex 2 |
| C | Splitting — one send, many recvs | TCP | A big send arrives in pieces | Ex 3 |
| D | Peer closed — recv returns b'' |
TCP | Degenerate/EOF input | Ex 4 |
| E | Boundaries preserved | UDP | One sendto = one recvfrom |
Ex 5 |
| F | Loss + reorder — the ugly case | UDP | Packets vanish / arrive jumbled | Ex 6 |
| G | Framing fix (length prefix) | TCP | The real-world word problem | Ex 7 |
| H | Wrong protocol / exam twist | mixed | listen on UDP, str not bytes, no bind |
Ex 8 |
Cells A–D exhaust TCP's behaviour, E–F exhaust UDP's, G shows the standard fix, H is the exam trap collection. Together they cover the matrix.

Example 1 — Cell A: the happy path (TCP)
- The client hands 5 bytes to the OS.
Why this step?
b"hello"is the ASCII bytesh e l l o= 5 bytes.sendallloops internally until all 5 are queued to the network. - The server asks for "up to 1024".
Why this step?
recv(1024)means "give me whatever is ready, at most 1024 bytes". Since only 5 bytes are in flight and they arrive together, it returns all 5. - Result:
data == b"hello",len(data) == 5.
Example 2 — Cell B: coalescing (TCP)
- TCP is a byte stream, not a message queue.
Why this step? The kernel is allowed to merge the outgoing bytes into one segment before they leave. There is no marker between
"AA"and"BB". - The server's single
recv(1024)scoops up everything available. Why this step? All 6 bytes may already be sitting in the receive buffer whenrecvruns, so it hands back all of them at once. - Result: one possible (very common on localhost) outcome is
data == b"AABBCC",len == 6.

Example 3 — Cell C: splitting (TCP)
recv(1024)can never return more than 1024 bytes. Why this step? We asked for a max of 1024. 3000 bytes cannot come out in one call.- Each call returns up to 1024, whatever is currently buffered.
Why this step? Network delivery is chunked by segment size; the receiver drains what's there. A plausible sequence is
1024, 1024, 952— three calls summing to 3000. The exact split is not guaranteed; only the total and order are. - Loop until you've collected 3000 bytes (or hit
b''). Why this step? You must keep reading — onerecvis almost never the whole message for large data.
Example 4 — Cell D: the degenerate input, recv returns b'' (TCP)
- A clean close sends a FIN to the peer.
Why this step?
close()tells the OS "no more bytes from me". The receiver's stream reaches its end. recvon a closed stream returns the empty bytes objectb''. Why this step?b''is the socket's way of saying EOF — end of file, i.e. the peer hung up. It is not "the client sent nothing".- The server must break the loop.
Why this step? If you don't test for
b'', you loop forever gettingb''and spin the CPU.
while True:
data = conn.recv(1024)
if data == b'': # or: if not data
break # peer closed — EOF
conn.sendall(data)Example 5 — Cell E: UDP preserves boundaries
- Each
sendtois one self-contained datagram. Why this step? UDP carries messages, not a stream. The boundary between"AA"and"BB"is baked into the packet structure. - Each
recvfromreturns exactly one datagram. Why this step? You cannot merge two UDP packets into one read, and you cannot split one across two reads. One packet ↔ onerecvfrom. - Result (no loss): the three calls return
b"AA",b"BB",b"CC"— neverb"AABBCC".

Example 6 — Cell F: the ugly case, loss and reorder (UDP)
- UDP gives no delivery guarantee.
Why this step? There is no handshake, no ack, no retransmit. A router under load simply discards packet
3. - UDP gives no ordering guarantee.
Why this step? Packets can take different paths;
4may arrive before2. Boundaries are still preserved (each read is one whole packet), but sequence is not. - Possible received sequence:
1, 4, 2, 5— four datagrams, one lost, two swapped.
Example 7 — Cell G: the real-world fix, length-prefix framing (TCP)
- Prefix the payload with its length as a 4-byte big-endian integer.
Why this step? Because TCP has no boundaries (Ex 2 & 3), the receiver needs an explicit "there are N bytes coming".
11packed big-endian is\x00\x00\x00\x0b. - Wire bytes = 4 (header) + 11 (payload) = 15 bytes.
Why this step? The receiver first reads exactly 4 bytes, decodes
N = 11, then loopsrecvuntil it has collected 11 more. - Receiver algorithm: read 4 → get
N→ readN. Now boundaries are recovered no matter how TCP chunked things.
import struct
def send_msg(sock, payload):
header = struct.pack('>I', len(payload)) # 4-byte big-endian length
sock.sendall(header + payload)
def recv_exact(sock, n):
buf = b''
while len(buf) < n:
chunk = sock.recv(n - len(buf))
if chunk == b'': # EOF (Ex 4!)
raise ConnectionError("peer closed mid-message")
buf += chunk
return buf
def recv_msg(sock):
n = struct.unpack('>I', recv_exact(sock, 4))[0]
return recv_exact(sock, n)Example 8 — Cell H: exam twists and wrong-protocol traps
(a) UDP with listen/accept:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.listen(5) # ???SOCK_DGRAMis UDP; UDP is connectionless. Why this step?listen/acceptset up connections, which UDP does not have. This raisesOSError. UDP only ever doesbindthenrecvfrom/sendto.
(b) Sending a str:
c.send("hi") # ???- Sockets carry bytes, not
str. Why this step?sendneeds a bytes-like object → this raisesTypeError. Fix:c.send(b"hi")or"hi".encode().
(c) Client that binds vs one that doesn't:
c.connect(('127.0.0.1', 9000)) # no bind before this- The client does not need
bind. Why this step? Onconnectthe OS auto-assigns an ephemeral port (from Ports and IP addressing). Only the server, whom others must find, needs a fixedbind.
(d) Fast restart, no SO_REUSEADDR:
s.bind(('127.0.0.1', 9000)) # right after a previous server closed- May raise
OSError: Address already in use. Why this step? The old socket lingers inTIME_WAIT(see TIME_WAIT and SO_REUSEADDR). SettingSO_REUSEADDRbeforebindlets you reclaim the port immediately.
Flashcards
In TCP, three sendall calls can arrive as how many recv calls?
Any number — even one merged
recv (stream coalescing).What does recv() returning b'' mean in TCP?
The peer closed the connection (EOF).
Does UDP merge or split datagrams?
Never — one
sendto equals exactly one recvfrom.Minimum recv(1024) calls to read 3000 bytes over TCP?
3 (ceil of 3000/1024).
How does a length-prefix protocol recover message boundaries on TCP?
Read a fixed header giving N, then read exactly N payload bytes.
Which side normally skips bind?
The client — the OS assigns it an ephemeral port.