本章导读
第 91 章解决了"正文的边界";本章解决"正文的内容编码"。Content-Encoding: gzip 的响应在分帧层读出的是压缩字节——业务要的是明文。xrtHttpDecodeCreate 由响应头字段直接构建流式解码器(自动串接多级编码——gzip, br 这样的链式声明),xrtHttpDecodeWrite 逐片段喂入、解出的明文推给回调,xrtHttpDecodeDone 校验结尾(gzip 的 CRC 与长度 trailer——防静默损坏)。设计姿态与第 91 章一致:片段随到达随解、明文直推消费方——全程不攒完整响应,内存占用与正文大小无关。配合发送侧的内容协商(Accept-Encoding 的 q 值)构成压缩的完整闭环,卷九 HTTP 核心部分到此收官。
93.1引入
下载一个 50 MB 的 gzip 压缩 JSON:天真实现是"收完整个响应→整块解压→得到 80 MB 明文"——峰值内存 130 MB,且要等最后一字节到齐才开始处理。流式实现是"每收到一段压缩片段→立刻解出一段明文→推给解析器"——内存恒定(一个滑动窗口量级)、首字节延迟约等于网络首段。差别不在 gzip 算法(第 29 章的解压器已是流式),而在接线:谁来把"HTTP 分帧层吐出的片段"接到"解压器的输入"上、谁来处理多级编码的串接、谁来校验结尾?
http_decode 模块就是这根线:输入端是第 91 章 BodyRead 吐出的 Data 视图,输出端是你给的回调,中间自动串接 Content-Encoding 声明的每一级。头字段声明什么就解什么——包括 identity(不解)与多级链;声明了本构建没编入的编码(如 br)创建时明确失败——不静默跳过(静默=把压缩数据当明文给业务,后面全错)。
93.2概念
由头构建:Create 的契约
xrtHttpDecodeCreate(字段数组, 数量, 可选配置) 扫描 Content-Encoding 字段(多条按出现顺序串接)构建解码器:gzip(第 29 章 Deflate 家族的 gzip 容器)、deflate(zlib 包装)、identity(直通)、多级链(gzip 之后再 gzip 的病态但合法的声明也正确处理)。没有 Content-Encoding 字段 = 直通。失败语义:未知编码或未编入的编码创建返回 NULL——调用方决定降级(丢弃响应)还是报错;绝不"猜一个相近的解一下"。
配置(可选 NULL 取默认)区分 compat 与 safe 两种模式:历史服务器(特别是老 deflate 实现)有一些不规范的输出,compat 模式按宽容窗口接受(浏览器同款兼容行为),safe 模式严格拒绝——默认取安全侧,与遗留后端对接时显式放宽。这是"严格解析"纪律面对历史包袱的标准折衷:默认严格、放宽必须显式。
流式喂入:Write 与回调
bFinal 参数标记"这是最后一段"——gzip 流的结尾块(含 CRC32 与原始长度)在此校验;Done 也可在 Write 之后单独调用(有些传输"喂完了"这件事与"最后一段"分离)。回调形态的意义:解码器不为你分配缓冲——明文推给你,缓冲策略(直接解析?写入管道?落盘?)完全归你。与第 29 章解压回调、第 91 章 Body 视图一脉相承。
与内容协商的闭环
解码是响应侧;请求侧要声明"我能解什么":Accept-Encoding: gzip;q=1.0, deflate(xrtHttpAcceptEncodingQuality 查 q 值——千分位整数模型,第 91/92 章同款)。服务端从请求头选双方都可接受的编码写进 Content-Encoding。客户端的纪律:只声明你真的能解的——把 br 写进 Accept-Encoding 却没编 brotli 解码器,服务端选了 br 你的 Create 就失败。http_encoding 模块提供请求侧的解析与构造。
组合全景:一次完整响应的消费管线
四层各司其职、层间只有视图与回调——这是卷九前五章搭出的完整骨架,第 98 章的 xhttp 运行时把它封装成高层 API。
93.3示例
第一个完整程序:从响应头到明文回调
下面的程序来自 examples/http/decode/main.c——42 字节的 gzip 流完整解出:
#include <xrt/http_decode.h> #include <stdio.h> /* 把解码片段直接写到标准输出,实际网络代码可改为业务消费回调。 */ static bool printBody(xbytesview Data, ptr pData) { FILE* pOutput = (FILE*)pData; return fwrite(Data.Data, 1, Data.Size, pOutput) == Data.Size; } */ /* 展示从 HTTP/1 Header 建立流式 gzip 解码器。 */ int main(void) { static const uint8 Gzip[] = { 0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF, 0xCB, 0x48, 0xCD, 0xC9, 0xC9, 0x57, 0x48, 0xCE, 0xCF, 0x2D, 0x28, 0x4A, 0x2D, 0x2E, 0x4E, 0x4D, 0x51, 0x28, 0xCF, 0x2F, 0xCA, 0x49, 0x01, 0x00, 0xA1, 0x2D, 0x94, 0x53, 0x16, 0x00, 0x00, 0x00 }; static const xhttpfield Fields[] = { { XRT_STR_INIT("Content-Encoding"), XRT_STR_INIT("gzip") } }; xhttpdecode* pDecode = xrtHttpDecodeCreate(Fields, 1, NULL); bool bSuccess; if ( pDecode == NULL ) { return 1; } bSuccess = xrtHttpDecodeWrite( pDecode, (xbytesview){ Gzip, sizeof(Gzip) }, true, printBody, stdout ) && xrtHttpDecodeDone(pDecode); xrtHttpDecodeDestroy(pDecode); return bSuccess ? 0 : 1; }
刚才发生了什么。 ① 输入是内嵌的 42 字节 gzip 流(1F 8B 魔数开头)与一条 Content-Encoding: gzip 字段——模拟第 91 章已经吐出的"头数组 + 压缩正文"。② DecodeCreate 扫字段建解码器:一级 gzip。③ DecodeWrite 一次喂入(bFinal=true——本例单段到达;流式场景每段 false、最后 true),解出的明文直接推给 printBody 回调——网络形态下这里换 JSON 解析器或管道写入,内存不随正文涨。④ DecodeDone 校验结尾:gzip 流尾部的 CRC32 与长度字段对不上会在这里失败——压缩数据在传输中被改坏,宁可报错不出错数据(静默损坏是压缩链路最阴的故障)。⑤ Destroy 释放——解码器是本模块唯一的拥有式对象。
第二个完整程序:解码器全特性巡检
第二个程序来自 examples/http/decode_tour/main.c——三行覆盖模式、内容模式与重置:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> /* 一段 gzip 压缩的 "identity-passthrough-check"。 */ static const uint8 arrGzip[46] = { 0x1F, 0x8B, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0A, 0xCB, 0x4C, 0x49, 0xCD, 0x2B, 0xC9, 0x2C, 0xA9, 0xD4, 0x2D, 0x48, 0x2C, 0x2E, 0x2E, 0xC9, 0x28, 0xCA, 0x2F, 0x4D, 0xCF, 0xD0, 0x4D, 0xCE, 0x48, 0x4D, 0xCE, 0x06, 0x00, 0x9A, 0xFB, 0x0B, 0xA2, 0x1A, 0x00, 0x00, 0x00 }; /* 输出回调:累计并比对内容。 */ typedef struct exampleout { size_t iBytes; char arrText[64]; } exampleout; static bool exampleOutput(xbytesview Data, ptr pUserData) { exampleout* pOut = (exampleout*)pUserData; if ( pOut->iBytes + Data.Size > sizeof(pOut->arrText) ) { return false; } memcpy(pOut->arrText + pOut->iBytes, Data.Data, Data.Size); pOut->iBytes += Data.Size; return true; } int main(void) { xhttpdecodeconfig Compat; xhttpdecodeconfig Safe; xhttpdecode* pDecode = NULL; xhttpfield Fields[2]; exampleout Out; int iResult = 1; /* ---- 两套配置 + gzip 字段建解码器。 ---- */ xrtHttpDecodeConfigInit(&Compat); xrtHttpDecodeConfigInitSafe(&Safe); Fields[0].Name = XRT_STR_LITERAL("Content-Encoding"); Fields[0].Value = XRT_STR_LITERAL("gzip"); pDecode = xrtHttpDecodeCreate(Fields, 1u, &Compat); if ( (pDecode == NULL) || /* 兼容配置不设输出上限;安全配置有显式上限。 */ (Compat.OutputLimit != XHTTP_DECODE_OUTPUT_UNLIMITED) || (Safe.OutputLimit == Compat.OutputLimit) ) { goto Cleanup; } printf("decode: config compat vs safe + create ok\n"); /* ---- gzip 模式 + 输入输出计量。 ---- */ memset(&Out, 0, sizeof(Out)); if ( (xrtHttpDecodeMode(pDecode) != XHTTP_DECODE_CONTENT) || !xrtHttpDecodeWrite(pDecode, (xbytesview) { arrGzip, sizeof(arrGzip) }, true, exampleOutput, (ptr)&Out) || !xrtHttpDecodeDone(pDecode) || (Out.iBytes != 26u) || (memcmp(Out.arrText, "identity-passthrough-check", 26u) != 0) || (xrtHttpDecodeInputSize(pDecode) != 46u) || (xrtHttpDecodeOutputSize(pDecode) != 26u) ) { goto Cleanup; } printf("decode: gzip content mode + input/output sizes ok\n"); /* ---- Reset 换 identity 字段:窗口复用 + 透传模式。 ---- */ Fields[0].Value = XRT_STR_LITERAL("identity"); memset(&Out, 0, sizeof(Out)); if ( !xrtHttpDecodeReset(pDecode, Fields, 1u, &Compat) || (xrtHttpDecodeMode(pDecode) != XHTTP_DECODE_IDENTITY) || !xrtHttpDecodeWrite(pDecode, (xbytesview) { arrGzip, 4u }, true, exampleOutput, (ptr)&Out) || !xrtHttpDecodeDone(pDecode) || (Out.iBytes != 4u) || (xrtHttpDecodeInputSize(pDecode) != 4u) ) { goto Cleanup; } printf("decode: reset to identity passthrough ok\n"); iResult = 0; Cleanup: xrtHttpDecodeDestroy(pDecode); return iResult; }
刚才发生了什么。 ① 第 1 行对比配置两种模式并验证创建——compat/safe 的分野在第 91 章讲过的"默认严格、放宽显式"。② 第 2 行验证 gzip 内容模式与输入输出尺寸统计——input/output sizes 给压缩比计算与监控指标直接可用的数据。③ 第 3 行 reset 到 identity 直通——同一连接上的下一响应可能不压缩:解码器状态归零、模式切换,对象复用不重建(高连接数服务的每响应成本再降一档)。
93.4契约
- 由头构建:
DecodeCreate扫 Content-Encoding(多条按序串接);无字段=直通;未知/未编入编码创建失败——不猜测不静默跳过。 - 模式:safe 默认(严格);compat 显式放宽(历史服务器的宽容窗口);配置
NULL取安全默认。 - 流式:Write 逐片段(bFinal 标记末段);明文即时推回调;回调不成立则该片段处理失败——解码器不替你缓冲。
- 收尾校验:Done 校验 gzip CRC 与长度 trailer;失败=内容损坏,正确动作是丢弃整响应而非使用部分。
- 重置:Reset 归零并切 identity——同连接多响应复用同一对象。
- 内容协商:Accept-Encoding 声明须与解码能力一致;
http_encoding模块(AcceptEncodingParse/Quality/Select)提供请求侧解析/构造;q 千分位整数。 - 对象模型:Create/Destroy 拥有式(本模块唯一分配点);Write/Done 无额外分配。
- 裁剪:
XRT_MODULE_HTTP_DECODE依赖 COMPRESS 与 HTTP 地基——不解压的客户端零成本裁掉。 - 组合边界:上游接 Body 的 Data 视图(第 91 章);下游接你的回调——层间零拷贝贯穿。
常见错误
症状:恒 true——多段流从第二段起全错(解码器把每段当完整流收尾校验);恒 false——Done 永远报"流不完整",CRC 校验形同虚设。
原因:bFinal 是"传输层知道这是最后一段"的信号,来自你对第 91 章 Body DONE 状态的判断——不是随便填的。
while ( read_chunk(&Data) ) { DecodeWrite(pDec, Data, true, cb, ctx); /* 每段都当末段 */ }
while ( (St = xrtHttp1BodyRead(&Body, In, bEof, &iUsed, &Data, NULL)) != XHTTP1_BODY_DONE ) { if ( St != XHTTP1_BODY_DATA ) { return false; } /* 最后一段:Body 层即将 DONE(下一轮就是 DONE)*/ DecodeWrite(pDec, Data, false /* 除真正末段外 */, cb, ctx); } xrtHttpDecodeDone(pDec); /* 分帧层收尾后再校验解码层 */
症状:CRC 校验失败(传输损坏),但回调已经推了 90% 的明文给下游——下游把残缺数据当完整处理了。
原因:流式的天然张力——数据已经推出去才知道结尾坏了。解法是下游也按事务处理:CRC 失败时下游必须能作废(丢弃/回滚),或者你选择"全部缓冲完再确认"的保守模式(牺牲流式换安全)。
/* 回调直接把明文写进数据库 */ static bool onPlain(xbytesview Data, ptr pCtx) { db_append((db*)pCtx, Data); /* Done 失败时已写入:脏数据 */ return true; }
/* 回调写进可作废的暂存(文件/内存段),Done 通过后一次性提交 */ static bool onPlain(xbytesview Data, ptr pCtx) { staging_append((staging*)pCtx, Data); return true; } /* ... */ if ( !xrtHttpDecodeDone(pDec) ) { staging_discard(&Staging); /* 整体作废:干净失败 */ } else { staging_commit(&Staging); }
症状:服务端按协商选了 brotli 压缩,客户端 DecodeCreate 失败——下载功能对一部分站点"随机"不可用。
原因:内容协商声明的是能力承诺。写进 Accept-Encoding 的每种编码,你的构建必须真的能解。XRT 当前支持 gzip/deflate/identity——br 不要写。
/* 从某处抄来的"标准头" */ send_header("Accept-Encoding: gzip, deflate, br, zstd"); /* 服务端选 br → Create 失败 */
/* 声明与能力一致:本构建支持 gzip/deflate */ send_header("Accept-Encoding: gzip, deflate"); /* 未声明 br → 服务端不会选它 */
编程练习
把 decode 示例的 42 字节流按 7/13/22 字节三段喂入(前两段 bFinal=false),验证输出不变。再故意翻转末段一个字节验证 Done 失败。验收标准:分段与整段输出一致;CRC 失败被捕获且报告为解码错误。
把第 90/91/93 章串成完整消费链:一段完整 chunked+gzip 响应字节(自备,可用 curl 生成)→ ResponseParse → BodyPlan/Body 流式读 → DecodeCreate/Write → 回调统计明文总长 → 双 Done 校验。验收标准:明文与 curl --compressed 结果一致;全链零整响应缓冲(第 6 章统计对照);任一层失败时后续层不再执行。
实现 fetch(Url, 支持的编码列表):请求侧按能力构造 Accept-Encoding(q:gzip 1000、deflate 900);响应侧走完整管线;对 Content-Encoding 不在能力内的响应(服务端不理会协商)返回明确错误而不是错误解码。验收标准:对 gzip/deflate/无编码三种响应都正确;能力外编码报"unsupported content-encoding";并发 8 路下载内存恒定。
小结
| 知识点 | 速查 |
|---|---|
| 三入口 | Create(由头字段构建)/ Write(逐片段推回调)/ Done(CRC 收尾校验) |
| 编码支持 | gzip / deflate / identity / 多级链;未知或未编入创建失败 |
| 模式 | safe 默认严格;compat 显式放宽历史兼容;NULL 配置取安全默认 |
| bFinal | 末段标记——来自 Body 层 DONE 判断;恒真/恒假都是错 |
| 回调形态 | 明文即时推送;缓冲策略归你;解码器零分配 |
| 收尾语义 | Done 失败=整响应作废;下游须可作废(staging 模式) |
| 重置 | Reset 归零切 identity——同连接多响应复用 |
| 内容协商 | 只声明真能解的;q 千分位;encoding 模块构造请求侧 |
| 组合边界 | 上游 Body 的 Data 视图、下游你的回调——零拷贝贯穿 |
| 裁剪 | XRT_MODULE_HTTP_DECODE 依赖 COMPRESS;不解压零成本裁掉 |
📖 本章涉及的 API 参考手册:http_decode(16 个条目)http_encoding(19 个条目)http1(39 个条目)
点击查看完整签名、参数约束、返回值与错误说明