R0KernelPath
module 195 / 262

The Transport Layer: TCP & UDP

TCP and UDP are the two workhorses of the internet's transport layer — one promises every byte arrives in order, the other gets out of the way and lets you move fast.

~12h focused study10 concepts

Overview

The network layer (IP) does one thing: deliver a packet from one machine to another, best-effort, with no guarantees. That is enough to build a global network, but it is nowhere near enough to build applications. A web browser needs to request a page and get every byte back, in order, without corruption. A video game needs to send position updates ten times a second and cannot afford to wait for retransmissions. The transport layer is the bridge between raw packet delivery and the rich semantics applications actually need.

The two protocols that dominate this layer — TCP (Transmission Control Protocol) and UDP (User Datagram Protocol) — represent a fundamental design trade-off that every network programmer eventually internalizes. TCP buys you reliability, ordering, and flow control by adding state, handshakes, and retransmission machinery. UDP strips all of that away and hands you a thin wrapper around IP datagrams, leaving every higher-level guarantee to the application. Neither is universally superior: the right choice depends on what your application actually requires.

This module builds the transport layer from first principles. You will understand why multiplexing with ports is necessary, how TCP's three-way handshake establishes shared state before any data flows, how sequence numbers and acknowledgments together implement reliable delivery over an unreliable channel, and how flow control prevents a fast sender from drowning a slow receiver. Along the way you will write real socket code and trace exactly what happens on the wire.

By the end, you will be able to read a Wireshark capture of a TCP connection and narrate every packet — not just recognize the acronyms, but understand the protocol's invariants and why they are necessary. That understanding is the foundation for everything built on top: HTTP, TLS, gRPC, QUIC, and every protocol you will ever implement yourself.

why this matters for the kernel

Every networked application you will ever write — a REST API, a game server, a distributed database, a chat app — relies on either TCP or UDP. Choosing the wrong one, or misunderstanding the guarantees each provides, leads to subtle bugs: data arriving out of order and being misinterpreted, buffers filling up and causing mysterious slowdowns, connections hanging because a FIN was never sent. These are not hypothetical; they are the bugs that junior engineers spend days tracking down and that experienced engineers recognize in minutes.

Beyond application programming, understanding the transport layer is essential for systems work of all kinds. Database engineers tune TCP socket options (SO_KEEPALIVE, TCP_NODELAY, TCP_CORK) to squeeze out latency. Cloud engineers debug connection-reset storms by reading sequence numbers. Security engineers find vulnerabilities in handshake state machines. Protocol designers building QUIC, WebRTC, or custom game networking protocols start from a deep understanding of what TCP gets right and where its assumptions break down. The transport layer is not an implementation detail you can safely ignore — it is the substrate on which the modern internet runs.

interactive visualization
CLIENT (active open)
CLOSED
SERVER (passive open)
LISTEN
clientserver—server opens a passive socketSYNseq=1000SYN-ACKseq=5000 ack=1001ACKseq=1001 ack=5001dataconnection ready · bytes flowFINseq=1001FIN-ACKack=1002 seq=5001
01/7

The server calls listen() and waits. The client hasn't sent anything yet.

SYN → SYN-ACK → ACK opens the pipe; ack = peer's seq + 1. Each side tracks its own state machine.

TCP Three-Way Handshake and Data Transfer

rendering diagram…

Complete TCP lifecycle: three-way handshake establishes shared sequence number state (ISN client=1000, ISN server=5000), followed by bidirectional data exchange with cumulative ACKs, and orderly four-way teardown. Client enters TIME-WAIT (2xMSL) before final close.

The concepts

01The Multiplexing Problem: Ports and Sockets

Imagine your laptop receives an IP packet. The packet is addressed to your machine's IP address — but your machine is simultaneously running a web browser, a Spotify client, an SSH session, and a Slack app. Which process should receive this packet?

This is the multiplexing problem, and ports are its solution. A port is a 16-bit integer (0–65535) that identifies a specific process or service on a host. When the network stack receives a packet, it uses a (source IP, source port, destination IP, destination port) 4-tuple — called a socket pair — to route the payload to exactly the right process.

