4.3.28 · D3Computer Networks

Worked examples — Socket programming — TCP server - client, UDP server - client in Python

2,469 words11 min readBack to topic

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 closedrecv 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.

Figure — Socket programming — TCP server - client, UDP server - client in Python

Example 1 — Cell A: the happy path (TCP)

  1. The client hands 5 bytes to the OS. Why this step? b"hello" is the ASCII bytes h e l l o = 5 bytes. sendall loops internally until all 5 are queued to the network.
  2. 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.
  3. Result: data == b"hello", len(data) == 5.

Example 2 — Cell B: coalescing (TCP)

  1. 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".
  2. The server's single recv(1024) scoops up everything available. Why this step? All 6 bytes may already be sitting in the receive buffer when recv runs, so it hands back all of them at once.
  3. Result: one possible (very common on localhost) outcome is data == b"AABBCC", len == 6.
Figure — Socket programming — TCP server - client, UDP server - client in Python

Example 3 — Cell C: splitting (TCP)

  1. 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.
  2. 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.
  3. Loop until you've collected 3000 bytes (or hit b''). Why this step? You must keep reading — one recv is almost never the whole message for large data.

Example 4 — Cell D: the degenerate input, recv returns b'' (TCP)

  1. 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.
  2. recv on a closed stream returns the empty bytes object b''. 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".
  3. The server must break the loop. Why this step? If you don't test for b'', you loop forever getting b'' 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

  1. Each sendto is 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.
  2. Each recvfrom returns 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 ↔ one recvfrom.
  3. Result (no loss): the three calls return b"AA", b"BB", b"CC" — never b"AABBCC".
Figure — Socket programming — TCP server - client, UDP server - client in Python

Example 6 — Cell F: the ugly case, loss and reorder (UDP)

  1. UDP gives no delivery guarantee. Why this step? There is no handshake, no ack, no retransmit. A router under load simply discards packet 3.
  2. UDP gives no ordering guarantee. Why this step? Packets can take different paths; 4 may arrive before 2. Boundaries are still preserved (each read is one whole packet), but sequence is not.
  3. 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)

  1. 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". 11 packed big-endian is \x00\x00\x00\x0b.
  2. Wire bytes = 4 (header) + 11 (payload) = 15 bytes. Why this step? The receiver first reads exactly 4 bytes, decodes N = 11, then loops recv until it has collected 11 more.
  3. Receiver algorithm: read 4 → get N → read N. 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)      # ???
  1. SOCK_DGRAM is UDP; UDP is connectionless. Why this step? listen/accept set up connections, which UDP does not have. This raises OSError. UDP only ever does bind then recvfrom/sendto.

(b) Sending a str:

c.send("hi")     # ???
  1. Sockets carry bytes, not str. Why this step? send needs a bytes-like object → this raises TypeError. 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
  1. The client does not need bind. Why this step? On connect the OS auto-assigns an ephemeral port (from Ports and IP addressing). Only the server, whom others must find, needs a fixed bind.

(d) Fast restart, no SO_REUSEADDR:

s.bind(('127.0.0.1', 9000))   # right after a previous server closed
  1. May raise OSError: Address already in use. Why this step? The old socket lingers in TIME_WAIT (see TIME_WAIT and SO_REUSEADDR). Setting SO_REUSEADDR before bind lets 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.