探索HTTP/2: HPACK协议简述(原)
探索HTTP/2: HPACK协议简述
探索HTTP/2系列的第一篇文章已经介绍了HTTP 2协议,本文则将简述用于HTTP/2头部压缩的 HPACK协议。(2016.10.01最后更新)
1. 基本原理
HPACK头部压缩的基本原理就是使用索引表和 Huffman编码。在压缩(编码)与解压(解码)过程,可将指定的头部字段(包含字段名与字段值)存储在索引表中。索引表中的每一个条目由索引(一个整数),字段名和字段值组成。对于存在索引表中的头部字段,在编码时可以仅使用索引作为该字段的代表,在解码时通过该索引从表中查找出对应的字段。对于其它的字符串,则可以使用Huffman编码进行压缩。
1.1 索引表
索引表由静态表与动态表组成。静态表由HPACK协议预定义的61个常用的头部字段组成,其中大部分字段的值为空。静态表是只读的,其中的条目及其位置均不可更改。HPACK协议中的 附录A列出了全部的静态表条目。动态表也由一系列头部字段组成,但其中的元素不固定,在实际操作中可以插入新的条目,也允许删除已有的条目。
HPACK协议要求静态表与动态表合并在同一个存储空间中,其中静态表置于前部,动态表紧随其后。那么在整个索引表空间中,动态表的第一个条目的索引将是62。动态表的维护原则是先进先出(FIFO)。当向动态表中增加条目时,将总是从第62位插入,原有的条目将全部向右移动一个位置。当从动态表中删除条目时,将总是从最后一位进行删除。
虽说,协议要求将静态表与动态表合并在一起,但这只是逻辑上的要求。只要动态表的索引是从62开始,那么各个实现可以根据自己的喜好自由地使用存储数据结构。比如,可以将静态表单独放在一个不可变的数组中,而动态表由另一个链表进行存储,这样可能会便于插入和删除条目。只不过,这个链表中元素的下标与动态表中条目的索引之间相差62。
(动态)索引表中的条目允许重复。
1.2 Huffman编码
Huffman编码是一种用于无损数据压缩的权路径编码算法。在使用该算法时,需要一张所有被编码字符的权重(出现频率)代码表。在对大量的HTTP头部样本进行统计之后,得出了一份适用于HPACK的Huffman代码表,由协议中的 附录B列出。
必须注意的是,HPACK协议并不要求该协议的实现一定要使用索引表,即便某个字段已经存在于索引表中了。而且也不要求一定要对字符串实施Huffman压缩。也就是说,理论上,在编码时可以不对头部字段进行任何形式的压缩,而只需将所有的字符转化成字节形式。
2. 基本数据类型表示法
HPACK协议使用的基本数据类型只有两种:整数;字符串。该协议使用整数去表示索引和字符串的长度。头部字段名和值中出现的数字,只会被当作字符串进行处理。
2.1 整数表示法
HPACK在表示整数时并不是把它简单的转换成二进制形式。因为HPACK希望每一个整数的表示能够从某个8比特位字节(octet,下文将其简写为"字节")中的任何一个比特位开始,但总是要在某个字节的最后一个比特位结束。比如表示127,让它从字节的第一个比特位开始填充,肯定会在最后一个比特位结束,如下图所示:
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
+---+---+---+---+---+---+---+---+
+---+---+---+---+---+---+---+---+
| 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
+---+---+---+---+---+---+---+---+
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| ? | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
+---+---+---+---+---+---+---+---+
+---+---+---+---+---+---+---+---+
| ? | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
+---+---+---+---+---+---+---+---+
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| ? | ? | 1 | 1 | 1 | 1 | 1 | 1 |
+---+---+---+-------------------+
| 1 | ? | ? | ? | ? | ? | ? | ? |
+---+---+---+---+---+---+---+---+
+---+---+---+---+---+---+---+---+
| ? | ? | 1 | 1 | 1 | 1 | 1 | 1 |
+---+---+---+-------------------+
| 1 | ? | ? | ? | ? | ? | ? | ? |
+---+---+---+---+---+---+---+---+
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| ? | ? | 1 | 1 1 1 1 1 |
+---+---+---+-------------------+
| 1 | Value-(2^N-1) LSB |
+---+---------------------------+
...
+---+---------------------------+
| 0 | Value-(2^N-1) MSB |
+---+---------------------------+
+---+---+---+---+---+---+---+---+
| ? | ? | 1 | 1 1 1 1 1 |
+---+---+---+-------------------+
| 1 | Value-(2^N-1) LSB |
+---+---------------------------+
...
+---+---------------------------+
| 0 | Value-(2^N-1) MSB |
+---+---------------------------+
public
void
encodeInteger(
int
value,
int
prefix) {
if (value >> prefix <= 0 ) {
printBinary(value);
} else {
int number = ( 1 << prefix) - 1 ;
printBinary(number);
for (value -= number; value >= 128 ; value /= 128 ) {
printBinary(value % 128 + 128 );
}
printBinary(value);
}
}
private void printBinary( int value) {
System.out.println(String.format( " %8s " , Integer.toBinaryString(value)).replace( " " , " 0 " ));
}
根据上述算法可知,当prefix为6时,127的表示法如下图所示:
if (value >> prefix <= 0 ) {
printBinary(value);
} else {
int number = ( 1 << prefix) - 1 ;
printBinary(number);
for (value -= number; value >= 128 ; value /= 128 ) {
printBinary(value % 128 + 128 );
}
printBinary(value);
}
}
private void printBinary( int value) {
System.out.println(String.format( " %8s " , Integer.toBinaryString(value)).replace( " " , " 0 " ));
}
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| ? | ? | 1 | 1 | 1 | 1 | 1 | 1 |
+---+---+---+-------------------+
| 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
+---+---+---+-------------------+
+---+---+---+---+---+---+---+---+
| ? | ? | 1 | 1 | 1 | 1 | 1 | 1 |
+---+---+---+-------------------+
| 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
+---+---+---+-------------------+
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| H | String Length (7+) |
+---+---------------------------+
| String Data (Length octets) |
+-------------------------------+
+---+---+---+---+---+---+---+---+
| H | String Length (7+) |
+---+---------------------------+
| String Data (Length octets) |
+-------------------------------+
[1]Huffman标志,表示该字符串是否为Huffman编码,占用一个比特位。
[2]字符串长度,一个使用2.1节所述方法表示的整数,其中prefix为7。
[3]字符串值。若Huffman标志为0,该值就是原始字符串的字节,否则该值是经Huffman编码过的数据。由于经Huffman编码过的数据并不总是能在一个字节的最后一个比特位处结束,所以可能会使用EOS(end-of-string)符号进行填充。
3. 头部字段表示法
有了第2节介绍的基本数据类型的表示法作为基础,现在就可以阐述头部字段的表示法了。HPACK协议将字段表示法分成3种类型。在表示法开头有一个或若干个比特位用于表示类型。
3.1 已在索引表的头部字段
类型标识占用1个比特位,值为1。索引使用prefix为7的整数表示法。在解码时,不会更新动态表。
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 1 | Index (7+) |
+---+---------------------------+
+---+---+---+---+---+---+---+---+
| 1 | Index (7+) |
+---+---------------------------+
类型标识占用2个比特位,值为01。在解码时,会向动态表内插入新条目。这种类型又被分成两种情况:
[1]头部字段名已在索引表中,字段名索引使用prefix为6的整数表示法,而字段值使用字符串表示法。
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 1 | Index (6+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
+---+---+---+---+---+---+---+---+
| 0 | 1 | Index (6+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
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) |
+-------------------------------+
+---+---+---+---+---+---+---+---+
| 0 | 1 | 0 |
+---+---+-----------------------+
| H | Name Length (7+) |
+---+---------------------------+
| Name String (Length octets) |
+---+---------------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
类型标识占用4个比特位,值为0000。在解码时,不向动态表内插入新条目。这种类型又被分成两种情况:
[1]头部字段名已在索引表中,字段名索引使用prefix为4的整数表示法,而字段值使用字符串表示法。
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | Index (4+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | Index (4+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | 0 |
+---+---+-----------------------+
| H | Name Length (7+) |
+---+---------------------------+
| Name String (Length octets) |
+---+---------------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+---+---------------------------+
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 0 | 0 |
+---+---+-----------------------+
| H | Name Length (7+) |
+---+---------------------------+
| Name String (Length octets) |
+---+---------------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+---+---------------------------+
类型标识占用4个比特位,值为0001。在解码时,不向动态表内插入新条目。这种类型又被分成两种情况:
[1]头部字段名已在索引表中,字段名索引使用prefix为4的整数表示法,而字段值使用字符串表示法。
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 | Index (4+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 | Index (4+) |
+---+---+-----------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 | 0 |
+---+---+-----------------------+
| H | Name Length (7+) |
+---+---------------------------+
| Name String (Length octets) |
+---+---------------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
+---+---+---+---+---+---+---+---+
| 0 | 0 | 0 | 1 | 0 |
+---+---+-----------------------+
| H | Name Length (7+) |
+---+---------------------------+
| Name String (Length octets) |
+---+---------------------------+
| H | Value Length (7+) |
+---+---------------------------+
| Value String (Length octets) |
+-------------------------------+
4. 动态表的管理
动态表中的条目被认为是有尺寸的,其计算公式为:字段名的字节长度+字段值的字节长度+32。字段名/值的长度是指它们的原始字节的长度,而非经过Huffman编码后的字节的长度。
动态表的尺寸就是其中所有条目的尺寸之和。动态表的最大尺寸是有限的,可以通过下面的整数表示法来通知协议的现实去改变动态表的最大尺寸。
0 1 2 3 4 5 6 7
+---+---+---+---+---+---+---+---+
| 0 | 0 | 1 | Max size (5+) |
+---+---------------------------+
+---+---+---+---+---+---+---+---+
| 0 | 0 | 1 | Max size (5+) |
+---+---------------------------+