Ports are divided into three ranges by convention:

  • Well-known ports (0–1023): Reserved for system services. HTTP is 80, HTTPS is 443, SSH is 22, DNS is 53. On Unix-like systems, binding to these ports requires root/administrator privilege.
  • Registered ports (1024–49151): Assigned by IANA for specific applications. PostgreSQL uses 5432, MySQL uses 3306.
  • Ephemeral ports (49152–65535): Dynamically assigned by the OS to client sockets. When your browser opens a connection to google.com:443, the OS picks a random ephemeral port (say, 54821) for the client side.

The critical insight is that a socket is identified by the full 4-tuple, not just a port number. This means a single server port (like 443) can simultaneously handle thousands of connections — each identified by a unique combination of client IP and client ephemeral port. Two connections from the same client to the same server are distinguished by their different ephemeral ports.

In Unix, a socket is a file descriptor — a handle that behaves like a file for reading and writing but carries network semantics. The socket() system call creates one, bind() assigns it an address, and then the protocol dictates what comes next.

import socket

# Create a TCP socket (AF_INET = IPv4, SOCK_STREAM = TCP)
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# SO_REUSEADDR lets us rebind quickly after restart
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

# Bind to port 8080 on all interfaces
s.bind(('0.0.0.0', 8080))

# The local address is now fixed:
print(s.getsockname())  # ('0.0.0.0', 8080)

# After connect() or accept(), getsockname() and getpeername()
# together give you the full 4-tuple that uniquely identifies the connection.

02UDP: The Minimal Transport

UDP (User Datagram Protocol, RFC 768) adds almost nothing to IP. Its header is just 8 bytes:

 0      15 16     31
+--------+--------+
|src port|dst port|
+--------+--------+
| length |checksum|
+--------+--------+
|     data ...    |

That is it. No connection setup, no state, no sequence numbers, no acknowledgments, no retransmission, no ordering guarantees. A UDP datagram is sent and immediately forgotten by the sender.

What UDP guarantees: Checksums catch corruption (though by default on IPv4 the checksum is optional — set to 0x0000 to disable). If a datagram arrives, it arrives whole (IP reassembly handles fragmentation transparently). Port multiplexing works exactly as described above.

What UDP does NOT guarantee: Delivery (the datagram might be dropped), ordering (datagrams may arrive out of order), deduplication (a datagram might arrive twice on certain paths), and no congestion control (you can flood the network freely).

When UDP is the right choice:

  1. Latency beats reliability: DNS queries, game state updates, VoIP frames, WebRTC video — a stale retransmission is worse than silence.
  2. You implement reliability yourself with better semantics: QUIC (the protocol under HTTP/3) runs over UDP and implements its own reliability at the stream level, avoiding TCP's head-of-line blocking.
  3. Multicast/broadcast: TCP is unicast-only; UDP supports one-to-many delivery.
  4. Tiny request-response: A DNS query + response fits in one round-trip without connection overhead.

A common misconception: 'UDP is unreliable therefore bad.' In practice, on modern wired networks packet loss is rare. UDP is not reckless; it is minimal, and minimalism is a feature when you understand what you need.

import socket

# UDP echo server
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  # SOCK_DGRAM = UDP
server.bind(('0.0.0.0', 9999))

while True:
    data, addr = server.recvfrom(4096)   # returns (data, (client_ip, client_port))
    print(f"Got {len(data)} bytes from {addr}")
    server.sendto(data, addr)            # echo back

# UDP client
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.sendto(b'hello', ('127.0.0.1', 9999))
response, _ = client.recvfrom(4096)
print(response)  # b'hello'
# No connect(), no handshake, no teardown required.

03TCP: Goals and Service Model

TCP (Transmission Control Protocol, RFC 793, updated by RFC 9293) provides a reliable, ordered, byte-stream transport between two endpoints. Let us unpack each word:

  • Reliable: Every byte sent is acknowledged. If the sender does not receive an acknowledgment in time, it retransmits. The application sees no holes.
  • Ordered: Bytes arrive at the receiver in exactly the order they were sent. TCP uses sequence numbers to reassemble out-of-order segments.
  • Byte-stream: TCP has no concept of message boundaries. If you send 100 bytes and then 200 bytes, the receiver might get 300 bytes in one read, or 1 byte followed by 299 bytes, depending on buffering. This is a crucial design decision — TCP is a pipe of bytes, not a pipe of messages. (This surprises many beginners who expect message framing to be automatic.)

