zlib的原理与使用

Zlib是一个广泛使用的开源数据压缩库,稳定高效。它的核心是Deflate算法,是一种结合Lz77算法和哈夫曼编码的高效压缩算法。

Lz77算法

让我们想象这样一个字符串

1
ABCDEFGWWABCDEFGABCDEFG

其中ABCDEFG重复了3次,一共23个字符,如果采用UTF-8编码,每个字母占据1字节,这个字符串共占据23字节。如果我们想要压缩这个字符串,利用字符串中的重复信息进行压缩无疑是最直观的想法。在Lz77算法中,我们可以用一组数字(offset,length)替换掉重复的信息,可以将上面的字符串表示为

1
ABCDEFGW(1,1)(9,7)(7,7)

(9,7)就表示从当前位置的前面第9个字节开始的7个字节的内容。假设offsetlength都用uint16_t表示,那我们就把字符串从23个字节压缩成了20个字节。

搜索缓冲区和前向缓冲区

为了最大化压缩率,理论上我们应该在整个历史数据中寻找最长的重复块。但这会导致搜索范围无限大,效率极低。

因此,LZ77 使用滑动窗口来平衡压缩效率和压缩率。这个窗口分为两部分:

  1. 搜索缓冲区 (Search Buffer):位于当前处理位置之前的数据,是寻找匹配项的“字典”。
  2. 前向缓冲区 (Lookahead Buffer):当前位置之后、等待被编码的数据。

zlib的编码器在滑动窗口的基础上采用贪心算法,也就是在搜索缓冲区中为前向缓冲区的内容寻找最长的匹配串。

例如

1
ABCABCABCABCABCABCABCABCABCABCABCABCABCABCABCABC

如果我们直接固定搜索缓冲区和前向缓冲区的大小为24字节,并直接用前半段24字节的数据填充搜索缓冲区,那么压缩的结果是

1
ABCABCABCABCABCABCABCABC(24,24)

如果我们固定搜索缓冲区和前向缓冲区的大小为3字节

1
ABC(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)

显然前者的压缩效率更高,后者压缩效率更低。

重叠复制

前面我们提到

1
ABC(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)(3,3)

这相当于编码器把“这个子串和2字节前那个长度为2的子串一样”这句话重复了15次,但实际上它只需要告诉我们“2字节前那个长度为2的子串又重复了15次”就行,也就是

1
ABC(3,45)

这就是重叠复制。这是一个很优雅的优化,甚至不用修改编码的格式,只要当我们解码的时候遇到lengthoffset要大的情况,我们就可以知道这里出现了重叠复制。

问题来了,如果原字符串是

1
AAABCDFEGHIGKLMNOPQRSTUVWXYZAAAAAAAAAAAAAAAAAAAAA

我们如果把它编码成

1
AAABCDFEGHIGKLMNOPQRSTUVWXYZ(28,21)

那就解码错误了。我们只要把它编码成

1
AAABCDFEGHIGKLMNOPQRSTUVWXYZAAA(3,18)

就行。这样压缩率也并不会损失多少。

最小匹配长度

上面的例子中,我们为什么不压缩成

1
AAABCDFEGHIGKLMNOPQRSTUVWXYZA(1,20)

呢?这是因为Lz77存在最小匹配长度,这个长度取决于lengthoffset的类型。如果一个匹配串的长度小于存储 (offset, length) 对所需的字节数,那么这次替换就是得不偿失的。

在zlib的Deflate算法中,一个字面量(literal byte)会被当成一个符号,一个 (offset, length) 对会被当成两个符号。为了保证压缩有效,匹配的长度 length 必须至少为3。因为替换掉3个字节的原文,我们只需要存储一个长度符号和一个距离符号,这通常是划算的。任何长度为1或2的匹配都不会被编码成 (offset, length) 对,而是直接作为字面量输出。

具体实现流程

