# HPACK 头部压缩

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

- 作者：David（道雾轩）
- 专栏：HTTP/2 与 HTTP/3 协议详解（https://daiw.org/manual/http2-3.md）
- 最后更新：2026-07-26
- 原文：https://daiw.org/manual/http2-3/hpack
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

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

## 为什么不用 gzip

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

HPACK 的对策：

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

## 两张表

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

```text
索引 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。

**致命约束**：动态表是**连接级共享状态**，编解码双方必须逐帧保持完全同步。这就是[字段块必须连续发送](https://daiw.org/manual/http2-3/h2-framing)（中间不得插入其他帧）的根本原因，也是 HPACK 解码错误 **MUST** 以 `COMPRESSION_ERROR` 关闭**整条连接**（而非单个流）的原因——状态一旦失步，后续全部字段块都会解错。

## 整数编码

带 N 位前缀的变长整数（N 由上下文决定，1–8）：

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

例（N=5，值 1337）：

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

## 字符串编码

```text
+---+---+---+---+---+---+---+---+
| 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`。

下一篇：[伪头字段](https://daiw.org/manual/http2-3/h2-pseudo-headers)。
