The problem
Build a production-grade HTTP/1.1 server from scratch — no third-party libraries, no Node.js, no Python. The goal: understand HTTP, sockets, CGI, and concurrent I/O at the system level.
What I built
An HTTP/1.1-compliant server written in C++98 that:
- Parses raw HTTP requests by hand (request line, headers, body, chunked transfer)
- Handles 50+ concurrent connections via non-blocking sockets and a single-threaded event loop
- Uses
poll()(orepoll/kqueuedepending on platform) for I/O multiplexing - Supports CGI for executing scripts (PHP, Python) per RFC 3875
- Reads NGINX-style configuration files (server blocks, location blocks, error pages, autoindex)
- Implements GET, POST, DELETE methods with file uploads
Architecture decisions
- Incremental request parsing — requests are parsed by a byte-at-a-time state machine (request line → headers → body). Each
poll()wakeup feeds available bytes into the current state, and the parser only advances once that state's terminator is found — so a request split across many reads never blocks the loop. - CGI process spawning — each CGI request forks and
execves the script, withpipe()pairs wired to the child's stdin/stdout and added to the samepoll()set as regular sockets. A slow CGI script never blocks other connections; the parent just treats the pipe fd like any other multiplexed I/O source. - Configuration parser — a hand-written tokenizer feeding a recursive-descent parser, mirroring NGINX's block structure (server → location → directives). A generic key-value format wouldn't capture the nesting, or location blocks inheriting from their parent server block.
- Memory model — strict RAII via small wrapper classes for fds and buffers, since smart pointers weren't available pre-C++11. Every resource-owning object frees in its destructor, so an exception or early return can't leak an fd — at the cost of more wrapper boilerplate than C++11 would need.
What I'd do differently
- Move to
epoll/kqueuefrom the start instead of retrofitting it later —poll()'s O(n) fd scan showed up under load testing well before 50 connections. - Separate CGI timeout/cleanup logic from the main event loop earlier — it ended up tangled with regular request handling and was the hardest part to debug.
Tech stack
C++98 · POSIX sockets · poll / epoll · CGI (RFC 3875) · HTTP/1.1 (RFC 7230)