前面我们引入了滑动窗口,为了举例方便,假设过程是静态的。而在实际的Lz77流程中,窗口是持续滑动的。其大致流程如下:

  1. 初始化:将滑动窗口定位在输入数据的开始处。搜索缓冲区为空,前向缓冲区填充了数据的开头部分。
  2. 查找匹配:指针指向前向缓冲区的第一个字节。在搜索缓冲区中从后向前搜索,查找与前向缓冲区开头部分最长的匹配串。
  3. 生成输出
    • 如果找到匹配:且匹配长度大于等于最小匹配长度(在zlib中是3),则输出一个 (offset, length) 对。然后,将滑动窗口整体向前移动 length 个字节。这是因为在找到匹配 (offset, length) 时,我们一次性“消费”了前向缓冲区里这 length 个字节,前移length 个字节后搜索缓冲区内仍是已经完成编码的数据。
    • 如果未找到匹配:或者最长匹配长度小于最小匹配长度,则输出前向缓冲区的第一个字节作为“字面量”(Literal)。然后,将滑动窗口向前移动 1 个字节。
  4. 重复:持续执行第2步和第3步,直到前向缓冲区中没有数据为止。

举个例子,处理abracadabra的步骤如下

步骤 状态 搜索缓冲区 前向缓冲区 操作描述 匹配结果 输出 窗口移动
1 | abracadabra (空) abracadabra 搜索缓冲区为空,无法匹配任何内容 → 输出字面量 未找到 Literal(‘a’) 前进 1 字节
2 a | bracadabra a bracadabra 尝试匹配 'b',在搜索缓冲区中没有出现 未找到 Literal(‘b’) 前进 1 字节
3 ab | racadabra ab racadabra 尝试匹配 'r',搜索缓冲区中没有 未找到 Literal(‘r’) 前进 1 字节
4 abr | acadabra abr acadabra 'a' 开始:匹配到 'a',但继续扩展 'ac' 时失败 → 匹配长度=1 < 3 匹配太短(无效) Literal(‘a’) 前进 1 字节
5 abra | cadabra abra cadabra 尝试匹配 'c',搜索缓冲区中没有 未找到 Literal(‘c’) 前进 1 字节
6 abrac | adabra abrac adabra 'a' 开始:匹配到 'a',但扩展 'ad' 时失败 → 匹配长度=1 < 3 匹配太短(无效) Literal(‘a’) 前进 1 字节
7 abraca | dabra abraca dabra 尝试匹配 'd',搜索缓冲区中没有 未找到 Literal(‘d’) 前进 1 字节
8 abracad | abra abracad abra 'a' 开始:匹配 'a''ab''abr''abra',继续 'abrac' 失败 → 最长匹配长度=4 找到,长度=4 ≥ 3 (offset=7,len=4) 前进 4 字节

最后输出abracad(7,4)

这套流程也能自然而然地实现重叠复制。

  1. 处理第一个字符 'a'

    • 状态: | ababababab
    • 搜索缓冲区: (空)
    • 前向缓冲区: abababababc
    • 操作: 搜索缓冲区为空,没有匹配。
    • 结果: 未找到匹配。
    • 输出: Literal('a')
    • 窗口移动: 向前移动 1 个字节。
  2. 处理第二个字符 'b'

    • 状态: a | bababababc
    • 搜索缓冲区: a
    • 前向缓冲区: babababab
    • 操作: 在搜索缓冲区中查找 'b',未找到匹配。
    • 结果: 未找到匹配。
    • 输出: Literal('b')
    • 窗口移动: 向前移动 1 个字节。
  3. 处理后续子串 'ababababc'

    • 状态: ab | ababababc

    • 搜索缓冲区: ab

    • 前向缓冲区: ababababc

    • 操作:

      • 前向缓冲区第1个字符 'a' 在 offset=2 找到匹配。
      • 第2个字符 'b' 继续匹配成功。
      • 搜索缓冲区只有 2 字节,但是搜索指针并不受搜索缓冲区的限制,它会继续向前进入前向缓冲区,直到指向'c'后终止匹配
      • 依次匹配 'ababababc',最终匹配长度=8。
    • 结果: 找到匹配 (offset=2, length=8)

    • 输出: (2, 8)

    • 窗口移动: 一次性前移 8 个字节

  4. 处理最后一个字符 'c'

    • 状态: ababababab | c (这是执行完上一步,窗口前移8个字节后的新状态)
    • 搜索缓冲区: ababababab
    • 前向缓冲区: c
    • 操作: 在搜索缓冲区中查找 'c'
    • 结果: 未找到匹配。
    • 输出: Literal('c')
    • 窗口移动: 向前移动 1 个字节,整个字符串处理完毕。

最终输出ab(2,8)c

这个过程的输出不是最终的压缩数据,而是一个由“字面量”和 (offset, length) 对组成的中间序列,这个序列将交给下一步的哈夫曼编码进行处理。


