Module 8: Sockets & Networking

Network code that actually works

Lessons

IDTitleDuration
8-1TCP/IP and the Socket API30 min
8-2Handling Multiple Clients — select and poll30 min
8-3Non-Blocking I/O and Timeouts25 min
8-4HTTP/1.1 as Text over TCP30 min
8-5MIME types, Content-Length, and Serving Files20 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_REUSEADDR in 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.c acceptTcpHandler() — 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)