zlib库剖析(3):使用示例zpipe

2023-01-31 02:01:38 zlib 示例 剖析

本文整理自Http://zlib.net/zlib_how.html, 在源码包zlib-1.2.7的examples/zlib_how.html中也有。

   我们常常疑惑不知道怎么样使用deflate()和inflate()。用户想知道应该在什么时候提供更多输入,什么时候使用更多输出,怎么处理Z_BUF_ERROR,怎么确保处理正确地终止,等等。example目录下有一个简单的例程zpipe.c,示范了使用deflate()和inflate()来把输入文件压缩或解压到输出文件。下面对各行代码进行解释。

   我们为需要的定义包含头文件。对stdio.h,要用到fopen(), fread(), fwrite(), feof(), ferror()和fclose(),以执行文件i/o,还有fputs()用来处理错误消息。对string.h,我们使用strcmp()进行命令行参数处理。对asser.h,我们使用assert()宏。对zlib.h,我们使用基本的压缩函数deflateInit(), defalte()和deflateEnd(),以及基本的解压函数inflateInit(), inflate()和inflateEnd()。

#include <stdio.h> 
#include <string.h> 
#include <assert.h> 
#include "zlib.h"

  这段难看的代码是为了防止在WIN/MS-DOS系统上出现输入输出数据损坏的问题。如果没有这段代码,上述操作系统会将输入、输出文件视为文本,将文件中的EOF字符转换为其他的形式。这会破坏二进制数据,尤其会使压缩数据不可用。这段代码将输入输出均设置为二进制模式,以禁止EOF转换,SET_BINARY_MODE()将会在main函数开始处用于stdin和stdout上。

#if defined(MSDOS) || defined(OS2) || defined(WIN32) || defined(__CYGWIN__) 
#  include <fcntl.h> 
#  include <io.h> 
#  define SET_BINARY_MODE(file) setmode(fileno(file), O_BINARY) 
#else 
#  define SET_BINARY_MODE(file) 
#endif

   CHUNK是向zlib例程输入数据及提取数据的缓冲区大小。缓冲区越大,效率越高,尤其是解压操作inflate()。如果内存足够,尽量使用128K、256K之类的大小。

#define CHUNK 16384

   def()函数压缩源文件的数据到目标文件。输出文件是zlib格式,与gzip和zip格式不同。zlib格式只有一个2字节的头部,标识这是一个zlib流,并提供解压信息;还有一个4字节的尾部,是用来在解压后校验数据完整性的。

   ret保存zlib函数的返回值。flush跟踪deflate()的flushing状态,要么是无flushing,要么在读到输入文件末尾后全部flush。have是deflate()返回的数据字节数。strm是核心结构,用来与zlib例程之间传递信息。in和out是deflate()的输入输出缓冲区。

 
