HTTP2 HPACK 介绍

2018-11-16 linux network

Google 直接在 HTTP1.X 的基础上设计了 SPDY 协议,对头部使用 deflate 算法进行压缩,一并解决了多路复用和优先级等问题。而 HTTP2 的实现就是参考了 SPDY 协议,但是专门为头部压缩设计了一套压缩算法,就是这里的 HPACK 。

简介

目前分别对应两个协议为 HTTP2 RFC7540 以及 HPACK RFC7541,其中 HPACK 主要是为了解决 HTTP1.X 中被诟病的无意义且重复的头部。

详解

简单的说,HPACK 使用 2 个索引表(静态、动态索引表)来把头部映射到索引值,并对不存在的头部使用 huffman 编码,并动态缓存到索引,从而达到压缩头部的效果。

头部的内容包括了 Header NameHeader 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 确定,例如 :authoritycookie 等;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) 和长度标识字节。