本章导读

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概念

两套姿势(先记形态再记函数)

1一次性DeflateAll 输入视图 + 配置 → 拥有式 gzip 缓冲;小数据三行收工
2流式Init 建 → Process 逐块喂 → Finish 收尾;内存与总大小无关
3复用Reset 归零流状态重新开始——第二段数据不重建内部结构

流式的三个状态与第 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:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single impl.c examples/compress/deflate/main.c -lws2_32 -liphlpapiplain=34 gzip=30

刚才发生了什么。 ① xrtDeflateConfigInit 取默认配置(Level 6、gzip 容器)——配置结构体先 Init 再改需要的字段,全库的统一姿势。② 34 字节高重复文本压出 30 字节:Deflate 流只占约 12 字节(重复模式被字典匹配吃掉),加上容器固定 18 字节——开销账本一目了然:数据再短,容器 18 字节跑不掉;这条 34→30 的线就是"小数据压缩收益薄"的实证。③ 产物确定性可自行验证:连续压两次逐字节比对(第 12 章 Hash64 给指纹最快)——同指纹即确定性的机器证言。

完整程序:流式全接口与往返

来自 examples/compress/stream_tour/main.c,分块压缩、流式解压、状态复用一网打尽:

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;
}
$ gcc -O1 -DXRT_MODULE_ALL -I single -include xrt.h impl.c examples/compress/stream_tour/main.c -lws2_32 -liphlpapicompress: deflate valid/done/size ok (split=2 writes)compress: inflate roundtrip original=51compress: reset reuse second-stream okcompress: config corrupt rejected

刚才发生了什么。 ① 流式 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。

常见错误

坑 1:小数据无脑压缩

症状:接口在加了"全量 gzip"中间件后小响应变慢,传输体积反而增大;监控显示 CPU 上升但带宽没省。

原因:容器固定开销 + 小数据低冗余——plain=34 gzip=30 的账本在小数据端是常态而非特例;已压缩格式(图片、zip)再压是纯亏。

错误写法
if ( Response.Size > 0 ) {   /* 只要有数据就压 */
	Compress(&Response);      /* 300 字节的 JSON 压完可能 320 字节 */
}
正确写法
if ( Response.Size >= 1024 && !AlreadyCompressed(Type) ) {
	Compress(&Response);      /* 超过经验线且内容可压才压 */
}
坑 2:解压不受信数据不设上限

症状:服务处理外部上传的压缩包时内存暴涨被打挂——安全报告写的是"解压炸弹"。

原因: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 指纹做最快比对。提示:这就是缓存键可信的前提证明。

挑战:HTTP 静态资源预压缩器(归档宿主的完全体)

扫描目录里的文本资源(.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 个条目)

点击查看完整签名、参数约束、返回值与错误说明