第 16 章 / 共 51 章
卷三 · 协议开发
请求体:定长与 chunked 的增量读取
请求体不是一块等你取用的内存,而是一个流。本章讲 xrtHttp1BodyRead 的状态机用法——并兑现第 4 章留下的 /echo 挑战。
本章导读
站位:POST/PUT 业务的入口。读完你能安全读取任意编码的请求体,并知道超限、畸形、慢速三种敌意输入各自的防线在哪。
16.1 引入
请求体有三种到达方式:定长(Content-Length)、分块(chunked)、以及连接关闭即结束(无长度,罕见且危险)。它们共享同一个读取接口——pReq->body 状态机 + xrtHttp1BodyRead。你不关心编码:状态机把三种模式统一成"读到多少、还有没有"的问答。
16.2 分帧状态机
xhttp1body · 分帧状态机(xsbase.h)
模式:NONE / FIXED(定长)/ CHUNKED / CLOSE / TUNNEL
Read(pBody, buf, cap)
│
├── 返回 > 0 本轮读到的字节数(可能少于请求量)
├── 返回 0 本轮没有新数据(稍后再试 / 等事件)
└── 错误码 畸形 chunk 行 / 冲突的长度声明等
完成判定:累计 == 声明长度(FIXED)或收到终止 chunk(CHUNKED)
trailer:chunked 末尾最多 8 个字段,经状态机取出
关键认知:HTTP/1.1 解析器会拒绝冲突的长度声明(同时出现 Content-Length 与 Transfer-Encoding 是协议错误)——35 个细粒度错误码里的一类,畸形输入在进入你的业务前就被挡下。
16.3 读取循环模式
完整读取循环(demo 模式)实机验证出处:demo-single/modules/http.h(模式节选)
/* 把请求体完整读入动态缓冲(小体量场景)。
* 大体量应边读边处理,见练习挑战。 */
static bool ReadBodyAll(XS_HttpReq* pReq, xbuffer** ppOut)
{
xbuffer* pBuf = xrtBufferCreate(...);
char arrChunk[8192];
for ( ;; ) {
xhttp1bodystatus st = xrtHttp1BodyRead(
pReq->body, arrChunk, sizeof(arrChunk), &iGot);
if ( st == XHTTP1_BODY_ERROR ) {
xrtBufferDestroy(pBuf);
return false; /* 畸形输入 */
}
if ( iGot > 0 ) xrtBufferAppend(pBuf, arrChunk, iGot);
if ( st == XHTTP1_BODY_DONE ) break;
if ( iGot == 0 ) {
/* 本轮无新数据:真实代码按事件再入此回调 */
}
}
*ppOut = pBuf;
return true;
}
刚才发生了什么:读取是增量的——每次调用给你"现在有的",数据没到齐就继续等事件(同一个连接的后续驱动循环里再进回调)。这就是为什么大文件上传不需要把整块 body 憋进内存:拿到一块处理一块。
现在可以兑现第 4 章的 /echo 了:POST 读完全部 body → 用第 15 章的 ReplyRaw 原样返回(Content-Type 回显请求的)。
16.4 完整性探针与 413
- body_limit(第 9 章 Custom 旋钮):超限 413——防线在驱动层,你的回调根本不会被叫到
- 完整性探针:驱动提供"不消费传输缓冲"的检查——判断 body 是否已完整到达,避免读一半就响应
- 畸形 chunked:坏 chunk 行、非法 trailer、超长 chunk 行都是解析器错误码,连接按 400 收口
- 慢速攻击:idle_timeout 是时间防线;逐字节喂 body 的连接到点被关
16.5 契约
- 读取线:body 只能通过 BodyRead 消费;你消费的字节数会被驱动与线路核对,越界即断连。
- 完成线:返回 XS_OK 前要么读完 body、要么明确交回驱动排空——半读状态返回 XS_OK 会破坏 keep-alive。
- 内存线:大 body 边读边处理,不要默认 ReadBodyAll。
常见错误
没读完 body 就返回 XS_OK
- 症状
- keep-alive 下一个请求解析错位 / 驱动断连。
- 原因
- body 还有余量没消费——驱动会替你排空,但你的响应语义已不完整(部分场景直接判失败)。读完或明确放弃(记录日志)再返回。
把"本轮读到 0"当"读完了"
- 症状
- 大 body 偶发只处理了前半段。
- 原因
- 0 只是"此刻没有新数据";完成信号是状态机的 DONE(或累计长度匹配)。循环条件要区分两者。
编程练习
基础/echo 兑现
实现第 4 章的 /echo:POST body 原样返回。
验收:50 字节与 50KB 各测一次;Content-Type 回显。
进阶chunked 客户端输入
用 curl -H "Transfer-Encoding: chunked" --data-binary 发送分块体,验证你的读取循环无需任何改动。
验收:两种编码的 body 处理结果一致。
挑战流式哈希上传
实现 /hash:边读边算 SHA-256(xrtSha 族),完成后返回十六进制摘要——全程不缓存完整 body。
验收:100MB 文件上传内存占用恒定;结果与 sha256sum 一致。
小结
| 要点 | 结论 |
|---|---|
| 统一状态机 | 定长 / chunked / 关闭三模式一个 Read 接口 |
| 增量读取 | 读一块处理一块;0 = 本轮无数据 ≠ 完成 |
| 防线 | body_limit 413 · 畸形 400 · idle_timeout 慢速 |
| 消费核对 | XS_OK 前 body 必须读完或明确交回驱动 |
本章涉及的 API 参考
xrt 延伸阅读
- xrt 参考 · http_te — Transfer-Encoding 的协议细节
- xrt 参考 · hash — 流式 SHA 家族(练习挑战用)