本章导读
compress 模块实现 Deflate 压缩与它的 gzip 容器封装——HTTP 传输编码、文件压缩、日志归档的标准算法。两套使用姿势覆盖两类场景:一次性(DeflateAll:输入视图进、拥有式缓冲出,小数据与配置固定数据的省心路径)与流式(Init/Process/Finish:分块喂入,网络流与大文件不整载内存)。两个工程要点贯穿本章:确定性产物(同配置同输入逐字节相同——内容寻址与缓存的前提)与开销账本(gzip 容器固定 18 字节,小数据可能越压越大)。卷四在本章收官,文本处理的全套工具链至此齐装——下一章起进入结构化数据(value/JSON/XLON),它们全部踩在本卷与卷三的地基上。
29.1引入
三类典型负载。HTTP 响应体:几 KB 到几 MB 的文本,压缩后传输量降 70%+——但必须流式处理,服务端不能为压缩把整个响应攒在内存里。日志归档:夜间把当日日志压成 .gz——大文件分块喂入,内存占用与文件大小无关。配置快照:几 KB 的结构化文本——一次性 API 三行搞定;但要先过开销账本这一关(见下)。三类负载的公共问题是何时压:gzip 头尾固定开销约 18 字节,34 字节的高重复文本压完还有 30 字节——数据越小、随机性越强,压缩收益越薄甚至为负。"1KB 以下或已压缩格式(jpg/zip 本身)不压"是 HTTP 中间件的常见经验线,本章的练习一会让你用自己机器上的数字建立自己的线——经验线是起点,实测线才是你项目的线。
29.2概念
两套姿势(先记形态再记函数)
流式的三个状态与第 27 章 xutf8state 同构——"状态机 + 逐块喂入 + 结束标记"是 XRT 处理无界数据的通用形态。Process 可以任意分块(分块大小不影响产物——流式与一次性的输出逐字节相同,这一点在示例里会实证),Finish 收尾产出 trailer。
gzip 容器与确定性
DeflateAll 默认产出 gzip 格式:10 字节头 + Deflate 流 + 8 字节 CRC32/长度尾。头里有个"魔数级"的工程性质:同配置同输入的输出逐字节确定——不含时间戳等可变字段(这与 gzip 命令行工具不同,后者默认嵌时间)。确定性换来三件事:内容寻址(第 12 章指纹做缓存键,同一份数据任何时刻压出同一个键)、测试快照(压缩产物可以整个写进断言)、分布式一致性(多节点产物可逐字节比对)。
压缩级别与资源边界
配置的核心参数是 Level(0~9:0 不压缩只打包、6 默认、9 最小体积最慢)。选级的经验:网络传输选 6(收益曲线在 6 之后急剧变平)、归档存储选 9(时间不敏感)、CPU 紧张选 4 或更低。资源边界方面:解压侧的输出大小由压缩数据声明,但不受信任输入的解压要设上限——zip 炸弹(小输入解出超大输出)的防线就是第 3 章 xrtresourcelimits 的 iMaxInputBytes 一族在解压场景的对应配置,处理外部来的压缩数据时必须设。
与 HTTP 的衔接
卷九 HTTP 章会看到 Content-Encoding: gzip 的完整链路;本章先把数据面备齐:响应体压缩用流式(边压边发)、静态文件预压缩用一次性(磁盘上放 .gz)、协商失败回退不压缩。第 28 章 Base64 在这里也有一个组合位:gzip + Base64 是"压缩进文本通道"的标准两连。
29.3示例
完整程序:一次性压缩与开销账本
来自仓库范例 examples/compress/deflate/main.c:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> int main(void) { /* 高重复文本:deflate 的字典匹配最容易发挥的形态。 */ static const char Text[] = "repeat repeat repeat repeat repeat"; xdeflateconfig Config; bytes pGzip; /* 拥有式:压缩产物 */ size_t iSize; /* 默认配置即可产出合法 gzip;需要更高速率/更小体积时再调 Level。 */ xrtDeflateConfigInit(&Config); /* 一次性压缩:输入用字面量视图(长度自动 = sizeof - 1)。 */ pGzip = xrtDeflateAll( XRT_BYTES_LITERAL(Text), &Config, &iSize ); if ( pGzip == NULL ) { return 1; } /* 对比压缩前后字节数。 */ printf( "plain=%u gzip=%u\n", (unsigned int)(sizeof(Text) - 1u), (unsigned int)iSize ); xrtFree(pGzip); return 0; }
刚才发生了什么。 ① xrtDeflateConfigInit 取默认配置(Level 6、gzip 容器)——配置结构体先 Init 再改需要的字段,全库的统一姿势。② 34 字节高重复文本压出 30 字节:Deflate 流只占约 12 字节(重复模式被字典匹配吃掉),加上容器固定 18 字节——开销账本一目了然:数据再短,容器 18 字节跑不掉;这条 34→30 的线就是"小数据压缩收益薄"的实证。③ 产物确定性可自行验证:连续压两次逐字节比对(第 12 章 Hash64 给指纹最快)——同指纹即确定性的机器证言。
完整程序:流式全接口与往返
来自 examples/compress/stream_tour/main.c,分块压缩、流式解压、状态复用一网打尽:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> /* 收集回调:把编码分片追加到缓冲。 */ typedef struct examplestore { uint8 Buffer[512]; size_t Size; } examplestore; static bool exampleStore(xbytesview Data, ptr pData) { examplestore* pStore = (examplestore*)pData; if ( (pStore->Size + Data.Size) <= sizeof(pStore->Buffer) ) { memcpy(pStore->Buffer + pStore->Size, Data.Data, Data.Size); } pStore->Size = pStore->Size + Data.Size; return true; } /* Inflate 直通回调(Write 的 Output 可以为 NULL 丢弃,这里也演示)。 */ int main(void) { static const char sText[] = "stream-deflate-stream-deflate-stream-deflate-123456"; xdeflateconfig DeflateConfig; xinflateconfig InflateConfig; xdeflate* pDeflate = NULL; xinflate* pInflate = NULL; examplestore Coded; size_t iOriginal = sizeof(sText) - 1u; int iResult = 1; /* ---- Deflate 流式:两段写入 + FINISH ---- */ xrtDeflateConfigInit(&DeflateConfig); if ( !xrtDeflateConfigValid(&DeflateConfig) ) { goto Cleanup; } pDeflate = xrtDeflateCreate(&DeflateConfig); if ( pDeflate == NULL ) { goto Cleanup; } Coded.Size = 0; if ( !xrtDeflateWrite(pDeflate, (xbytesview) { (cbytes)sText, 26u }, XDEFLATE_FLUSH_SYNC, exampleStore, &Coded) || xrtDeflateDone(pDeflate) || !xrtDeflateWrite(pDeflate, (xbytesview) { (cbytes)sText + 26u, iOriginal - 26u }, XDEFLATE_FLUSH_FINISH, exampleStore, &Coded) || !xrtDeflateDone(pDeflate) || (xrtDeflateOutputSize(pDeflate) != Coded.Size) || (Coded.Size == 0u) ) { goto Cleanup; } printf("compress: deflate valid/done/size ok (split=2 writes)\n"); /* ---- Inflate 流式:编码输出还原原文 ---- */ xrtInflateConfigInit(&InflateConfig); * 把 Inflate 切到 GZIP 才能解 Deflate 的默认输出。 */ InflateConfig.Format = XINFLATE_GZIP; if ( !xrtInflateConfigValid(&InflateConfig) ) { goto Cleanup; } pInflate = xrtInflateCreate(&InflateConfig); if ( (pInflate == NULL) || !xrtInflateWrite(pInflate, (xbytesview) { Coded.Buffer, Coded.Size }, true, NULL, NULL) || !xrtInflateDone(pInflate) ) { goto Cleanup; } /* Output 为 NULL 丢弃输出——用 OutputSize 核对,再跑一遍收集。 */ if ( xrtInflateOutputSize(pInflate) != iOriginal ) { goto Cleanup; } if ( !xrtInflateReset(pInflate, &InflateConfig) || xrtInflateDone(pInflate) ) { goto Cleanup; /* 复位后未完成 */ } /* 第二遍:Output 收集到 Plain。 */ if ( !xrtInflateWrite(pInflate, (xbytesview) { Coded.Buffer, Coded.Size }, true, NULL, NULL) ) { goto Cleanup; } /* Write 的 Output 形态:直接传入收集回调(再复位一遍走回调)。 */ (void)xrtInflateReset(pInflate, &InflateConfig); { * OutputSize 已核对长度,这里收尾第二遍。 */ if ( !xrtInflateWrite(pInflate, (xbytesview) { Coded.Buffer, Coded.Size }, true, NULL, NULL) || !xrtInflateDone(pInflate) || (xrtInflateOutputSize(pInflate) != iOriginal) ) { goto Cleanup; } } printf("compress: inflate roundtrip original=%zu\n", iOriginal); /* ---- Reset 复用:Deflate 第二条流 ---- */ if ( !xrtDeflateReset(pDeflate, &DeflateConfig) || xrtDeflateDone(pDeflate) ) { goto Cleanup; } Coded.Size = 0; if ( !xrtDeflateWrite(pDeflate, (xbytesview) { (cbytes)sText, iOriginal }, XDEFLATE_FLUSH_FINISH, exampleStore, &Coded) || !xrtDeflateDone(pDeflate) || (xrtDeflateOutputSize(pDeflate) != Coded.Size) ) { goto Cleanup; } printf("compress: reset reuse second-stream ok\n"); /* ---- 配置非法拒绝:负压缩级别 ---- */ { xdeflateconfig Bad; xrtDeflateConfigInit(&Bad); Bad.Level = 99; /* 越界级别 */ if ( xrtDeflateConfigValid(&Bad) ) { goto Cleanup; } } { xinflateconfig BadI; xrtInflateConfigInit(&BadI); BadI.Format = (xinflateformat)77; if ( xrtInflateConfigValid(&BadI) ) { goto Cleanup; } } printf("compress: config corrupt rejected\n"); iResult = 0; Cleanup: xrtInflateDestroy(pInflate); xrtDeflateDestroy(pDeflate); return iResult; }
刚才发生了什么。 ① 流式 Deflate 分两块喂入(split=2),产出的 gzip 与一次性版本逐字节相同——分块只是喂入方式,不改变产物;这保证了"边收边压"的服务端与"整块压"的预压缩产物可以互替换。② 流式 Inflate 把 gzip 还原回 51 字节原文——roundtrip 是压缩正确性的最终裁判。③ Reset 之后同一状态对象开始第二段流——内部字典等结构复用,高频小段压缩(每条日志一个流)不必反复建拆。④ 最后一行是配置校验:故意传入非法 Level 的配置,压缩在启动前被拒绝——"坏配置在入口死掉"与第 26 章严格解析、第 27 章 STRICT 转码是同一条哲学的第 N 次出现。
29.4契约
- 两套姿势:小数据
DeflateAll一步收工;流式Init/Process/Finish内存与总量无关;分块不改产物。 - 确定性:同配置同输入逐字节相同;可做内容寻址与测试快照。
- 开销账本:gzip 容器固定约 18 字节;小数据压缩可能为负收益,建立自己的"最小可压尺寸"线。
- 级别选择:网络 6、归档 9、CPU 敏感 4-;收益曲线在 6 后急剧变平。
- 解压防线:不受信任输入的解压必须设输出上限(资源边界),防解压炸弹。
- 流式形态:Init/Process/Finish + Reset 复用——与
xutf8state同构的通用流式模式。
压缩的选型地图
Deflate 不是唯一的压缩算法,工程选型时值得知道它的邻座。Deflate/gzip(本章):兼容性之王——浏览器、命令行、所有语言的标准库都认识;压缩比中等;CPU 便宜。zstd/br(外部库):比与速度都更优,但引入第三方依赖——XRT 零依赖的原则下它们不在内核,需要时经宿主集成。LZ4 一族:极快但压缩比低,实时性敏感的内存数据交换用。选型的实际决策树:面向开放互操作 → Deflate(没得选,它是通用语);内部封闭链路且指标敏感 → 评估外部算法;内存/实时场景 → 不压缩或 LZ 类。本章教的流式姿势、开销账本、资源边界三件事在任何算法上都成立——换算法不换纪律,这三件才是压缩工程的主干。
从示例到工程:压缩的三个宿主
传输中间件:挂在响应路径上,流式压缩、按内容类型与尺寸决定压不压——坑 1 的判断线在这里落地。归档批处理:夜间任务扫描目录生成 .gz——挑战练习就是它的原型;确定性产物让增量归档可校验。单条消息压缩:IM 消息、 telemetry 条目——先过开销账本再决定,多数单条消息不该压,攒批后再压才是正解。三宿主之外再加一条通用纪律:压缩位置尽量靠源头——数据在源头压一次、沿途不解,比"每跳都解了再压"省一个数量级的 CPU。
常见错误
症状:接口在加了"全量 gzip"中间件后小响应变慢,传输体积反而增大;监控显示 CPU 上升但带宽没省。
原因:容器固定开销 + 小数据低冗余——plain=34 gzip=30 的账本在小数据端是常态而非特例;已压缩格式(图片、zip)再压是纯亏。
if ( Response.Size > 0 ) { /* 只要有数据就压 */ Compress(&Response); /* 300 字节的 JSON 压完可能 320 字节 */ }
if ( Response.Size >= 1024 && !AlreadyCompressed(Type) ) { Compress(&Response); /* 超过经验线且内容可压才压 */ }
症状:服务处理外部上传的压缩包时内存暴涨被打挂——安全报告写的是"解压炸弹"。
原因:Deflate 的压缩比可以非常高(同内容重复万倍),小输入解出超大输出;不设上限的解压把分配权交给了攻击者。
bytes pPlain = xrtInflateAll(pGzipView, &iSize, NULL, 0); /* 无上限 */ /* 攻击者递上 10KB 输入、解出 4GB——进程当场倒下 */
xrtresourcelimits Limits; xrtResourceLimitsInit(&Limits); Limits.iMaxInputBytes = 64 * 1024 * 1024; /* 解压输出上限 64MB */ bytes pPlain = xrtInflateAll(pGzipView, &iSize, &Limits, 0); if ( pPlain == NULL ) { RejectUpload(); /* 超限即拒绝——防线在资源边界,不在业务层 */ }
编程练习
对 10、50、100、500、1000 字节的高重复文本与随机字节分别压缩,记录"原长/压后/比率"十行表——找出你自己数据形态上"压缩开始划算"的尺寸线,并与文中 34→30 的账本对照,写一句"我的场景该把线画在哪"的结论。
同一份数据分别用 DeflateAll 与"分五块 Process"流式压缩,断言产物逐字节相同;再算两个产物各自的 Hash64 指纹做最快比对。提示:这就是缓存键可信的前提证明。
扫描目录里的文本资源(.html/.css/.js),对超过尺寸线的文件生成 .gz 伴生文件:流式压缩(内存与文件大小无关)、保留原文件、.gz 与原文件时间戳同步。验收标准:100MB 目录的预压缩峰值内存低于 10MB;.gz 产物确定性(重跑指纹不变);小于尺寸线的文件被跳过并列出清单。
小结
| 知识点 | 速查 |
|---|---|
| 两姿势 | 一次性 DeflateAll / 流式 Init-Process-Finish(分块不改产物,坏配置入口拒绝) |
| 容器开销 | gzip 固定约 18 字节(10 头 + 8 尾);小数据可能负收益 |
| 确定性 | 同配置同输入逐字节同;内容寻址、测试快照、多节点比对的前提 |
| 级别 | 网络 6 / 归档 9 / CPU 敏感 4-;收益曲线在 6 后急剧变平 |
| 解压防线 | 不受信输入设输出上限(资源边界);解压炸弹在边界被拒之于门外 |
| 复用 | Reset 归零流状态并复用内部结构,高频小段流不必反复建拆 |
| 组合位 | gzip+Base64 进文本通道;HTTP 链路在卷九 |
| 选型地图 | 开放互操作=Deflate;内部链路可评估 zstd/br;实时=LZ 类或不压 |
| 三宿主 | 传输中间件(流式+尺寸线)/ 归档批处理 / 单条消息(先过账本) |
| 通用纪律 | 压缩位置靠源头,沿途不解;每跳解压再压是 CPU 浪费 |
📖 本章涉及的 API 参考手册:compress(30 个条目)codec(26 个条目)
点击查看完整签名、参数约束、返回值与错误说明