The cost of these guarantees:

  • A connection must be established before data flows (the three-way handshake).
  • The sender must buffer sent data until it is acknowledged (in case retransmission is needed).
  • The receiver must buffer out-of-order segments until they can be delivered in sequence.
  • Flow control and congestion control add complexity and can throttle throughput.
  • Teardown requires a four-way exchange.

TCP is a state machine. Both endpoints maintain state (LISTEN, SYN-SENT, SYN-RECEIVED, ESTABLISHED, FIN-WAIT-1, FIN-WAIT-2, TIME-WAIT, CLOSE-WAIT, LAST-ACK, CLOSED). Understanding the state machine is essential for debugging real network issues — a socket stuck in TIME-WAIT or CLOSE-WAIT almost always means a bug in teardown logic.

# The TCP service model in pseudocode:

# SENDER perspective:
create byte_stream
for each segment of bytes:
    assign sequence_number = next_unsent_byte_index
    send segment
    start retransmit_timer
    if ACK received before timeout:
        advance send_window
        cancel timer
    else:
        retransmit segment
        back_off timer (exponential backoff)

# RECEIVER perspective:
for each arriving segment:
    if segment.seq == next_expected:
        deliver to application
        send ACK(next_expected + len(segment))
    else:  # out of order
        buffer segment
        send duplicate ACK for last in-order byte

04Sequence Numbers and Reliable Delivery

TCP's sequence number space is the foundation of everything else. Each byte in the stream has a sequence number — a 32-bit index. The Initial Sequence Number (ISN) is chosen randomly at connection setup (to prevent old segments from a previous connection being confused with new ones, and as a basic defense against TCP sequence-number prediction attacks).

A segment carries:

  • SEQ: the sequence number of its first byte
  • ACK: the next sequence number the sender expects (i.e., all bytes before this have been received)
  • LEN: the payload length in bytes

TCP uses cumulative acknowledgment: ACK=N means "I have received all bytes through N-1, please send N next." This is elegant — even if ACK packets are lost, the next ACK implicitly acknowledges everything before it.

