天天看點

淺談在c#中使用Zlib壓縮與解壓的方法

作者:Compasslg

介紹

近期用c#開發一個遊戲的存檔編輯工具需要用 Zlib 标準的 Deflate 算法對資料進行解壓。 在 StackOverflow 上逛了一圈,發現 c# 比較常用到的方式是微軟提供的 System.IO.Compression, zlib.net, 以及 ICSharpCode 的SharpZipLib。我簡單的測試和包裝了一下,便在這裡分享一下成果以及我個人的看法。

System.IO.Compression

通常來說,使用c#開發時能用微軟官方提供的工具就盡量用,一個是bug會比較少,維護會比較穩定。此外,官方提供的方案往往在優化上也會高于第三方工具。

雖然在.NET Framework 4.5 開始 System.IO.Compression.DeflateStream 也使用Zlib進行Deflate格式的壓縮與解壓了,但經過測試其壓縮和解壓結果與其他Zlib庫有所不同.

仔細觀察就會發現,用 DeflateStream 壓縮後的資料開頭比Zlib壓縮的資料少兩個位元組,結尾比Zlib少四個位元組; 這種輸出格式叫做 Raw Deflate 。

經過查證,C# 提供的 DeflateStream隻能壓縮成或者解壓這種Raw Deflate, 而不能處理标準的 Zlib Deflate 格式 (不過據說可以自己生成); 但反過來,Zlib 可以處理或生成這種不包含頭尾資料的Raw Deflate.

當然,你也可以選擇手動添加 header 和 trailer. 具體怎麼添加可以閱讀文末連結的參考資料,由于不是特别重要,我就偷個懶了。

以下是我使用此方法簡單包裝的壓縮與解壓資料的代碼:

// 使用System.IO.Compression進行Deflate壓縮
public static byte[] MicrosoftCompress(byte[] data)
{
    MemoryStream uncompressed = new MemoryStream(data); // 這裡舉例用的是記憶體中的資料;需要對文本進行壓縮的話,使用 FileStream 即可
    MemoryStream compressed = new MemoryStream();
    DeflateStream deflateStream = new DeflateStream(compressed, CompressionMode.Compress); // 注意:這裡第一個參數填寫的是壓縮後的資料應該被輸出到的地方
    uncompressed.CopyTo(deflateStream); // 用 CopyTo 将需要壓縮的資料一次性輸入;也可以使用Write進行部分輸入
    deflateStream.Close();  // 在Close中,會先後執行 Finish 和 Flush 操作。
    byte[] result = compressed.ToArray();
    return result;
}
           
// 使用System.IO.Compression進行Deflate解壓
public static byte[] MicrosoftDecompress(byte[] data)
{
    MemoryStream compressed = new MemoryStream(data);
    MemoryStream decompressed = new MemoryStream();
    DeflateStream deflateStream = new DeflateStream(compressed, CompressionMode.Decompress); // 注意: 這裡第一個參數同樣是填寫壓縮的資料,但是這次是作為輸入的資料
    deflateStream.CopyTo(decompressed); 
    byte[] result = decompressed.ToArray();
    return result;
}
           

zlib.net

zlib.net是一個非常小體量的開源的第三方工具。經過本人有限的研究和了解,這個庫其實更像是一個半成品,許多功能都不完善,不過優點在于非常輕巧,而且與c++端使用 boost::iostreams::zlib 效果相同。

以下是用 zlib.net 提供的 ZOutputStream 類來壓縮資料的代碼

public static byte[] ZLibDotnetCompress(byte[] data)
{
    MemoryStream compressed = new MemoryStream();
    ZOutputStream outputStream = new ZOutputStream(compressed, 2); 
    outputStream.Write(data, 0, data.Length); // 這裡采用的是用 Write 來寫入需要壓縮的資料;也可以采用和上面一樣的方法
    outputStream.Close();
    byte[] result = compressed.ToArray();
    return result;
}
           

以下是用zlib.net 提供的 ZInputStream 類來解壓資料的代碼

public static byte[] ZLibDotnetDecompress(byte[] data, int size)
{
    MemoryStream compressed = new MemoryStream(data);
    ZInputStream inputStream = new ZInputStream(compressed);
    byte[] result = new byte[size];   // 由于ZInputStream 繼承的是BinaryReader而不是Stream, 隻能提前準備好輸出的 buffer 然後用 read 擷取定長資料。
    inputStream.read(result, 0, result.Length); // 注意這裡的 read 首字母是小寫
    return result;
}
           

你需要通過read來擷取解壓後的資料,同時你要在調用其解壓的方法時提前提供好外部的buffer用于儲存輸出的資料,這個buffer的大小就是一個問題了。

