Skip to main content

2. 压缩过程概述 (Compression Process Overview)

HPACK使用两个表来将头部字段关联到索引: 静态表和动态表。静态表是预定义的,包含常见的头部字段。动态表是动态的,可以由编码器用于根据特定连接内交换的头部字段重复编码头部字段。

2.1 索引地址空间 (Indexing Address Space)

静态表和动态表被组合成单个索引地址空间。

索引编址模型:

┌──────────────────────┐
│ 索引 1 │ ← 静态表起始
│ :authority │
├──────────────────────┤
│ 索引 2 │
│ :method GET │
├──────────────────────┤
│ ... │
├──────────────────────┤
│ 索引 61 │ ← 静态表结束
│ www-authenticate │
├──────────────────────┤
│ 索引 62 │ ← 动态表起始
│ (最新插入) │
├──────────────────────┤
│ 索引 63 │
│ (次新插入) │
├──────────────────────┤
│ ... │
└──────────────────────┘

查找逻辑:
if index >= 1 and index <= 61:
return static_table[index]
elif index >= 62:
return dynamic_table[index - 62]

索引从1开始。静态表中的索引从1到静态表的长度(参见第2.3.1节)。动态表中的索引从静态表长度+1开始。

def lookup(index):
"""查找头部字段"""
STATIC_TABLE_SIZE = 61

if index == 0:
raise ValueError("索引0无效")
elif index <= STATIC_TABLE_SIZE:
return static_table[index - 1]
else:
dynamic_index = index - STATIC_TABLE_SIZE - 1
return dynamic_table[dynamic_index]

2.2 编码器 (Encoder)

编码器负责将头部列表编码为头部块 (Header Block)。

2.2.1 编码流程

输入: 头部字段列表
输出: 编码的头部块

流程:
对于每个头部字段 (name, value):

步骤1: 查找完全匹配
┌────────────────────────────────┐
│ 在静态表或动态表中查找 │
│ 是否存在完全相同的(name, value)│
└────────────────────────────────┘

├─→ 找到: 使用索引编码 (1字节)
│ [完成此字段]

└─→ 未找到: 继续步骤2

步骤2: 查找名称匹配
┌────────────────────────────────┐
│ 在静态表或动态表中查找 │
│ 是否存在相同的name │
└────────────────────────────────┘

├─→ 找到: 使用名称索引 + 字面值
│ 决定是否添加到动态表

└─→ 未找到: 继续步骤3

步骤3: 完全字面编码
┌────────────────────────────────┐
│ 名称和值都使用字面编码 │
│ 决定是否添加到动态表 │
└────────────────────────────────┘

2.2.2 编码决策

编码器需要决定如何编码每个头部字段:

决策树:

头部字段: name: value

├─→ 静态/动态表中有完全匹配?
│ YES → [索引编码] (最优,1-2字节)
│ NO → 继续

├─→ 这是敏感数据 (如Cookie/Auth)?
│ YES → [永不索引的字面编码]
│ 不添加到动态表
│ 保护隐私
│ NO → 继续

├─→ 这个头部会重复出现?
│ YES → [带索引的字面编码]
│ 添加到动态表
│ 未来请求可索引
│ NO → 继续

└─→ [不索引的字面编码]
不添加到动态表
避免污染动态表

2.2.3 编码示例

示例: 编码第一个请求

输入头部:
:method: GET
:scheme: https
:path: /resource
:authority: example.com
cache-control: no-cache
custom-key: custom-value

编码过程:

1. :method: GET
→ 静态表索引2完全匹配
→ 编码: 0x82 (1字节)

2. :scheme: https
→ 静态表索引7完全匹配
→ 编码: 0x87 (1字节)

3. :path: /resource
→ 静态表索引4有名称 :path
→ 值 /resource 不匹配
→ 使用名称索引 + 字面值
→ 带索引编码 (将添加到动态表62)
→ 编码: 0x44 0x09 "/resource" (12字节)