Retransmission triggers:

  1. Timeout (RTO): The sender waits for an ACK. If none arrives before the Retransmission Timeout, it resends. RTO is estimated using a smoothed RTT (Karn's algorithm, RFC 6298).
  2. Fast retransmit: If the sender receives three duplicate ACKs (the same ACK number repeated), it infers a segment was lost and retransmits immediately, without waiting for a timeout. This is much faster than waiting for RTO.

Why random ISNs? If ISNs were predictable (e.g., always starting at 0), an attacker could forge TCP segments with valid sequence numbers. Random ISNs make this attack impractical without seeing the actual handshake.

The 32-bit sequence space wraps around at 2^32 bytes (~4 GB). On a fast link, this wrap-around happens in seconds, which is why TCP timestamps (RFC 7323) and PAWS (Protection Against Wrapped Sequences) exist.

# Trace of a simple TCP exchange:
# Client sends "Hello" (5 bytes), then "World" (5 bytes)
# Client ISN = 1000, Server ISN = 5000

# Segment 1: Client -> Server
#   SEQ=1001, LEN=5, data="Hello"
# Segment 2: Server -> Client (ACK for Hello)
#   SEQ=5001, ACK=1006  (1001 + 5 = 1006, expect byte 1006 next)
# Segment 3: Client -> Server
#   SEQ=1006, LEN=5, data="World"
# Segment 4: Server -> Client (ACK for World)
#   SEQ=5001, ACK=1011  (cumulative: all 10 bytes received)

# Fast retransmit scenario:
# SEQ=100 arrives, ACK=100 sent
# SEQ=110 arrives (SEQ=100 gap!), ACK=100 sent (dup)
# SEQ=120 arrives (gap still), ACK=100 sent (dup)
# SEQ=130 arrives (gap still), ACK=100 sent (dup)
# <- 3 duplicate ACKs -> sender retransmits SEQ=100 immediately

05The Three-Way Handshake

Before any data flows over TCP, both endpoints must agree on initial sequence numbers and transition to the ESTABLISHED state. This is accomplished by the three-way handshake (SYN, SYN-ACK, ACK).

Why three messages and not two? With two messages, the server could not confirm that the client received the server's ISN. Three messages ensure both parties have successfully communicated their ISNs to each other. This is the minimum required to establish bidirectional synchronized state.

Step by step:

  1. SYN: The client sends a segment with the SYN flag set and its randomly chosen ISN (client_isn). Client transitions to SYN-SENT.
  2. SYN-ACK: The server responds with SYN and ACK flags set. It acknowledges the client (ACK = client_isn + 1) and announces its own ISN (server_isn). Server transitions to SYN-RECEIVED.
  3. ACK: The client acknowledges the server (ACK = server_isn + 1). Both sides transition to ESTABLISHED.

After the handshake, both sides know each other's ISNs and can correctly interpret sequence numbers. Data can now flow in both directions simultaneously (TCP is full-duplex).

SYN flood attacks: An attacker can flood a server with SYN segments using spoofed source IPs. The server allocates state for each half-open connection, exhausting memory. The defense is SYN cookies: the server encodes connection state into the ISN it sends back, rather than storing it. If the client completes the handshake, the server reconstructs state from the cookie — if not, nothing was allocated.

TCP connection teardown is a four-way exchange (FIN, ACK, FIN, ACK) because each direction must be closed independently. The TIME-WAIT state (2×MSL = 4 minutes by default) ensures stray packets from a previous connection cannot corrupt a new one on the same 4-tuple.

# Python TCP server/client showing the handshake implicitly
import socket, threading

def server():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(('127.0.0.1', 7777))
    s.listen(5)           # kernel now sends SYN-ACK to incoming SYNs
    conn, addr = s.accept()  # blocks until 3-way handshake completes
    print(f"Connected from {addr}")
    data = conn.recv(1024)
    conn.send(data)
    conn.close()          # sends FIN
    s.close()

def client():
    c = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    c.connect(('127.0.0.1', 7777))   # triggers 3-way handshake
    c.send(b'hello')
    print(c.recv(1024))  # b'hello'
    c.close()            # sends FIN

t = threading.Thread(target=server)
t.start()
client()
t.join()
# Run `sudo tcpdump -i lo port 7777 -S` alongside to watch SYN/SYN-ACK/ACK

06Flow Control: The Receive Window

Reliability guarantees that every byte arrives, but it says nothing about rate. Without flow control, a fast sender could transmit faster than the receiver can process, overflowing the receiver's buffer and forcing the receiver to drop segments — which would then be retransmitted, wasting bandwidth in a vicious cycle.

TCP solves this with a receive window (rwnd): the receiver advertises, in every ACK segment, how many bytes of free space remain in its receive buffer. The sender may not have more than rwnd unacknowledged bytes in flight at any time.

Sender constraint:  LastByteSent - LastByteAcked <= rwnd

This creates a sliding window protocol. As the receiver's application reads data from the buffer, rwnd grows and the sender is allowed to send more. If the application stops reading (busy doing other work), rwnd shrinks to zero and the sender must stop. The sender will probe periodically with 1-byte segments (persist timer) to detect when the window re-opens, since the window-update ACK might have been lost.

Zero-window probing: When rwnd = 0, the sender sets a persist timer. On expiry, it sends a 1-byte probe. If the receiver's window is still zero, it sends a new zero-window ACK. This prevents deadlock.

The silly window syndrome: If the application reads data one byte at a time, the receiver might advertise tiny windows (1 or 2 bytes), causing the sender to transmit many tiny segments — extremely inefficient. Clark's solution: the receiver does not advertise a window update until it has at least min(MSS, half of receive buffer) free. Nagle's algorithm on the sender side is the complementary fix: buffer small writes and send only when either a full MSS is ready or there are no outstanding unacknowledged segments.

# Demonstrating receive buffer / window in Python
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# Explicitly set receive buffer size (rwnd tracks this)
s.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4096)
print(s.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF))  # OS may double it

