跳到正文
ZLIB × LEAN / 算法与接口

压缩调用与接口地图

把公开调用、引擎工作和字节交付放在同一张地图中。

从这里查调用顺序与完整函数约定;沿引擎流程进入算法精读。

这一环节的任务

应用需要交出输入、接收输出,并处理返回结果。缓冲区、回调和文件接口提供不同的使用方式,底层仍然复用压缩与解压引擎。

在全局图中定位 ↗

假设你要压缩 ABABABAB,而输出目标每次只接收五个字节。应用程序需要控制这些缓冲边界时,可以选择流式 API。输入是一段原始字节和刷新请求;输出是一段已写前缀与返回码。更换缓冲区时,同一个压缩器继续存活。如果完整输入和目标容量都已经具备,缓冲区工具可以代为执行这套协议。

一套完整的压缩调用

z_stream 记录把持续的任务与临时缓冲区分开。next_in 指向下一个可读原文字节,avail_in 记录还剩多少;next_out 指向下一个可写输出位置,avail_out 记录那里还有多少空间,计数单位都是字节。记录还指向压缩器内部状态;五字节 chunk 数组反复使用时,内部状态必须继续保留。

下面这个完整教学程序把 zlib 封装的编码写到标准输出。八个源字节没有字符串结束符。由于输入一次全部提供,程序直接使用 Z_FINISH。五字节只是输出批次大小,不是整个压缩流的大小限制。具体编码字节和长度由压缩器选择。

#include <stdio.h>
#include <zlib.h>

int main(void) {
    unsigned char source[] = {'A','B','A','B','A','B','A','B'};
    unsigned char chunk[5];
    z_stream stream = {0};
    if (deflateInit(&stream, Z_DEFAULT_COMPRESSION) != Z_OK) return 1;
    stream.next_in = source;
    stream.avail_in = sizeof source;
    int ret, ok = 1;
    do {
        stream.next_out = chunk;
        stream.avail_out = sizeof chunk;
        ret = deflate(&stream, Z_FINISH);
        if (ret != Z_OK && ret != Z_STREAM_END) {
            ok = 0;
            break;
        }
        unsigned made = sizeof chunk - stream.avail_out;
        if (fwrite(chunk, 1, made, stdout) != made) {
            ok = 0;
            break;
        }
    } while (ret != Z_STREAM_END);
    int ended = deflateEnd(&stream);
    return !(ok && ended == Z_OK);
}

每轮提供五字节可写切片,只写出返回的前缀,并保留同一个 stream。Z_STREAM_END 结束数据任务,之后 deflateEnd 才结束分配的生命周期。错误出口也会到达 deflateEnd。即使输出目标在收到部分内容后失败,这个清理顺序仍然适用。

压缩器的生命周期

deflateInit 是宏,把版本和结构大小传给 deflateInit_。需要其他封装、窗口、内存级别或策略时,使用 deflateInit2_。初始化成功返回 Z_OK,没有消费输入,也没有产生压缩字节。输入逐步到达时,普通调用顺序如下:

flowchart TD
    Init["初始化:分配状态"] --> Work["Z_NO_FLUSH:逐步提供输入与输出切片"]
    Work -->|"Z_OK;输入或输出工作未完"| Work
    Work -->|"全部源数据已经提供"| Finish["Z_FINISH:保留未读输入;补充输出空间"]
    Finish -->|"Z_OK;需要更多输出空间"| Finish
    Finish -->|Z_STREAM_END| End["结束操作:释放状态"]
    Work -->|"出错后中止"| End
    Finish -->|"出错后中止"| End

本例初始化后直接进入完成阶段。开始完成后,要保留未读输入,不加入新输入,并继续使用 Z_FINISH。chunk 填满,不代表最终块或尾部已经交付。Z_STREAM_END 才报告完成;分配状态却仍然存在,直到结束操作,或者通过合适的重置重新使用。

deflate 契约给出精确的拒绝行为。输出空间为零时返回 Z_BUF_ERROR,next_out 仍须非 NULL。原样重试耗尽的缓冲区不会取得进展。开始完成后改用其他刷新请求,会返回 Z_STREAM_ERROR。中止时,deflateEnd丢弃未完成工作并释放存储;它可能在已经释放存储之后返回 Z_DATA_ERROR,报告放弃了忙碌状态的数据,不会代为完成输出。

输入与输出记账

程序计算 made = 5 - stream.avail_out。一般情形先记住调用前的可用量:

已接收输入 = 调用前 avail_in  - 调用后 avail_in
已收到输出 = 调用前 avail_out - 调用后 avail_out

next_in 按已接收量前移,next_out 按已收到量前移。总计数按相应数量增加,遵循 C 无符号整数的位宽。重用输出存储前,先处理已经写入的前缀。输入后缀则要保留到消费完成。更换任何一个缓冲区都不需要重置。

接收输入与交付输出是两个边界。avail_in == 0 只说明当前切片已经接收,不说明对应压缩块已经交付。接收的字节可以先作为预读数据保留,再变成等待块选择的符号,随后成为缓冲位或完整的待交付字节。后一次调用可能先交付旧字节,再接收新输入。因此,单独一个 Z_OK 不能报告完成。

内部状态把交付队列明确记录下来:pending_out 指向下一个未交付的压缩字节,pending 统计仍在等待的字节数。局部变量 len 是本次复制量,单位为字节,受待交付数量和调用者输出空间共同限制;total_out 是累计交付字节数。下面六项更新让这些字段始终描述同一次交付。

下面真实的 flush_pending 主体,deflate.c:978–983显示了交付边界:

    zmemcpy(strm->next_out, s->pending_out, len);
    strm->next_out  += len;
    s->pending_out  += len;
    strm->total_out += len;
    strm->avail_out -= len;
    s->pending      -= len;

前面的语句把数量限制在可用输出空间内。若五个待交付字节复制进空的 chunk,avail_out 从 5 降到 0,next_out 前移 5,made 得到 5。待交付游标也前移相同数量。因此,更换 chunk 必须保留内部流:尚未交付字节还属于后一次调用。

输出的实现路径

字面量表示一个原文字节,匹配则用长度/距离引用较早字节。压缩器先收集这些符号,再选择怎样描述一个块,并把符号编码成长度不一的比特。位累积器 bi_buf 暂存尚未形成完整交付字节的位;待交付存储保存已经形成、但还在等待调用者空间的压缩字节。这些是处理进展的不同表示,不能用同一个计数代替。

这些待交付字节从哪里来?下面的简明调用地图用来定位工作。内部函数由 zlib 调用,应用程序不需要分别调用它们。

flowchart LR
    API["deflate:缓冲区 + 刷新请求"] --> Match["deflate.c:窗口、匹配搜索与策略"]
    Match --> Symbols["_tr_tally:字面量与匹配"]
    Symbols --> Block["_tr_flush_block:选择表示"]
    Block --> Bits["send_bits:编码进入待交付字节"]
    Bits --> Deliver["flush_pending:交付输出前缀"]
    Deliver --> Caller["调用者收到字节"]
    API -->|"级别 0"| Stored["原样存储块的边界与载荷"]
    Stored --> Deliver
    Stored -->|"空间足够:直接复制载荷"| Caller

对 ABABABAB,字面量 A、B 后面跟长度 6、距离 2 的匹配,是一种有效描述。默认编码器也可以选择别的表示。块选择解释固定编码、动态编码和原样存储三种表示。位输出追踪字段怎样经过 bi_buf 成为完整字节。共享存储则解释:同一分配中的待交付输出与未读符号记录为什么不能互相追越。

配置会选择不同策略,因此普通匹配路线不覆盖所有路径。级别 0 在空间充足时,允许原样存储载荷直接进入调用者输出,省去待交付中转;需要缓冲的工作走另一条边。这些策略路径的完整执行证明未完成;还需要把普通符号输出与级别 0 的直接复制分别接到公开调用的记账和内存条件上。

交付、格式刷新和结束操作有不同目的。flush_pending 复制已经可用的字节。Z_SYNC_FLUSH 请求在完全交付后形成对齐标记;Z_FULL_FLUSH 还在完成的边界重置匹配历史。一次刷新请求填满输出时,应提供空间并继续同一种刷新方式。它们都不能替代 Z_FINISH 或结束操作。状态操作一章进一步解释这些边界选择。

契约与其余接口

沿此路线,可在压缩索引中查阅初始化、处理、待交付输出查询、重置和结束。图中没有表达出的共享存储与调用顺序前提,仍由契约保留。

全部 API 索引还覆盖解压流、回调解码、整块缓冲区工具与库信息、校验和,以及 gzip 文件。按任务需要的状态和 I/O 协议选择类别。证明一章继续从已接收输入追踪符号、位与已交付输出;只保住最终字节数,还不能建立这些内容的含义、中间状态有效性或资源清理。

ZLIB × LEAN

全局目录

当前位置压缩调用与接口地图