GZipStream和DeflateStream无法解压所有字节

36

我需要一种在.NET中压缩图像的方法,于是我研究了使用.NET GZipStream类(或DeflateStream)。但是我发现解压有时候并不成功,有时候图像可以正常解压,而有时候会出现GDI+错误提示图像已损坏。

经过调查,我发现解压没有返回所有压缩的字节。因此,如果我压缩了2257974个字节,有时候只会得到2257870个字节(实际数字)。

最有趣的是,有时候它会工作。因此,我创建了这个小测试方法,仅压缩10个字节,但现在我根本就得不到任何东西。

我尝试使用两种压缩类,即GZipStream和DeflateStream,并仔细检查了可能出现的错误。我甚至尝试将流定位到0并清空所有流,但仍然没有运气。

以下是我的代码:

    public static void TestCompression()
    {
        byte[] test = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

        byte[] result = Decompress(Compress(test));

        // This will fail, result.Length is 0
        Debug.Assert(result.Length == test.Length);
    }

    public static byte[] Compress(byte[] data)
    {
        var compressedStream = new MemoryStream();
        var zipStream = new GZipStream(compressedStream, CompressionMode.Compress);
        zipStream.Write(data, 0, data.Length);
        return compressedStream.ToArray();
    }

    public static byte[] Decompress(byte[] data)
    {
        var compressedStream = new MemoryStream(data);
        var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress);
        var resultStream = new MemoryStream();

        var buffer = new byte[4096];
        int read;

        while ((read = zipStream.Read(buffer, 0, buffer.Length)) > 0) {
            resultStream.Write(buffer, 0, read);
        }

        return resultStream.ToArray();
    }

关于您的评论 - 这归结于不同级别的缓冲区; 如果它们没有全部被清空(按正确顺序),那么您就无法获取所有数据。 - Marc Gravell
注意,例如,我没有在MemoryStream本身上调用Close() - 所以我在某种程度上同意;-p - Marc Gravell
我也会在这方面进行更新... - Marc Gravell
有一次我尝试在内存流上使用Close()方法,但ToArray()方法报错了,这意味着我必须创建一个新的缓冲区,在其中清空流,关闭流,然后返回新的缓冲区。这太麻烦了。 - Bobby Z
1
我忘了说:这是一个措辞精准的问题,因为代码示例非常容易表明:a:它已经破了,b:何时修复。三声欢呼。 - Marc Gravell
2个回答

51

在将想要压缩的所有数据添加完毕后,您需要Close() ZipStream; 它会在内部保留一个未写入的字节缓冲区(即使您使用Flush()),需要写入。

更一般地说,StreamIDisposable,因此您还应该using每个...(是的,我知道MemoryStream不会丢失任何数据,但如果您不养成这个习惯,它会在其他Stream中捉住您)。

public static byte[] Compress(byte[] data)
{
    using (var compressedStream = new MemoryStream())
    using (var zipStream = new GZipStream(compressedStream, CompressionMode.Compress))
    {
        zipStream.Write(data, 0, data.Length);
        zipStream.Close();
        return compressedStream.ToArray();
    }
}

public static byte[] Decompress(byte[] data)
{
    using(var compressedStream = new MemoryStream(data))
    using(var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress))
    using (var resultStream = new MemoryStream())
    { ... }
}

[编辑:评论已更新] 关于不使用像MemoryStream这样的东西-这总是一个有趣的问题,两边都有很多支持者:但最终...

(修辞性质-我们都知道答案...) MemoryStream是如何实现的?它是一个byte[](由.NET拥有)?还是一个内存映射文件(由操作系统拥有)?

你不使用它的原因是因为你让内部实现细节的知识改变了你对公共API编码的方式-也就是说,你打破了封装的法则。公共API说:我是;你“拥有”我;因此,当你使用完我后,你需要Dispose()我。


哇,它像魔法一样奏效了。有趣的是,我没有想到对于内部内存 Close() 是必要的,因为没有涉及到 Windows 资源(同样适用于 using 块——没有它们更加简洁)。 - Bobby Z
1
Close() 在这里不是关于释放 Windows 资源的。GZip 在数据末尾需要一个页脚,而 Close() 告诉 GZipStream 您已经完成了写入数据,并且它应该写出页脚。 - stevemegson
顺便提一下,新的MVC框架有一个Html.Form()方法,可以在using块中使用,自动关闭html表单。(基本上是利用using块的便利性,在某些操作后自动调用某些方法,例如在渲染整个页面后关闭表单) - Bobby Z
4
“我本以为我可以利用额外的性能” - 首先,说实话,我不认为额外的尝试/最终会有很大的区别。 - Marc Gravell
+1 有帮助、信息丰富的回答,加上清晰的代码和对问题的回应 - 在学习压缩流时帮了我大忙。 - J M
显示剩余2条评论

3
此外,请记住 System.IO.Compression 中的 DeflateStream 并未实现最有效的压缩算法。如果您愿意,可以使用一种替代 BCL GZipStream 和 DeflateStream 的方法,它是基于 zlib 代码实现的完全托管库,在这方面的性能比内置的 {Deflate,GZip}Stream 更好。这些流类包含在 DotNetZlib 组件中,可在 DotNetZip 发行版中获得,链接为 http://DotNetZip.codeplex.com/

1
您会很高兴听到,.NET 4.5现在已经将zlib算法编入了BCL中(具有现有压缩数据的向后兼容性)。有关更多信息,请参阅此处:http://msdn.microsoft.com/en-us/magazine/jj133817.aspx。 - pattermeister
1
太棒了!花了很长时间,但我很高兴它终于完成了! - Cheeso

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接