哈夫曼树

LZ77算法通过消除重复数据,极大地减少了数据量。但它的输出(字面量和指针)仍然是未经压缩的。哈夫曼编码的作用就是对LZ77的输出进行第二轮压缩,让最终的数据变得更小。

它的核心思想是:为出现频率高的符号分配更短的编码,为出现频率低的符号分配更长的编码。

例如,在一段文本中,字母 e 可能出现最频繁,而字母 z 可能很少出现。那么,哈夫曼编码可能会用 10 来表示 e,而用 11010011 来表示 z

Deflate算法中的哈夫曼编码有几个特点:

  1. 处理两种符号:它需要处理两种类型的符号:

    • 字面量/长度 (Literal/Length):0-255的ASCII码值是字面量。此外,还有一些特殊值代表匹配的长度(例如,长度为3、长度为4等)。
    • 距离 (Distance):代表LZ77中的 offset
  2. 两棵哈夫曼树:Deflate标准为这两种符号分别构建了两棵独立的哈夫曼树。一棵用于压缩“字面量/长度”符号,另一棵用于压缩“距离”符号。

  3. 动态构建:zlib可以分析输入数据块,统计每种符号的出现频率,然后为该数据块动态生成最优的哈夫曼树。这使得压缩非常自适应。当然,zlib也提供了使用预定义的“静态哈夫曼树”的选项,这在处理小数据块时可以节省编码树本身占用的空间。

结合过程
LZ77处理后的数据流 A, B, (10, 5), C, ... 会被转换成一个符号流:Literal(A), Literal(B), Length(5), Distance(10), Literal(C), ...。然后,哈夫曼编码器会用对应哈夫曼树中的可变长码字来替换这些符号,生成最终的二进制压缩流。


常用接口

zlib 提供了多个层次的接口,包括简单的一体化函数和底层的流式接口。

一体化接口 (compress / uncompress)

这是最简单、最直接的接口,适用于待处理的数据可以一次性完全载入内存的场景。

compress(dest, destLen, source, sourceLen)

  • 功能: 将 source 指向的内存块中的数据进行压缩,并将结果写入 dest 指向的内存块。此函数使用默认的压缩级别 (Z_DEFAULT_COMPRESSION)。

  • 参数含义:

    • Bytef *dest: 指向用于存储压缩后数据的目标内存缓冲区的指针。
    • uLongf *destLen: 一个指向 uLong 类型变量的指针。
      • 输入时: *destLen 的值必须是 dest 缓冲区的大小(以字节为单位)。
      • 输出时: 如果函数成功,*destLen 的值会被更新为实际压缩后数据的大小。
    • const Bytef *source: 指向源数据(待压缩数据)的指针。
    • uLong sourceLen: 源数据的长度(以字节为单位)。
  • 内部工作流程:

    1. 初始化: 在函数内部,它会临时创建一个 z_stream 结构体,并使用默认参数进行初始化。
    2. 设置流参数: 将 sourcedest 的地址和长度赋值给内部的 z_stream 结构体。
    3. 执行压缩: 调用核心的 deflate 引擎,一次性完成整个压缩过程。
    4. 清理: 释放所有临时分配的资源。
    5. 返回: 通过更新 *destLen 和返回状态码来告知调用者结果。如果 dest 缓冲区不够大,会返回 Z_BUF_ERROR

uncompress(dest, destLen, source, sourceLen)

  • 功能: 将 source 指向的已经压缩的数据块解压到 dest 指向的内存块中。

  • 参数含义:

    • Bytef *dest: 指向用于存储解压后数据的目标内存缓冲区的指针。
    • uLongf *destLen: 一个指向 uLong 类型变量的指针。
      • 输入时: *destLen 的值必须是 dest 缓冲区的大小。你必须提前分配足够大的空间来存放原始数据。
      • 输出时: 如果函数成功,*destLen 的值会被更新为实际解压后数据的大小。
    • const Bytef *source: 指向待解压数据的指针。
    • uLong sourceLen: 待解压数据的长度。
  • 内部工作流程:

    1. 初始化: 与 compress 类似,函数内部会临时创建一个 z_stream 结构体并初始化。
    2. 设置流参数: 将输入和输出缓冲区的相关信息设置到内部的 z_stream 中。
    3. 执行解压: 调用核心的 inflate 引擎,一次性完成整个解压过程。
    4. 清理: 释放所有内部资源。
    5. 返回: 更新 *destLen 并返回状态码。如果 dest 缓冲区不足,会返回 Z_BUF_ERROR;如果输入数据损坏,会返回 Z_DATA_ERROR

