DeflateStream无法解压数据(第一次)

8

这里有一个奇怪的问题。我有一个方法可以将Base64编码的压缩字符串转换回原始数据:

public static string Base64Decompress(string base64data)
{
    byte[] b = Convert.FromBase64String(base64data);
    using (var orig = new MemoryStream(b))
    {
        using (var inflate = new MemoryStream())
        {
            using (var ds = new DeflateStream(orig, CompressionMode.Decompress))
            {
                ds.CopyTo(inflate);
                return Encoding.ASCII.GetString(inflate.ToArray());
            }
        }
    }
}

如果我不添加第二个调用ds.CopyTo(inflate),则会返回一个空字符串(什么鬼?)

   ...
            using (var ds = new DeflateStream(orig, CompressionMode.Decompress))
            {
                ds.CopyTo(inflate);
                ds.CopyTo(inflate);
                return Encoding.ASCII.GetString(inflate.ToArray());
            }
   ...

(Flush/Close/Disposeds上没有影响。)

DeflateStream为什么在第一次调用时会复制0字节?我也尝试用Read()循环,但它在第一次调用时也返回零,然后在第二次调用时才能工作。


更新:这是我用来压缩数据的方法。

public static string Base64Compress(string data, Encoding enc)
{
    using (var ms = new MemoryStream())
    {
        using (var ds = new DeflateStream(ms, CompressionMode.Compress))
        {
            byte[] b = enc.GetBytes(data);
            ds.Write(b, 0, b.Length);
            ds.Flush();
            return Convert.ToBase64String(ms.ToArray());
        }
    }
}

你确定它是Deflate压缩的,而不是Gzip压缩的吗?你确定在Deflate(或Gzip?)数据前面没有其他东西吗? - nos
@nos:没错。我使用DeflateStream生成了数据。我还使用了一个外部工具来测试我的压缩方法生成的数据,它没有任何抱怨。我也会发布压缩方法。 - josh3736
我以前见过这种情况,如果压缩流的最后一个块没有完全写出(即不完整);第一次调用读/复制将失败,随后的调用将访问数据。我会看看能否找到一些参考资料... - Chris Baxter
DeflateStream必须关闭才能写入最终块;请参见更新的答案。 - Chris Baxter
@josh3736:我面临着相同的问题。在将输入文件流复制到DeflateCompress之后,如果输入文件大小小于100kb,则内存流的大小为0kb。 - Saroop Trivedi
显示剩余2条评论
1个回答

7

当压缩字节不完整时发生这种情况(即未写出所有块)。

如果我使用您的Base64Compress,并使用以下Decompress方法,我将收到一个InvalidDataException异常,其中包含消息“未知块类型。流可能已损坏。”

解压缩

public static string Decompress(Byte[] bytes)
{
  using (var uncompressed = new MemoryStream())
  using (var compressed = new MemoryStream(bytes))
  using (var ds = new DeflateStream(compressed, CompressionMode.Decompress))
  {
    ds.CopyTo(uncompressed);
    return Encoding.ASCII.GetString(uncompressed.ToArray());
  }
}

请注意,当使用以下压缩方法时,所有内容都按预期工作。
public Byte[] Compress(Byte[] bytes)
{
  using (var memoryStream = new MemoryStream())
  {
    using (var deflateStream = new DeflateStream(memoryStream, CompressionMode.Compress))
      deflateStream.Write(bytes, 0, bytes.Length);

    return memoryStream.ToArray();
  }
}

更新

糟糕,我太傻了...在释放DeflateStream之前不能将内存流转换为ToArray(因为Flush实际上没有实现(而Deflate/GZip会压缩数据块);最后一个数据块只有在关闭/释放时才会被写入。

请重新编写压缩:

public static string Base64Compress(string data, Encoding enc)
{
  using (var ms = new MemoryStream())
  {
    using (var ds = new DeflateStream(ms, CompressionMode.Compress))
    {
      byte[] b = enc.GetBytes(data);
      ds.Write(b, 0, b.Length);
    }

    return Convert.ToBase64String(ms.ToArray());
  }
} 

是的,这就是问题所在。从技术上讲,你应该使用带有leaveOpen参数的DeflateStream()重载,并传递true。如果没有它,关闭/释放DeflateStream也会释放MemoryStream。现在这不会引起问题是偶然的。 - Hans Passant
@Hans,绝对不是一个坏主意,虽然释放MemoryStream并不会清除缓冲区;而只是防止在MemoryStream上进行进一步的读写。因此,在MemoryStream上存在重复的Dispose,通过ToArray访问的字节仍然是可访问的。 - Chris Baxter
是的。我因为指出释放MemoryStream是愚蠢的而在SO上受到了大量的批评。很高兴能够回击一些 :) - Hans Passant

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