背景
HTTP协议中请求头Accept-Encoding,返回头Content-Encoding,可以使用的候选值为deflate, gzip, br, compress,其中compress设计专利不常使用,br是不是事实上标准,但是也应用广泛。本次使用deflate,gzip为切入点,讨论下zlib压缩。
HTTP协议示例
以下示例使用nodejs官方例子。Nodejs压缩相关也是使用zlib库,这个库后面讨论。
代码
js
const zlib = require('node:zlib');
const http = require('node:http');
const fs = require('node:fs');
const { pipeline } = require('node:stream');
http.createServer((request, response) => {
const raw = fs.createReadStream('index.html');
// Store both a compressed and an uncompressed version of the resource.
response.setHeader('Vary', 'Accept-Encoding');
let acceptEncoding = request.headers['accept-encoding'];
if (!acceptEncoding) {
acceptEncoding = '';
}
const onError = (err) => {
if (err) {
// If an error occurs, there's not much we can do because
// the server has already sent the 200 response code and
// some amount of data has already been sent to the client.
// The best we can do is terminate the response immediately
// and log the error.
response.end();
console.error('An error occurred:', err);
}
};
// Note: This is not a conformant accept-encoding parser.
// See https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3
if (/\bdeflate\b/.test(acceptEncoding)) {
response.writeHead(200, { 'Content-Encoding': 'deflate' });
pipeline(raw, zlib.createDeflate(), response, onError);
} else if (/\bgzip\b/.test(acceptEncoding)) {
response.writeHead(200, { 'Content-Encoding': 'gzip' });
pipeline(raw, zlib.createGzip(), response, onError);
} else if (/\bbr\b/.test(acceptEncoding)) {
response.writeHead(200, { 'Content-Encoding': 'br' });
pipeline(raw, zlib.createBrotliCompress(), response, onError);
} else {
response.writeHead(200, {});
pipeline(raw, response, onError);
}
}).listen(1337);
将上面的代码保存为server.js
并且运行上上面的JS代码,然后使用chrome浏览器访问,使用tshark或者wireshark查看HTTP流量:
运行与保存流量
shell
echo "hello,world" > index.html # 注意这个结尾会有一个'\n'
node server.js # 监听1337,输出index.html
# 如果机器没有GUI,可以使用tshark
tshark -i lo -w zlib.pcapng 'tcp and port 1337' # 保存相关流量
curl -H 'Accept-Encoding: deflate' localhost:1337 # 发送请求,也可以使用chrome等浏览器请求
tshark -r zlib.pcapng -Px -Y http # 查看流量,也可以导入到wireshark查看
# OR
tshark -i lo -Px -Y 'http' 'tcp and port 1337' # 不输出到文件,直接输出到控制台
# OR
# 直接使用wireshark查看更直观
分析流量
一切正常,可以在wireshark或者保存的zlib.pcapng中看到相关的字节码
deflate
bash
54 72 61 6e 73 66 65 72 2d Transfer-
00a0 45 6e 63 6f 64 69 6e 67 3a 20 63 68 75 6e 6b 65 Encoding: chunke
00b0 64 0d 0a 0d 0a 32 0d 0a 78 9c 0d 0a 31 33 0d 0a d....2..x...13..
00c0 cb 48 cd c9 c9 d7 29 cf 2f ca 49 51 e4 02 00 23 .H....)./.IQ...#
00d0 71 04 94 0d 0a 30 0d 0a 0d 0a
可以看到,response使用Transfer-Encoding: chunked的方式传输,可以查看相关文档。 上述代码第三行的0d0a0d0a
后面开始是数据部分。
bash
32 0d 0a # 表示接下来数据是2个字节,32为2的ASCII值
78 9c 0d 0a # 表示deflate或者zlib封装格式的头部数据,刚好2字节
31 33 0d 0a # 表示接下来数据长度为13
# 以下为DEFLATE算法计算出的数据部分,共13个字节(包括deflate或者zlib的trailer,4字节的adler32校验值)
cb 48 cd c9 c9 d7 29 cf 2f ca 49 51 e4 02 00 23 71 04 94
gzip
可以使用同样的方法捕获gzip的流量
shell
curl -H 'Accept-Encoding: gzip' --compressed localhost:1337
以下是gzip相关的流量字节码
erlang
54 72 61 6e 73 66 65 72 2d 45 6e 63 Transfer-Enc
00a0 6f 64 69 6e 67 3a 20 63 68 75 6e 6b 65 64 0d 0a oding: chunked..
00b0 0d 0a 61 0d 0a 1f 8b 08 00 00 00 00 00 00 03 0d ..a.............
00c0 0a 31 37 0d 0a cb 48 cd c9 c9 d7 29 cf 2f ca 49 .17...H....)./.I
00d0 51 e4 02 00 fb ba 78 56 0d 00 00 00 0d 0a 30 0d Q.....xV......0.
00e0 0a 0d 0a
同样的分析格式,只是gzip的头部和尾部不一样,从上述代码的第三行0d0a
开始
bash
61 0d 0a # 表示接下来数据为10个字节,61是a的ASCII值,正好是gzip的头部字节
1f 8b 08 00 00 00 00 00 00 03 # gzip头部10个字节,详细后面
31 37 0d 0a # 表示接下来17个字节数据
# 包括DEFLATE算法压缩数据(9个字节), 4字节原始数据CRC32校验值和4字节原始数据长度
cb 48 cd c9 c9 d7 29 cf 2f ca 49 51 e4 02 00 fb ba 78 56 0d 00 00 00
zlib库
以上分析了HTTP协议中数据压缩相关内容,查看了其他语言压缩相关的处理,底层都是用zlib库。zlib库提供内存压缩和解压缩功能,包括未压缩数据的完整性检查。
概念
Deflate
(通常按早期计算机编程习惯写为DEFLATE
)是同时使用了LZ77
算法与哈夫曼编码(Huffman Coding
)的一个无损数据压缩算法, 已经标准化参考RFC 1951zlib
可以被认为是一种DEFALTE
算法的封装格式, 标准化参考 RFC 1950. 目前zlib
库只支持DEFLATE
算法,zlib
已经成为了事实上的业界标准,标准文档中,zlib
和DEFLATE
常常互换使用, 比如常见的http
协议压缩格式就使用deflate
代表zlib
封装格式(Content-Encoding: defalte
)gzip
也可以认为是一种DEFLATE
算法的封装格式, 标准化参考RFC 1952, 由于gzip
仅用来压缩单个文件,多个文件的压缩归档先合并成tar包,然后再使用gzip进行压缩,最后生成.tar.gz
文件(tarball
或者tar压缩包)。gunzip
是解压缩gzip包命令。其中g
表示graits
(免费)的意思; gzip也是http协议内容压缩的选项之一。zip
格式,也使用DEFLATE算法,相对于gzip来说,可以包容多个文件,但是zip是对每个文件单独压缩,没有利用文件间的冗余信息,压缩率会稍逊于tar压缩包
各种语言相关表达
下面以 hello,world!\n
为数据看下各个版本的实现。
shell
# 原始DEFLATE算法压缩数据
# cb48cdc9c9d729cf2fca4951e40200
# zlib或者defalte格式数据
# 789c cb48cdc9c9d729cf2fca4951e40200 23710494
# 789c 表示认为是zlib或者deflate格式的magic number; 23710494是原始数据的adler32校验数据
# gzip一般使用10字节的头部,尾部由4字节的CRC校验和4字节原始数据大小组成, 不同语言的头部字段有可能不一样,比如日期可以是0等
# 1f8b08000867dd6502ff cb48cdc9c9d729cf2fca4951e40200 fbba78560d000000
# 1f8b08可以认为是gzip的magic number; 00 表示flags, 没有任何附加字段; 0867dd65当前时间戳,如果是文件可能是修改的时间戳;
# 02 DEFLATE算法使用的算法等级(最慢的,04表示最快); ff 表示OS代码(unknown, 03表示unix)
# 后缀fbba7856表示原始数据的CRC校验码; d000000表示原始数据的长度(13)
python主要使用zlib和gzip库
python
import zlib, gzip, datetime
raw_data = b"hello,world!\n"
# 注意二进制数据的大小端
hex(zlib.crc32(raw_data))
# fbba7856
hex(zlib.adler32(raw_data))
# 23710494
"".join([ f"{i:02x}" for i in zlib.compress(raw_data) ])
# 789ccb48cdc9c9d729cf2fca4951e4020023710494
"".join([ f"{i:02x}" for i in gzip.compress(raw_data) ])
# 1f8b08000867dd6502ffcb48cdc9c9d729cf2fca4951e40200fbba78560d000000
int.from_bytes(b'\x86\x7d\xd6\x50', byteorder="little")
# 1709008648
datetime.datetime.fromtimestamp(1709008648)
C语言参考zlib.c文件和pigz命令,需要手动编译zlib库并且安装
shell
# install zlib
gcc -Wall -Wextra -pedantic -o zpipe zpipe.c $(pkg-config --libs zlib)
./zpipe <<< $'hello,world!' > compressed.bin
./zpipe -d < compressed.bin
# OR
pigz -d < compressed.bin
xxd -ps compress.bin
# zlib或者defalte格式压缩数据
# 789ccb48cdc9c9d729cf2fca4951e4020023710494
JS语言参考zlib.js文件
总结
各个语言直接生成的数据基本一致,不同的是gzip头部,因为有些头部数据可以灵活处理,比如时间戳,扩展等,这个可以查看RFC文档。
TODO
RFC 1950, RFC 1952文档解释