流式接口 (deflate / inflate)

这是 zlib 更灵活、更强大的核心接口。它适用于处理大文件、网络数据流等无法一次性加载到内存的数据。这种方式允许你分块送入数据,并分块获取处理结果,极大地降低了内存消耗。

z_stream 结构体

流式接口的核心是 z_stream 结构体,它像一个“会话”或“上下文”,记录了压缩/解压过程中的所有状态信息。我们需要在整个流式处理的生命周期中维护这个结构体。

  • z_stream 结构体的主要成员:
    • const Bytef *next_in: (输入) 指向下一块待处理的输入数据。
    • uInt avail_in: (输入) next_in 指针指向的缓冲区中,可用的输入数据字节数。
    • uLong total_in: (只读) 从流开始到现在,总共读取的输入字节数。
    • Bytef *next_out: (输出) 指向用于存放处理结果的输出缓冲区的下一个可用位置。
    • uInt avail_out: (输出) next_out 指针指向的缓冲区中,剩余的可用空间字节数。
    • uLong total_out: (只读) 从流开始到现在,总共生成的输出字节数。
    • char *msg: 当发生错误时,这里可能会包含一个描述错误的字符串。
    • voidpf opaque: 一个数据指针,zlib 不会使用它,但可以由应用程序用来传递自定义数据。通常设为 Z_NULL
    • alloc_func zalloc: 用于动态内存分配的函数指针。通常设为 Z_NULL,让 zlib 使用标准库的 malloc
    • free_func zfree: 用于释放内存的函数指针。通常设为 Z_NULL,让 zlib 使用标准库的 free

压缩流程

  1. deflateInit(z_streamp strm, int level):

    • 功能: 初始化一个 z_stream 结构体用于压缩。这个函数必须在调用任何 deflate 之前被调用。
    • 参数含义:
      • z_streamp strm: 指向 z_stream 结构体的指针。
      • int level: 压缩级别,取值范围:
        • Z_NO_COMPRESSION (0): 不压缩
        • Z_BEST_SPEED (1): 最快速度
        • Z_BEST_COMPRESSION (9): 最高压缩率
        • Z_DEFAULT_COMPRESSION (-1): 默认级别,在速度和压缩率之间取得平衡。
    • 内部工作流程:
      • 为内部压缩状态分配内存(例如哈希表、滑动窗口等)。
      • 根据指定的 level 设置压缩算法参数。
      • 初始化 strm 结构体中的各个字段。
  2. deflate(z_streamp strm, int flush):

    • 功能: 核心的压缩函数。它会从 strm->next_in 读取数据,进行压缩,然后将结果写入 strm->next_out。这个函数通常在一个循环中被反复调用,直到所有输入数据都被处理完毕。
    • 参数含义:
      • z_streamp strm: 指向已初始化的 z_stream 结构体的指针。
      • int flush: 一个非常重要的参数,用于控制压缩引擎的行为和输出缓冲区的刷新策略。常用值包括:
        • Z_NO_FLUSH: 正常处理,允许 zlib 在内部积累数据以获得更好的压缩率。
        • Z_SYNC_FLUSH: 刷新所有待处理的输出。所有输出都会被写入 next_out。这对于网络数据包之类的应用很有用。
        • Z_FINISH: 表明这是最后一批输入数据。它会处理完所有剩余的输入,并输出压缩流的结束标记。调用此 flush 模式后,必须持续调用 deflate 直到它返回 Z_STREAM_END
    • 内部工作流程:
      • 检查 strm->avail_in,如果有输入数据,则进行压缩。
      • 使用 LZ77 算法查找重复字符串,并使用霍夫曼编码对结果进行编码。
      • 将压缩后的数据写入 strm->next_out,并更新 strm->avail_outstrm->next_out
      • 更新 strm->avail_in, strm->next_in, strm->total_in, strm->total_out
      • 根据 flush 参数的指令,执行相应的刷新操作。
  3. deflateEnd(z_streamp strm):

    • 功能: 清理和释放由 deflateInit 分配的所有内部状态和内存。
    • 参数含义:
      • z_streamp strm: 指向需要被清理的 z_stream 结构体的指针。
    • 内部工作流程:
      • 释放所有与该压缩流相关的动态分配的内存。
      • strm 的内部状态重置,以防止误用。

