天天看點

zlib庫剖析(3):使用示例zpipe.c

本文整理自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()的輸入輸出緩沖區。

/* Compress from file source to file dest until EOF on source.
   def() returns Z_OK on success, Z_MEM_ERROR if memory could not be
   allocated for processing, Z_STREAM_ERROR if an invalid compression
   level is supplied, Z_VERSION_ERROR if the version of zlib.h and the
   version of the library linked do not match, or Z_ERRNO if there is
   an error reading or writing the files. */ 
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方法可重入。

/* allocate deflate state */ 
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必須由應用程式自己更新)。

/* compress until end of file */ 
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; 
                            
    /* run deflate() on input until output buffer not full, finish
       compression if all of source has been read in */ 
    do { 
        strm.avail_out = CHUNK; 
        strm.next_out = out; 
        ret = deflate(&strm, flush);    /* no bad return value */ 
        assert(ret != Z_STREAM_ERROR);  /* state not clobbered */ 
        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);     /* all input will be used */ 
                            
    /* done when last data in file processed */ 
} while (flush != Z_FINISH); 
assert(ret == Z_STREAM_END);        /* stream will be complete */ 
                            
/* clean up and return */ 
(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,表示沒有提供輸入資料。

/* Decompress from file source to file dest until stream ends or EOF.
   inf() returns Z_OK on success, Z_MEM_ERROR if memory could not be
   allocated for processing, Z_DATA_ERROR if the deflate data is
   invalid or incomplete, Z_VERSION_ERROR if the version of zlib.h and
   the version of the library linked do not match, or Z_ERRNO if there
   is an error reading or writing the files. */ 
int inf(FILE *source, FILE *dest) 
{ 
    int ret; 
    unsigned have; 
    z_stream strm; 
    unsigned char in[CHUNK]; 
    unsigned char out[CHUNK]; 
                       
    /* allocate inflate state */ 
    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流後面的哪裡繼續開始。

/* decompress until deflate stream ends or end of file */ 
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; 
                  
    /* run inflate() on input until output buffer not full */ 
    do { 
        strm.avail_out = CHUNK; 
        strm.next_out = out; 
        ret = inflate(&strm, Z_NO_FLUSH); 
        assert(ret != Z_STREAM_ERROR);  /* state not clobbered */ 
        switch (ret) { 
        case Z_NEED_DICT: 
            ret = Z_DATA_ERROR;     /* and fall through */ 
        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); 
                  
    /* done when inflate() says it's done */ 
} while (ret != Z_STREAM_END); 
                  
/* clean up and return */ 
(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()以防止記憶體洩漏。

/* report a zlib or i/o error */ 
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); 
    } 
}      
/* compress or decompress from stdin to stdout */ 
int main(int argc, char **argv) 
{ 
    int ret; 
       
    /* avoid end-of-line conversions */ 
    SET_BINARY_MODE(stdin); 
    SET_BINARY_MODE(stdout); 
       
    /* do compression if no arguments */ 
    if (argc == 1) { 
        ret = def(stdin, stdout, Z_DEFAULT_COMPRESSION); 
        if (ret != Z_OK) 
            zerr(ret); 
        return ret; 
    } 
       
    /* do decompression if -d specified */ 
    else if (argc == 2 && strcmp(argv[1], "-d") == 0) { 
        ret = inf(stdin, stdout); 
        if (ret != Z_OK) 
            zerr(ret); 
        return ret; 
    } 
       
    /* otherwise, report usage */ 
    else { 
        fputs("zpipe usage: zpipe [-d] < source > dest\n", stderr); 
        return 1; 
    } 
}      

繼續閱讀