# TCP_NODELAY disables Nagle's algorithm — good for low-latency apps
# like terminals (SSH) or games that send tiny, time-sensitive payloads
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)

# Note: actual rwnd visible in Wireshark as "Window" field in TCP header
# A shrinking window during a transfer is flow control in action.

07Congestion Control: Not Flooding the Network

Flow control protects the receiver's buffer. Congestion control protects the network — specifically, the routers between sender and receiver whose queues can overflow if traffic exceeds link capacity.

TCP infers congestion from two signals: segment loss (timeout or triple duplicate ACK) and, in modern variants, delay increase. It responds by reducing its send rate.

The key concept is the congestion window (cwnd), maintained by the sender. The effective send window is min(rwnd, cwnd) — both constraints apply simultaneously.

TCP Reno (the classic algorithm):

  1. Slow Start: Begin with cwnd = 1 MSS (Maximum Segment Size, typically 1460 bytes). Double cwnd each RTT (exponential growth). Despite the name, this grows very fast — it is 'slow' only relative to sending at full line rate immediately.
  2. Congestion Avoidance: When cwnd reaches ssthresh (slow-start threshold), switch to additive increase: cwnd += 1 MSS per RTT. Linear growth.
  3. Loss detected (triple dup ACK — Fast Recovery): ssthresh = cwnd/2. cwnd = ssthresh. Return to Congestion Avoidance. (Multiplicative decrease.)
  4. Timeout: ssthresh = cwnd/2. cwnd = 1 MSS. Restart Slow Start.

This is AIMD (Additive Increase, Multiplicative Decrease), the mathematical foundation of TCP fairness. Multiple TCP flows sharing a bottleneck converge to equal bandwidth shares over time — a remarkable emergent property.

Modern variants: TCP CUBIC (default in Linux) uses a cubic function of time since last loss event instead of linear increase, achieving better utilization on high-bandwidth-delay-product links. BBR (Bottleneck Bandwidth and RTT) explicitly models bandwidth and RTT rather than inferring congestion from loss.

# Pseudocode for TCP Reno congestion control
cwnd = 1 * MSS
ssthresh = 65535  # initial threshold, often set to max

def on_new_ack(acked_bytes):
    global cwnd, ssthresh
    if cwnd < ssthresh:          # Slow Start
        cwnd += acked_bytes      # approx doubles per RTT
    else:                        # Congestion Avoidance
        cwnd += MSS * (MSS / cwnd)  # +1 MSS per RTT