解压流程

解压流程与压缩流程在结构上非常相似。

  1. inflateInit(z_streamp strm):

    • 功能: 初始化一个 z_stream 结构体用于解压。
    • 参数含义:
      • z_streamp strm: 指向你所分配的 z_stream 结构体的指针。
    • 内部工作流程:
      • 为内部解压状态分配所需的内存(如滑动窗口、霍夫曼树等)。
      • 初始化 strm 结构体。
  2. inflate(z_streamp strm, int flush):

    • 功能: 核心的解压函数。它从 strm->next_in 读取已压缩的数据,进行解压,然后将结果写入 strm->next_out。同样,它也通常在循环中被调用。
    • 参数含义:
      • z_streamp strm: 指向已初始化的 z_stream 结构体的指针。
      • int flush: 对于解压,flush 参数通常使用 Z_NO_FLUSHZ_SYNC_FLUSH 等值也有特定用途,但不如压缩时常用。
    • 内部工作流程:
      • 从输入流中读取数据,解析霍夫曼编码。
      • 重建原始数据,处理 LZ77 算法中的匹配(”距离-长度”对)。
      • 将解压后的数据写入 strm->next_out,并更新相应的计数器和指针。
      • 当解压到数据流的末尾时,它会返回 Z_STREAM_END
  3. inflateEnd(z_streamp strm):

    • 功能: 清理和释放由 inflateInit 分配的所有资源。
    • 参数含义:
      • z_streamp strm: 指向需要被清理的 z_stream 结构体的指针。
    • 内部工作流程:
      • 释放所有与该解压流相关的动态分配的内存。
      • 重置 strm 结构体。

源码解析

compress2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int ZEXPORT compress2(dest, destLen, source, sourceLen, level)
Bytef *dest;​
uLongf *destLen;​
const Bytef *source;​
uLong sourceLen;​
int level;​
{​
z_stream stream;​
int err;​
const uInt max = (uInt)-1;​
uLong left;​

left = *destLen;​
*destLen = 0;​

stream.zalloc = (alloc_func)0;​
stream.zfree = (free_func)0;​
stream.opaque = (voidpf)0;​

err = deflateInit(&stream, level);​
if (err != Z_OK) return err;​

stream.next_out = dest;​
stream.avail_out = 0;​
stream.next_in = (z_const Bytef *)source;​
stream.avail_in = 0;​

do {​
if (stream.avail_out == 0) {​
stream.avail_out = left > (uLong)max ? max : (uInt)left;​
left -= stream.avail_out;​
}​
if (stream.avail_in == 0) {​
stream.avail_in = sourceLen > (uLong)max ? max : (uInt)sourceLen;​
sourceLen -= stream.avail_in;​
}​
err = deflate(&stream, sourceLen ? Z_NO_FLUSH : Z_FINISH);​
} while (err == Z_OK);​

*destLen = stream.total_out;​
deflateEnd(&stream);​
return err == Z_STREAM_END ? Z_OK : err;​
}

compress2是zlib库中最主要的实现函数,它处理一个完整的、已知的内存块,将其压缩后放入另一个内存块中,用 zlib 提供的更底层的流式接口(deflateInit, deflate, deflateEnd)封装了整个压缩流程。

compress的区别在于多出了level参数,compress其实就是调用了level = Z_DEFAULT_COMPRESSION也就是使用默认压缩级别的compress2

首先来看函数签名

