Google 直接在 HTTP1.X 的基础上设计了 SPDY 协议,对头部使用 deflate 算法进行压缩,一并解决了多路复用和优先级等问题。而 HTTP2 的实现就是参考了 SPDY 协议,但是专门为头部压缩设计了一套压缩算法,就是这里的 HPACK 。
简介
目前分别对应两个协议为 HTTP2 RFC7540 以及 HPACK RFC7541,其中 HPACK 主要是为了解决 HTTP1.X 中被诟病的无意义且重复的头部。
详解
简单的说,HPACK 使用 2 个索引表(静态、动态索引表)来把头部映射到索引值,并对不存在的头部使用 huffman 编码,并动态缓存到索引,从而达到压缩头部的效果。
头部的内容包括了 Header Name
和 Header Value
两部分,不同的类型包含了不同的内容。
静态索引表
预先定义好的内容,只有固定的几十个值,如果要发送的值符合静态表时,用对应的 Index 替换即可,这样就大大压缩了头部的大小,如果遇到不在静态表中的值,就会用到动态表。
详细可以参考协议中的 Static Table Definition
部分的介绍,如下是部分示例。
+-------+-----------------------------+---------------+
| Index | Header Name | Header Value |
+-------+-----------------------------+---------------+
| 1 | :authority | |
| 2 | :method | GET |
| 3 | :method | POST |
| 4 | :path | / |
| 5 | :path | /index.html |
| 6 | :scheme | http |
| 7 | :scheme | https |
| 8 | :status | 200 |
| 9 | :status | 204 |
| 10 | :status | 206 |
| 11 | :status | 304 |
| 12 | :status | 400 |
| 13 | :status | 404 |
| 14 | :status | 500 |
| 15 | accept-charset | |
| 16 | accept-encoding | gzip, deflate |
| 17 | accept-language | |
| 18 | accept-ranges | |
| 19 | accept | |
| 20 | access-control-allow-origin | |
| 21 | age | |
| 22 | allow | |
| 23 | authorization | |
| 24 | cache-control | |
| 25 | content-disposition | |
... ...
| 60 | via | |
| 61 | www-authenticate | |
+-------+-----------------------------+---------------+
动态索引
动态表是一个由先进先出的队列维护的有空间限制的表,同样维护的是头部与对应的索引。每个动态表只针对一个连接,每个连接的压缩解压缩的上下文有且仅有一个动态表。
那么动态表就是,当一个头部没有出现过的时候,会把他插入动态表中,下次同名的值就可能会在表中查到到索引并替换掉头部。
数据格式
基本的数据类型包括了两种:无符号整形(用来标示索引、字符串长度)、字符串长度。
数字表示
编码时通过前面的几位 prefix
标示不同类型的报文,所以对于数值来说有如下的解析方法:
- 剩余字节足够表示数值,那么直接使用。
- 如果不足,则依次增加 8bit ,其中第一位标示是否结束。
示例如下。
用 5 位前缀表示 10
这里限制位数为 5,由于 10 小于 2^5-1
可以直接表示为 01010
,那么结果为:
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| X | X | X | 0 | 1 | 0 | 1 | 0 | 10 stored on 5 bits
+---+---+---+---+---+---+---+---
用 5 位前缀表示 1337
因为 1337>2^5-1
那么前 5 位只能表示到 31 ,剩余 1337-31 = 13061
;接下来 1306>2^7 =128
,因为八位字节第一位是标志位,所以表示范围只有 2^7-1
。
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| X | X | X | 1 | 1 | 1 | 1 | 1 | Prefix = 31, I = 1306
| 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1306>=128, encode(154), I=1306/128
| 0 | 0 | 0 | 0 | 1 | 0 | 1 | 0 | 10<128, encode(10), done
+---+---+---+---+---+---+---+---+
字符串编码
有了无符号整数编码的基础,接着可以对字符串进行编码,如下所示:
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| H | String Length (7+) |
+---+---------------------------+
| String Data (Length octets) |
+-------------------------------+
索引表使用
静态索引表和动态索引表是联合使用的,某些场景下,动态索引表可以转换成类似静态索引表的场景,如下会有详细的介绍。
关于静态索引表可以在规范中直接搜索 Static Table Definition
即可,在处理索引表时,包括了三种场景类型:A) name 和 value 都确定,例如 :method: GET
、:status:200
;B) 只有 Name 确定,例如 :authority
、cookie
等;C) Name 和 Value 都不在静态索引中。
动态索引表最初是一个空表,当每次解压头部的时候,就可能会添加新的条目,而且允许有完全相同的 Name-Value 对,在添加完动态索引表之后,那么下次的请求就可以直接引用了,从而后续的压缩会更高。
需要更新动态索引表的包括了上述的后面两种场景。注意,为了防止解压时内存占用过大,动态索引表的大小是有限制的。
场景 A
预定义字段中有 Name 以及 Value 的值,那么二进制数据格式如下所示,第一位固定为 1 后面 7 位为映射的索引值。
可以在静态索引表和动态索引表。
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 1 | Index (7+) |
+---+---------------------------+
注意,索引表是从 1 开始编码的,如果出现 0 那么则认为是错误的。
场景 B
也就是在静态索引表中有 Name 但是没有 Value 的值,报文的格式如下:
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 1 | Index (6+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
此时,前两位固定为 01
,后 6 位表示索引值,取到对应的 Name ,例如 01010000
对应的索引为 32 对应了静态表中的 Name 是 cookie ,接下来就是使用字符串表示法表示对应的 Value 字段,解码之后,这个字段就被加到动态表中,下次编码的时候会直接使用情况类似静态索引的方法。
另外有一种类似的场景,就是 Name 在索引空间,但是 Value 不在且不需要更新动态表,此时前四位固定为 0000,其它情况与上述一致。
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | Index (4+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
还有一种,是严格不需要更新的报文,其前四位是 0001
。
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 | Index (4+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
场景 C
Name 和 Value 都不在索引空间,且需要更新动态表。这与场景 B 类似,只需要将 Name 的字符串表示补充上即可,并且把 Index 值设置为 0 。
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 1 | 0 |
+---+---+-----------------------+
| H | Name Length (7+) |
+---+---------------------------+
| Name String (Length octets) |
+---+---------------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
同样,有一种 Name 和 Value 都不在索引空间,且不需要更新动态表,此时前 4 位是 0 ,其它与上述相同。
还有一种严格不需要更新的,此时前 4 位是 0001
。
简言之,需要更新的前两位是 01
,不需要更新的 00
,严格不需要更新的是第 3、4 位是 01
。
其它
关于不需要更新和绝对不允许更新区别如下:
- 不需要更新,本次的发送过程不更新该字段到动态表,如果有多次转发,那么并不对转发做要求;
- 绝对不允许更新,如果这个请求被多次转发才到目标,那么转发的所有中间对于该字段也必须采用相同的处理方案。
压缩
简单来说,gzip 是一种数据格式,在原始数据前增加了 10 字节的头信息;尾部增加了校验码和长度字节,仅使用 deflate 算法对数据部分进行了压缩。
deflate 是一种压缩算法,是 huffman 编码的一种加强,也就是说 deflate 是最基础的算法,gzip 在 deflate 的 raw data 前增加了 10 个字节的 gzheader,尾部添加了 8 个字节的校验字节(可选 crc32 和 adler32) 和长度标识字节。