Chapter 10 — step9 HTTP 服务

对应实践course/practice/labs/lab10-step9/
主要修改文件course/practice/labs/lab10-step9/framework/student.c
验证命令make clean && make test

到上一章为止,课程已经把“多轮对话”里的字符串协议拆清楚了。
你已经知道 prompt 怎样组织,回复又该怎样从模型输出里截出来。

但只停在命令行程序里,还谈不上真正的服务化。
一旦你想让前端、脚本或别的程序来调这个模型,就需要把 chat 能力封装成一个可通信的接口。

在当前课程阶段,这个接口最自然的形态就是 HTTP。

需要特别说明的是:
这一章并不要求学员自己从头写 socket 服务器。课程把范围压得很小,只让你实现 HTTP 协议层两个最小积木:

  • 解析请求行;
  • 组装响应帧。

也就是说,Chapter 10 的核心问题不是“网络编程有多复杂”,而是:

当一个模型能力要被外部程序调用时,最基础的请求/响应协议长什么样?

10.1 为什么服务化首先是协议问题

初学者第一次接触“把模型做成服务”时,很容易把注意力都放在端口、线程、监听循环上。
这些当然都重要,但它们还不是服务化的第一层边界。

第一层边界其实是协议:

  • 客户端发来什么文本;
  • 服务端如何把它拆成 method / path / body;
  • 服务端回去的内容该怎样拼成合法 HTTP 响应。

如果这一层都没弄清楚,那么就算 socket 已经连上,客户端也不知道你在说什么。

所以本章故意把实践缩到两道协议题里,是为了让学员先掌握:

  1. 请求帧的最小结构;
  2. 响应帧的最小结构。

10.2 本章你要建立哪些判断

本章结束后,你应当能够清楚说出:

  1. 一个 HTTP 请求行为什么通常长成 METHOD PATH HTTP/1.1
  2. 为什么 headers 和 body 之间必须有一个空行。
  3. Content-Length 为什么是必须的,而不是“可选信息”。
  4. 为什么响应行的 \r\n 不是普通换行细节,而是协议要求。
  5. 为什么 OpenAI 兼容接口的价值,本质上是“客户端和服务端共享了一套 JSON 契约”。

这些判断一旦建立起来,后面真正去跑 curl 或前端调用时,你就不会只把服务端当黑盒。

10.3 先看 practice target:这章改哪里

本章实践目录是:

course/practice/labs/lab10-step9/
├── TASK.md
├── Makefile
└── framework/
    ├── student.c      <- 主要修改这里
    ├── student.h
    ├── verify.c       <- 自动验证,不改
    └── verify.h

当前需要你实现的函数有两个:

  • student_http_parse_request_line
  • student_http_build_response

这个切法非常合理,因为它刚好对应 HTTP 协议里最基础的两个动作:

  1. 从客户端来的第一行里拆出“请求意图”;
  2. 把服务端结果重新打包成客户端能理解的响应文本。

10.4 为什么请求行解析是“结构恢复”

请求行看起来只是一行字符串,例如:

GET /health HTTP/1.1

但对服务器来说,它不是普通文本,而是一段被压平的结构数据。
解析请求行做的事情,本质上是在把这段压平字符串重新恢复成三个字段:

  • method
  • path
  • version

这跟前一章“从模型原始输出里截出 assistant 回复”的思路其实非常像。
两者都是在做一件事:从一段线性文本里恢复协议结构。

10.5 为什么响应构建是“主动声明边界”

响应构建和请求解析正好反过来。

这里服务器不是在恢复结构,而是在主动声明结构。
你要明确告诉客户端:

  1. 这是 HTTP/1.1 响应;
  2. 状态码是多少;
  3. 内容类型是什么;
  4. body 有多长;
  5. headers 到哪里结束,body 从哪里开始。

这里最不能忽略的就是 Content-Length
因为 HTTP 是字节流协议,客户端如果不知道 body 的长度,就无法稳妥判断响应到底什么时候结束。

