Module 8: Sockets & Networking
Network code that actually works
Lessons
| ID | Title | Duration |
|---|---|---|
| 8-1 | TCP/IP and the Socket API | 30 min |
| 8-2 | Handling Multiple Clients — select and poll | 30 min |
| 8-3 | Non-Blocking I/O and Timeouts | 25 min |
| 8-4 | HTTP/1.1 as Text over TCP | 30 min |
| 8-5 | MIME types, Content-Length, and Serving Files | 20 min |
Project
| 8-proj | Project: HTTP/1.1 Static File Server | 90 min |
Key Concepts
sockets, select-poll-epoll, tcp-state-machine
Pedagogical Notes
What this module is really teaching: The socket API is not a networking API
— it is a file-descriptor API applied to network connections. Every operation
maps to something students have seen before: socket() is like open(),
send()/recv() are like write()/read(), and close() is close().
The novelty is the three-step server setup (bind/listen/accept) and the
fact that both ends of a stream can be writing simultaneously.
The central trap: forgetting that TCP is a byte stream, not a message stream.
Students assume that one send() corresponds to one recv(). It doesn’t. TCP
may coalesce or split your writes arbitrarily. The recv() on the other end
may get half your message, or two messages joined together. The correct
abstraction is a framing protocol: either prefix every message with its length
(length-prefixed), or use a delimiter (\r\n in HTTP). Students who don’t
internalize this write servers that work in testing (localhost, fast, never
congested) but fail on real networks.
The blocking vs. non-blocking trap. A blocking recv() in the middle of a
select() loop defeats the purpose — it stalls all other connections while
waiting for one. Students who understand select() conceptually still
sometimes call recv() without checking it’s actually ready, or set
O_NONBLOCK but don’t handle EAGAIN. The rule: in a select() loop,
call recv()/send() only on FDs that select() indicated are ready; with
O_NONBLOCK set, always handle EAGAIN as “try again later, not an error”.
SO_REUSEADDR and TIME_WAIT. Students restart their server after a
crash and hit “Address already in use”. This is TIME_WAIT — the kernel holds
the port busy for ~60 seconds to absorb delayed packets. SO_REUSEADDR
bypasses this for the listen socket. Students who don’t add it first believe
their server is broken; adding it is a one-line fix that should be reflexive.
SIGPIPE kills the server silently. When a client closes the connection and
the server writes to it, the kernel delivers SIGPIPE. The default action is
process termination with no error message — an extremely confusing production
failure. The fix (signal(SIGPIPE, SIG_IGN) at startup, or MSG_NOSIGNAL
on each send()) should be taught as unconditional practice for all servers.
Key Code Examples
The server setup sequence (must be memorized):
int server = socket(AF_INET, SOCK_STREAM, 0);
int yes = 1;
setsockopt(server, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes)); /* always */
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(PORT),
.sin_addr.s_addr = htonl(INADDR_ANY),
};
bind(server, (struct sockaddr *)&addr, sizeof(addr));
listen(server, 16);
/* Now accept() connections */select() loop — the core pattern (fd_set rebuilt each iteration):
while (1) {
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(server, &rfds);
int maxfd = server;
for (int i = 0; i < n; i++) {
FD_SET(clients[i], &rfds);
if (clients[i] > maxfd) maxfd = clients[i];
}
select(maxfd + 1, &rfds, NULL, NULL, NULL); /* blocks */
if (FD_ISSET(server, &rfds)) {
int c = accept(server, NULL, NULL);
clients[n++] = c;
}
for (int i = 0; i < n; i++) {
if (!FD_ISSET(clients[i], &rfds)) continue;
char buf[1024];
ssize_t r = recv(clients[i], buf, sizeof(buf), 0);
if (r <= 0) { close(clients[i]); clients[i] = clients[--n]; }
else send(clients[i], buf, r, MSG_NOSIGNAL);
}
}errno == EAGAIN on a non-blocking recv — not an error:
ssize_t n = recv(fd, buf, sizeof(buf), 0);
if (n < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK)
return 0; /* no data yet — try again later */
perror("recv"); /* real error */
return -1;
}Cross-Module Dependencies
- Requires m06-file-io-syscalls (m6): Sockets are file descriptors.
The three-table model (per-process fd table → open file table → inode/socket
struct) applies directly.
close(),dup2(),fcntl()all work on socket FDs exactly as on file FDs. - Requires m07-processes-signals (m7): SIGPIPE handling,
SO_REUSEADDRin the context of TIME_WAIT, and fork-per-client servers all draw on signal and process model knowledge from m7. The accept queue model (SYN queue + accept queue) connects to the process scheduling model. - Informs m09-concurrency (m9): The select()/poll()/epoll progression is the event-driven alternative to threading. epoll is the foundation of io_uring and all modern async I/O. The tension between event-driven and threaded architectures is the central theme of m9.
PDF Status
- PDF ch09 (Sockets and Networking) — Complete (written 2026-04-09)
- Key Concepts: 8-row table (socket, bind/listen/accept, connect, send/recv, select/poll, TCP state machine, SO_REUSEADDR, non-blocking I/O) + TCP vs UDP callout
- Going Deeper: socket FD and kernel buffers + Nagle’s algorithm; accept() queue model (SYN queue vs accept queue, backlog, thundering herd); select() internals (fd_set bitmask, FD_SETSIZE, O(n) scan, poll vs epoll); HTTP as a protocol (CRLF framing, Content-Length, keep-alive, why parsing is hard)
- Real-World Connection: Redis
src/networking.cacceptTcpHandler() — how the ae event loop wires a listening socket to a read handler that calls accept() and registers readQueryFromClient - Solutions: 9.1 (iterative echo server with SIGPIPE suppression), 9.2 (select() multi-client with swap-remove), 9.3 (HTTP/1.0 GET server with path sanitisation and correct Content-Length), 9.4 (fork-based Nagle measurement with TCP_NODELAY comparison)
- Extended Project: HTTP/1.1 server with keep-alive, pipelining, connection timeouts, and graceful SIGINT shutdown using select()
- Test files: tests/ch09/ex9_1–ex9_4 (all compile clean under -Wall -Wextra -Werror -std=c11)