应用常把大任务拆成多批输入和输出。输入暂时不够,或本轮输出空间用完时,流状态记住处理到哪里;下一次换了缓冲区仍能继续。zlib 与 gzip 的头尾信息也沿同一个任务处理。
在全局图中定位 ↗假设应用要解压一个很大的文件,希望每次只拿到一小批原文字节,交给后续程序后,再重复使用同一块小内存。它不想一次为整个解压结果分配空间;压缩输入也可能分批到达。流式调用就是多次调用同一个解码器,让它记住上一次处理到哪里。
解压的方向是:压缩字节作为输入 → 恢复出的原文字节作为输出。“输出缓冲区”是应用提供的可写内存。它的容量限制本轮最多写出多少原文字节,不是整个文件的大小。应用先处理本轮写出的字节,再让下轮调用覆盖这块内存。
用五字节的输出空间解出八字节
使用完整的裸 DEFLATE 压缩数据 73 74 82 40 00。它表示两个字面量 A、B,再执行长度 6、距离 2 的匹配,最后结束最终块。恢复出的完整结果是 ABABABAB。
这里的 inflate 是解压操作。传入的 z_stream 是持续保存的控制记录:它记录当前输入、输出的位置,并指向解码器内部状态。保留这份记录,下一次调用才能接续任务;五字节的 output 数组只负责暂时接收本轮结果。
一次提供全部五个压缩字节,但只给 inflate 五个输出位置。下一轮保持同一个 z_stream,重复使用这五个位置。完整 C 程序包括裸模式模式初始化、调用循环和资源释放。next_in 最初指向这五个压缩字节,avail_in 为 5。这两个输入字段在循环前设置一次;每次调用都保持上一轮返回的剩余输入位置。每轮提供输出空间并调用解码器的部分如下:
stream.next_out = output; /* writable destination */
stream.avail_out = sizeof output; /* five bytes of room */
int result = inflate(&stream, Z_NO_FLUSH);
unsigned produced = sizeof output - stream.avail_out;
next_out 指向第一个可写位置,avail_out 表示可写位置的数量。inflate 一边写入,一边前移指针、减少数量。因此,produced 就是返回后应用可以从 output 读取的字节数。重新指定输出位置,不会创建一个新的解码任务。
这个程序在固定版本的 C 上运行,得到以下记录:
| 调用 | 消耗的压缩输入 | 写出的原文字节 | 返回值 | 应用接下来做什么 |
|---|---|---|---|---|
| 第 1 次 | 所提供 5 字节中的 4 字节 | 5 字节:ABABA |
Z_OK |
处理这五个字节,再把同一块缓冲区交给下一轮写入。 |
| 第 2 次 | 剩余 1 字节 | 3 字节:BAB |
Z_STREAM_END |
处理这三个字节;这个裸模式流已完成。 |
应用先收到 ABABA,再收到 BAB,合起来是八字节。物理输出缓冲区只有五个位置,反复使用,从未同时装下八字节。这是一次真实 C 执行的结果,不是所有流式调用都正确的普遍证明。
五字节输出缓冲区先装入 ABABA,再被重复使用,装入 BAB。应用把两批结果接成 ABABABAB;解码器负责记住尚未完成的匹配。
两次调用之间的保存状态
最初两次写入来自字面量,接下来三次来自匹配。第一次返回时,匹配还欠最后三个字节:
已经收到: A B A B A
匹配复制: 1 2 3 还需复制 = 3,距离 = 2
保存的阶段是 MATCH,length 表示还剩 3 字节,offset 表示距离为 2。下次要继续复制,不能重新解析同一组长度和距离。距离所指向的字节也必须还在。返回之前,解码器把必要后缀留在自己的历史窗口中。应用之后可以在调用者存储契约允许的范围内交付、覆盖或释放旧输出存储。
| 第 2 次调用接下来写入 | 距离 2 对应的来源 | 字节 |
|---|---|---|
| 第 1 个 | 保留历史中的 B |
B |
| 第 2 个 | 保留历史末尾的 A |
A |
| 第 3 个 | 本轮刚写出的第一个 B |
B |
因此,仅保存两个整数还不够,还必须保有有效的字节存储。第二次调用不能读取指向旧调用者内存的失效指针。匹配复制逐次解释来源读取。
解码器用容量有限的环形数组保留旧输出。写到最后一个位置之后,再从索引零继续。它保存的是最新后缀,不一定保存整个解压结果。先用八字节教学窗口接收 ABABA:
| 保存量 | 本例的值 | 读者怎样使用它 |
|---|---|---|
容量 wsize |
8 字节 | 数组最多保留多少历史。 |
有效量 whave |
5 字节 | 此时只有 ABABA 可用,其余空格不能作为来源。 |
下一写入下标 wnext |
5 | 下一字节放在数组第 5 格,下标从零开始。 |
窗口填满后,再写入会覆盖最老的字节,时间顺序便从 wnext 开始,经过数组末端再绕回开头。负责更新窗口的函数叫 updatewindow:若本轮输出已经达到一个完整窗口,只复制最新的那个窗口;否则把较短的新后缀追加进去,跨过数组末端时分两段复制。这个例子只解释存储,真实窗口参数范围见封装与配置。
C 在哪里暂停
state->mode 决定下一步执行什么工作。MATCH 表示展开已经解析好的长度/距离对,LEN 表示读取下一个字面量/长度符号。局部变量 left 以字节计量本轮剩余的输出空间;它变成零,只能说明当前匹配需要暂停,不能说明匹配已经结束。
固定版本的 MATCH 分支从这里开始:
state->mode = MATCH;
/* fallthrough */
case MATCH:
if (left == 0) goto inf_leave;
left 是本轮还可写入的空间。第一次调用中,它变成零时,匹配还剩三个字节。这里的 goto 让 mode 保持为 MATCH。同一分支后面按实际复制量减少 state->length,只有剩余长度为零才回到 LEN。这就是“继续同一个任务”在代码中的含义。跨调用组合还未完成:要把这个模式和剩余长度关系从初始化保持到每个返回边界。
保存中的工作
inflate 使用状态机,是因为几乎每个阶段都可能缺少输入或输出空间。一次调用不一定包含完整头部、完整 Huffman 码,也不一定容得下整个匹配。内部的 inflate_state 保存停在哪个阶段,以及已经解析出的信息。
表中有几项反复出现的保存量:hold 是暂存已取入但尚未消耗的比特的整数,bits 计量其中有效的位数。lens 数组则在构造动态 Huffman 表时保存各符号的码长,单位也是位。它们不同于输入字节数,也不同于输出匹配的长度。
| 尚未完成的工作 | 相关模式与保存信息 |
|---|---|
| 封装头部 | HEAD 和 gzip 头部模式保存标志、字段长度和元数据读取进度。 |
| 块头部或数值字段 | hold、bits 保存已经从输入中取出、但尚未消耗的低位。 |
| 动态表描述 | TABLE、LENLENS、CODELENS 保存数量、lens 已初始化的前缀和描述用的表。 |
| 符号解释 | LEN、LENEXT、DIST、DISTEXT 保存长度、距离及额外位的解析阶段。 |
| 原始字节或匹配展开 | COPY、MATCH 保存还需产生多少字节;匹配还保存距离。 |
| 尾部 | CHECK 及 gzip 的 LENGTH 等待尾部位,同时保存校验和与已产生长度。 |
表格按任务分组,没有合并真实 C 中的各个 case。尤其是距离尚未解析完成时,恢复调用不能假装匹配来源已经验证完毕。
位缓冲和调用者输入是两个不同的位置。一个字节被取入后,next_in 已经前移,但其中部分位仍可能保存在 hold。因此 avail_in == 0 只说明调用者当前提供的范围已耗尽,不说明解码器已经没有位可以解释。反过来,缓冲区里还有位,也不保证下一个字段所需的位已经足够。输入位逐位追踪了这个边界。
C 把频繁使用的寄存器加载到局部变量。正常暂停进入 inf_leave,由 RESTORE 写回当前指针和位寄存器。这里的“当前”包括此前已经完成的读取和写入。离开循环不会撤销一次做了一半的操作。完整公开调用历史的证明还未完成,其中需要逐条连接局部变化与返回堆,涵盖输入耗尽、输出耗尽和错误出口。
调用者可见的进展
调用前,应用通过 next_in/avail_in 提供可读字节,通过 next_out/avail_out 提供可写空间。返回后,根据剩余数量计算本轮进展:
已消耗 = 调用前 avail_in - 返回后 avail_in
已产生 = 调用前 avail_out - 返回后 avail_out
输入指针前移“已消耗”个字节,输出指针前移“已产生”个字节。尚未读取的输入必须保留。若解码器只用了一个输入块的前半段,就用无关新数据替换整块,会丢失压缩流后半段。可以在范围耗尽后补入新块;也可以明确移动或追加数据,但必须保持未读后缀及其正确指针和数量。
Z_OK 表示有进展,但流尚未完整终止。Z_BUF_ERROR 不一定表示数据畸形:当前输入和空间可能不足以继续,也可能是 Z_FINISH 尚不能完成。反复重试同一份受阻配置,不会自动补齐缺少的资源。Z_NEED_DICT 请求 zlib 字典;Z_DATA_ERROR 报告检测到的格式错误或已启用的完整性检查错误,此前可能已经产生输出。Z_STREAM_END 表示当前流或 gzip 成员完成,后面仍可留下未读输入。全部情形和 flush 值见 inflate 完整契约。
总计数还需要另作区分。普通返回路径根据产生的字节更新总量和已启用的输出校验和。但历史窗口的延迟分配可能在输出已经写出、指针已经恢复后失败。此时的 Z_MEM_ERROR 跳过后续总量与校验和更新,不能解释为“什么都没发生”。本轮影响应从返回后的指针和可用量识别。证明章会说明,为什么无条件使用同一条总计数等式是错误的。
压缩与完成协议
压缩的方向则是:原文字节作为输入 → 压缩字节作为输出。它也有多级进展位置:接收的输入进入窗口,其中尚未处理的后缀称为预读输入,供比较可能的匹配使用。匹配决策提交符号,块输出把符号编码,待发送存储再把字节交给应用。输入字节被接收时,表示它的压缩字节可能还没有交付;没有新输入的调用,也可能继续交付已有待发送输出。
普通调用先以 Z_NO_FLUSH 送入数据,再请求 Z_FINISH。进入完成阶段后,继续使用 Z_FINISH,按需提供输出空间,不再追加新输入,直到 Z_STREAM_END。输出缓冲区写满,不代表最终块、对齐和尾部都已交付。deflateEnd 释放资源,不会补做缺失的完成步骤。
Z_SYNC_FLUSH 把已经接受的数据推进到完成且按字节对齐的刷新边界,同时保留压缩历史。Z_FULL_FLUSH 还建立重新开始的边界,清除后续匹配使用的历史。两者都需要足够的调用与空间来完成所请求的刷新。Z_BLOCK 停在块边界,没有相同的字节对齐承诺;Z_PARTIAL_FLUSH 则使用其规定的部分刷新机制。这些操作都不等同于完成整个流。准确的允许序列见 deflate。
下一章把 DEFLATE 主体放进头部、字典和尾部。之后,流式执行的证明把输入前缀、保留历史、实际返回路径和完成条件连接起来。