使用wireshark抓取http2.0请求 html
点击查看算法
method:get
、status:200
等等,随着网页增加到须要数十到数百个请求,这些请求中的冗余标头字段没必要要地消耗带宽,从而显著增长了延迟,所以,Hpack技术应时而生。首先介绍下压缩的概念(比较简单,熟悉的能够跳过):segmentfault
这个例子已经简单介绍了压缩的好处:能够在传输的过程,简化消息内容,从而下降消息的大小cookie
官方文档里的对Hpack的主要思想说明:工具
- 将header里的字段列表视为可包括重复对的name-value键值对的有序集合,分别使用8位字节表示name和value
- 当字段被编码/解码时,对应的字典会不断扩充
- 在编码形式中,header字段能够直接表示,也可使用header field tables 中对应的引用。所以,可使用引用和文字值的混合来header字段列表。
- 文字值要么直接编码,要么使用静态huffman代码
- 编码器负责决定在标题字段表中插入哪些标题字段做为新条目。解码器执行对编码器规定的报头字段表的修改,重建处理中的报头字段列表
以上摘自RFC 7541协议使用翻译工具直接翻译-_-,因此看起来有点艰涩,不要紧,先往下看编码
因为理论内容比较枯燥,因此先来几张图看一下效果,这里使用wireshark来抓取对同一个页面的两次请求,查看对比。spa
以cookie
这个字段为例,在上述两次请求中:翻译
因此经过简单观察,咱们能够简单得出如下结论:code
简单描述一下Hpack算法的过程:htm
首先介绍一下前面在说明“压缩过程”时,提到的字典。在Hpack
中,一共使用2个表来充当字典的角色:静态表和动态表。
静态表很简单,只包含已知的header字段。点此查看完整的静态表,分为两种:
:metho: GET
、:status: 200
:authority
、cookie
name
部分先用一个字符(好比cookie)来表示,同时,根据状况判断是否告知服务端,将 cookie: xxxxxxx
添加到动态表中(咱们这里默认假定是从客户端向服务端发送消息)cookie: xxxxxxx
就有可能被添加到动态表了,至因而否添加要根据后面提到的指令判断)静态表和动态表一块儿组成一个索引地址空间。设静态表长度为s,动态表长度为k,那么最终的索引空间以下:
<---------- Index Address Space ----------> <-- Static Table --> <-- Dynamic Table --> +---+-----------+---+ +---+-----------+---+ | 1 | ... | s | |s+1| ... |s+k| +---+-----------+---+ +---+-----------+---+ ^ | | V Insertion Point Dropping Point
其中:
有了这个索引空间之后,header的字段一共有如下几种表示方法:
header字段的表示法一共分2种,下面逐一说明。
数字主要用来表示上文中索引空间的索引值,具体的规则以下:
接下来看官方的一些例子帮助理解:
首先这里限制位数为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 +---+---+---+---+---+---+---+---
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 +---+---+---+---+---+---+---+---+
直接从边界开始,也就是使用8位前缀,42小于2^8-1=255 因此直接表示:
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 42 stored on 8 bits +---+---+---+---+---+---+---+---+
header的字段能够用字符串文原本表示,具体的规则以下:
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | H | String Length (7+) | +---+---------------------------+ | String Data (Length octets) | +-------------------------------+
这种状况下,第一个字节固定为1,而后用7位前缀法表示索引的值
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 1 | Index (7+) | +---+---------------------------+ Figure 5: Indexed Header Field An indexed header field starts with the '1
例如10000010,表示索引值为2,查找静态表可知,对应的header字段是method:GET
注意咱们前面说索引空间的时候提到,索引空间地址是从1开始的,0的话会被视为错误,也就是10000000解码时会出错。
这种状况下,前两位固定为01,后面6位表示索引值,取到对应的name,例如01010000对应32,查静态表可知name是cookie
,接下来使用字符串表示法表示对应的value字段,在解码以后,这个字段就被加到动态表中,下次编码的时候会直接使用状况1,(这里也就说明了为何后续请求压缩程度更大,由于动态表在不断扩充,扩充的界限请看官方文档这里暂时不说明)
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 1 | Index (6+) | +---+---+-----------------------+ | H | Value Length (7+) | +---+---------------------------+ | Value String (Length octets) | +-------------------------------+
这种状况和上面的很类似,只要补上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) | +-------------------------------+
观察状况2和状况3可知,若是须要更新动态表,前两位标志位都是01
这种状况,前四位固定为0000,其余和状况2一致,
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 0 | 0 | 0 | 0 | Index (4+) | +---+---+-----------------------+ | H | Value Length (7+) | +---+---------------------------+ | Value String (Length octets) | +-------------------------------+
同理,前四位固定为0000,其余和状况3一致,
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) | +-------------------------------+
观察状况2和状况3可知,若是须要更新动态表,前两位标志位都是0000
这种状况下和状况4基本一致,只是前四位固定为0001,区别在于:
0 1 2 3 4 5 6 7 +---+---+---+---+---+---+---+---+ | 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 | 1 | 0 | +---+---+-----------------------+ | H | Name Length (7+) | +---+---------------------------+ | Name String (Length octets) | +---+---------------------------+ | H | Value Length (7+) | +---+---------------------------+ | Value String (Length octets) | +-------------------------------+
和上面一种状况同理,就略过了。
主要内容已经都说完了,接下来抓一些请求来看看具体的内容,好比直接抓取segment下的请求。
接下来看authority字段,咱们抓第一次和第二次请求的进行对比:
能够看出,这个字段符合前面提到的状况2:第一次编码是01000001,表示name直接使用索引,索引值为1,且value不在索引空间中,后面的部分表示具体的value值
第二次请求,发现已是直接使用索引空间的值(由于前一次请求已经要求更新到动态表),因此本次只要一个字符长度直接表示这个字段110001101,第一个1表示状况1,后面1001101=64+8+4+1 =77 也就是此时对应的索引值
咱们前面提到动态表会随请求增长不断更新,可是动态表实际上是有大小限制的,所以动态表在增长条目时也可能会删除条目,具体的更新规则等限于篇幅(没错,不是由于懒)不在本文更新。还有就是相关的huffman编码等也不在此说明,本文主要仍是针对Hpack算法的过程和编码规则作一些说明。主要参照RFC 7541协议
惯例:若是内容有错误的地方欢迎指出(以为看着不理解不舒服想吐槽也彻底没问题);若是对你有帮助,欢迎点赞和收藏,转载请征得赞成后著明出处,若是有问题也欢迎私信交流,主页有邮箱地址