*构建CCL树
压缩数据已经成了比特流,基本没有继续压缩的空间。仔细观察literal/length码字长度数列和distance码字长度数列发现,这两个数列中有大量的0存在,就像海绵里的水,挤一挤应该还能继续压缩。所以,我们现在要压缩的就是literal/length码字长度数列和distance码字长度数列。回顾游程编码原理以及性质。
已知literal/length码字长度数列记录的是literal的码字长度和length区间码的码字,distance码字长度数列记录的是distance区间码的码字长度。压缩中规定,literal的码字长度、length区间码的码字长度、distance区间码的码字长度,都不能超过15bits。源码中的注释是这样说的:“Allcodes must not exceed MAX_BITS bits”,MAX_BITS是一个宏,值为15。实际压缩过程中,很多时候码字长度都是超过15的,源码在函数gen_bitlen()中对超过15的情况进行了专门处理,我称该处理过程为哈夫曼树的“嫁接”!所谓嫁接就是说找到码字长度超过15的那一枝,把超过的部分“掰”下来嫁接到深度不够15的分枝上去同时保证新的树枝深度不超过15(有点像把不平衡树变平衡的过程)。嫁接的过程后续源码分析章节还会涉及,这里只要知道这个规定即可。
知道了这个规定,再来看这两个数列:
Literal/length码字长度数列,
0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、3、0、0、0、0、0、0、0、0、0、0、0、6、0、6、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、6、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、4、6、6、5、3、6、0、5、5、0、6、4、5、4、5、0、0、5、4、4、6、6、6、0、5、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、6、5、6、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0
Distance区间码码字长度数列,
0、0、0、2、0、0、0、0、1、0、2、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0
规定要求数列中每一个数必须在闭区间[0, 15]中,这两个数列是符合这个要求的。现在来看怎么压缩这两个数列。
在一个窗口中,某些literal或length可能根本不存在,尤其是对英文文本编码的时候,非ASCII字符就根本不会出现;length较大的值出现概率也很小; distance也类似。所以这两个数列都出现大段的0,而且还是连续的,这种情况是非常正常的,而且并不仅仅限于这个例子。既然有这种“特性”,那么压缩就可以在这种特性上做文章了。
首先,我们可以把这两个数列截短。对于literal/length数列,从第259个数(从0开始的)开始往后,就全部是0了,这些0留着一点用处也没有,所以该数列现在就是,
0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、3、0、0、0、0、0、0、0、0、0、0、0、6、0、6、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、6、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、4、6、6、5、3、6、0、5、5、0、6、4、5、4、5、0、0、5、4、4、6、6、6、0、5、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、0、6、5、6
共有259个数。同理,对于distance数列,从第11个数开始往后,全部截掉,所以该数列现在就是,
0、0、0、2、0、0、0、0、1、0、2
共有11个数。注意这个259和11,这两个个数统计值在后面至关重要。
还记得游程编码吗,我们现在用游程编码来处理这两个数列。对于literal/length数列,经过游程编码,得到结果,
18、21、3、18、0、6、0、6、18、7、6、18、20、4、6、6、5、3、6、0、5、5、0、6、4、5、4、5、0、0、5、4、4、6、6、6、0、5、18、123、6、5、6;
对于distance数列,经过游程编码,得到结果,
17、0、2、17、1、1、0、2;
可以看出,经过游程编码,这两个数列所含有的“项数”已经大大减少了。注意,顿号是我为了方便说明而自己加的,不是数列原本就有的。
在这两个数列中,“码字长度”不会超过15,游程编码的“头”只有16、17、18三个数,游程编码的“游程”类似length区间码表和distance区间码表的“extra bits”部分,可自行编码,所以游程编码之后的这两个数列,刨掉游程部分,每一项的大小都不会超过18,取值范围对应闭区间[0, 18]。对这两个数列的压缩,再次用到哈夫曼编码。因为这两个数列每项大小都在闭区间[0, 18]范围内,所以把这两个数列合成一个数列,构建一棵哈夫曼树即可。注意,我这里说的“合成”并不是真的把这两个数列连在一起变成一个数列,只是放在一起同时统计每个数的出现频率罢了;“游程”可自行编码,所以不参与哈夫曼编码过程。
对这两个数列的哈夫曼编码过程与对LZ77之后结果的哈夫曼编码过程相同,都使用范式哈夫曼编码。不同的是,压缩规定,literal/length区间码和distance区间码的码字长度不超过15bits,而这两个数列中,每一项的码字长度都不能超过7bits,并且没有“区间码”的概念,直接对闭区间[0,18]中的数编码。构建完哈夫曼树,如果发现有叶子节点深度超过7,同样会使用“嫁接”的方式将码字长度限制在7bits。
统计这两个数列中,[0, 18]每个数的出现频率,构建哈夫曼树,过程与前面完全相同。为简便起见,直接使用范式哈夫曼树作为图示,如下,
码字长度数列码表 | ||
码字长度值/游程头 | 码字直接计算结果 | 实际码字(作为压缩结果,从右往左看) |
5 | 00 | 00 |
6 | 01 | 10 |
0 | 100 | 001 |
4 | 101 | 101 |
18 | 110 | 011 |
1 | 11100 | 00111 |
2 | 11101 | 10111 |
3 | 11110 | 01111 |
17 | 11111 | 11111 |
“游程”值的码表如下:
“游程”值码表 | ||||
游程头 | 0的个数 | 游程值 | 占用比特数 | 游程值码字(作为压缩结果) |
18 | 32 | 21 | 7 | 0010101 |
18 | 11 | 0 | 7 | 0000000 |
18 | 18 | 7 | 7 | 0000111 |
18 | 31 | 20 | 7 | 0010100 |
18 | 134 | 123 | 7 | 1111011 |
17 | 3 | 0 | 3 | 000 |
17 | 4 | 1 | 3 | 001 |
提示,只有哈夫曼编码的码字需要按位逆序,其他码字不用。
得到这两张码表,现在将literal/length码字长度数列和distance码字长度数列转换成比特流,
18(011)21(0010101)3(01111)18(011)0(0000000)6(10)0(001)6(10)18(011)7(0000111)
6(10)18(011)20(0010100)4(101)6(10)6(10)5(00)3(01111)6(10)0(001)5(00)5(00)0(001)
6(10)4(101)5(00)4(101)5(00)0(001)0(001)5(00)4(101)4(101)6(10)6(10)6(10)0(001)5(00)
18(011)123(1111011)6(10)5(00)6(10)
和
17(11111)0(000)2(10111)17(11111)1(001)1(00111)0(001)2(10111);
但这并不是最终的比特流,必须将其按规则放到一个个“字节”中,这个规则与前面对实际数据的压缩相同,结果如下,
(将literal/length码字长度数列比特流让出一位,不足一个字节先不管,原因后面介绍)
1
01010101 11011110 00000000 11000110 00011101 00011100 10100101 11001010
00110011 00010000 01001011 01001001 01101000 11010101 10110000 10111101
1000
和
(将distance码字长度数列比特流让出四位,不足一个字节先不管,原因后面分析)
1111
01110001 01111111 01001110
101110
对应的十六进制分别是
55 DE 00 C6 1D 1C A5 CA
33 10 4B 49 68 D5 B0 BD
和
71 7F 4E
这是我们人工压缩的结果,看看压缩工具的结果,如下图,
现在,我们已经得到了literal/length码字长度比特流、distance码字长度比特流和实际压缩数据比特流,不足一个字节的部分,我用“*”暂时占位,这三个比特流分别为,
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1*******
01010101 11011110 00000000 11000110 00011101 00011100 10100101 11001010
00110011 00010000 01001011 01001001 01101000 11010101 10110000 10111101
****1000
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1111****
01110001 01111111 01001110
**101110
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11******
11100110 10101000 10110100 00101000 11001101 10011000 00100000 01111011
01111011 10111000 01000100 01100110 00100111 01100100 01010110 11000101
00000110 10101110 01100010 11001001 11010001 01001110 10111100 10100101
01010011 11101001 00001110 00011111 00011110 10101100 11010011 11111110
00010011 10010001 11000101 01110000 01010000 11110101 01010110 11101001
11101011 00000111
+++++++++++++++++++++++++++++++++++++++++++++++++++++++=+++++++++++
到这个时候,压缩一共用了三次哈夫曼编码,前两次用来编码实际压缩数据,最后一次用来编码码字长度数列,该码字长度数列包括literal/length码字长度数列和distance码字长度数列。对实际数据的哈夫曼编码,要求码字长度不能超过15bits(length和distance的区间码码字长度,而不是具体length值和distance值的码字长度;literal是一对一的,所以就是literal的码字长度);对码字长度数列的哈夫曼编码,要求码字长度不能超过7bits。
从上面的哈夫曼编码码表可以看出,所有哈夫曼编码的码字,作为实际压缩结果输出时都需要一个“逆序”的过程。非哈夫曼编码的码字或输出比特,比如extra bits的编码,“游程”的编码,标明是动态哈夫曼编码还是静态哈夫曼编码还是存储的那两比特等,都不需要按位逆序。
比特流真正作为压缩结果输出时是以字节为单位的,所以必须把比特流填充到一个个字节中去。填充的过程,要从字节的低位开始填起,如果一个字节不够,就将码字剩余部分填充到下一个字节中,当然,也是从下一个字节的低位开始填起。
压缩结束了吗,不,还差临门一脚。literal/length码字长度比特流、distance码字长度比特流记录实际压缩数据比特流的码字长度信息,可谁来记录literal/length码字长度比特流和distance码字长度比特流的码字长度信息?接下来我们分析如何记录literal/length码字长度比特流和distance码字长度比特流的码字长度信息。
我们已经知道,这两条比特流使用同一棵哈夫曼树,所以只要把这“一棵”哈夫曼树每个叶子节点的深度记录下来即可。再回顾一下这棵树,如下图所示,
叶子节点 | 深度(码字长度,不超过7bits) |
5 | 2 bits |
6 | 2 bits |
0 | 3 bits |
4 | 3 bits |
18 | 3 bits |
1 | 5 bits |
2 | 5 bits |
3 | 5 bits |
17 | 5 bits |
码字长度的记录方式都是一样的,这个也不例外。游程编码之后的结果,刨掉无需哈夫曼编码的“游程”,剩下的数都在闭区间[0, 18]范围内,所以该码字长度序列就是,
3、5、5、5、3、2、2、0、0、0、0、0、0、0、0、0、0、5、3,
每个数在该序列中的“顺序(从0开始)”就是闭区间[0, 18]中的值,即哈夫曼树的叶子节点,而这个数就是对应叶子节点的深度,即码字长度。
在这个数列中,我们看到有很多0出现,这些0没有任何用处,所以压缩会对该数列做一个排序,使这些0项尽可能的排到数列的末尾。排序方式非常简单粗暴,就是将序号这样的数列
0、1、2、3、4、5、6、7、8、9、10、11、12、13、14、15、16、17、18
直接排成这样
16、17、18、0、8、7、9、6、10、5、11、4、12、3、13、2、14、1、15
的数列。把第16个数排到第0个,第17个数排到第1个,第……等。这种排序方式与压缩内容无关,无论什么内容,到了这一步,都按这个顺序重新排列。至于为什么这样,我猜可能是基于某种统计结果,发现这样排序对于大多数待压缩内容来讲可以把码字长度为0的项排到数列的后面。
按照这个顺序,将码字长度序列
3、5、5、5、3、2、2、0、0、0、0、0、0、0、0、0、0、5、3,
重新排序后就是
0、5、3、3、0、0、0、2、0、2、0、3、0、5、0、5、0、5、0,
从这里可以看出这个排序对于本例的支持并不好,因为只有一个0排在该数列的后面。
与literal/length码字长度数列和distance码字长度数列的处理方式相同,我们把重排后的这个数列(可以称其为“literal/length码字长度数列和distance码字长度数列的码字长度数列^o^”)末尾的0项全部去掉。放到这个例子里,也就去掉末尾的那一个0而已。得到新的数列,
0、5、3、3、0、0、0、2、0、2、0、3、0、5、0、5、0、5,
该数列共有18项(闭区间[0, 18]共19个数,这里去掉了一个,所以共18个)。
把这个数列转换成比特流。不要怕,这次不用哈夫曼编码了。该数列最多也就有19个数(代表闭区间[0, 18]),每个数都不会超过7(别忘了“对码字长度数列的哈夫曼编码,要求码字长度不能超过7bits”),所以压缩直接规定用三比特对这个数列中的每一个数编码。编码方式异常简单,和extra bits的编码方式相同,0就是000,1就是001,2就是010……7就是111。
将该数列转换成比特流就是(提示,这里不用逆序):
0(000)5(101)3(011)3(011)0(000)0(000)0(000)2(010)0(000)
2(010)0(000)3(011)0(000)5(101)0(000)5(101)0(000)5(101)
把它们放到一个个字节中,才能成为真正的比特流,即
(空出一位,原因后面解释)
1101000
00001101 10000000 00100000 00001100 01000101
1010001
对应十六进制为
0D 80 20 0C 45
这是我们人工压缩的结果,压缩工具的压缩结果如下图所示,
现在,我们一共得到四条比特流,分别是“literal/length码字长度数列和distance码字长度数列的码字长度比特流(即,这两个数列的码字长度比特流)”、literal/length码字长度比特流、distance码字长度比特流和实际压缩数据比特流,不足一个字节的部分,我用“*”暂时占位,这四个比特流分别为,
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1101000*
00001101 10000000 00100000 00001100 01000101
*1010001
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1*******
01010101 11011110 00000000 11000110 00011101 00011100 10100101 11001010
00110011 00010000 01001011 01001001 01101000 11010101 10110000 10111101
****1000
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1111****
01110001 01111111 01001110
**101110
++++++++++++++++++++++++++++++++++++++++++++++++++=+++++++++++++++
11******
11100110 10101000 10110100 00101000 11001101 10011000 00100000 01111011
01111011 10111000 01000100 01100110 00100111 01100100 01010110 11000101
00000110 10101110 01100010 11001001 11010001 01001110 10111100 10100101
01010011 11101001 00001110 00011111 00011110 10101100 11010011 11111110
00010011 10010001 11000101 01110000 01010000 11110101 01010110 11101001
11101011 00000111
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
得到三个关键的数,即259、11、18,分别表示literal/length码字长度数列的有效个数、distance码字长度数列的有效个数、以上两个数列的码字长度数列的有效个数。将这四条比特流咬合在一起,就是,
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1101000*
00001101 10000000 00100000 00001100 01000101
11010001
01010101 11011110 00000000 11000110 00011101 00011100 10100101 11001010
00110011 00010000 01001011 01001001 01101000 11010101 10110000 10111101
11111000
01110001 01111111 01001110
11101110
11100110 10101000 10110100 00101000 11001101 10011000 00100000 01111011
01111011 10111000 01000100 01100110 00100111 01100100 01010110 11000101
00000110 10101110 01100010 11001001 11010001 01001110 10111100 10100101
01010011 11101001 00001110 00011111 00011110 10101100 11010011 11111110
00010011 10010001 11000101 01110000 01010000 11110101 01010110 11101001
11101011 00000111
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
衔接部分的三个字节,十六进制表示为D1、F8、EE,对应的实际压缩结果如下图,
现在这条比特流是不能被解压缩工具解开的!虽然我们记录了所有涉及到的码字长度信息,但是,从该比特流开始到实际压缩数据比特流还有很长的距离,不知道这个距离,就不知道什么时候应该开始用构造好的哈夫曼树去解码实际压缩数据。现在来解释一下这张图,
该压缩块是最后一个压缩块,所以块首部为一比特的1;该压缩块使用动态哈夫曼编码,所以是两比特的10;带上HLIT(00010)、HDIST(01010) 、HCLEN(1110),放到一个个字节中,这部分的比特流就是:
00010101 11001010 1
对应十六进制表示就是
15 CA
压缩工具的压缩结果如下图所示,
(不足一个字节的地方用“*”占位)
00010101 11001010 *******1
将该比特流和前面的比特流咬合在一起,
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
00010101 11001010
*******1
+++++++++++++++++++++++++++++++++++++++++=+++++++++++++++++++
1101000*
00001101 10000000 00100000 00001100 01000101
11010001
01010101 11011110 00000000 11000110 00011101 00011100 10100101 11001010
00110011 00010000 01001011 01001001 01101000 11010101 10110000 10111101
11111000
01110001 01111111 01001110
11101110
11100110 10101000 10110100 00101000 11001101 10011000 00100000 01111011
01111011 10111000 01000100 01100110 00100111 01100100 01010110 11000101
00000110 10101110 01100010 11001001 11010001 01001110 10111100 10100101
01010011 11101001 00001110 00011111 00011110 10101100 11010011 11111110
00010011 10010001 11000101 01110000 01010000 11110101 01010110 11101001
11101011 00000111
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
衔接处的十六进制值为D1,实际压缩结果如下图所示,
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
00010101 11001010 11010001 00001101 10000000 00100000 00001100 01000101
11010001 01010101 11011110 00000000 11000110 00011101 00011100 10100101
11001010 00110011 00010000 01001011 01001001 01101000 11010101 10110000
10111101 11111000 01110001 01111111 01001110 11101110 11100110 10101000
10110100 00101000 11001101 10011000 00100000 01111011 01111011 10111000
01000100 01100110 00100111 01100100 01010110 11000101 00000110 10101110
01100010 11001001 11010001 01001110 10111100 10100101 01010011 11101001
00001110 00011111 00011110 10101100 11010011 11111110 00010011 10010001
11000101 01110000 01010000 11110101 01010110 11101001 11101011 00000111
+++++++++++++++++++++++++++++++++++++++++++++++++++++=+++++++++++
这才是一个完整的压缩块。如果带上gzip文件头和尾,就组成一个完整的gzip压缩文件了。
注意:不论几个压缩块,整条比特流中间任何位置都绝对不能有间断的地方。比如,两个压缩块之间隔了几个比特,这样的比特流是无法被正常解压的,因为解压缩工具不会意识到那是“间隔”的几个比特,只会对照码表继续解压,当发现无法解压时就会产生异常。这一点在HTTP流压缩中尤为重要。
整个动态哈夫曼编码过程结束。上面整个压缩过程都是我们人工按照压缩原理进行的,可以看到,通过逐步推导,人工压缩结果与压缩工具的压缩结果是完全相同的,这个过程证明我们的分析基本是正确的。以上整个人工压缩过程,就是gzip压缩的实际流程,压缩原理不过如此吧。