4. :authority: example.com
→ 静态表索引1有名称 :authority
→ 带索引编码 (添加到动态表63)
→ 编码: 0x41 0x0b "example.com" (14字节)

5. cache-control: no-cache
→ 静态表索引24有名称 cache-control
→ 带索引编码 (添加到动态表64)
→ 编码: 0x58 0x08 "no-cache" (11字节)

6. custom-key: custom-value
→ 表中不存在
→ 完全字面编码,带索引 (添加到动态表65)
→ 编码: 0x40 0x0a "custom-key" 0x0c "custom-value" (26字节)

总计: 65字节 (原始约150字节)
压缩率: 56%

动态表状态:
[62] :path: /resource
[63] :authority: example.com
[64] cache-control: no-cache
[65] custom-key: custom-value

2.3 解码器 (Decoder)

解码器负责将编码的头部块解码回头部列表。

2.3.1 解码流程

输入: 编码的头部块 (字节流)
输出: 头部字段列表

流程:
while 还有数据:

步骤1: 读取首字节,识别表示类型
┌────────────────────────────────┐
│ 检查首字节的模式 │
│ 1xxxxxxx: 索引编码 │
│ 01xxxxxx: 带索引的字面 │
│ 0000xxxx: 不索引的字面 │
│ 0001xxxx: 永不索引的字面 │
│ 001xxxxx: 动态表大小更新 │
└────────────────────────────────┘

步骤2: 根据类型解码
┌────────────────────────────────┐
│ 索引编码: │
│ 1. 读取索引 │
│ 2. 从表中查找 │
│ 3. 输出 (name, value) │
├────────────────────────────────┤
│ 字面编码: │
│ 1. 读取名称 (索引或字面) │
│ 2. 读取值 (字面) │
│ 3. 如果需要,添加到动态表 │
│ 4. 输出 (name, value) │
└────────────────────────────────┘

2.3.2 解码器状态

解码器必须维护:

解码器状态:
1. 动态表
- 当前条目列表
- 当前大小
- 最大大小

2. 解码缓冲区
- 当前正在解码的头部块
- 当前位置

3. 输出列表
- 已解码的头部字段

2.3.3 解码示例

示例: 解码头部块

输入: 0x82 0x87 0x44 0x09 "/resource" ...

解码过程:

1. 读取 0x82 = 10000010
→ 首位1: 索引编码
→ 索引2
→ 查表: :method: GET
→ 输出: (:method, GET)

2. 读取 0x87 = 10000111
→ 首位1: 索引编码
→ 索引7
→ 查表: :scheme: https
→ 输出: (:scheme, https)

3. 读取 0x44 = 01000100
→ 前缀01: 带索引的字面
→ 名称索引4
→ 查表: :path
→ 读取值长度 0x09 = 9
→ 读取9字节: "/resource"
→ 输出: (:path, /resource)
→ 添加到动态表索引62

4. 继续解码...

输出列表:
:method: GET
:scheme: https
:path: /resource
:authority: example.com
cache-control: no-cache
custom-key: custom-value

2.4 头部块的编码和解码

2.4.1 头部块格式

头部块 = 一系列头部字段表示

Header Block

├─ Header Field Representation 1
├─ Header Field Representation 2
├─ Header Field Representation 3
└─ ...

每个表示可以是:
- 索引头部字段
- 带索引的字面头部字段
- 不索引的字面头部字段
- 永不索引的字面头部字段
- 动态表大小更新

2.4.2 处理顺序

重要: 头部字段必须按顺序处理

原因:
1. 动态表更新有顺序依赖
2. 索引引用依赖于之前的插入
3. 保证编码器和解码器状态同步

示例问题场景:
如果乱序处理:

编码器发送:
[插入 custom: value 到索引62]
[引用索引62]

解码器如果先处理引用:
→ 索引62不存在!
→ 解码失败

必须按序:
1. 先处理插入 → 创建索引62
2. 再处理引用 → 找到索引62

上一章: 1. 引言 (Introduction)
下一章: 3. 头部字段表示 (Header Field Representation)