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 | :method | GET |
| 3 | :method | POST |
| 4 | :path | / |
| 6 | :scheme | https |
| 8 | :status | 200 |
| 16 | accept-encoding | gzip, deflate |
| 28 | content-length | — |
| 32 | cookie | — |
| 58 | user-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。
下一篇:伪头字段。