def on_triple_duplicate_ack():
    global cwnd, ssthresh
    ssthresh = max(cwnd // 2, 2 * MSS)  # halve threshold
    cwnd = ssthresh                      # Fast Recovery start

def on_timeout():
    global cwnd, ssthresh
    ssthresh = max(cwnd // 2, 2 * MSS)
    cwnd = 1 * MSS                       # back to Slow Start

08The Socket API: Programming the Transport Layer

The socket API is the Unix interface that applications use to access the transport layer. Designed at Berkeley in the early 1980s alongside BSD Unix, it has become one of the most influential APIs in computing history — virtually every operating system implements it, and every network program uses it.

The socket API has two distinct workflows depending on whether you are building a server (passive, waits for connections) or a client (active, initiates connections).

Server workflow (TCP):

  1. socket() — Create a socket file descriptor.
  2. bind() — Assign a local address (IP + port).
  3. listen(backlog) — Mark the socket as passive. backlog is the size of the kernel's queue for completed (3-way handshake done) connections waiting to be accept()ed.
  4. accept() — Block until a new connection arrives. Returns a new socket fd for the connection (the original socket stays listening).
  5. read()/write() (or recv()/send()) — Exchange data.
  6. close() — Trigger FIN and teardown.

Client workflow (TCP):

  1. socket() — Create socket.
  2. connect(server_addr) — Triggers the three-way handshake. Blocks until ESTABLISHED.
  3. read()/write() — Exchange data.
  4. close() — Teardown.

Important subtlety: read() on a TCP socket may return fewer bytes than requested. Because TCP is a byte stream, you must loop until you have received all the bytes you need. This is one of the most common bugs in network code written by beginners.

Non-blocking I/O and multiplexing: A server that calls accept() and read() sequentially can only handle one client at a time. Real servers use select(), poll(), or epoll() (Linux) to monitor multiple sockets simultaneously, or use threads/async I/O. This is the foundation of the C10K problem and event-driven server architectures.

import socket

# --- Complete echo server ---
def run_server(host='127.0.0.1', port=8888):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as srv:
        srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        srv.bind((host, port))
        srv.listen(5)
        print(f'Listening on {host}:{port}')
        conn, addr = srv.accept()
        with conn:
            print(f'Connection from {addr}')
            while True:
                # MUST loop: recv may return partial data
                chunk = conn.recv(4096)
                if not chunk:
                    break  # FIN received
                conn.sendall(chunk)  # sendall loops internally

# --- Complete echo client ---
def run_client(message: bytes, host='127.0.0.1', port=8888):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as c:
        c.connect((host, port))
        c.sendall(message)
        received = b''
        while len(received) < len(message):
            chunk = c.recv(4096)
            if not chunk:
                break
            received += chunk
        return received

09TCP Connection Teardown and TIME-WAIT

TCP teardown is asymmetric — each direction of the byte stream is closed independently. This is important: even after one side sends a FIN, the other side can continue sending data. This is called a half-closed connection.

Four-way teardown (orderly close):

  1. Active closer sends FIN (data up to this point, then done sending).
  2. Passive closer sends ACK.
  3. Passive closer sends its own FIN (when its application calls close()).
  4. Active closer sends ACK, enters TIME-WAIT.

TIME-WAIT lasts 2 × MSL (Maximum Segment Lifetime), typically 60-120 seconds. During this period, the 4-tuple cannot be reused. The reasons:

  • The final ACK might have been lost. The passive closer will retransmit its FIN, and the active closer must be able to respond.
  • Any stray segments from this connection must expire before a new connection on the same 4-tuple begins, preventing data corruption.

TIME-WAIT exhaustion is a real operational problem. A server that rapidly cycles through many short-lived connections (like an HTTP/1.1 server) accumulates TIME-WAIT sockets. The OS limits the number of local port numbers, so TIME-WAIT can exhaust the ephemeral port space. Solutions: SO_LINGER with linger=0 (RST instead of orderly close), net.ipv4.tcp_tw_reuse (Linux), or simply using persistent connections (HTTP/1.1 keep-alive, HTTP/2).

RST (Reset): Either side can send a RST segment to immediately abort the connection — no graceful teardown, no TIME-WAIT. This is used when a connection request arrives on a port with no listener, or when an application crashes without closing its sockets cleanly.

# Observing TIME-WAIT in practice (run in shell, not Python)
# After a short-lived server/client exchange:
#
# $ netstat -an | grep TIME_WAIT
# tcp  0  0  127.0.0.1.8888  127.0.0.1.54321  TIME_WAIT
#
# To allow fast reuse on Linux (server side):
# $ sysctl -w net.ipv4.tcp_tw_reuse=1
#
# In Python, to get RST on close (skip TIME-WAIT):
import socket, struct
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# l_onoff=1, l_linger=0 -> RST on close(), no TIME-WAIT
s.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER,
             struct.pack('ii', 1, 0))
# Use sparingly: RST discards any data in kernel send buffer

10TCP Segment Structure

Understanding the TCP header in detail lets you read packet captures, understand flags, and reason about protocol behavior precisely. The TCP header is minimum 20 bytes, and up to 60 bytes with options.

Key fields:

  • Source port / Destination port (16 bits each): Multiplexing.
  • Sequence number (32 bits): Byte offset of first byte in this segment's payload.
  • Acknowledgment number (32 bits): Next expected byte from the other side (valid only if ACK flag set).
  • Data offset (4 bits): Header length in 32-bit words (allows for options).
  • Flags (9 bits): URG, ACK, PSH, RST, SYN, FIN (plus ECE, CWR for ECN, NS for nonce).
  • Window size (16 bits): Receive window (rwnd) in bytes. Can be scaled up to 30 bits via TCP window scaling option (RFC 7323).
  • Checksum (16 bits): Covers TCP header + data + IP pseudo-header. Mandatory (unlike UDP).
  • Urgent pointer (16 bits): If URG set, offset to urgent data. Rarely used.
  • Options: Variable length (padded to 32-bit boundary). Common options: MSS (Maximum Segment Size, negotiated in handshake), SACK (Selective Acknowledgment), Timestamps, Window Scale.

PSH flag: Tells the receiver to pass buffered data to the application immediately, without waiting for more data. Set by the sender when it knows no more data is coming soon.

ECN (Explicit Congestion Notification): Routers can set the ECN bits in the IP header to signal congestion without dropping packets. TCP uses the ECE and CWR flags to act on this signal, reducing congestion response latency.

# Reading a raw TCP header with Python's struct module
import struct

def parse_tcp_header(raw_bytes):
    # First 20 bytes: fixed TCP header
    (src_port, dst_port, seq, ack,
     offset_reserved_flags, window,
     checksum, urgent_ptr) = struct.unpack('!HHIIHHH H', raw_bytes[:20])
    # Note: offset_reserved_flags packs data_offset (4b) + reserved (3b) + flags (9b)
    data_offset = (offset_reserved_flags >> 12) * 4  # in bytes
    flags = offset_reserved_flags & 0x1FF
    syn = bool(flags & 0x002)
    ack_flag = bool(flags & 0x010)
    fin = bool(flags & 0x001)
    return {
        'src': src_port, 'dst': dst_port,
        'seq': seq, 'ack': ack,
        'window': window, 'SYN': syn, 'ACK': ack_flag, 'FIN': fin,
        'header_len': data_offset
    }

Key insights

  • ✦A socket is uniquely identified by a 4-tuple (src IP, src port, dst IP, dst port) — this is why a single server port can handle thousands of simultaneous connections.
  • ✦TCP's three-way handshake requires exactly three messages because both parties must confirm receipt of each other's ISN; two messages leave the server unconfirmed.
  • ✦TCP is a byte stream, not a message stream — recv() can return any number of bytes up to the requested amount, and production code must loop to receive complete messages.
  • ✦Reliable delivery requires the sender to buffer all unacknowledged data; the receive window (rwnd) and congestion window (cwnd) together determine how much can be in flight.
  • ✦AIMD (Additive Increase, Multiplicative Decrease) is not arbitrary — it is the unique control law that provably drives competing flows to equal bandwidth shares.
  • ✦TIME-WAIT exists to handle lost final ACKs and prevent stray packets from corrupting future connections; it is a correctness mechanism, not a performance bug.
  • ✦UDP is not 'unreliable TCP' — it is a deliberate design for applications where latency beats reliability, or where the application implements its own, better-suited reliability mechanism.
  • ✦Nagle's algorithm and Clark's algorithm are complementary solutions to silly window syndrome — Nagle buffers small sends on the sender, Clark delays small window advertisements on the receiver.

Common pitfalls

  • ⚠Treating TCP as a message protocol: calling send('hello') on one side and expecting recv() to return exactly b'hello'. TCP may split or coalesce data; always frame messages explicitly (length prefix, delimiter, fixed size).
  • ⚠Not looping on recv(): a single recv() call returns up to N bytes, not exactly N bytes. Missing this is the single most common bug in beginner socket code.
  • ⚠Forgetting SO_REUSEADDR on server sockets: after restarting a server, the port is in TIME-WAIT and bind() fails with 'Address already in use'. Always set SO_REUSEADDR before bind().
  • ⚠Confusing flow control (rwnd, protects receiver) with congestion control (cwnd, protects network). They are orthogonal mechanisms that both constrain the sender's window.
  • ⚠Assuming UDP datagrams arrive in order: UDP provides no ordering guarantee. If order matters, you must add sequence numbers in your application layer.
  • ⚠Calling close() and assuming data was sent: close() queues a FIN but may return before the kernel has transmitted all buffered data. Use shutdown(SHUT_WR) + drain to ensure all data is sent and acknowledged before closing.
  • ⚠Ignoring partial writes from send()/sendall(): use sendall() in Python, which loops internally. In C, always check the return value of write()/send() and loop.

Practice

  1. 01
    medium

    Implement a reliable message-framing layer on top of a raw TCP socket. Design a protocol where each message is prefixed with a 4-byte big-endian length field. Write send_message(sock, data) and recv_message(sock) functions. recv_message must correctly handle the case where a single recv() call returns a partial header or partial body.

  2. 02
    medium

    Build a minimal UDP-based ping tool. The client sends a 'PING' datagram with a timestamp and the server responds with 'PONG' + the same timestamp. Measure RTT. Then introduce 10% artificial packet loss on the client side (drop 1 in 10 sends) and add a 1-second timeout with retransmission. Compare the behavior with TCP's built-in reliability.

  3. 03
    easy

    Write a port scanner that determines which TCP ports in the range 1-1024 on localhost are open. A port is open if connect() succeeds; closed if it gets connection refused. Use non-blocking sockets or a thread pool to scan all 1024 ports in under 2 seconds.

  4. 04
    medium

    Trace TCP's congestion window (cwnd) over time by simulating the Reno algorithm: start with cwnd=1 MSS, ssthresh=8 MSS, and simulate 30 RTTs. At RTT 10, inject a triple-duplicate-ACK loss event. At RTT 20, inject a timeout. Plot cwnd vs RTT. Verify AIMD behavior analytically: after the loss at RTT 10, what is cwnd at RTT 15?

  5. 05
    hard

    Read the raw bytes of a TCP handshake using Python's socket module with AF_PACKET (Linux) or a pcap library like scapy. Parse the IP and TCP headers manually using struct.unpack(). Print the src/dst ports, sequence numbers, and flags for each packet in the three-way handshake. Verify that ACK = other_side_SEQ + 1.

Resources

📕Computer Networking: A Top-Down Approach (Kurose & Ross)
bookpaidPearson; 8th edition. Chapter 3 covers the transport layer exhaustively. The gold standard undergraduate textbook for this topic.
📄RFC 9293 — Transmission Control Protocol (updated RFC 793)
docsfree

The authoritative TCP specification. Readable as a reference; sections 3.1-3.4 cover the state machine and segment structure.

📄RFC 768 — User Datagram Protocol
docsfree

Only 3 pages. Read the entire thing. A masterpiece of minimalist protocol design.

✎Beej's Guide to Network Programming
articlefree

The best practical introduction to the socket API in C. Explains every syscall with working examples. Free online.

🎓MIT 6.829 Computer Networks (OCW)
coursefree

Graduate-level MIT course with lecture notes and papers on TCP, congestion control, and network architecture.

📕TCP/IP Illustrated, Volume 1 (Stevens)
bookpaidAddison-Wesley Professional. W. Richard Stevens. The definitive deep dive — every packet traced with tcpdump. Chapters 17-24 cover TCP in exhaustive detail.
📕Computer Networks (Tanenbaum & Wetherall)
bookpaidPearson; 5th edition. Stronger on protocol internals and the OSI model than Kurose. Chapter 6 on the transport layer is superb.
🔧Wireshark — Network Protocol Analyzer
toolfree

Essential companion to this module. Capture a TCP handshake to your own server and inspect every field. Use the TCP stream graph to visualize window behavior.

Checkpoints — you can now…

  • ✓You can explain why a 4-tuple (src IP, src port, dst IP, dst port) is necessary and sufficient to identify a TCP connection, and why a server port can handle thousands of simultaneous clients.
  • ✓You can trace a TCP three-way handshake packet by packet, labeling SEQ and ACK numbers, and explain why three messages are the minimum required.
  • ✓You can write a correct TCP echo server and client in Python, with proper looping on recv() and use of sendall(), and explain why a naive single recv() is incorrect.
  • ✓You can describe the roles of rwnd (flow control) and cwnd (congestion control) separately, and state the sender constraint that combines them: flight size <= min(rwnd, cwnd).
  • ✓You can simulate TCP Reno through a loss event and correctly compute cwnd values after fast recovery and linear growth.
  • ✓You can explain what TIME-WAIT is, why it lasts 2xMSL, and what operational problem it causes in high-connection-rate servers.
  • ✓You can explain three scenarios where UDP is preferable to TCP and implement a simple reliable datagram protocol on top of UDP.