所以本章最应该建立的不是“能拼出一串文本”,而是:

一次合法响应,必须把自己的边界写清楚。

10.6 为什么课程现在只做协议骨架,不让你写完整服务

如果在这一章同时让学员:

  • 管 socket;
  • 管请求解析;
  • 管 JSON;
  • 管模型调用;
  • 管并发或阻塞行为;

那教学负担会一下子跳得非常高。

课程当前的选择更务实:
先把“协议骨架”单独拿出来练。

因为只要请求行解析和响应组装是清楚的,后面的完整服务化路径就已经有了坚实基础。
反过来,如果这两层还混乱,直接跑完整 server 只会让错误来源更加难找。

10.7 本章实践步骤

task 10.1:先读 student.cverify.c

进入:

cd course/practice/labs/lab10-step9

建议先读:

  • framework/student.h
  • framework/student.c
  • framework/verify.c

当前验证器主要检查:

  1. GET /health HTTP/1.1 能否被正确拆成三段;
  2. POST /v1/chat/completions HTTP/1.1 能否被正确拆成三段;
  3. 非法请求行是否会被拒绝;
  4. 200 OK 响应是否包含正确的 header 与 body;
  5. 404 Not Found 响应是否仍然是合法 HTTP 帧。

task 10.2:实现 student_http_parse_request_line

这一步建议你始终记住目标不是“做复杂解析器”,而是从最小协议格式里拿出三段字段。

实现时最值得关注的是:

  1. 找两个空格;
  2. 段长不要越界;
  3. 任何结构异常都应该返回失败,而不是悄悄截断。

这是一个很典型的协议解析习惯:
宁可明确报错,也不要默默吞掉坏输入。

task 10.3:实现 student_http_build_response

这一题最值得在脑子里保持一张“响应骨架图”:

status line
headers
空行
body

只要这张骨架没有乱,你再逐项补:

  • Content-Type
  • Content-Length
  • Connection: close

代码就不容易写偏。

task 10.4:运行当前真实基线

在还没补完两个函数前,先执行:

make clean && make test

当前 Lab10 的真实初始状态是:

  • 1 通过,4 失败

已经通过的那一项通常只是非法请求行失败这类保底路径。
真正与协议骨架直接相关的部分,例如:

  • 请求行拆分;
  • 200/404 响应组装;

目前都还没有完成。

这说明当前 lab 已经能运行,但 HTTP 协议层的核心责任仍然留给你自己补完。

task 10.5:完成后重新验证

当你补完两个函数后,再运行:

make clean && make test

如果实现正确,你应当看到:

  • 常见请求行都能被稳定解析;
  • 非法输入被拒绝;
  • 响应文本包含合法头部、空行和 body。

到这里,chat 系统就已经具备了向外暴露接口的最小协议层。

10.8 常见错误与排查顺序

最常见的错误通常是:

  1. 请求行没有正确按两个空格切开;
  2. 缓冲区长度检查不严;
  3. 响应行用了 \n 而不是 \r\n
  4. Content-Length 算错;
  5. headers 和 body 之间忘了空行。

建议排查顺序是:

  1. 先看请求行三段是否拆对;
  2. 再看响应状态行;
  3. 最后看 header/body 边界是否正确。

10.9 思考题

  1. 为什么 Content-Length 对 HTTP/1.1 响应如此关键?
  2. 为什么协议里要求 \r\n,而不是普通的 \n
  3. 为什么说 OpenAI 兼容接口的价值,本质上是一种共享协议而不是某个具体模型实现?

10.10 本章小结

Chapter 10 把课程从“命令行里的模型能力”推进到了“可被外部程序调用的模型能力”。

更重要的是,它让学员第一次明确看到:
服务化的第一层不是 socket 技巧,而是协议边界。

下一章继续往前走时,这层边界就会变得非常有用。因为一旦模型开始被频繁调用,推理效率问题就会立刻浮现出来,而 KV cache 正是为那个问题准备的。