第 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 必须读完或明确交回驱动
xrt 延伸阅读