HPACK 头部压缩

静态表、动态表、整数与字符串编码、六种字段表示格式,以及为何不用 gzip。

作者 David更新于 第 4 篇(共 16 篇)

RFC 7541。解决 1.1 的头部冗余:同一连接上每个请求都重发 user-agent、cookie、accept 等数百字节的相同内容。

为什么不用 gzip

1.1 时代曾有 Transfer-Encoding 压缩头部的想法,被 CRIME 攻击否决:攻击者若能控制部分请求内容,可通过观察压缩后长度逐字节猜出同一上下文中的机密(如 Cookie)——因为 DEFLATE 会把重复串压掉,命中即变短。

HPACK 的对策:

  • 不做通用字符串匹配压缩,改用索引表——命中与否只泄露「该字段名值对是否出现过」,粒度远粗于逐字节。
  • 对敏感字段提供 Never Indexed 表示(见下),强制不进动态表。
  • 字符串用静态 Huffman 表而非自适应字典,长度不随上下文历史变化。

两张表

编解码双方各维护一个索引空间,由静态表与动态表拼接而成:

索引 1 .. 61        静态表(只读,规范固定)
索引 62 .. 62+n-1   动态表(可增删,最近插入的索引最小)

静态表

61 个最常见的字段(名或名值对),双方硬编码,无需传输。节选:

索引字段名值
1:authority—
2:methodGET
3:methodPOST
4:path/
6:schemehttps
8:status200
16accept-encodinggzip, deflate
28content-length—
32cookie—
58user-agent—

于是 :method: GET 只需 1 个字节(索引 2 的编码)。

动态表

  • 先进先出,容量以字节计:每项开销 = 名长 + 值长 + 32 字节(估算的内存开销常量)。
  • 上限由 SETTINGS_HEADER_TABLE_SIZE 声明(默认 4096 字节),编码方可用「动态表大小更新」指令主动调小。
  • 插入新项时若超容,从表尾逐个淘汰,直到容纳得下;单项大于全表容量时清空全表且不插入(合法操作,非错误)。
  • 索引编号随插入动态偏移——新插入的项恒为索引 62。

致命约束:动态表是连接级共享状态,编解码双方必须逐帧保持完全同步。这就是字段块必须连续发送(中间不得插入其他帧)的根本原因,也是 HPACK 解码错误 MUST 以 COMPRESSION_ERROR 关闭整条连接(而非单个流)的原因——状态一旦失步,后续全部字段块都会解错。

整数编码

带 N 位前缀的变长整数(N 由上下文决定,1–8):

若 值 < 2^N - 1:
    直接填入前缀位,结束
否则:
    前缀位全置 1
    剩余量 = 值 - (2^N - 1)
    循环: 取低 7 位,若还有更多则最高位置 1,输出该字节

例(N=5,值 1337):

1337 >= 31  → 前缀 11111
1337 - 31 = 1306
1306 = 0b10100011010
  低7位 0011010 → 有后续 → 10011010
  剩余 1010     → 无后续 → 00001010
结果: 11111 10011010 00001010

字符串编码

+---+---+---+---+---+---+---+---+
| H |    String Length (7+)     |
+---+---------------------------+
|  String Data (Length octets)  |
+-------------------------------+
  • H=1 表示数据经静态 Huffman 编码(规范附录固定的码表,按 HTTP 头部字符频率优化,平均省 20–30%)。
  • H=0 表示原样字节。
  • 编码方 SHOULD 择短者输出——Huffman 对随机串(如已 Base64 的令牌)可能更长。
  • Huffman 解码时,末尾填充 MUST 是 EOS 符号的前缀且长度 < 8 位;填充超长或出现完整 EOS 编码 → COMPRESSION_ERROR。

六种字段表示

由首字节的高位比特区分:

前缀位表示是否入动态表
1xxxxxxx索引字段:名与值都取自表—
01xxxxxx字面量 + 增量索引:名可索引可字面量是
0000xxxx字面量,不索引否
0001xxxx字面量,永不索引否,且中间件转发时也不得索引
001xxxxx动态表大小更新—

Never Indexed 的用途

0001xxxx 是安全机制:携带敏感值(会话令牌、authorization)的字段应用此表示,保证它永远不进任何一跳的动态表。这样即使攻击者能在同连接上注入请求,也无法通过观察压缩后长度来探测该值——这是对 CRIME 类攻击的直接防御。

代理 MUST 在重新编码时保留该语义。

实际效果

一个典型的浏览器请求头(约 700 字节明文):

请求HPACK 后
第 1 个~200 字节(字面量入表 + Huffman)
第 2 个起~10–30 字节(多数字段命中动态表,只发索引)

这是 h2 在小请求密集场景下收益最大的部分——100 个请求可省下数万字节的上行流量。

攻击面

  • HPACK 炸弹:构造大量字段引用同一巨大动态表项,解压后膨胀数千倍耗尽内存。缓解:设置 SETTINGS_MAX_HEADER_LIST_SIZE,并在解码过程中而非解码后检查累计大小。
  • 动态表内存:每条连接都持有一份动态表,服务端需按连接数控制 HEADER_TABLE_SIZE。

下一篇:伪头字段。

本页目录