为什么Java和Go的gzip得到不同的结果?

问题描述 投票:0回答:2

首先是我的Java版本:

string str = "helloworld";
ByteArrayOutputStream localByteArrayOutputStream = new ByteArrayOutputStream(str.length());
GZIPOutputStream localGZIPOutputStream = new GZIPOutputStream(localByteArrayOutputStream);
localGZIPOutputStream.write(str.getBytes("UTF-8"));
localGZIPOutputStream.close();
localByteArrayOutputStream.close();
for(int i = 0;i < localByteArrayOutputStream.toByteArray().length;i ++){
    System.out.println(localByteArrayOutputStream.toByteArray()[i]);
}

输出为:

31 -117 8 0 0 0 0 0 0 0 -53 72 -51 -55 -55 47 -49 47 -54 73 1 0 -83 32 -21 -7 10 0 0 0

然后是Go版本:

var gzBf bytes.Buffer
gzSizeBf := bufio.NewWriterSize(&gzBf, len(str))
gz := gzip.NewWriter(gzSizeBf)
gz.Write([]byte(str))
gz.Flush()
gz.Close()
gzSizeBf.Flush()
GB := (&gzBf).Bytes()
for i := 0; i < len(GB); i++ {
    fmt.Println(GB[i])
}

输出:

31 139 8 0 0 9 110 136 0 255 第202章 72 205 201 201 47 207 47 第202章 73 1 0 0 0 255 255 1 0 0 255 255 173 32 235 249 10 0 0 0

为什么?

一开始我以为可能是这两种语言的字节读取方式不同造成的。但我注意到 0 永远无法转换为 9。而且

[]byte
的大小也不同。

我写错代码了吗?有没有办法让我的Go程序得到与Java程序相同的输出?

谢谢!

java go gzipoutputstream
2个回答
18
投票

首先,Java 中的

byte
类型是有符号的,它的范围是
-128..127
,而 Go 中
byte
uint8
的别名,范围是
0..255
。因此,如果您想比较结果,则必须将负 Java 值移动
256
(添加
256
)。

提示:要以无符号方式显示 Java

byte
值,请使用:
byteValue & 0xff
,使用
int
的 8 位作为
byte
中的最低 8 位,将其转换为
int
。或者更好:以十六进制形式显示两个结果,这样您就不必关心符号性...

即使你进行轮班,你仍然会看到不同的结果。这可能是由于不同语言的默认压缩级别不同。请注意,虽然 Java 和 Go 中的默认压缩级别都是

6
,但这并没有指定,并且允许不同的实现选择不同的值,并且在未来的版本中也可能会改变。

即使压缩级别相同,您仍然可能会遇到差异,因为 gzip 基于 LZ77Huffman 编码,它使用基于频率(概率)构建的树来决定输出代码以及是否有不同的输入字符或者位模式具有相同的频率,它们之间分配的代码可能会有所不同,而且多个输出位模式可能具有相同的长度,因此可能会选择不同的一个。

如果您想要相同的输出,唯一的方法是(参见下面的注释!)使用0压缩级别(根本不压缩)。在 Go 中使用压缩级别

gzip.NoCompression
,在 Java 中使用
Deflater.NO_COMPRESSION

Java:

GZIPOutputStream gzip = new GZIPOutputStream(localByteArrayOutputStream) {
    {
        def.setLevel(Deflater.NO_COMPRESSION);
    }
};

去:

gz, err := gzip.NewWriterLevel(gzSizeBf, gzip.NoCompression)

但我不会担心不同的输出。 Gzip 是一个标准,即使输出不一样,您仍然可以使用任何用于压缩数据的 gzip 解码器来解压缩输出,并且解码后的数据将完全相同。

以下是简化的扩展版本:

这并不重要,但你的代码不必要地复杂。您可以像这样简化它们(这些版本还包括设置 0 压缩级别和转换负 Java

byte
值):

Java版本:

ByteArrayOutputStream buf = new ByteArrayOutputStream();
GZIPOutputStream gz = new GZIPOutputStream(buf) {
    { def.setLevel(Deflater.NO_COMPRESSION); }
};
gz.write("helloworld".getBytes("UTF-8"));
gz.close();
for (byte b : buf.toByteArray())
    System.out.print((b & 0xff) + " ");

Go版本:

var buf bytes.Buffer
gz, _ := gzip.NewWriterLevel(&buf, gzip.NoCompression)
gz.Write([]byte("helloworld"))
gz.Close()
fmt.Println(buf.Bytes())

注意事项:

gzip 格式允许在输出中包含一些额外的字段(标头)。

在 Go 中,这些由

gzip.Header
类型表示:

type Header struct {
    Comment string    // comment
    Extra   []byte    // "extra data"
    ModTime time.Time // modification time
    Name    string    // file name
    OS      byte      // operating system type
}

并且可以通过

Writer.Header
结构体字段访问它。 Go 设置并插入它们,而 Java 不会(将标头字段保留为零)。因此,即使您在两种语言中将压缩级别设置为 0,输出也不会相同(但“压缩”数据在两种输出中都会匹配)。

不幸的是,标准 Java 没有提供设置/添加这些字段的方法/接口,并且 Go 也没有提供在输出中填充

Header
字段的可选功能,因此您将无法生成准确的输出。

一种选择是使用支持设置这些字段的第 3 方 GZip Java 库。 Apache Commons Compress 就是这样一个例子,它包含一个

GzipCompressorOutputStream
类,该类具有一个允许传递
GzipParameters
实例的构造函数。这个
GzipParameters
相当于
gzip.Header
结构。只有使用这个才能生成准确的输出。

但正如前面提到的,生成精确的输出没有现实价值。


9
投票

根据 RFC 1952,GZip 文件头的结构如下:

+---+---+---+---+---+---+---+---+---+---+
|ID1|ID2|CM |FLG|     MTIME     |XFL|OS | (more-->)
+---+---+---+---+---+---+---+---+---+---+

查看您提供的输出,我们有:

                          |    Java |          Go
ID1                       |      31 |          31
ID2                       |     139 |         139
CM (compression method)   |       8 |           8
FLG (flags)               |       0 |           0
MTIME (modification time) | 0 0 0 0 | 0 9 110 136
XFL (extra flags)         |       0 |           0
OS (operating system)     |       0 |         255

所以我们可以看到Go正在设置头的修改时间字段,并将操作系统设置为

255
(未知)而不是
0
(FAT文件系统)。在其他方面,它们表明文件是以相同的方式压缩的。

一般来说,这些差异是无害的。如果您想确定两个压缩文件是否相同,那么您应该真正比较文件的解压缩版本。

© www.soinside.com 2019 - 2024. All rights reserved.