编程与开发 · 第 10 期

自己写个 Web 服务器,socket 到 HTTP

from socket() to HTTP parser

你每天访问的网页,底层就是一个 socket 上的字节流解析游戏。socket() 建管道、bind() 占端口、listen() 开门、accept() 接客;HTTP 请求就是按 \r\n 切字节流。理解这条链路,Web 服务器就不神秘了。
⏱ 约 11 分钟 🎯 用框架但不懂底层的人 📦 源:build-your-own-x §7

01一个反共识:HTTP 不是协议,是 socket 上的"切字节"约定

很多人以为 HTTP 是个高深协议。其实它就是一段约定:在 socket 字节流里,第一行是"方法 路径 版本",后面跟着头,空一行,再是体。仅此而已。

socket 字节流 → 按 \r\n 切 → 请求行 + 头 + 体

socket 给你的是一串无结构的字节,TCP 不保证一次 recv() 收到完整请求——可能收到半个头,也可能一次收到三个请求。所以 HTTP 解析器本质是个状态机:边收边解析,收够了就处理,没收够就等。这就是为什么 Nginx、Express 都有"请求未完整"的中间态——它们不是在等协议,是在等字节到齐。

从零写一遍这个解析器(按 CRLF 切、处理半包、处理 keep-alive 多请求复用),你会突然理解所有 Web 框架的"中间件"——它们就是切完字节、组装成请求对象后,挂在请求对象上的一串处理函数。

HTTP 不是协议,
是 socket 上的切字节约定。
灏天文库 · 编程与开发 P.30

02请求响应时序演示:点请求看 socket/HTTP 各阶段

点下面的请求,看它从"TCP 握手"到"响应发出"各阶段耗时。注意 socket 阶段(握手+收字节)和 HTTP 阶段(解析+路由+处理)是两段不同的时间——很多人调优调错了地方。

🌐 请求响应时序演示
简化模型:TCP 握手 → 收请求字节 → 解析 HTTP → 路由匹配 → 业务处理 → 序列化响应 → 发送。点请求看各阶段。
← 点上面的请求看时序

03四步开张:socket 服务器最小骨架

剥掉所有框架,一个 Web 服务器就这么几步:

1. s = socket() // 建管道
2. s.bind((':8080')) // 占端口
3. s.listen(128) // 开门,128=排队数
4. while True:
    conn, addr = s.accept() // 接一个客
    data = conn.recv(4096) // 收字节
    req = parse_http(data) // 切字节成请求
    resp = handle(req) // 业务
    conn.send(serialize(resp)) // 回字节

关键认知:第 4 步的 while 循环是单线程的——一次只能处理一个客。这就是为什么真实服务器要上多线程/多进程/IO 多路复用(epoll/select)。Nginx 用 epoll 一个线程管几万连接,靠的就是"哪个 socket 有字节了就处理哪个",而不是傻等一个连接。从单线程写起,再亲眼看到它被一个慢请求卡死,你才会真正理解为什么需要 epoll。

写单线程服务器被卡死一次,
才懂epoll 为什么伟大。
灏天文库 · 编程与开发 P.31

04调优的真相:80% 的慢,不在你写的代码里

从时序演示能看出:一个请求的总耗时里,TCP 握手、TLS 握手、收字节、序列化响应这些"非业务"时间往往占大头。很多人调优盯着业务代码改,却忽略了:

所以"我的接口慢"的第一步不是看业务代码,是看时序图——是握手慢、收字节慢、还是处理慢。没有时序图就调优,等于蒙眼开车。这也是为什么可观测性(tracing)是现代后端的标配:它把"慢"定位到具体阶段。

05带走这套清单

✅ Web 服务器 6 条可执行规则

  1. HTTP 是 socket 上按 \r\n 切字节的约定,不是高深协议。
  2. 解析器是状态机:边收边解析,处理半包和 keep-alive 多请求。
  3. 从单线程写起,被慢请求卡死一次,才懂为什么需要 epoll。
  4. 框架的"中间件"= 切完字节组装成请求后挂上的一串处理函数。
  5. 调优先看时序图:握手/收字节/处理/发送,哪段慢调哪段。
  6. keep-alive + HTTP/2 + gzip 是性价比最高的三件套,先上再谈别的。
造过 Web 服务器,
框架就不黑盒了。
灏天文库 · 编程与开发 P.32