在HTTP/1.x
时代,支持Body
压缩,Header
不支持压缩。而现在一个网页可能有几十到上百个请求,一个请求Header
至少600Byte
以上。那么这些页面的请求Header
会消耗不必要的带宽,增加延迟。
在SPDY
协议中,SPDY
通过使用 DEFLATE 格式,有效的压缩了Headers
。但是这种方法暴露了安全风险,比如 CRIME攻击 (轻松实现压缩率信息泄漏)。
在HTTP/2.0
中,引入了Header Compression
(rfc7541),头部压缩技术使用了HPACK
,有效的压缩了Header
,同时避免了未知的安全风险。
压缩效果
我们先来看一下HPACK
压缩效果。比如说一个简单的HTTP
请求,Header
大约是 600 Byte,在HTTP/2.0
中,首次请求会被压缩到 300 Byte,接着在后续的请求中会被压缩到 30 Byte,首次请求压缩率达到 50%,后续达到 95%。可以说效果是非常明显的,如下图(wireshark
抓包):
压缩原理
HPACK
其实包含两个压缩模块:Indexs Table
索引表和Static Huffman Encoding
静态霍夫曼编码。结构图如下:
接下来我们详细的了解一下它们是如何工作。
Indexs Table
索引表(rfc7541#section-2.3),包含Static Table
静态表、Dynamic Table
动态表。静态表包含61个预定义的key value
,动态表包含自定义的key value
。
HPACK
使用Static Table
、Dynamic Table
将Header
字段与索引Index
相关联,这两个表合并为一个地址空间,用于定义索引值,如下图:
Static Table
静态表(rfc7541#section-2.3.1),包含61个预定义Header
key-value
,传输的时候使用对应的索引Index
替换。静态表内容具体的定义可以参考这里 rfc7541#appendix-A。
+-------+-----------------------------+---------------+
| 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 | |
...太多了,省略掉了
| 58 | user-agent | |
| 59 | vary | |
| 60 | via | |
| 61 | www-authenticate | |
+-------+-----------------------------+---------------+
实际传输的时候,是怎么对应的呢?举几个简单的例子:
:method GET = 2 = 00000010
:method POST = 3 = 00000011
:path / = 4 = 00000100
:path /index.html = 5 = 00000101
:scheme https = 7 = 00000111
这些很好理解,那么如果是下面这几个呢?
:authority www.laoqingcai.com
:path /api/user
:token xxxxxx
这时候仅仅使用静态表是无法压缩了,轮到Dynamic Table
动态表登场了。
Dynamic Table
动态表(rfc7541#section-2.3.2),是一个FIFO
队列,初始是空的,它有以下特点:
解压
header
的时候按需添加,每次添加的时候,放在队首,移除的时候,从队尾开始。大小不是无限制的,可以通过
SETTING Frame
里的SETTINGS_HEADER_TABLE_SIZE
设置,默认256Byte
。
如上图,每次添加的时候都是在62
的位置(因为静态表已经固定61条了
),移除的时候都是从结尾开始移除。比如说现在有两个自定义的Header
:
:authority www.laoqingcai.com
:token 123456789
那么动态表的变化流程如下:
那么到这里,静态表和动态表已经能存储任何header
了。
Static Huffman Encoding
静态霍夫曼编码(rfc7541#appendix-B),HTTP/2.0
还支持对String
的Key
Value
进行静态编码,一方面减少了传输字符串的体积,另一方面相当于加密了传书内容。
根据官方 rfc7541#section-7.2 描述,目前还没有针对静态霍夫曼编码的攻击。使用静态霍夫曼编码表创建的信息可能会泄漏,但是攻击者无法利用泄漏的信息来恢复任何有意义的信息。
Huffman Coding
那么什么是霍夫曼编码呢?
霍夫曼编码是一种最小冗余的算法,核心思想就是用短位表示频率大的符号,用长位表示频率小的符号。霍夫曼编码基于霍夫曼树生成的一种编码,那么霍夫曼树是怎么生成的呢?
- 首先通过大量数据,计算每一项出现的频率,
- 然后用二叉树的方式来表示这些频率,合并频率最小的两个,组成一个二叉树。
- 然后继续将上一步得到的二叉树和剩下最小的频率合并,组成二叉树,上一步得到的二叉树作为右节点。
- 循环步骤3,直到合并成一个二叉树为止。
这样就得到一个最短带权路径的二叉树,也就是最优二叉树。比如下面这个例子:
比如上图,第三步得出最优的二叉树,树的带权路径长度 = 53+53+72+131 = 57。
然后根据霍夫曼树求得霍夫曼编码,为每一个左子节点路径分配编码0,右子节点路径分配编码1,就得到霍夫曼编码了。然后A、B、C、D可以替换成以下的表现形式。
A:0
B:10
C:110
D:111
Static Huffman Encoding
了解了霍夫曼树、霍夫曼编码之后,我们再来看一下HTTP/2.0
里的静态霍夫曼编码,引用官当文档 rfc7541#appendix-B 里的一句话:
This Huffman code was generated from statistics obtained on a large sample of HTTP headers
其实和Huffman Coding
原理一样,也是通过大量HTTP Header
数据样本,分析出来每个字符出现的频率,根据频率生成霍夫曼树,最后得出静态霍夫曼编码表。这里列出部分编码:
...省略部分内容
'B' ( 66) |1011101 5d [ 7]
'C' ( 67) |1011110 5e [ 7]
'D' ( 68) |1011111 5f [ 7]
'E' ( 69) |1100000 60 [ 7]
'F' ( 70) |1100001 61 [ 7]
'G' ( 71) |1100010 62 [ 7]
...省略部分内容
'd' (100) |100100 24 [ 6]
'e' (101) |00101 5 [ 5]
'f' (102) |100101 25 [ 6]
'g' (103) |100110 26 [ 6]
'h' (104) |100111 27 [ 6]
'i' (105) |00110 6 [ 5]
'j' (106) |1110100 74 [ 7]
'k' (107) |1110101 75 [ 7]
'l' (108) |101000 28 [ 6]
'm' (109) |101001 29 [ 6]
举个例子,字符h
对应100111
,相当于8bit
压缩成6bit
了;字符e
对应00101
,相当于8bit
压缩成5bit
。
The Next
通过以上的分析,我们了解到了HTTP/2.0 HPACK
压缩的核心实现,下一篇 我们继续了解HPACK
实际压缩的过程。
参考链接
若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏
扫描二维码,分享此文章