前段时间公司的web服务器需要增加代理的gz解压功能。刚好手里有一些基础库,试着写了gz的解码函数。
开始以为很简单,后面读了不少相关的文档才发觉还是比较复杂的,花了不少时间才理清楚。
本文就对gz文件格式作一个完整的描述,因为复杂的部分主要是deflate(rfc1951)格式包的实现,所以本文也可以作为
deflate格式的参考简要说明。
为了本文理解上的准确性和rfc1951的标准的一致性,
定义如下的一些术语。
1, 符号 指输入数据的最小分隔单位,一般指0-255的字符。
2, 回退距离(backward distance) 指LZ77往后回退的距离大小,大小从1开始。
3, 匹配长度(length) 指LZ77中符号串在和字典窗口中相等的最长大小。
4, 匹配对 指<匹配长度, 回退距离>的lZ77的记录对,LZ77算法中匹配结果要么是一个匹配中要么是一个字符。
5, 编码表 指符号和编码值(可以是固定值,也可以是huffman编码)的一一对应表
首先,deflate文件的压缩原理说明一下:
deflate使用了lz77的字典压缩和huffman压缩两种方式,扩展位编码及长度压缩内容还使用了类似游程编码的处理方法。
lz77压缩通过查找字典窗口字符串使用匹配对或单个字符记录。
然后使用huffman字符对字符或匹配对进行编码。
具体的huffman及lz77编码方法网上有比较多的参考教程。
deflate格式中字典窗口大小固定为32k, 最长匹配大小为258, 最小匹配距离为3。
其它需要的细化的地方我这再作说明。
实现了上述算法之后,便发现huffman压缩后的编码数据,如何保存编码表才是最主要的工作。
rfc1951的deflate实质上是定义了一种LZ77和huffman编码的保存方案。
下面先说明deflate的字符存储方式(这一部分可参考rfc1951相关部分)。
A, 基本的字节存储方案
deflate格式对于编码的保存采用字节为单位,从左到右依次为第0字节,第1字节...。
对于一个字节的字节内容deflate的存储方案有两种,一种可以称为数值型,另一种称作编码型。
对于编码型(包括huffman码和固定的编码)采用从低位到高位的填充方式,对于数值型则使用由高位到低位的方式。
对于小端机器,可认为编码型存储值和内存二进制值相反,数值编码值和内存二进制值一致。
B, 块存储方案
deflate以流块为基本单位,所有的编码后的数据最后以块存储下来,块中的字节则按A中定义方式进行存储。
每一个块以三位的编码头开始
其中第一位指示是否为最后一个块(1表示为最后一块),后两位分别为二进制数值(注意是数值)表示块类型
00表示非压缩块(简写为ncm(noncompression mode)) 无数据压缩
01表示固定编码压缩块(简写为fcm) 固定编码表的压缩,数据中无需加入编码表
10表示带有编码表的块(简写为dcm) huffman编码表压缩,数据中含有huffman编码表信息
ncm,fcm,dcm格式块头分别如下(其中第一位(图示第三位)1表示是最后一块:
+---+---+---+
| 0 0 0/1
+---+---+---+
+---+---+----+
| 0 1 0/1
+---+---+---+
+---+---+---+
| 1 0 0/1
+---+---+---+
ncm模式(非压缩模式)
一个ncm块块头后紧接使用如下存储格式
0 1 2 3 4...
+---+---+---+---+================================+
| LEN | NLEN |... LEN bytes of literal data... |
+---+---+---+---+================================+
其中len是两字节的数值,nlen是对len按位取反的两字节数值,后面是len长度的数据的直接拷贝。
这里说明一下,rfc1951规定中起始位置不一定是整数字节,所以对于ncm块如果从非整数位开始,
完成块头信息后,其它位信息不处理直到下一字节,但实际GNU gzip中需要把后续位置为0.
fcm模式(固定huffman压缩模式)
fcm块使用如下编码表存储编码后的数据
如上所言,LZ77匹配数据要么为一个符号,要么为匹配对。匹配中的最大匹配长度为3-258, 低于3个字节
则无压缩必要。fcm中把符号和长度一起编码,对于符号0-255直接使用编码符号;对于3-258的长度
则使用如下编码表,扩展码+扩展位(数值型)可表示对应范围的长度值。这样长度和符号一起编码为0-285的编码。
其中256表示块的结束。
Extra Extra Extra
Code Bits Length(s) Code Bits Lengths Code Bits Length(s)
---- ---- ------ ---- ---- ------- ---- ---- -------
257 0 3 267 1 15,16 277 4 67-82
258 0 4 268 1 17,18 278 4 83-98
259 0 5 269 2 19-22 279 4 99-114
260 0 6 270 2 23-26 280 4 115-130
261 0 7 271 2 27-30 281 5 131-162
262 0 8 272 2 31-34 282 5 163-194
263 0 9 273 3 35-42 283 5 195-226
264 0 10 274 3 43-50 284 5 227-257
265 1 11,12 275 3 51-58 285 0 258
266 1 13,14 276 3 59-66
然后对0-285使用如下的固定huffman编码表编码
Lit Value Bits Codes
--------- - --- -----
0 - 143 8 00110000-10111111
144 - 255 9 110010000-111111111
256 - 279 7 0000000-0010111
280 - 287 8 11000000-11000111
回退距离则使用同样方法使用如下的距离编码表编码为0-29的值,然后直接作为5位数值存储。
Extra Extra Extra
Code Bits Dist Code Bits Dist Code Bits Distance
---- ---- ---- ---- ---- ------ ---- ---- --------
0 0 1 10 4 33-48 20 9 1025-1536
1 0 2 11 4 49-64 21 9 1537-2048
2 0 3 12 5 65-96 22 10 2049-3072
3 0 4 13 5 97-128 23 10 3073-4096
4 1 5,6 14 6 129-192 24 11 4097-6144
5 1 7,8 15 6 193-256 25 11 6145-8192
6 2 9-12 16 7 257-384 26 12 8193-12288
7 2 13-16 17 7 385-512 27 12 12289-16384
8 3 17-24 18 8 513-768 28 13 16385-24576
9 3 25-32 19 8 769-1024 29 13 24577-32768
一个fcm块块头后紧接如下格式
+--------+--------- +----------------------+------------+
| H/P | H/P | ........... | 0000000 |(最后块部分标志256)
+--------+----- ----+----------------------+------------+
每个存储单元中要么为LZ77符号(不存在的匹配,返回的单个符号)的huffman编码H,其为上述的00110000-10111111、110010000-111111111、0000000-0010111、11000000-11000111中的范围之一,存储顺序在小端机器上和图示表示相反(编码类型)。要么为LZ77匹配对P,其存储格式如下:
+-------+------- +---5--+----------------------------+
| H | E | D | ED |
+------+--------+-------+---------------- -----------+
其中H为匹配长度huffman编码,E为扩展位存储长度扩展信息(0-5位),然后存储距离0-29的距离5位数值编码D,然后是距离扩展信息数值编码ED(0-13位)。
dcm模式(动态huffman压缩模式)
dcm模式块同样使用了fcm中的编码表作为第一步,把符号和长度一起变成0-285的编码,距离变为0-29。
不同于fcm中不需要存储huffman编码表,dcm中把0-285的编码作为符号使用huffman进行编码,生成一个长度/符号huffman树。
接着是deflate不同于普通的huffman树的地方,dcm中并没有存储符号和对应的编码,而是把huffman树作了一个变化,
所谓的huffman正规化/范式化,范式化处理后可以保证huffman按照符号的排序(从小到大)后输出的编码刚好为字典序。
其处理方法可以通过保证右边任意节点深度大于左边所有节点深度来保证或使用rfc1951中的数值方法进行处理。
假设符号按0-285的顺序,存储其对应的编码长度,符号不需要再存储;只需存储对应的长度+符号的总数,便可还原长度符号编码表。同样方法应用于距离编码表。
此时,便是如何存储这两个长度序列,由于长度存储的是huffman编码的码长度值范围为0-15(后面说明部分介绍如何保证长度范围),
为了存储这两个固定长度的长度序列,dcm对长度/符号的长度及距离序列分别进行如下编码(类似游程编码):
0 - 15: 表示码长度0-15.
16: 拷贝先前码长度3-6次 后扩展两位(数值)表示3-6范围
17: 重复长度码'0' 3-10次 后扩展三位(数值)表示3-10范围
18: 重复长度码'0' 11-138次 后扩展七位(数值)表示11-138范围
编码后数值范围为[0-18],然后把两个数列拼接一起(记为CS),记录符号(长度值)总数;同样使用长度+符号总数便可还原。
因此再次使用huffman编码,正规化后huffman树长度范围为0-7(见说明部分长度约束),再经过rfc1951的序列重排(序列为16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15),最后直接使用3位数值存储。
加上原来的距离序列(记为ds)、长度/符号编码序列(记为LS),块头后紧接如下格式:
5位 5位 4位
+---------------+-----------------+----------------+--------------+---------------+---------------+---------------+
|LS长度-257 DS长度-1 CS长度-4 CS LS DS 压缩数据 |
+---------------+------------------+---------------+--------------+---------------+---------------+---------------+
注意:
1,huffman树的正规化实际上在rfc1951中提供一种数值计算的算法,可以使用进行计算。
2,上面的符号/长度编码序列及距离要求码值长度小于15,最后的序列CS长度要求小于7,实际数据处理会出现大于的情况。
可以通过剪去大于分支,移到最下层最低频率/权重数据位置,相应位置节点上移。
实际上这一部分是rfc1951中并没有清晰说明的地方。修改之后的树不再是huffman树,因此不同的实现可能存储结果不同。
3,huffman中距离编码存在1位也没有的情况(无匹配对),但rfc中要求DS存储值为DS长度-1,因此无法存储值为0情况,
实际过程中通过设置对应编码为空实现(rfc中说明), 这一部分可能需要处理一下huffman树。
4,rfc1951中要求ncm中数据最大长度不超过65535(两个字节长度最大值),除此外数据块大小无限制。同时要求数据块
编码树独立,但解压可以使用前个块中字典,可以通过传入字典窗口保证。
完成了deflate包,余下的就是gz格式(rfc1952)的封装,包头很简单
分别为签名(0x1f,0x8b),压缩方法(8),标志位,创建时间,扩展标志,系统类型。
+----+----+----+----+----+----+----+----+----+----+
|ID1|ID2|CM|FLG| MTIME |XFL|OS | (more-->)
+----+----+----+----+----+----+----+----+----+----+
标志位
bit 0 FTEXT 文本模式
bit 1 FHCRC 包头CRC32低位检验值
bit 2 FEXTRA 扩展标志
bit 3 FNAME 文件名
bit 4 FCOMMENT 注释信息
后三位保留使用,全部置为0
如果设置标志依次写入扩展信息然后就是deflate数据体
最后附加4位crc32校验和和文件模2^32值
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| CRC32 | ISIZE |
+---+---+---+---+---+---+---+---+
说明:
deflate格式是比较复杂的一个压缩格式,主要难点在以下几个地方。
1,LZ77压缩算法中的最长距离匹配是比较复杂的一部分,zlib使用了Rabin & Karp算法匹配,加入了
不同策略来平衡速度和压缩比。不同的实现方法下压缩速度和压缩率差异比较大。是数据压缩率和速度差异的根本部分。实现时优先保证这部分的算法效率。
2,huffman树的剪枝(长度范围约束)是比较复杂的一部分,一般数据的超出部分有限,否则处理时间有一定要求。
deflate从使用上过于复杂化了huffman表存储这一部分,因为本身符号数目有限,直接存储要方便的多,
而deflate设计多次使用huffman压缩,同时为了满足位长度要进行修剪,这样树不再是huffman树,可能存储的压缩比也会损失一些。效果存储的最主要部分是lz77的压缩部分,受于限制最长匹配只能258,对于特别长的重复内容压缩效果可能会损失一些。
参考:
https://tools.ietf.org/html/rfc1951
https://tools.ietf.org/html/rfc1952
https://blog.csdn.net/jison_r_wang/article/details/52075870
https://www.cnblogs.com/en-heng/p/4992916.html