bigywater.tech
网络协议 · 80

内容协商与压缩:Accept-Encoding 与 gzip / br

“Accept-Encoding: gzip, br”只是一行请求头,背后是一整套协商:服务器选哪种压缩、q 值有没有用、压缩率和 CPU 的权衡、什么数据不该压。这篇在真实服务器上试各种写法,在本机比较 gzip、brotli、zstd 的各级别,最后用 netlab 看压缩在不同带宽下省多少时间。

349113 → 125147
python.org 的 CSS 经 gzip 后的字节数(−64%)
431.6 ms
brotli q11 压缩这个 CSS 的耗时,约为 q5(5.4 ms)的 80 倍
2889 → 1094 ms
1 Mbit/s 链路上下载同一个 CSS 原样与 gzip 的总时间

协商

实测 · 本机执行输出
  python.org 的 CSS(文本):
    Accept-Encoding: identity        → Content-Encoding = None  传输  349113 字节,解压后  349113 字节,与 identity 内容一致;Vary = Accept-Encoding
    Accept-Encoding: gzip            → Content-Encoding = gzip  传输  125147 字节,解压后  349113 字节,与 identity 内容一致;Vary = Accept-Encoding
    Accept-Encoding: br              → Content-Encoding = None  传输  349113 字节,解压后  349113 字节,与 identity 内容一致;Vary = Accept-Encoding
    Accept-Encoding: zstd            → Content-Encoding = None  传输  349113 字节,解压后  349113 字节,与 identity 内容一致;Vary = Accept-Encoding
    Accept-Encoding: deflate         → Content-Encoding = None  传输  349113 字节,解压后  349113 字节,与 identity 内容一致;Vary = Accept-Encoding
    Accept-Encoding: gzip, br, zstd  → Content-Encoding = gzip  传输  125147 字节,解压后  349113 字节,与 identity 内容一致;Vary = Accept-Encoding
  example.com 首页(HTML):
    Accept-Encoding: identity        → Content-Encoding = None  传输     577 字节,解压后     577 字节,与 identity 内容一致;Vary = None
    Accept-Encoding: gzip            → Content-Encoding = gzip  传输     400 字节,解压后     577 字节,与 identity 内容一致;Vary = None
    Accept-Encoding: br              → Content-Encoding = br    传输     334 字节,解压后     577 字节,与 identity 内容一致;Vary = None
    Accept-Encoding: zstd            → Content-Encoding = None  传输     577 字节,解压后     577 字节,与 identity 内容一致;Vary = None
    Accept-Encoding: deflate         → Content-Encoding = None  传输     577 字节,解压后     577 字节,与 identity 内容一致;Vary = None
    Accept-Encoding: gzip, br, zstd  → Content-Encoding = br    传输     334 字节,解压后     577 字节,与 identity 内容一致;Vary = None
  python.org 首页 /:发 Accept-Encoding: identity,响应的 Content-Encoding = gzip,11161 字节(服务器没有听从 identity,依旧发 gzip;它的 Vary 只写了 Cookie)

服务器按请求头选择:python.org 的 CSS 只认 gzip;example.com 偏爱 br;q=0 的“拒绝”被无视。

q 值

实测 · 本机执行输出
  Accept-Encoding: gzip;q=0.5, br;q=1    → 200,Content-Encoding = br   ,334 字节
  Accept-Encoding: br;q=0.1, gzip;q=0.9  → 200,Content-Encoding = br   ,334 字节
  Accept-Encoding: br;q=0, gzip          → 200,Content-Encoding = br   ,334 字节
  Accept-Encoding: gzip;q=0, br;q=0      → 200,Content-Encoding = br   ,334 字节
  Accept-Encoding: *                     → 200,Content-Encoding = None ,577 字节
  Accept-Encoding: *;q=0                 → 200,Content-Encoding = None ,577 字节
  Accept-Encoding: identity;q=0          → 200,Content-Encoding = None ,577 字节
  Accept-Encoding: compress              → 200,Content-Encoding = None ,577 字节
  Accept-Encoding: GZIP                  → 200,Content-Encoding = gzip ,400 字节

example.com 不按 q 值排序,q=0 的编码仍被选中;编码名不区分大小写。

算法与级别

实测 · 本机执行输出
    算法 / 级别         压缩后字节    比例     压缩耗时   解压耗时
    gzip -1                142494   40.8%        5.6 ms     0.8 ms
    gzip -6(常用默认)          125147   35.8%       10.7 ms     0.5 ms
    gzip -9                124789   35.7%       12.3 ms     0.4 ms
    brotli q1              131508   37.7%        1.2 ms     0.8 ms
    brotli q5              109810   31.5%        5.4 ms     0.7 ms
    brotli q11(最高)          98381   28.2%      431.6 ms     1.0 ms
    zstd -1                129255   37.0%        0.7 ms     0.3 ms
    zstd -3(默认)            119137   34.1%        1.0 ms     0.3 ms
    zstd -19               106191   30.4%       57.4 ms     0.3 ms
  原始 349113 字节;真实服务器的 gzip 版本是 125147 字节(第一节),相当于本机的哪个级别附近:gzip -6(常用默认)

gzip -6 与真实服务器同大;brotli q5、zstd -3 又小又快;brotli q11 小但慢 77 倍。

已压缩数据

实测 · 本机执行输出
  CSS 文本                 原始  349113 字节 → gzip  125147( 35.8%),brotli q5  109810( 31.5%)
  PNG 图片(已压缩格式)          原始   15770 字节 → gzip   15496( 98.3%),brotli q5   15630( 99.1%)
  随机字节                   原始  100000 字节 → gzip  100053(100.1%),brotli q5  100005(100.0%)
  已经 gzip 过的 CSS         原始  125147 字节 → gzip  124913( 99.8%),brotli q5  125152(100.0%)

PNG、随机字节、已 gzip 的内容再压只有 98%~100%,甚至变大。

链路的影响

实测 · 本机执行输出
    带宽                原样(raw)         gzip -6           brotli q5      (每格:下载 ms + 解压 ms = 总 ms)
    1 Mbit/s        2889 +  0.0 =   2889     1093 +  0.7 =   1094      969 +  0.9 =    970
    10 Mbit/s        366 +  0.0 =    366      185 +  0.7 =    186      173 +  1.3 =    174
    1000 Mbit/s      122 +  0.0 =    122       82 +  0.6 =     83       82 +  0.9 =     83

慢链路上压缩收益巨大,不限速时也快 32%;解压只要 1 ms 左右。

分步演示

内容协商与压缩
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。第一、二节是对 www.python.org 与 example.com 的真实请求(python.org 为 HTTPS,example.com 为明文);第三、四节是本机单线程压缩计时(取 3 次最小值),brotli 与 zstandard 是 Python 包(pip 安装);第五节用本机服务器 + netlab(每方向 20 ms),每格只测 2 次取最小值
    • 没有验证:Content-Encoding 与 Transfer-Encoding 的关系;压缩对 TLS 的攻击(BREACH / CRIME);服务器端预压缩 vs 动态压缩的 CPU 成本;字典压缩(Brotli 共享字典、zstd 字典)与 HTTP 压缩字典传输;Accept-Language、Accept-Charset 等其它内容协商头;Content-Encoding 叠加(gzip, br)
    京ICP备2026067502号-1京公网安备11010502063487号