Skip to content
IMRAN RAHMAN::SWE
Menu
← All projects

Cerveur

A custom web server written in C — a UNIX-socket HTTP wrapper with static file serving and a BST-based routing system. 400+ stars on Github.

  • C
  • Sockets
  • HTTP
View on Github →

What it is

Cerveur is an HTTP server built from scratch in C, on top of the raw UNIX socket API. No framework underneath — just accepting connections, parsing requests, and writing responses directly.

It handles static file serving and routes requests through a custom binary search tree based router.

Why build another web server

Nobody needs another HTTP server. Every real project I ship uses Django or Spring or something that already handles this. But “the framework handles that” is exactly the kind of sentence that stops meaning anything once you’ve never had to do the thing yourself. Cerveur was an attempt to replace that sentence with an actual mental model — accept a connection, read some bytes, parse them, write some bytes back, close the connection.

The shape of it

Cerveur is small on purpose — four source files, under 250 lines total. Three pieces do all the work:

  • HTTP_Server — opens a TCP socket, binds it, and starts listening.
  • Routes — a binary search tree mapping a URL path to a template file.
  • Response — reads a static file off disk into memory. main.c wires those three together in a loop.

Request lifecycle

 Client                         Server
 ──────                         ──────
 
┌──────────┐   connect    ┌───────────────┐
│ Browser  │ ───────────▶ │   accept()    │
└──────────┘               └───────┬───────┘
                                    │
                                    ▼
                            ┌───────────────┐
                            │ read() + parse│
                            │ method + path │
                            └───────┬───────┘
                                    │
                                    ▼
                            ┌───────────────┐
                            │  BST search   │
                            │ path → file   │
                            └───────┬───────┘
                                    │
                                    ▼
                            ┌───────────────┐
                            │  read file    │
                            │ into memory   │
                            └───────┬───────┘
                                    │
                                    ▼
┌──────────┐   response    ┌───────────────┐
│ Browser  │ ◀──────────── │ send() + close│
└──────────┘               └───────────────┘
  1. The server calls accept() and blocks until a client connects. There’s no thread pool or event loop behind it just yet. One connection is handled fully, start to finish, before the next one is even accepted. That’s about as far from “production-ready” as a server can get, and at the time of building this, that was fine because the point was seeing every step, not making it scale.

  2. The first line of the request gets read into a fixed buffer and split with strtok to pull out the method and the path. No header parsing beyond that, no body handling, just enough to know what was actually asked for.

  3. The path gets looked up in the route BST. I’d just learned BSTs properly around the time I built this and wanted an actual use case for one instead of a textbook exercise. A router is a genuinely reasonable place to put one, since paths compare and order the same way any other string key would.

  4. Whatever file that lookup resolves to gets read fully into memory and wrapped in a hardcoded HTTP/1.1 200 OK header.

  5. The response gets sent back over the socket and the connection is closed.

What I’d fix first

Cerveur isn’t a security case study or a triumph of edge-case handling, and going back through the source now, a few things stand out. The response is always 200 OK, even when the route lookup misses and it’s actually serving 404.html. There’s a hardcoded shortcut for /static/ that only ever serves index.css, no matter what was actually requested under that path. And with no concurrency, a slow client blocks everyone behind it.

None of that was a surprise while writing it — it’s what “minimal, just to understand it” actually looks like once the scaffolding of a real framework is gone. If I picked it back up, concurrency is what I’d tackle first: even a simple thread-per-connection version would change the shape of the whole accept loop.

Takeaway

The value was never in the finished server. It was in every “the framework handles that” sentence turning into something I could actually trace, line by line, the next time something in a real service behaved unexpectedly.