HTTP协议中内容压缩

背景

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 1951
  • zlib 可以被认为是一种 DEFALTE 算法的封装格式, 标准化参考 RFC 1950. 目前zlib库只支持DEFLATE算法,zlib已经成为了事实上的业界标准,标准文档中,zlibDEFLATE常常互换使用, 比如常见的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文档解释

相关推荐
Martin -Tang5 分钟前
vite和webpack的区别
前端·webpack·node.js·vite
迷途小码农零零发6 分钟前
解锁微前端的优秀库
前端
王解1 小时前
webpack loader全解析,从入门到精通(10)
前端·webpack·node.js
我不当帕鲁谁当帕鲁1 小时前
arcgis for js实现FeatureLayer图层弹窗展示所有field字段
前端·javascript·arcgis
那一抹阳光多灿烂1 小时前
工程化实战内功修炼测试题
前端·javascript
放逐者-保持本心,方可放逐2 小时前
微信小程序=》基础=》常见问题=》性能总结
前端·微信小程序·小程序·前端框架
毋若成4 小时前
前端三大组件之CSS,三大选择器,游戏网页仿写
前端·css
红中马喽4 小时前
JS学习日记(webAPI—DOM)
开发语言·前端·javascript·笔记·vscode·学习
Black蜡笔小新5 小时前
网页直播/点播播放器EasyPlayer.js播放器OffscreenCanvas这个特性是否需要特殊的环境和硬件支持
前端·javascript·html
秦jh_6 小时前
【Linux】多线程(概念,控制)
linux·运维·前端