DEV Community

Sangyog Puri
Sangyog Puri

Posted on

CSAPP Chapter 11 : Network Programming - Deep Reference

This blog is for personal reference of CSAPP Ch 11 Reference • Network Programming

1. The Network Model - Three Layers That Matter

Ch 11 focuses on three layers of the network stack. Everything else (physical cables, ethernet, WiFi) is below this level and hidden by the OS.

CORE IDEA Your code only ever touches the Application and Transport layers. The OS handles everything below. When you call read() on a socket fd, the OS transparently handles IP routing, TCP reliability, and physical transmission. You just see bytes.

2. Addressing - IP, Ports, and the Four-Tuple

2.1 IP Addresses - Finding the Machine

An IP address identifies a specific machine on the network. IPv4 addresses are 32-bit integers, written as four decimal numbers: 192.168.1.5. IPv6 addresses are 128-bit, written as hex groups: 2001:db8::1.

Important IP addresses to know:

  • 127.0.0.1 (localhost) - loopback address. Packets sent here never leave the machine. Used for local inter-process communication.
  • 0.0.0.0 - bind to ALL network interfaces. A server binding to this accepts connections on any network interface (WiFi, ethernet, loopback).
  • 255.255.255.255 - broadcast address. Sends to all machines on local network.

2.2 Ports - Finding the Program

A port is a 16-bit integer (0-65535) that identifies a specific program on a machine. The OS uses it to route incoming packets to the correct process.

Port Range Name Who uses it Examples
0-1023 Well-known ports Privileged - requires root to bind 80 (HTTP), 443 (HTTPS), 22 (SSH), 25 (SMTP)
1024-49151 Registered ports Any process 5432 (PostgreSQL), 6379 (Redis), 8080 (dev HTTP)
49152-65535 Ephemeral ports OS assigns automatically to clients Randomly chosen per outgoing connection

Ephemeral ports: when a client calls connect(), the OS automatically picks an unused port from the ephemeral range for that connection. The client doesn't choose it - the kernel does. This is what makes the four-tuple unique.

2.3 The Socket Address - IP + Port Combined

A socket address is the combination of an IP address and a port - uniquely identifying one endpoint of a connection.

2.4 The Four-Tuple - Uniquely Identifying Every Connection

Every TCP connection is uniquely identified by four values:

KEY INSIGHT A server on port 80 can handle thousands of simultaneous connections because each connection has a unique four-tuple. The server port (80) is the same for all of them - the client port differs for each. The kernel routes incoming packets to the right connection using the full four-tuple, not just the port.

2.5 Byte Ordering - Network vs Host

Different CPU architectures store multi-byte integers differently (big-endian vs little-endian, from Ch 2). The network uses big-endian (network byte order) by convention. x86 CPUs use little-endian (host byte order). You must convert between them when putting addresses/ports in socket structs.

Function Converts Use when
htons(x) host → network (16-bit) Setting sin_port in sockaddr_in
htonl(x) host → network (32-bit) Setting sin_addr in sockaddr_in
ntohs(x) network → host (16-bit) Reading port from received sockaddr_in
ntohl(x) network → host (32-bit) Reading IP from received sockaddr_in

Helper functions for IP strings: inet_pton() converts "192.168.1.5" → binary. inet_ntop() converts binary → "192.168.1.5". Always use these instead of manual conversion.

3. TCP vs UDP - When to Use Each

Property TCP UDP
Connection Connection-oriented - 3-way handshake before data Connectionless - just send packets
Reliability Guaranteed delivery - lost packets retransmitted No guarantee - packets can be lost silently
Ordering Guaranteed in-order delivery No ordering guarantee - packets can arrive out of order
Message boundaries NONE - byte stream, any chunking possible Preserved - one send = one receive (or nothing)
Flow control Yes - slows sender if receiver buffer full None - sender can overwhelm receiver
Congestion control Yes - slows down when network is congested None - can make congestion worse
Speed/latency Slower - handshake overhead, retransmit delays Faster - no setup, no waiting for retransmits
Use cases HTTP, databases, SSH, email, file transfer DNS, video streaming, gaming, QUIC
TCP BYTE STREAM WARNING TCP has NO concept of message boundaries. Two write() calls on the sender can arrive as one read() on the receiver, or one write() can arrive as multiple reads. Every TCP protocol must explicitly handle message framing. This is the most common networking bug for beginners.
REAL WORLD QUIC (used by HTTP/3) is built on UDP because it needs to implement its own multiplexed streams with independent reliability per stream. TCP's head-of-line blocking (one lost packet stalls all data) is unacceptable for HTTP/3's multiple parallel streams. UDP gives QUIC full control to implement exactly the reliability semantics it needs.

4. The TCP 3-Way Handshake

4.1 The Handshake Sequence

4.2 What Each Step Establishes

  • SYN: 'I want to connect, my sequence numbers start at x.' Sequence numbers are used to detect lost packets and reorder out-of-order delivery.
  • SYN-ACK: 'I got your SYN (ack=x+1 means I expect x+1 next), and my sequence numbers start at y.' Server also allocates kernel buffers for this connection here.
  • ACK: 'I got your SYN (ack=y+1 means I expect y+1 next).' Connection is fully open in both directions.