如果打算使用這個的話,建議除了儲存壓縮的資料以外,在不會被壓縮的位置添加壓縮前大小的資料。

但總體來說,個人不建議使用這個工具。

https://github.com/zyborg/zlib.net

http://www.componentace.com/zlib_.NET.htm

SharpZipLib

我最終選擇使用的是 SharpZipLib. (編輯:當時沒做速度測試,且我需要解壓的檔案不是太大,速度也不是很重要,否則的話不推薦選擇這個方案。。。)

ICSharpCode 不愧是開發了 ILSpy 的團隊,SharpZipLib 在提供強大的功能的同時,使用也很友善。限于主題,這裡隻讨論用 Deflate 格式來壓縮資料流。

簡單來說,你需要做的就是通過 DeflaterOutputStream 來壓縮,InflaterInputStream 來解壓,而除了壓縮和解壓分在兩個不同的類以外,其他的操作方式和 System.IO.Compression.DeflateStream 可以做到完全一樣。

而且其壓縮和解壓的結果和直接使用Zlib官方的庫一模一樣,開發輔助其他程式的工具時不用擔心頭尾資料的問題,算是非常省事了。

以下是我使用該方案簡單包裝的方法:

public static byte[] SharpZipLibCompress(byte[] data)
{
    MemoryStream compressed = new MemoryStream();
    DeflaterOutputStream outputStream = new DeflaterOutputStream(compressed);
    outputStream.Write(data, 0, data.Length);
    outputStream.Close();
    return compressed.ToArray();
}
           
public static byte[] SharpZipLibDecompress(byte[] data)
{
    MemoryStream compressed = new MemoryStream(data);
    MemoryStream decompressed = new MemoryStream();
    InflaterInputStream inputStream = new InflaterInputStream(compressed);
    inputStream.CopyTo(decompressed);
    return decompressed.ToArray();
}
           

速度對比

為了對比幾種方法在壓縮與解壓效率上的優劣,我準備了兩組資料做了一個簡單的測試。

第一組為短資料,是一個簡單的字元串 "this is just a string for testing, see how this compression thing works."

第二組為長資料,是在網上下載下傳到的英文版的 《冰與火之歌:權利的遊戲》txt文本,大小約1.7mb。

我分别用每個方法壓縮和解壓短資料1000次,長資料100次, 最終的結果如下:

Length of Short Data: 144
Length of Long Data: 1685502

============================================
Compress and decompress with Microsoft Zlib Compression (1000 times): 54
Compress and decompress with Microsoft Zlib Compression (long data 100 times): 7924

============================================
Compress and decompress with Zlib.net Compression (1000 times): 254
Compress and decompress with Zlib.net Compression (long data 100 times): 9924

============================================
Compress and decompress with SharpZipLib Compression (1000 times): 442
Compress and decompress with SharpZipLib Compression (long data 100 times): 26782
           

顯而易見的,無論是長資料還是短資料的壓縮與解壓,System.IO.Compression中提供的方法都優于另外兩種方法。

Zlib.net在速度上的劣勢不明顯,而同樣的算法SharpZipLib要花兩到三倍的時間。

總結

最終,不出所料的,微軟官方提供的 System.IO.Compression 中的方法在速度上有着明顯的優勢;雖然不會提供Deflate的頭尾資訊,但可以想辦法自己生成,而且這一缺點基本上是可以完全忽略的。 Zlib.net 雖然在速度上表現也不錯,同時也會生成Deflate壓縮的頭尾資訊,但因為其包裝比較潦草,使用起來相對不友善。而 SharpZipLib 很可惜,雖然其他各方面都很友善,但速度上的缺陷相當緻命,隻能在一定需要 Deflate 而非 RawDeflate 或者使用的.Net Framework早于4.5的時候(且運作中時間消耗不重要)偷懶的用一用了。

參考與延申

關于Zlib

https://zlib.net/

關于 Deflate 和 Raw Deflate

https://stackoverflow.com/questions/37845440/net-deflatestream-vs-linux-zlib-difference

https://www.ietf.org/rfc/rfc1950.txt

https://www.ietf.org/rfc/rfc1951.txt

關于CSharp System.IO.Compression.DeflateStream

https://docs.microsoft.com/en-us/dotnet/api/system.io.compression.deflatestream?view=net-5.0

開發者之一 Mark Adler 在 StackOverflow 上的回答

deflate

compress

函數的差別

https://stackoverflow.com/questions/10166122/zlib-differences-between-the-deflate-and-compress-functions/10168441#10168441

如何手動添加 header 和 trailer

https://stackoverflow.com/questions/39939869/data-format-for-system-io-compression-deflatestream