1
2
3
4
5
6
7
int ZEXPORT compress2(
Bytef *dest,
uLongf *destLen,
const Bytef *source,
uLong sourceLen,
int level
);
  • int ZEXPORT:

    • int: 函数的返回值。它是一个状态码,用于表示压缩是否成功。Z_OK (值为0) 代表成功,其他负数值代表不同类型的错误(如内存不足 Z_MEM_ERROR、缓冲区太小 Z_BUF_ERROR 等)。
    • ZEXPORT: 这是一个宏,用于在构建动态链接库(DLL)或共享库(.so)时,将此函数导出,使其能被其他程序调用。
  • Bytef *dest

    • 作用: 指向目标缓冲区的指针。压缩后的数据将被写入这个缓冲区。
    • 类型: Bytef 是 zlib 中对 unsigned char 的别名,代表一个字节。
  • uLongf *destLen

    • 输入时: *destLen 的值必须是 dest 指向的缓冲区的大小(以字节为单位)。函数需要知道目标缓冲区有多大,以防止写入时发生溢出。
    • 输出时: 如果压缩成功,*destLen 的值会被函数修改为实际压缩后数据的大小(以字节为单位)。
    • 类型: uLongf 是 zlib 中对 unsigned long 的别名。
  • const Bytef *source

    • 指向源数据缓冲区的指针,也就是需要被压缩的数据。
    • const 关键字: 表示这个函数不会修改源数据的内容。
  • uLong sourceLen

    • 作用: 源数据的长度(以字节为单位)。
  • int level

    • 指定压缩级别。这是一个在压缩速度和压缩率之间进行权衡的参数。
    • 取值范围:
      • 0: 不进行压缩。
      • 19: 压缩级别从低到高。1 (Z_BEST_SPEED) 速度最快,但压缩率最低;9 (Z_BEST_COMPRESSION) 压缩率最高,但速度最慢。
      • -1: Z_DEFAULT_COMPRESSION,这是一个在速度和压缩率之间取得良好平衡的默认值(通常是 6)。

代码的核心是围绕一个 z_stream 结构体来组织的,它保存了整个压缩过程的状态。

  1. 初始化 z_stream

    1
    2
    3
    4
    5
    6
    z_stream stream;
    int err;
    // ...
    stream.zalloc = (alloc_func)0;
    stream.zfree = (free_func)0;
    stream.opaque = (voidpf)0;
    • z_stream stream;: 定义一个核心的状态结构体。
    • stream.zalloc, stream.zfree, stream.opaque: 这三个字段用于自定义内存分配。这里将它们都设置为 0NULL,是告诉 zlib 使用其内部默认的 malloc/free 函数来管理内存。
  2. 初始化压缩器状态

    1
    2
    err = deflateInit(&stream, level);
    if (err != Z_OK) return err;
    • deflateInit(&stream, level): 这是 zlib 中用于初始化压缩流的函数。
      • 它会为 stream 结构体分配内部状态所需的所有内存(例如,内部缓冲区、哈希表等)。
      • level 参数被传入,以决定压缩策略。
      • 如果内存分配失败或 level 参数无效,它会返回一个错误码。
  3. 设置输入和输出缓冲区

    1
    2
    3
    4
    stream.next_out = dest;
    stream.avail_out = 0; // 初始设为0,循环内会设置
    stream.next_in = (z_const Bytef *)source;
    stream.avail_in = 0; // 初始设为0,循环内会设置
    • stream.next_in / stream.next_out: 分别指向下一次要读取的输入字节和下一次要写入的输出字节的位置。
    • stream.avail_in / stream.avail_out: 分别表示输入缓冲区中还有多少可用字节和输出缓冲区中还有多少剩余空间。
    • 注意: 这里将 avail_inavail_out 初始化为 0,是因为实际的赋值操作在 do-while 循环内部进行,以处理可能超过 uInt 最大值的大块数据。
  4. 核心压缩循环

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    do {
    // A. 如果输出缓冲区满了,就重新填充 avail_out
    if (stream.avail_out == 0) {
    stream.avail_out = left > (uLong)max ? max : (uInt)left;
    left -= stream.avail_out;
    }
    // B. 如果输入数据消耗完了,就重新填充 avail_in
    if (stream.avail_in == 0) {
    stream.avail_in = sourceLen > (uLong)max ? max : (uInt)sourceLen;
    sourceLen -= stream.avail_in;
    }
    // C. 调用核心压缩引擎
    err = deflate(&stream, sourceLen ? Z_NO_FLUSH : Z_FINISH);
    } while (err == Z_OK);

    这是整个函数最核心的部分。zlib 的 deflate 函数是分块处理数据的。

    • A 和 B (分块处理逻辑): stream.avail_instream.avail_out 的类型是 uInt(通常是 32 位无符号整数)。如果原始数据长度 sourceLen 或目标缓冲区长度 destLen 超过了 uInt 的最大值(在 64 位系统上,uLong 可以远大于 uInt),就需要分块送入 deflate 函数。这里的代码确保每次送入的数据块大小不超过 uInt 的最大值 max
    • C (deflate 函数调用):
      • deflate(&stream, flush_mode): 这是执行压缩的引擎函数。它会从 stream.next_in 读取数据,将压缩结果写入 stream.next_out,并相应地更新 avail_in, avail_out, next_in, next_out 的值。
      • flush_mode 参数是关键:
        • sourceLen ? Z_NO_FLUSH : Z_FINISH: 这是一个三元运算符。
        • Z_NO_FLUSH: 如果还有源数据没有提供给 deflate (sourceLen > 0),就使用这个模式。它告诉 deflate 尽可能多地压缩数据,但不要结束整个压缩流,因为后面还有数据。
        • Z_FINISH: 当所有源数据都已经被 deflate 读取 (sourceLen == 0),就使用这个模式。它告诉 deflate 处理所有剩余的缓冲数据,并写入压缩流的结束标记。这是完成压缩的最后一步。
    • 循环条件 (while (err == Z_OK)):
      • 只要 deflate 返回 Z_OK,就意味着压缩仍在进行中,但尚未完成(通常是因为输出缓冲区满了)。循环会继续,为 deflate 提供新的空间。
      • deflate 返回 Z_STREAM_END 时,表示压缩已成功完成,循环会终止。
      • 如果 deflate 返回任何其他错误代码,循环也会终止。
  5. 清理和返回

    1
    2
    3
    *destLen = stream.total_out;
    deflateEnd(&stream);
    return err == Z_STREAM_END ? Z_OK : err;
    • *destLen = stream.total_out;: stream.total_out 记录了整个压缩过程中总共输出的字节数。函数在结束前,用这个最终值更新调用者传入的 destLen
    • deflateEnd(&stream);: 这是 deflateInit 的配对函数。它会释放 deflateInit 分配的所有内部状态和内存,防止内存泄漏。
    • return err == Z_STREAM_END ? Z_OK : err;:
      • 如果 deflate 最终返回 Z_STREAM_END,说明压缩流已成功结束,整个 compress2 函数就向调用者返回 Z_OK,表示一切顺利。
      • 如果循环因为其他错误(比如输出缓冲区不够大,deflate 返回 Z_BUF_ERROR)而终止,则将这个原始的错误码返回给调用者。