The kernel handles the handshake - not your application. Your server process is asleep in accept() during the entire handshake. The kernel completes it, then wakes your process when the connection is ready.

4.3 The Connection Queues

The kernel maintains two queues per listening socket:

SYN FLOOD ATTACK An attacker sends thousands of SYNs without completing the handshake. The incomplete queue fills up, server allocates buffers for each half-open connection, memory exhausted, legitimate connections refused. Defense: SYN cookies - server encodes connection state in the SYN-ACK's sequence number instead of allocating buffers, so no memory is used until the final ACK arrives.

4.4 TCP Connection Teardown - 4-Way Handshake

Half-closed connections: after client sends FIN, client can no longer send data - but can still receive. The server→client direction is still open until the server sends its own FIN. This asymmetry is used by HTTP keep-alive and streaming responses.

TIME_WAIT state: after sending the final ACK, the client enters TIME_WAIT for 2×MSL (Maximum Segment Lifetime, ~60s). This prevents delayed packets from a dead connection being misinterpreted by a new connection using the same four-tuple. Servers that rapidly recycle connections (like load balancers) can run out of ephemeral ports due to TIME_WAIT accumulation - tunable via SO_REUSEADDR.

5. The Socket Interface - Syscalls in Detail

5.1 Server Side - 4 Syscalls in Order

SO_REUSEADDR: always set this option before bind() on server sockets. Without it, if your server crashes and restarts, bind() fails with 'Address already in use' because the old socket is in TIME_WAIT. setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) before bind() fixes this.

5.2 Client Side - 2 Syscalls

socket() → connect()

connect() - Initiate connection to server

5.3 Server vs Client Syscall Comparison

Syscall Server Client What it does
socket() Yes - first call Yes - first call Create socket fd
bind() Yes - assign port No - OS assigns ephemeral port automatically Attach address to socket
listen() Yes - mark as passive No Enable incoming connections, set queue size
accept() Yes - get connections No Block until client connects, return connfd
connect() No Yes - initiate connection Trigger 3-way handshake, block until done

5.4 getaddrinfo() - Modern Address Lookup

Instead of manually filling sockaddr_in structs, modern code uses getaddrinfo() - it resolves hostnames ("www.google.com") to IP addresses, handles IPv4/IPv6 transparently, and returns a linked list of possible addresses to try.

REAL WORLD getaddrinfo() is what Go's net.Dial() and Rust's TcpStream::connect() use internally when you pass a hostname. It handles DNS resolution transparently. The linked list of results matters: a hostname may resolve to multiple IPs (for redundancy/load balancing) - trying each one is called happy eyeballs.

6. The accept() Lifecycle - Full Kernel Picture

KEY INSIGHT The client's connect() returns BEFORE the server's accept() returns. The client can start sending data immediately after connect() returns - data sits in the kernel's receive buffer until the server calls read(). The server's application code is not involved in the handshake at all.

7. Message Framing - Solving the Byte Stream Problem

TCP has NO message boundaries. Two writes may arrive as one read, or one write may arrive as multiple reads. Every TCP protocol must explicitly solve the framing problem.

7.1 Solution 1 - Fixed-Length Messages

7.2 Solution 2 - Delimiter-Based Framing

7.3 Solution 3 - Length-Prefixed Framing

7.4 How Real Protocols Frame Messages

Protocol Framing Approach Details
HTTP/1.1 Delimiter + Content-Length Headers delimited by \r\n, blank line ends headers, Content-Length header specifies body size
HTTP/2 Length-prefixed binary frames 9-byte frame header (3 bytes length + 1 type + 1 flags + 4 stream ID)
gRPC 5-byte header + body 1 byte compression flag + 4 bytes length, then body
Redis (RESP) Delimiter-based *2\r\n\$5\r\nHELLO\r\n - type prefix + length + data + \r\n
WebSocket Length-prefixed binary frames 2-byte header minimum, extended for large payloads
PostgreSQL Length-prefixed 1-byte message type + 4-byte length + body
PRODUCTION BUG Treating TCP like it preserves message boundaries (one write = one read) is one of the most common and hardest-to-debug networking bugs. It works perfectly on localhost (data almost always arrives in one chunk on loopback) and breaks randomly in production when network conditions split messages. Always implement explicit framing.

8. Concurrent Servers - Handling Multiple Clients

8.1 The Iterative Server Problem

8.2 Fork-Per-Connection - Process-Based Concurrency

Why each close is mandatory:

  • Child closes listenfd: child will never call accept(). Holding listenfd open wastes an fd slot and inflates the open file table's ref count. If enough children accumulate, the listening socket's resources are never freed.
  • Parent closes connfd: parent won't talk to this client. If parent doesn't close connfd, the connection's ref count stays at 2. Even after child finishes (ref count→1), TCP connection CANNOT close - no FIN is sent to client - because parent still holds a reference. Connection hangs open indefinitely, leaking kernel resources.