int def(FILE *source, FILE *dest, int level) 
{ 
    int ret, flush; 
    unsigned have; 
    z_stream strm; 
    unsigned char in[CHUNK]; 
    unsigned char out[CHUNK];

   首先我们要用deflateInit()初始化zlib状态。strm结构中的zalloc, zfree, opaque必须在调用deflateInit()之前初始化。本例中都设为了Z_NULL,要求zlib使用默认的内存分配策略。应用程序也可以在这里使用定制的内存分配策略。deflateInit()会为内部状态分配256K字节大小的内存。deflateInit()有两个参数,一个是要初始化的结构的指针(strm),一个是压缩的等级(level)。level的值在-1~9之间。压缩等级越低,执行速度越快,压缩率越低。常量Z_DEFAULT_COMPRESSION(等于-1)表示在压缩率和速度方面寻求平衡,实际上和等级6一样。等级0实际上不做任何压缩,只对输入数据做略微改变以形成zlib格式(并不是简单的一字节一字节的拷贝)。高级程序可以在这里使用deflateInit2()以降低内存消耗,但是同时要付出压缩率的代价;或者使用gzip头部和尾部来代替zlib;或者不要头尾部而使用原始编码。

   我们必须检查deflateInit()的返回值,如果为Z_OK,则说明内存分配成功,参数合法。deflateInit()还会检查zlib.h头文件所使用的zlib库版本和链接器使用的zlib库版本是否一致,这对于共享zlib库的环境尤为重要。

   注意,应用程序可以初始化多个相互独立的zlib流,它们可以并行执行。z_stream结构中保存的状态信息可以让zlib方法可重入。

 
strm.zalloc = Z_NULL; 
strm.zfree = Z_NULL; 
strm.opaque = Z_NULL; 
ret = deflateInit(&strm, level); 
if (ret != Z_OK) 
    return ret;

   外层的do-while循环读入所有的输入数据,如果读到了文件结尾则结束循环。这个循环里面只调用了函数deflate()。我们必须确保所有的输入数据都被处理而且所有的输出数据都被输出了(此例中是写入输出文件),然后才可以退出循环。

   从输入文件读取数据到输入缓冲区,读取的字节数被赋给avail_in,指向这些数据的指针被赋给next_in。用feof检查是否读到了输入文件的文件尾,如果读到了文件尾,那么flush被置为Z_FINISH,flush变量稍后会传递给deflate(),表明这是最后一段要被压缩的输入数据了。如果还没到文件尾,flush被置为Z_NO_FLUSH,表明我们还有未压缩的数据。

   如果在读输入文件中遇到错误,结束进程。在结束之前,要调用deflateEnd()释放zlib的状态。deflateEnd()不会出错,不必检查返回值。

   内层do-while循环将我们读到的一段输入数据传递给deflate(),然后不停调用deflate()直到输出产生完毕。一旦没有新的输出,也就意味着deflate()已经处理掉了所有的输入,avail_in的值变为0。

   先设置输出缓冲区:设置avail_out为可用输出缓冲字节数,next_out为指向缓冲区的指针。然后调用压缩引擎deflate()。它尽可能多地使用next_in指向的长度为avail_in的输入缓冲区中的数据,向next_out指向的输出缓冲区中写入长度为next_out的数据。上述计数器和指针随之更新,跳过已经消耗掉的输入数据和已经写出的输出数据。输出缓冲区的大小可能会限制能消耗多少输入数据。因此内层循环通过每次增加输出缓冲区来确保所有的输入数据都被处理了。因为avail_in和next_in都是由deflate()更新的,在输入缓冲区内的数据被消耗完之前我们不必管它们。(但是因为每次deflate()都输出avail_out的数据,将输出缓冲区填满,所以next_out和avail_out必须由应用程序自己更新)。

 
do { 
    strm.avail_in = fread(in, 1, CHUNK, source); 
    if (ferror(source)) { 
        (void)deflateEnd(&strm); 
        return Z_ERRNO; 
    } 
    flush = feof(source) ? Z_FINISH : Z_NO_FLUSH; 
    strm.next_in = in; 
                            
     
    do { 
        strm.avail_out = CHUNK; 
        strm.next_out = out; 
        ret = deflate(&strm, flush);     
        assert(ret != Z_STREAM_ERROR);   
        have = CHUNK - strm.avail_out; 
        if (fwrite(out, 1, have, dest) != have || ferror(dest)) { 
            (void)deflateEnd(&strm); 
            return Z_ERRNO; 
        } 
    } while (strm.avail_out == 0); 
    assert(strm.avail_in == 0);      
                            
     
} while (flush != Z_FINISH); 
assert(ret == Z_STREAM_END);         
                            
 
(void)deflateEnd(&strm); 
return Z_OK;

   deflate()有两个参数:z_stream结构的参数保存了输入输出信息和内部压缩引擎的状态;int类型的flush参数指明了是否以及如何将数据flush到输出缓冲区。通常,为了加速压缩,deflate会处理K字节的输入数据,然后才产生输出(头部除外)。deflate会突然输出一段压缩后的数据,然后再获取更多输入,然后再突然输出。最后,必须告知deflate()停止读取新的数据,将已读取的输入数据进行压缩,输出,并加上最后的尾部的校验。只要flush参数是Z_NO_FLUSH ,deflate()会一直进行压缩。一旦flush参数变为Z_FINISH,deflate()会开始结束压缩过程。但是,取决于输出缓冲区的大小,即使已经读取完所有输入,仍可能要多次调用deflate()才能使其完成全部压缩输出。在这些连续调用过程中,flush参数必须保持为Z_FINISH。在高级应用程序中,flush参数还可以有其他值。

   deflate()有返回值来告知错误,但是这个例子中我们并不需要检查返回值。为什么?让我们逐一来看deflate()可能的返回值。Z_OK,没有错误。Z_STREAM_END,说明读到输入文件尾部了,但并没有关系,我们的代码连续调用deflate()直到不再产生输出(产生输入已读完,而仍需要调用deflate()进行压缩的原因是输出缓冲区的大小的限制)。Z_STREAM_ERROR只会在流未被正确初始化的情况下出现,但是我们确实正确初始化了。当然,检查一下Z_STREAM_ERROR也没有坏处,因为有可能程序的其他部分不经意地改变了zlib的内存。Z_BUF_ERROR表明deflate()不能再读取更多输入或者产生任何输出了,这种情况下可以通过给予更多输入或者分配更多输出缓冲来再次调用deflate(),下面还会提到Z_BUF_ERROR。

   我们现在计算上一次调用deflate()时产生了多少输出,即是调用前分配的输出缓冲区大小减去调用后还剩下的输出缓冲区大小。然后将输出缓冲区的数据写入输出文件。然后下次调用deflate()时又可以重新使用这片输出缓冲区了。如果有文件I/O错误,我们先调用deflateEnd()然后再返回,以免内存泄露。

   内层do-while循环直到deflate()不能填满给定的输出缓冲区为止,即剩余可用空间大小不为0(前面提到了,每次deflate()尽可能多地消耗输入数据,但是一次“突然”burst产生的输出数据大小都和输出缓冲区大小一样,除非最后一次产生的输出填不满输出缓冲区)。此时我们知道deflate()已经消耗完了输入缓冲区内的数据,我们可以退出内层循环,然后重新利用这片输入缓冲区。

   我们通过看deflate()是否填满了输出缓冲区来判断其是否还有更多的输出没做,如果avail_out大于0,说明输出已经做完了。但是假设这样一种巧合,最后一次的deflate()产生的输出刚好填满了输出缓冲区,avail_out为0,但是deflate()确实已经处理完了所有输入,所有输出也已经做了。这种情况其实没关系,我们再调用一次deflate(),此时会返回Z_BUF_ERROR。

   如果flush参数设成Z_FINISH,最后的几次deflate()调用会完成输出流工作。一旦这个做完了,之后的deflate()调用如果flush参数不为Z_FINISH,则会返回Z_STREAM_ERROR,并且不进行任何操作,直到重新初始化z_stream状态。

   有些程序用两层循环来代替我们这里的内层循环。第一层循环flush设为Z_NO_FLUSH,将所有输入都读进去,第二层循环将flush设置为Z_FINISH,不再输入,让deflate()完成输出。我们的代码里避免了这样做,因为保持追踪flush的状态更方便。(代码中对应flush = feof(source) ? Z_FINISH : Z_NO_FLUSH;)

   我们通过检查flush是否为Z_FINISH(因为flush = feof(source) ? Z_FINISH : Z_NO_FLUSH;)来判断是否还有输入文件数据未被读取。最后一次调用deflate()的返回值必然是Z_STREAM_END,因为所有的输入都已经读完,所有的输出也都产生。整个def()就要结束了,调用deflateEnd防止内存泄露。

   下面的inf()是解压。输入数据应该是从输入文件读到的合法zlib流,将解压后的数据写入输出文件。大部分和def()类似。下面主要讲不一样的地方。

   状态的初始化是一样的,除了没有level这个参数(当然了,这个得从输入的zlib数据里获取)。z_stream除了要初始化zalloc, zfree ,opaque外,还要初始化next_in和avail_in。这里avail_in设为0,next_in设为Z_NULL,表示没有提供输入数据。

 
int inf(FILE *source, FILE *dest) 
{ 
    int ret; 
    unsigned have; 
    z_stream strm; 
    unsigned char in[CHUNK]; 
    unsigned char out[CHUNK]; 
                       
     
    strm.zalloc = Z_NULL; 
    strm.zfree = Z_NULL; 
    strm.opaque = Z_NULL; 
    strm.avail_in = 0; 
    strm.next_in = Z_NULL; 
    ret = inflateInit(&strm); 
    if (ret != Z_OK) 
        return ret;

   外层循环以inflate()是否返回Z_STREAM_END作为循环终止条件。因为如果inflate()返回Z_STREAM_END,说明输入已读完,而且所有输出都产生了(这里和deflate()不同,和flush参数是否为Z_FINISH无关。deflate()返回Z_STREAM_END说明输入已读完,而且如果设置了Z_FINISH的话,所有输出都会产生)。这个和def()相反,def()是判断输入是否已经读完,inf()是判断输出是否已经全部做了。

   如果在读到压缩文件尾之前读到文件尾EOF,说明压缩数据不完整,结束外部循环,报错。注意读到的数据可能比inflate()最终消耗的数据要多。在这样的程序中,要注意返回未用到的数据,至少也要指明还有多少输入数据没有被inflate()使用,使得程序可以知道从zlib流后面的哪里继续开始。

 
do { 
    strm.avail_in = fread(in, 1, CHUNK, source); 
    if (ferror(source)) { 
        (void)inflateEnd(&strm); 
        return Z_ERRNO; 
    } 
    if (strm.avail_in == 0) 
        break; 
    strm.next_in = in; 
                  
     
    do { 
        strm.avail_out = CHUNK; 
        strm.next_out = out; 
        ret = inflate(&strm, Z_NO_FLUSH); 
        assert(ret != Z_STREAM_ERROR);   
        switch (ret) { 
        case Z_NEED_DICT: 
            ret = Z_DATA_ERROR;      
        case Z_DATA_ERROR: 
        case Z_MEM_ERROR: 
            (void)inflateEnd(&strm); 
            return ret; 
        } 
        have = CHUNK - strm.avail_out; 
        if (fwrite(out, 1, have, dest) != have || ferror(dest)) { 
            (void)inflateEnd(&strm); 
            return Z_ERRNO; 
        } 
    } while (strm.avail_out == 0); 
                  
     
} while (ret != Z_STREAM_END); 
                  
 
(void)inflateEnd(&strm); 
return ret == Z_STREAM_END ? Z_OK : Z_DATA_ERROR;

   内层循环和def()类似,不断调用inflate()直到给定输入被处理完,产生的所有输出都做了。和def()中一样,每次调用inflate()都先提供输出缓冲区,然后可以跑解压引擎了。不需要设置flush参数,因为zlib格式是自终止的(这里不明白)。主要的不同在于,需要注意inflate()的返回值。Z_DATA_ERROR说明读取的数据有错,要么不是zlib格式的,要么某个地方数据有错误。Z_MEM_ERROR说明内存不足。deflate()里的内存在deflateInit()中就被分配好了,而inflate()的内存分配是延迟的,inflate()需要才分配。

   高级程序中deflate()可能使用了字典(用deflateSetDictionary()设置)进行压缩,那么inflate()就需要使用同样的字典进行解压。如果没有,返回Z_DATA_ERROR。和def()中一样,Z_STREAM_ERROR不可能出现。Z_BUF_ERROR也不必特别处理。

   当inflate()没有更多输出的时候,内层循环结束。同样通过看输出缓冲区是否大于0来判断。如果inflate()返回Z_STREAM_END(说明它读到了输入zlib流的尾部,完成了解压和完整性校验,所有的输出已经做了),外层循环结束。内层循环中的inflate()在读到输入文件里有zlib流的尾部时,确保返回Z_STREAM_END,外层循环因此终结。如果不为Z_STREAM_END,继续进行外层循环,读输入文件。

   解压完成后,如果外层循环是因为Z_STREAM_END结束的,则顺利结束,否则就是碰到了Z_DATA_ERROR或者Z_MEM_ERROR。当然,我们要调用inflateEnd()以防止内存泄漏。

   下面的代码创建命令行程序,通过使用上面的函数从stdin输入到stdout输出,并且处理def()和inf()报告的错误。zerr()用来解析来自def()和inf()的可能错误码,打印一条错误消息。注意这只是deflate()和inflate()可能的返回值的子集。

 
void zerr(int ret) 
{ 
    fputs("zpipe: ", stderr); 
    switch (ret) { 
    case Z_ERRNO: 
        if (ferror(stdin)) 
            fputs("error reading stdin\n", stderr); 
        if (ferror(stdout)) 
            fputs("error writing stdout\n", stderr); 
        break; 
    case Z_STREAM_ERROR: 
        fputs("invalid compression level\n", stderr); 
        break; 
    case Z_DATA_ERROR: 
        fputs("invalid or incomplete deflate data\n", stderr); 
        break; 
    case Z_MEM_ERROR: 
        fputs("out of memory\n", stderr); 
        break; 
    case Z_VERSION_ERROR: 
        fputs("zlib version mismatch!\n", stderr); 
    } 
}

   这里是main()程序,用来测试def()和inf()。zpipe命令只是一个简单的从stdin到stdout的压缩管道,如果没指定参数,则做压缩操作,如果指定-d参数,则是一个解压工具。其他情况则打印帮助信息。例如zpipe < foo.txt > foo.txt.z执行压缩,zpipe -d < foo.txt.z > foo.txt执行解压。

 
int main(int arGC, char **argv) 
{ 
    int ret; 
       
     
    SET_BINARY_MODE(stdin); 
    SET_BINARY_MODE(stdout); 
       
     
    if (argc == 1) { 
        ret = def(stdin, stdout, Z_DEFAULT_COMPRESSION); 
        if (ret != Z_OK) 
            zerr(ret); 
        return ret; 
    } 
       
     
    else if (argc == 2 && strcmp(argv[1], "-d") == 0) { 
        ret = inf(stdin, stdout); 
        if (ret != Z_OK) 
            zerr(ret); 
        return ret; 
    } 
       
     
    else { 
        fputs("zpipe usage: zpipe [-d] < source > dest\n", stderr); 
        return 1; 
    } 
}

    linux下编译zpipe程序:先编译zlib库(./configure; make test),再切换到examples/目录,运行gcc -O3  zpipe.c -D_LARGEFILE64_SOURCE=1 -DHAVE_HIDDEN -o zpipe -L.. ../libz.a。

相关文章