uncompress

uncompress 函数的内部结构与 compress2 非常相似,同样是基于流式接口的封装。让我们简单看一下它的实现,以了解解压过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int ZEXPORT uncompress (dest, destLen, source, sourceLen)
Bytef *dest;
uLongf *destLen;
const Bytef *source;
uLong sourceLen;
{
z_stream stream;
int err;

stream.next_in = (z_const Bytef *)source;
stream.avail_in = (uInt)sourceLen;
/* Check for sourceLen <= uInt_MAX is not needed here, since
uLong sourceLen <= uLongf *destLen and uLongf == uLong. */

stream.next_out = dest;
stream.avail_out = (uInt)*destLen;
if ((uLong)stream.avail_out != *destLen) return Z_BUF_ERROR;

stream.zalloc = (alloc_func)0;
stream.zfree = (free_func)0;

err = inflateInit(&stream);
if (err != Z_OK) return err;

err = inflate(&stream, Z_FINISH);
if (err != Z_STREAM_END) {
inflateEnd(&stream);
if (err == Z_NEED_DICT || (err == Z_BUF_ERROR && stream.avail_in == 0))
return Z_DATA_ERROR;
return err;
}
*destLen = stream.total_out;

err = inflateEnd(&stream);
return err;
}

compress2的对比:

  1. 核心函数不同:使用 inflateInit, inflate, inflateEnd 这一套解压接口。
  2. 无需循环uncompress 假设输入和输出缓冲区都足够大,可以一次性完成所有操作。因此,它只调用一次 inflate 函数,而不是像 compress2 那样使用 do-while 循环。
  3. Flush模式:调用 inflate 时,直接使用 Z_FINISH。这告诉 inflate 引擎,提供的输入数据就是完整的压缩块,解压到数据流末尾就应该停止。
  4. 错误处理:解压过程可能遇到更多数据本身的问题(如数据损坏 Z_DATA_ERROR),因此错误处理逻辑更复杂一些。

zlib的原理与使用
https://guts.homes/2025/09/01/zlib/
作者
guts
发布于
2025年9月1日
许可协议