FD LEAK PATTERN A server that forks but doesn't close unused inherited fds accumulates one leaked fd per connection. After thousands of connections, the server hits the fd limit (EMFILE) and can no longer accept new connections. lsof -p <pid> reveals the accumulated fds. Always close every fd you inherit but don't need.

8.3 Limitations of Fork-Per-Connection

Limitation Why it matters At what scale it breaks
fork() overhead Creating a process takes time - PCB allocation, page table copy, kernel setup ~1000+ connections/second
Memory per process Each process has its own address space - even with COW, metadata is duplicated ~10,000+ concurrent clients
IPC complexity Processes can't share variables - need pipes/shared memory for shared state Any shared state (cache, rate limiting, connection counts)
Zombie accumulation Every dead child needs reaping - requires SIGCHLD handler calling waitpid() Busy server with no reaping
C10K problem Can't sustain 10,000+ simultaneous connections with one process per connection 10,000+ concurrent connections

8.4 The Evolution of Server Concurrency Models

REAL WORLD The C10K problem (handling 10,000 simultaneous connections) drove the invention of epoll (Linux) and kqueue (BSD/macOS) in the early 2000s. nginx was specifically designed around epoll to solve C10K. Today Go's goroutines and Rust's Tokio are the standard answers - both abstract over epoll under the hood.

9. HTTP - Application Layer Protocol

9.1 HTTP Request Format

9.2 HTTP Response Format

9.3 HTTP Status Codes

Code Meaning Common cause
200 OK Success Request processed successfully
301 Moved Permanently Permanent redirect URL has moved, update bookmarks
302 Found Temporary redirect URL temporarily at different location
400 Bad Request Malformed request Client sent invalid HTTP
401 Unauthorized Authentication required No/invalid credentials
403 Forbidden Permission denied Valid credentials but no access
404 Not Found Resource doesn't exist Wrong URL or deleted resource
500 Internal Server Error Server-side bug Unhandled exception in server code
502 Bad Gateway Upstream error Reverse proxy couldn't reach backend
503 Service Unavailable Server overloaded/down Too many requests or maintenance

9.4 HTTP Framing - How It Solves the Byte Stream Problem

HTTP/1.1 uses a combination of delimiter and length-prefix framing:

  • Headers: delimited by \r\n. Each header line ends with \r\n. The header block ends with \r\n\r\n (blank line). Read line by line until blank line.
  • Body: length specified by Content-Length header. Read exactly that many bytes after the blank line. For streaming: Transfer-Encoding: chunked uses its own framing within the body.

9.5 Static vs Dynamic Content

Type What the server does Example URLs Modern equivalent
Static content Finds file on disk, sends it directly. No computation. stat() → open() → send bytes. /index.html, /style.css, /logo.png CDN serving files, nginx serving static assets
Dynamic content Forks a child, execs a CGI program, connects its stdout to the socket. Program generates response. /cgi-bin/search?q=foo, /cgi-bin/login Web frameworks (Django, Express, Rails), API servers

11. Quick Reference - Things to Remember Cold

Addressing

  • IP address: identifies the machine
  • Port: identifies the program (0-1023 privileged, 49152-65535 ephemeral)
  • Four-tuple: (client IP:port, server IP:port) uniquely identifies every TCP connection
  • Socket address: IP + port combined. Store in sockaddr_in struct.
  • Byte order: always htons()/htonl() when putting values into sockaddr structs, ntohs()/ntohl() when reading them out

TCP guarantees (and non-guarantees)

  • Guarantees: reliable delivery, ordered bytes, flow control, congestion control
  • Does NOT guarantee: message boundaries. TCP is a byte stream. Always frame messages explicitly.
  • UDP: no reliability, no ordering, but preserves message boundaries and is faster

Server syscall order

  • socket() → create fd
  • bind() → assign IP:port to socket
  • listen() → mark passive, set backlog queue size
  • accept() → block until client connects, return connfd
  • read(connfd) / write(connfd) → communicate with client
  • close(connfd) → close connection

Client syscall order

  • socket() → create fd
  • connect() → trigger 3-way handshake, block until done
  • read/write → communicate

Three-way handshake one-liner

  • SYN: client starts, sends sequence number x
  • SYN-ACK: server acknowledges x, sends own sequence number y
  • ACK: client acknowledges y. Connection open. Kernel handles all of this - your process is asleep in accept().

Message framing - three approaches

  • Fixed length: always read/write exactly N bytes. Simple, inflexible.
  • Delimiter: read until \n or \r\n. Used by HTTP headers, Redis, SMTP.
  • Length-prefix: send 4-byte length then body. Used by gRPC, PostgreSQL, most binary protocols.

Fork-per-connection rules

  • Child: always close(listenfd) immediately after fork
  • Parent: always close(connfd) immediately after fork
  • Why: open file table ref counts - connection won't fully close until ALL references are closed

HTTP status codes cold

  • 200 = OK, 301 = permanent redirect, 302 = temp redirect
  • 400 = bad request, 401 = unauthorized, 403 = forbidden, 404 = not found
  • 500 = server error, 502 = bad gateway, 503 = unavailable

CSAPP Ch 11 Reference • Network Programming

Top comments (0)