一、HPACK 如何减少 HTTP 头部的大小
HTTP/1.1 的头部是明文重复传输 (比如每次请求都带Host: example.com、User-Agent: Chrome/...),占带宽且冗余;HPACK 是 HTTP/2 的头部压缩算法,通过 3 个核心机制减少头部体积:
1. 静态字典(Static Dictionary)
- 原理 :预定义 61 个高频头部字段(如
:method=GET、:path=/、Host),用索引编号 代替完整字符串。- 示例:传输
:method: GET时,直接用编号 2(静态字典中2 → :method: GET),仅占 1 字节,替代原本的 11 字节明文。
- 示例:传输
- 优势:高频头部无需重复传输完整字段,直接用索引压缩。
2. 动态字典(Dynamic Dictionary)
- 原理 :连接过程中,双方维护一个动态更新的字典 ,将本次请求 / 响应中出现的新头部(如
X-Custom-Header: value)存入字典,后续传输时用索引代替。- 示例:首次传输
X-Token: abc123(占 14 字节),存入动态字典索引 62;下次传输时直接用 62,仅占 1 字节。
- 示例:首次传输
- 优势:针对业务自定义头部,实现 "一次传输、多次复用" 的压缩。
3. Huffman 编码
- 原理 :对头部的值(或未在字典中的字段名) 用 Huffman 树进行变长编码 (高频字符用短码,低频字符用长码),进一步压缩字节数。
- 示例:字符串
example.com的 ASCII 编码占 11 字节,Huffman 编码后可压缩至 8 字节左右。
- 示例:字符串
我们拆解
example.com的哈夫曼压缩过程:HPACK 内置了一份 预定义的静态哈夫曼码表 ,表中对 HTTP 头部常见的字符(比如
a-z、.、-、:等)分配了不等长的二进制编码------ 出现频率越高的字符,编码越短。
- 高频字符(如
e、a、c):短编码(比如 3~4 位)- 低频字符(如
x、z、_):长编码(比如 5~8 位)
example.com拆分为单个字符:e x a m p l e . c o m,共 11 个 ASCII 字符(每个 ASCII 字符占 1 字节 = 8 位,原始总长度11×8=88 位)。我们对照 HPACK 静态哈夫曼码表的高频字符编码规则(简化版):
字符 出现频率 HPACK 哈夫曼编码(二进制) 编码长度(位) e极高 002 a高 0103 m高 01104 p中 10014 l中 10104 x中低 110015 .高 10114 c高 01114 o高 110005 我们把
example.com的每个字符替换成哈夫曼编码,再统计总位数:
e→00→ 2 位x→11001→ 5 位a→010→ 3 位m→0110→ 4 位p→1001→ 4 位l→1010→ 4 位e→00→ 2 位.→1011→ 4 位c→0111→ 4 位o→11000→ 5 位m→0110→ 4 位总位数 = 2+5+3+4+4+4+2+4+4+5+4 = 41 位
- 原始长度:11 个 ASCII 字符 = 88 位 = 11 字节
- 哈夫曼编码后长度:41 位 ≈ 5.125 字节
- 实际压缩到 8 字节的补充 :哈夫曼编码后的二进制流需要按 字节(8 位)对齐 存储,同时 HPACK 会给编码结果加一个 1~2 字节的「编码类型标识」(告诉解码器这是哈夫曼编码的字符串)。加上对齐和标识的开销后,总长度就变成了 8 字节左右,比原始 11 字节少了 3 字节。
- 优势:对非字典字段的文本内容,额外减少 30%~50% 的体积。
最终效果
- 典型 HTTP 请求头部(约 200 字节),经 HPACK 压缩后可降至50 字节以内,压缩率达 75% 以上;
- 重复请求的头部(如同一页面的资源请求),压缩率可接近 90%。
二、HPACK 中如何使用 Huffman 树
HPACK 的 Huffman 树是预定义的静态树(RFC 7541 附录 C),双方无需协商,直接使用,流程如下:
1. Huffman 树的结构
- 树的叶子节点对应可打印 ASCII 字符 + 部分控制字符(共 256 个可能字符);
- 每个字符对应唯一的二进制编码 ,编码长度与字符出现频率负相关:
- 高频字符(如
e、t、/):编码长度 2~4 位; - 低频字符(如
~、^):编码长度 10~13 位。
- 高频字符(如
2. Huffman 编码流程
当需要传输未在字典中的字段名 / 值时,执行以下步骤:
- 标识编码方式:在字段的开头用 1 个 bit 标记 "是否使用 Huffman 编码"(1 = 使用,0 = 不使用);
- 字符转编码:将字符串的每个字符,替换为 Huffman 树中对应的二进制编码;
- 补位对齐:将二进制流补 0,对齐到字节(8 位);
- 添加结束符 :在末尾添加 Huffman 树的 "结束符"(0xFF,对应编码
111111110),标识编码结束。
3. Huffman 解码流程
接收方收到编码后的字节流后:
- 解析标识位:判断是否为 Huffman 编码;
- 逐位匹配 Huffman 树:从根节点开始,按二进制位遍历树,直到匹配到叶子节点(得到对应字符);
- 处理结束符:遇到结束符时停止解码,忽略后续补位的 0。
三、HPACK 中整型数字的编码
HPACK 中,字典索引、字段长度等都需要用整型表示,为了压缩数字的字节数,HPACK 采用 **"前缀 + 扩展" 的变长编码 **,流程如下:
1. 选择前缀位数(N)
根据数字的大小,选择不同的前缀位数 N(N 可选 4、5、6、7、8):
- 若数字≤2^N - 1:直接用 N 位表示;
- 若数字 > 2^N - 1:先写 N 位的最大值(全 1),再用后续字节表示剩余值(每个字节的最高位是 "续传位":1 = 还有后续字节,0 = 结束)。
2. 编码示例(以 N=5 为例)
N=5 时,前缀最大能表示 31(2^5-1):
- 编码数字
10:直接用 5 位01010; - 编码数字
40:- 前缀写 5 位最大值
11111; - 剩余值 = 40-31=9,转二进制
1001; - 剩余值不足 7 位,补 0 成
0001001,最高位加续传位(这里只有 1 个后续字节,续传位写 0)→ 字节为00001001; - 最终编码:
11111 00001001(共 13 位)。
- 前缀写 5 位最大值
3. 解码示例
接收方收到11111 00001001(N=5):
- 前缀是
11111,表示数字 > 31; - 后续字节
00001001,续传位是 0,取低 7 位0001001→值为 9; - 最终数字 = 31+9=40。
四、HPACK 中头部名称与值的编码
HPACK 的头部字段(name: value)编码分为 3 种场景,核心是 "优先复用字典,减少明文传输":
场景 1:头部在静态 / 动态字典中(完全匹配)
直接用索引表示,编码格式:
- 1 个 bit 标记 "索引字段"(
0); - 后续是字典索引的整型编码(N=7)。
示例:传输:method: GET(静态字典索引 2)→ 编码为0 0000010(二进制)。
场景 2:头部名称在字典中,值不在(部分匹配)
用 "索引 + 值" 表示,编码格式:
- 1 个 bit 标记 "文字字段,名称索引"(
10); - 后续是名称的字典索引(N=6);
- 再后续是值的编码(可选 Huffman 压缩)。
示例:传输Host: test.com(Host在静态字典索引 7):
- 标记位
10; - 名称索引 7→编码
000111; - 值
test.com用 Huffman 编码; - 最终编码:
10 000111 [Huffman编码后的test.com]。
场景 3:头部名称和值都不在字典中(完全不匹配)
用 "明文名称 + 明文值" 表示,编码格式:
- 1 个 bit 标记 "文字字段,名称文字"(
11); - 后续是名称的长度编码(可选 Huffman)+ 名称的编码;
- 再后续是值的长度编码(可选 Huffman)+ 值的编码。
示例:传输X-New-Header: abc(无字典匹配):
- 标记位
11; - 名称
X-New-Header:先编码长度(比如 12),再用 Huffman 编码字符串; - 值
abc:编码长度 3,再用 Huffman 编码; - 最终编码:
11 [名称长度+名称编码] [值长度+值编码]。