Everything in this module has been building to a single, satisfying payoff: you now know enough to build a web server from raw sockets — the thing that sits behind every URL you have ever visited. And the wonderful secret, once you strip away the frameworks, is how small the core idea is. An HTTP server accepts a TCP connection, reads some text off it, figures out what the client asked for, writes some text back, and decides whether to keep the line open. That's it. HTTP is, at heart, a text protocol over a byte stream — and you have already met every hard part.
This is the capstone. We will assemble the pieces you now hold — the
HTTP's genius is that a request and a response have the same three-part shape, all in plain
text with \r\n (carriage-return + line-feed) line endings:
GET /hello?name=alice HTTP/1.1 — method,
target, version. For a response: HTTP/1.1 200 OK — version, status code, reason phrase.
Name: value per line
(Host: example.com, Content-Type: text/plain, Content-Length: 12).
\r\n\r\n) — and then, optionally, the
body.
Look at how HTTP frames itself, because it is the exact combination from the framing page. The
header block is delimiter-framed: it ends at the first blank line
(\r\n\r\n). The body is length-prefixed: the Content-Length
header tells you exactly how many body bytes follow. So parsing an HTTP message means: read until you
see the blank line (you now have all the headers), read Content-Length, then read exactly
that many more bytes for the body. Both framing strategies, in one message.
Here is the whole loop a server runs per connection. Trace it once and every web server you ever touch is a variation on it.
Two subtleties, both familiar. Reading a whole request is a
read() may deliver half a request or several glued together, so you buffer
and re-check. Keep-alive means one TCP connection carries many requests in sequence
(HTTP/1.1's default), so after writing a response you loop back rather than closing — saving a handshake
per request.
Now the payoff — a genuinely working HTTP engine. It parses a raw request string exactly as it
arrives on the wire (with real \r\n line endings), splits headers from body at the blank
line, routes on method + path, and produces a correct raw response string. Pure string logic — no
sockets — so it runs right here.
Read the output as bytes: each response is a status line, some headers, a blank line
(\r\n\r\n), then the body — and Content-Length tells the client exactly how
many body bytes to expect, so it knows when the response ends without closing the connection. That is a
real, if tiny, web server.
A real server wraps that engine in a concurrency model. The
But the engine above is naïve, and a server exposed to the open internet meets input that is not merely well-formed test data. This is where beginners' servers get exploited or fall over.
A production HTTP server faces malformed, partial, and hostile requests as a matter of routine, and the parser above handles none of them safely. The three classic traps:
Content-Length blindly. A hostile client can claim
Content-Length: 999999999 and dribble the body one byte per second, tying up a
connection forever (a "Slowloris" attack), or lie about the length to smuggle a second request past
a proxy (request smuggling). Real servers cap the body size, time
out slow reads, and reconcile Content-Length with Transfer-Encoding
strictly.
\r\n\r\n that separates headers from
body is the frame boundary. Miss it and you either read body bytes as headers or hang
forever waiting for a request you have already fully received. A request that never sends the blank
line must eventually be timed out, not awaited indefinitely.
\r\n, an attacker can inject their own headers or
split your response in two (HTTP response splitting / header injection). Never echo
untrusted bytes into the header block unescaped.
The one-line moral of the whole module: a byte stream from the network is adversarial input. Frame it carefully, bound it, time it out, and validate every field before you believe it.
HTTP/1.1 is gloriously human-readable — you can literally telnet to port 80, type
GET / HTTP/1.1, and read the reply. That debuggability is a big reason it conquered the
world. But text has costs (parsing ambiguity, header repetition, head-of-line blocking), so
HTTP/2 re-encoded the very same semantics as a binary, multiplexed,
header-compressed framing — and HTTP/3 moved it all onto