您的位置:新葡亰496net > 新葡亰官网 > 2首部裁减,完全剖判

2首部裁减,完全剖判

发布时间:2019-06-17 08:31编辑:新葡亰官网浏览(164)

    HTTP/2 头部压缩技术介绍

    2016/04/13 · 基础技术 · HTTP/2

    本文作者: 伯乐在线 - JerryQu 。未经作者许可,禁止转载!
    欢迎加入伯乐在线 专栏作者。

    我们知道,HTTP/2 协议由两个 RFC 组成:一个是 RFC 7540,描述了 HTTP/2 协议本身;一个是 RFC 7541,描述了 HTTP/2 协议中使用的头部压缩技术。本文将通过实际案例带领大家详细地认识 HTTP/2 头部压缩这门技术。

    HTTP协议(HyperTextTransferProtocol,超文本传输协议)是用于从WWW服务器传输超文本到本地浏览器的传输协议。

    当前网络环境中,同一个页面发出几十个HTTP请求已经是司空见惯的事情了。在HTTP/1.1中,请求之间完全相互独立,使得请求中冗余的首部字段不必要地浪费了大量的网络带宽,并增加了网络延时。以对某站点的一次页面访问为例,直观地看一下这种状况:

    去年五月, IETF 正式发布了 HTTP/2 协议与之配套的 HPACK 头部压缩算法。 RFC 如下:

    为什么要压缩

    在 HTTP/1 中,HTTP 请求和响应都是由「状态行、请求 / 响应头部、消息主体」三部分组成。一般而言,消息主体都会经过 gzip 压缩,或者本身传输的就是压缩过后的二进制文件(例如图片、音频),但状态行和头部却没有经过任何压缩,直接以纯文本传输。

    随着 Web 功能越来越复杂,每个页面产生的请求数也越来越多,根据 HTTP Archive 的统计,当前平均每个页面都会产生上百个请求。越来越多的请求导致消耗在头部的流量越来越多,尤其是每次都要传输 UserAgent、Cookie 这类不会频繁变动的内容,完全是一种浪费。

    以下是我随手打开的一个页面的抓包结果。可以看到,传输头部的网络开销超过 100kb,比 HTML 还多:

    图片 1

    下面是其中一个请求的明细。可以看到,为了获得 58 字节的数据,在头部传输上花费了好几倍的流量:

    图片 2

    HTTP/1 时代,为了减少头部消耗的流量,有很多优化方案可以尝试,例如合并请求、启用 Cookie-Free 域名等等,但是这些方案或多或少会引入一些新的问题,这里不展开讨论。

    图片 3img

    图片 4

    • Hypertext Transfer Protocol Version 2 RFC 7540
    • HPACK: Header Compression for HTTP/2 RFC 7541

    压缩后的效果

    接下来我将使用访问本博客的抓包记录来说明 HTTP/2 头部压缩带来的变化。如何使用 Wireshark 对 HTTPS 网站进行抓包并解密,请看我的这篇文章。

    首先直接上图。下图选中的 Stream 是首次访问本站,浏览器发出的请求头:

    图片 5

    从图片中可以看到这个 HEADERS 流的长度是 206 个字节,而解码后的头部长度有 451 个字节。由此可见,压缩后的头部大小减少了一半多。

    然而这就是全部吗?再上一张图。下图选中的 Stream 是点击本站链接后,浏览器发出的请求头:

    图片 6

    可以看到这一次,HEADERS 流的长度只有 49 个字节,但是解码后的头部长度却有 470 个字节。这一次,压缩后的头部大小几乎只有原始大小的 1/10。

    为什么前后两次差距这么大呢?我们把两次的头部信息展开,查看同一个字段两次传输所占用的字节数:

    图片 7

    图片 8

    对比后可以发现,第二次的请求头部之所以非常小,是因为大部分键值对只占用了一个字节。尤其是 UserAgent、Cookie 这样的头部,首次请求中需要占用很多字节,后续请求中都只需要一个字节。

    HTTP 2.0 的出现,相比于 HTTP 1.x ,大幅度的提升了 web 性能。

    Header 1

    笔者在研究 HPACK 时,翻阅了部分网上的博客与教程,不甚满意。要么泛泛而谈,要么漏洞百出,要么讲解不够完整。于是,笔者研读了 RFC7541 ,希望能写出一篇完备的 HPACK 讲解,给想要学习这个算法的朋友一些帮助。

    技术原理

    下面这张截图,取自 Google 的性能专家 Ilya Grigorik 在 Velocity 2015 • SC 会议中分享的「HTTP/2 is here, let’s optimize!」,非常直观地描述了 HTTP/2 中头部压缩的原理:

    图片 9

    我再用通俗的语言解释下,头部压缩需要在支持 HTTP/2 的浏览器和服务端之间:

    • 维护一份相同的静态字典(Static Table),包含常见的头部名称,以及特别常见的头部名称与值的组合;
    • 维护一份相同的动态字典(Dynamic Table),可以动态地添加内容;
    • 支持基于静态哈夫曼码表的哈夫曼编码(Huffman Coding);

    静态字典的作用有两个:1)对于完全匹配的头部键值对,例如 :method: GET,可以直接使用一个字符表示;2)对于头部名称可以匹配的键值对,例如 cookie: xxxxxxx,可以将名称使用一个字符表示。HTTP/2 中的静态字典如下(以下只截取了部分,完整表格在这里):

    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
    32 cookie
    60 via
    61 www-authenticate

    同时,浏览器可以告知服务端,将 cookie: xxxxxxx 添加到动态字典中,这样后续整个键值对就可以使用一个字符表示了。类似的,服务端也可以更新对方的动态字典。需要注意的是,动态字典上下文有关,需要为每个 HTTP/2 连接维护不同的字典。

    使用字典可以极大地提升压缩效果,其中静态字典在首次请求中就可以使用。对于静态、动态字典中不存在的内容,还可以使用哈夫曼编码来减小体积。HTTP/2 使用了一份静态哈夫曼码表(详见),也需要内置在客户端和服务端之中。

    这里顺便说一下,HTTP/1 的状态行信息(Method、Path、Status 等),在 HTTP/2 中被拆成键值对放入头部(冒号开头的那些),同样可以享受到字典和哈夫曼压缩。另外,HTTP/2 中所有头部名称必须小写。

    图片 10img

    图片 11

    如有不足或者疑惑之处,欢迎大家指出。

    实现细节

    了解了 HTTP/2 头部压缩的基本原理,最后我们来看一下具体的实现细节。HTTP/2 的头部键值对有以下这些情况:

    1)整个头部键值对都在字典中

    JavaScript

    0 1 2 3 4 5 6 7 --- --- --- --- --- --- --- --- | 1 | Index (7 ) | --- ---------------------------

    1
    2
    3
    4
    5
      0   1   2   3   4   5   6   7
    --- --- --- --- --- --- --- ---
    | 1 |        Index (7 )         |
    --- ---------------------------
     

    这是最简单的情况,使用一个字节就可以表示这个头部了,最左一位固定为 1,之后七位存放键值对在静态或动态字典中的索引。例如下图中,头部索引值为 2(0000010),在静态字典中查询可得 :method: GET

    图片 12

    2)头部名称在字典中,更新动态字典

    JavaScript

    0 1 2 3 4 5 6 7 --- --- --- --- --- --- --- --- | 0 | 1 | Index (6 ) | --- --- ----------------------- | H | Value Length (7 ) | --- --------------------------- | Value String (Length octets) | -------------------------------

    1
    2
    3
    4
    5
    6
    7
    8
    9
      0   1   2   3   4   5   6   7
    --- --- --- --- --- --- --- ---
    | 0 | 1 |      Index (6 )       |
    --- --- -----------------------
    | H |     Value Length (7 )     |
    --- ---------------------------
    | Value String (Length octets)  |
    -------------------------------
     

    对于这种情况,首先需要使用一个字节表示头部名称:左两位固定为 01,之后六位存放头部名称在静态或动态字典中的索引。接下来的一个字节第一位 H 表示头部值是否使用了哈夫曼编码,剩余七位表示头部值的长度 L,后续 L 个字节就是头部值的具体内容了。例如下图中索引值为 32(100000),在静态字典中查询可得 cookie;头部值使用了哈夫曼编码(1),长度是 28(0011100);接下来的 28 个字节是 cookie 的值,将其进行哈夫曼解码就能得到具体内容。

    图片 13

    客户端或服务端看到这种格式的头部键值对,会将其添加到自己的动态字典中。后续传输这样的内容,就符合第 1 种情况了。

    3)头部名称不在字典中,更新动态字典

    JavaScript

    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) | -------------------------------

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
      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 种情况类似,只是由于头部名称不在字典中,所以第一个字节固定为 01000000;接着申明名称是否使用哈夫曼编码及长度,并放上名称的具体内容;再申明值是否使用哈夫曼编码及长度,最后放上值的具体内容。例如下图中名称的长度是 5(0000101),值的长度是 6(0000110)。对其具体内容进行哈夫曼解码后,可得 pragma: no-cache

    图片 14

    客户端或服务端看到这种格式的头部键值对,会将其添加到自己的动态字典中。后续传输这样的内容,就符合第 1 种情况了。

    4)头部名称在字典中,不允许更新动态字典

    JavaScript

    0 1 2 3 4 5 6 7 --- --- --- --- --- --- --- --- | 0 | 0 | 0 | 1 | Index (4 ) | --- --- ----------------------- | H | Value Length (7 ) | --- --------------------------- | Value String (Length octets) | -------------------------------

    1
    2
    3
    4
    5
    6
    7
    8
    9
      0   1   2   3   4   5   6   7
    --- --- --- --- --- --- --- ---
    | 0 | 0 | 0 | 1 |  Index (4 )   |
    --- --- -----------------------
    | H |     Value Length (7 )     |
    --- ---------------------------
    | Value String (Length octets)  |
    -------------------------------
     

    这种情况与第 2 种情况非常类似,唯一不同之处是:第一个字节左四位固定为 0001,只剩下四位来存放索引了,如下图:

    图片 15

    这里需要介绍另外一个知识点:对整数的解码。上图中第一个字节为 00011111,并不代表头部名称的索引为 15(1111)。第一个字节去掉固定的 0001,只剩四位可用,将位数用 N 表示,它只能用来表示小于「2 ^ N – 1 = 15」的整数 I。对于 I,需要按照以下规则求值(RFC 7541 中的伪代码,via):

    JavaScript

    if I < 2 ^ N - 1, return I # I 小于 2 ^ N - 1 时,直接返回 else M = 0 repeat B = next octet # 让 B 等于下一个八位 I = I (B & 127) * 2 ^ M # I = I (B 低七位 * 2 ^ M) M = M 7 while B & 128 == 128 # B 最高位 = 1 时继续,否则返回 I return I

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    if I &lt; 2 ^ N - 1, return I         # I 小于 2 ^ N - 1 时,直接返回
    else
        M = 0
        repeat
            B = next octet             # 让 B 等于下一个八位
            I = I (B &amp; 127) * 2 ^ M  # I = I (B 低七位 * 2 ^ M)
            M = M 7
        while B &amp; 128 == 128           # B 最高位 = 1 时继续,否则返回 I
        return I
     

    对于上图中的数据,按照这个规则算出索引值为 32(00011111 00010001,15 17),代表 cookie。需要注意的是,协议中所有写成(N )的数字,例如 Index (4 )、Name Length (7 ),都需要按照这个规则来编码和解码。

    这种格式的头部键值对,不允许被添加到动态字典中(但可以使用哈夫曼编码)。对于一些非常敏感的头部,比如用来认证的 Cookie,这么做可以提高安全性。

    5)头部名称不在字典中,不允许更新动态字典

    JavaScript

    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) | -------------------------------

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
      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)  |
    -------------------------------
     

    这种情况与第 3 种情况非常类似,唯一不同之处是:第一个字节固定为 00010000。这种情况比较少见,没有截图,各位可以脑补。同样,这种格式的头部键值对,也不允许被添加到动态字典中,只能使用哈夫曼编码来减少体积。

    实际上,协议中还规定了与 4、5 非常类似的另外两种格式:将 4、5 格式中的第一个字节第四位由 1 改为 0 即可。它表示「本次不更新动态词典」,而 4、5 表示「绝对不允许更新动态词典」。区别不是很大,这里略过。

    明白了头部压缩的技术细节,理论上可以很轻松写出 HTTP/2 头部解码工具了。我比较懒,直接找来 node-http2 中的 compressor.js 验证一下:

    JavaScript

    var Decompressor = require('./compressor').Decompressor; var testLog = require('bunyan').createLogger({name: 'test'}); var decompressor = new Decompressor(testLog, 'REQUEST'); var buffer = new Buffer('820481634188353daded6ae43d3f877abdd07f66a281b0dae053fad0321aa49d13fda992a49685340c8a6adca7e28102e10fda9677b8d05707f6a62293a9d810020004015309ac2ca7f2c3415c1f53b0497ca589d34d1f43aeba0c41a4c7a98f33a69a3fdf9a68fa1d75d0620d263d4c79a68fbed00177febe58f9fbed00177b518b2d4b70ddf45abefb4005db901f1184ef034eff609cb60725034f48e1561c8469669f081678ae3eb3afba465f7cb234db9f4085aec1cd48ff86a8eb10649cbf', 'hex'); console.log(decompressor.decompress(buffer)); decompressor._table.forEach(function(row, index) { console.log(index 1, row[0], row[1]); });

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var Decompressor = require('./compressor').Decompressor;
     
    var testLog = require('bunyan').createLogger({name: 'test'});
    var decompressor = new Decompressor(testLog, 'REQUEST');
     
    var buffer = new Buffer('820481634188353daded6ae43d3f877abdd07f66a281b0dae053fad0321aa49d13fda992a49685340c8a6adca7e28102e10fda9677b8d05707f6a62293a9d810020004015309ac2ca7f2c3415c1f53b0497ca589d34d1f43aeba0c41a4c7a98f33a69a3fdf9a68fa1d75d0620d263d4c79a68fbed00177febe58f9fbed00177b518b2d4b70ddf45abefb4005db901f1184ef034eff609cb60725034f48e1561c8469669f081678ae3eb3afba465f7cb234db9f4085aec1cd48ff86a8eb10649cbf', 'hex');
     
    console.log(decompressor.decompress(buffer));
     
    decompressor._table.forEach(function(row, index) {
        console.log(index 1, row[0], row[1]);
    });
     

    头部原始数据来自于本文第三张截图,运行结果如下(静态字典只截取了一部分):

    JavaScript

    { ':method': 'GET', ':path': '/', ':authority': 'imququ.com', ':scheme': 'https', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:41.0) Gecko/20100101 Firefox/41.0', accept: 'text/html,application/xhtml xml,application/xml;q=0.9,*/*;q=0.8', 'accept-language': 'en-US,en;q=0.5', 'accept-encoding': 'gzip, deflate', cookie: 'v=47; u=6f048d6e-adc4-4910-8e69-797c399ed456', pragma: 'no-cache' } 1 ':authority' '' 2 ':method' 'GET' 3 ':method' 'POST' 4 ':path' '/' 5 ':path' '/index.html' 6 ':scheme' 'http' 7 ':scheme' 'https' 8 ':status' '200' ... ... 32 'cookie' '' ... ... 60 'via' '' 61 'www-authenticate' '' 62 'pragma' 'no-cache' 63 'cookie' 'u=6f048d6e-adc4-4910-8e69-797c399ed456' 64 'accept-language' 'en-US,en;q=0.5' 65 'accept' 'text/html,application/xhtml xml,application/xml;q=0.9,*/*;q=0.8' 66 'user-agent' 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:41.0) Gecko/20100101 Firefox/41.0' 67 ':authority' 'imququ.com'

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    { ':method': 'GET',
      ':path': '/',
      ':authority': 'imququ.com',
      ':scheme': 'https',
      'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:41.0) Gecko/20100101 Firefox/41.0',
      accept: 'text/html,application/xhtml xml,application/xml;q=0.9,*/*;q=0.8',
      'accept-language': 'en-US,en;q=0.5',
      'accept-encoding': 'gzip, deflate',
      cookie: 'v=47; u=6f048d6e-adc4-4910-8e69-797c399ed456',
      pragma: 'no-cache' }
    1 ':authority' ''
    2 ':method' 'GET'
    3 ':method' 'POST'
    4 ':path' '/'
    5 ':path' '/index.html'
    6 ':scheme' 'http'
    7 ':scheme' 'https'
    8 ':status' '200'
    ... ...
    32 'cookie' ''
    ... ...
    60 'via' ''
    61 'www-authenticate' ''
    62 'pragma' 'no-cache'
    63 'cookie' 'u=6f048d6e-adc4-4910-8e69-797c399ed456'
    64 'accept-language' 'en-US,en;q=0.5'
    65 'accept' 'text/html,application/xhtml xml,application/xml;q=0.9,*/*;q=0.8'
    66 'user-agent' 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:41.0) Gecko/20100101 Firefox/41.0'
    67 ':authority' 'imququ.com'
     

    可以看到,这段从 Wireshark 拷出来的头部数据可以正常解码,动态字典也得到了更新(62 – 67)。

    这是 Akamai 公司建立的一个官方的演示,用以说明 HTTP/2 相比于之前的 HTTP/1.1 在性能上的大幅度提升。 同时请求 379 张图片,从Load time 的对比可以看出 HTTP/2 在速度上的优势。

    Header 2

    HPACK的由来

    HTTP1.X 由于其设计的缺陷,被大家诟病已久,其中头疼的问题之一,就是无意义的重复的头部。

    于是出现了各种各样的解决方案, 如 Google 直接在 HTTP1.X 的基础上设计了 SPDY 协议, 对头部使用 deflate 算法进行压缩,一并解决了多路复用和优先级等问题。

    而 HTTP/2 的实现就是参考了 SPDY 协议, 但是专门为头部压缩设计了一套压缩算法,就是我们的 HPACK 。

    总结

    在进行 HTTP/2 网站性能优化时很重要一点是「使用尽可能少的连接数」,本文提到的头部压缩是其中一个很重要的原因:同一个连接上产生的请求和响应越多,动态字典积累得越全,头部压缩效果也就越好。所以,针对 HTTP/2 网站,最佳实践是不要合并资源,不要散列域名。

    默认情况下,浏览器会针对这些情况使用同一个连接:

    • 同一域名下的资源;
    • 不同域名下的资源,但是满足两个条件:1)解析到同一个 IP;2)使用同一个证书;

    上面第一点容易理解,第二点则很容易被忽略。实际上 Google 已经这么做了,Google 一系列网站都共用了同一个证书,可以这样验证:

    JavaScript

    $ openssl s_client -connect google.com:443 |openssl x509 -noout -text | grep DNS depth=2 C = US, O = GeoTrust Inc., CN = GeoTrust Global CA verify error:num=20:unable to get local issuer certificate verify return:0 DNS:*.google.com, DNS:*.android.com, DNS:*.appengine.google.com, DNS:*.cloud.google.com, DNS:*.google-analytics.com, DNS:*.google.ca, DNS:*.google.cl, DNS:*.google.co.in, DNS:*.google.co.jp, DNS:*.google.co.uk, DNS:*.google.com.ar, DNS:*.google.com.au, DNS:*.google.com.br, DNS:*.google.com.co, DNS:*.google.com.mx, DNS:*.google.com.tr, DNS:*.google.com.vn, DNS:*.google.de, DNS:*.google.es, DNS:*.google.fr, DNS:*.google.hu, DNS:*.google.it, DNS:*.google.nl, DNS:*.google.pl, DNS:*.google.pt, DNS:*.googleadapis.com, DNS:*.googleapis.cn, DNS:*.googlecommerce.com, DNS:*.googlevideo.com, DNS:*.gstatic.cn, DNS:*.gstatic.com, DNS:*.gvt1.com, DNS:*.gvt2.com, DNS:*.metric.gstatic.com, DNS:*.urchin.com, DNS:*.url.google.com, DNS:*.youtube-nocookie.com, DNS:*.youtube.com, DNS:*.youtubeeducation.com, DNS:*.ytimg.com, DNS:android.com, DNS:g.co, DNS:goo.gl, DNS:google-analytics.com, DNS:google.com, DNS:googlecommerce.com, DNS:urchin.com, DNS:youtu.be, DNS:youtube.com, DNS:youtubeeducation.com

    1
    2
    3
    4
    5
    6
    7
    $ openssl s_client -connect google.com:443 |openssl x509 -noout -text | grep DNS
     
    depth=2 C = US, O = GeoTrust Inc., CN = GeoTrust Global CA
    verify error:num=20:unable to get local issuer certificate
    verify return:0
                    DNS:*.google.com, DNS:*.android.com, DNS:*.appengine.google.com, DNS:*.cloud.google.com, DNS:*.google-analytics.com, DNS:*.google.ca, DNS:*.google.cl, DNS:*.google.co.in, DNS:*.google.co.jp, DNS:*.google.co.uk, DNS:*.google.com.ar, DNS:*.google.com.au, DNS:*.google.com.br, DNS:*.google.com.co, DNS:*.google.com.mx, DNS:*.google.com.tr, DNS:*.google.com.vn, DNS:*.google.de, DNS:*.google.es, DNS:*.google.fr, DNS:*.google.hu, DNS:*.google.it, DNS:*.google.nl, DNS:*.google.pl, DNS:*.google.pt, DNS:*.googleadapis.com, DNS:*.googleapis.cn, DNS:*.googlecommerce.com, DNS:*.googlevideo.com, DNS:*.gstatic.cn, DNS:*.gstatic.com, DNS:*.gvt1.com, DNS:*.gvt2.com, DNS:*.metric.gstatic.com, DNS:*.urchin.com, DNS:*.url.google.com, DNS:*.youtube-nocookie.com, DNS:*.youtube.com, DNS:*.youtubeeducation.com, DNS:*.ytimg.com, DNS:android.com, DNS:g.co, DNS:goo.gl, DNS:google-analytics.com, DNS:google.com, DNS:googlecommerce.com, DNS:urchin.com, DNS:youtu.be, DNS:youtube.com, DNS:youtubeeducation.com
     

    使用多域名加上相同的 IP 和证书部署 Web 服务有特殊的意义:让支持 HTTP/2 的终端只建立一个连接,用上 HTTP/2 协议带来的各种好处;而只支持 HTTP/1.1 的终端则会建立多个连接,达到同时更多并发请求的目的。这在 HTTP/2 完全普及前也是一个不错的选择。

    本文就写到这里,希望能给对 HTTP/2 感兴趣的同学带来帮助,也欢迎大家继续关注本博客的「HTTP/2 专题」。

    打赏支持我写出更多好文章,谢谢!

    打赏作者

    后面我们将通过几个方面来说说HTTP 2.0 和 HTTP1.1 区别,并且和你解释下其中的原理。

    如上图,同一个页面中对两个资源的请求,请求中的头部字段绝大部分是完全相同的。"User-Agent" 等头部字段通常还会消耗大量的带宽。

    HPACK的实现

    打赏支持我写出更多好文章,谢谢!

    任选一种支付方式

    图片 16 图片 17

    1 赞 3 收藏 评论

    区别一:多路复用

    多路复用允许单一的 HTTP/2 连接同时发起多重的请求-响应消息。看个例子:

    图片 18img

    整个访问流程第一次请求index.html页面,之后浏览器会去请求style.css和scripts.js的文件。左边的图是顺序加载两个个文件的,右边则是并行加载两个文件。

    我们知道HTTP底层其实依赖的是TCP协议,那问题是在同一个连接里面同时发生两个请求响应着是怎么做到的?

    首先你要知道,TCP连接相当于两根管道(一个用于服务器到客户端,一个用于客户端到服务器),管道里面数据传输是通过字节码传输,传输是有序的,每个字节都是一个一个来传输。

    例如客户端要向服务器发送Hello、World两个单词,只能是先发送Hello再发送World,没办法同时发送这两个单词。不然服务器收到的可能就是HWeolrllod(注意是穿插着发过去了,但是顺序还是不会乱)。这样服务器就懵b了。

    接上面的问题,能否同时发送Hello和World两个单词能,当然也是可以的,可以将数据拆成包,给每个包打上标签。发的时候是这样的①H ②W ①e ②o ①l ②r ①l ②l ①o ②d。这样到了服务器,服务器根据标签把两个单词区分开来。实际的发送效果如下图:

    图片 19img

    要实现上面的效果我们引入一个新的概念就是:二进制分帧。

    二进制分帧层 在 应用层和传输层(TCP or UDP)之间。HTTP/2并没有去修改TCP协议而是尽可能的利用TCP的特性。

    图片 20img

    在二进制分帧层中, HTTP/2 会将所有传输的信息分割为帧,并对它们采用二进制格式的编码 ,其中 首部信息会被封装到 HEADER frame,而相应的 Request Body 则封装到 DATA frame 里面。

    HTTP 性能优化的关键并不在于高带宽,而是低延迟。TCP 连接会随着时间进行自我「调谐」,起初会限制连接的最大速度,如果数据成功传输,会随着时间的推移提高传输的速度。这种调谐则被称为 TCP 慢启动。由于这种原因,让原本就具有突发性和短时性的 HTTP 连接变的十分低效。

    HTTP/2 通过让所有数据流共用同一个连接,可以更有效地使用 TCP 连接,让高带宽也能真正的服务于 HTTP 的性能提升。

    通过下面两张图,我们可以更加深入的认识多路复用:

    图片 21img

    HTTP/1

    图片 22img

    HTTP/2

    总结下:多路复用技术:单连接多资源的方式,减少服务端的链接压力,内存占用更少,连接吞吐量更大;由于减少TCP 慢启动时间,提高传输的速度

    首部压缩正是为了解决这样的问题而设计。

    基本原理

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

    关于作者:JerryQu

    图片 23

    专注 Web 开发,关注 Web 性能优化与安全。 个人主页 · 我的文章 · 2 ·   

    图片 24

    区别二:首部压缩

    为什么要压缩?在 HTTP/1 中,HTTP 请求和响应都是由「状态行、请求 / 响应头部、消息主体」三部分组成。一般而言,消息主体都会经过 gzip 压缩,或者本身传输的就是压缩过后的二进制文件,但状态行和头部却没有经过任何压缩,直接以纯文本传输。

    随着 Web 功能越来越复杂,每个页面产生的请求数也越来越多,导致消耗在头部的流量越来越多,尤其是每次都要传输 UserAgent、Cookie 这类不会频繁变动的内容,完全是一种浪费。掌握这 11 个方法论,搞定一场完美技术面试!

    我们再用通俗的语言解释下,压缩的原理。头部压缩需要在支持 HTTP/2 的浏览器和服务端之间。

    • 维护一份相同的静态字典(Static Table),包含常见的头部名称,以及特别常见的头部名称与值的组合;
    • 维护一份相同的动态字典(Dynamic Table),可以动态的添加内容;
    • 支持基于静态哈夫曼码表的哈夫曼编码(Huffman Coding);

    静态字典的作用有两个:

    1)对于完全匹配的头部键值对,例如 “:method :GET”,可以直接使用一个字符表示;

    2)对于头部名称可以匹配的键值对,例如 “cookie :xxxxxxx”,可以将名称使用一个字符表示。

    HTTP/2 中的静态字典如下(以下只截取了部分,完整表格在这里):

    图片 25img

    同时,浏览器和服务端都可以向动态字典中添加键值对,之后这个键值对就可以使用一个字符表示了。需要注意的是,动态字典上下文有关,需要为每个 HTTP/2 连接维护不同的字典。在传输过程中使用,使用字符代替键值对大大减少传输的数据量。

    首部压缩是HTTP/2中一个非常重要的特性,它大大减少了网络中HTTP请求/响应头部传输所需的带宽。HTTP/2的首部压缩,主要从两个方面实现,一是首部表示,二是请求间首部字段内容的复用。

    实现细节

    HPACK 中有2个索引表,分别是静态索引表和动态索引表。

    区别三:HTTP2支持服务器推送

    服务端推送是一种在客户端请求之前发送数据的机制。当代网页使用了许多资源:HTML、样式表、脚本、图片等等。在HTTP/1.x中这些资源每一个都必须明确地请求。这可能是一个很慢的过程。浏览器从获取HTML开始,然后在它解析和评估页面的时候,增量地获取更多的资源。因为服务器必须等待浏览器做每一个请求,网络经常是空闲的和未充分使用的。

    为了改善延迟,HTTP/2引入了server push,它允许服务端推送资源给浏览器,在浏览器明确地请求之前。一个服务器经常知道一个页面需要很多附加资源,在它响应浏览器第一个请求的时候,可以开始推送这些资源。这允许服务端去完全充分地利用一个可能空闲的网络,改善页面加载时间。

    图片 26img

    首部表示

    在HTTP中,首部字段是一个名值队,所有的首部字段组成首部字段列表。在HTTP/1.x中,首部字段都被表示为字符串,一行一行的首部字段字符串组成首部字段列表。而在HTTP/2的首部压缩HPACK算法中,则有着不同的表示方法。

    HPACK算法表示的对象,主要有原始数据类型的整型值和字符串,头部字段,以及头部字段列表。

    静态索引表

    是预先定义在 RFC 里的固定的头部,这里展示部分:

    Index Header Name
    1 :authority
    2 :method
    3 :method
    4 :path
    5 :path
    6 :scheme
    7 :scheme
    8 :status
    9 :status
    10 :status
    11 :status
    12 :status
    13 :status
    14 :status
    15 accept-charset
    16 accept-encoding
    ... ...

    也就是说当要发送的值符合静态表时,用对应的 Index 替换即可,这样就大大压缩了头部的大小。

    当然,这个表是预先定义好的,只有固定的几十个值,如果遇到不在静态表中的值,就会用到我们的动态表。

    整数的表示

    在HPACK中,整数用于表示 头部字段的名字的索引头部字段索引字符串长度。整数的表示可在字节内的任何位置开始。但为了处理上的优化,整数的表示总是在字节的结尾处结束。

    整数由两部分表示:填满当前字节的前缀,以及在前缀不足以表示整数时的一个可选字节列表。如果整数值足够小,比如,小于2^N-1,那么把它编码进前缀即可,而不需要额外的空间。如:

      0   1   2   3   4   5   6   7
     --- --- --- --- --- --- --- --- 
    | ? | ? | ? |       Value       |
     --- --- --- ------------------- 
    

    在这个图中,前缀有5位,而要表示的数足够小,因此无需更多空间就可以表示整数了。

    当前缀不足以表示整数时,前缀的所有位被置为1,再将值减去2^N-1之后用一个或多个字节编码。每个字节的最高有效位被用作连续标记:除列表的最后一个字节外,该位的值都被设为1。字节中剩余的位被用于编码减小后的值。

      0   1   2   3   4   5   6   7
     --- --- --- --- --- --- --- --- 
    | ? | ? | ? | 1   1   1   1   1 |
     --- --- --- ------------------- 
    | 1 |    Value-(2^N-1) LSB      |
     --- --------------------------- 
                   ...
     --- --------------------------- 
    | 0 |    Value-(2^N-1) MSB      |
     --- --------------------------- 
    

    要由字节列表解码出整数值,首先需要将列表中的字节顺序反过来。然后,移除每个字节的最高有效位。连接字节的剩余位,再将结果加2^N-1获得整数值。

    前缀大小N,总是在1到8之间。从字节边界处开始编码的整数值其前缀为8位。

    这种整数表示法允许编码无限大的值。

    表示整数I的伪代码如下:

    if I < 2^N - 1, encode I on N bits
    else
        encode (2^N - 1) on N bits
        I = I - (2^N - 1)
        while I >= 128
             encode (I % 128   128) on 8 bits
             I = I / 128
        encode I on 8 bits
    

    encode (I % 128 128) on 8 bits 一行中,加上128的意思是,最高有效位是1。如果要编码的整数值等于 (2^N - 1),则用前缀和紧跟在前缀后面的值位0的一个字节来表示。

    OkHttp中,这个算法的实现在 okhttp3.internal.http2.Hpack.Writer 中:

        // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-4.1.1
        void writeInt(int value, int prefixMask, int bits) {
          // Write the raw value for a single byte value.
          if (value < prefixMask) {
            out.writeByte(bits | value);
            return;
          }
    
          // Write the mask to start a multibyte value.
          out.writeByte(bits | prefixMask);
          value -= prefixMask;
    
          // Write 7 bits at a time 'til we're done.
          while (value >= 0x80) {
            int b = value & 0x7f;
            out.writeByte(b | 0x80);
            value >>>= 7;
          }
          out.writeByte(value);
        }
    

    这里给最高有效位置 1 的方法就不是加上128,而是与0x80执行或操作。

    解码整数I的伪代码如下:

    decode I from the next N bits
    if I < 2^N - 1, return I
    else
        M = 0
        repeat
            B = next octet
            I = I   (B & 127) * 2^M
            M = M   7
        while B & 128 == 128
        return I
    

    decode I from the next N bits 这一行等价于一个赋值语句 ****I = byteValue & (2^N - 1)***

    OkHttp中,这个算法的实现在 okhttp3.internal.http2.Hpack.Reader

        int readInt(int firstByte, int prefixMask) throws IOException {
          int prefix = firstByte & prefixMask;
          if (prefix < prefixMask) {
            return prefix; // This was a single byte value.
          }
    
          // This is a multibyte value. Read 7 bits at a time.
          int result = prefixMask;
          int shift = 0;
          while (true) {
            int b = readByte();
            if ((b & 0x80) != 0) { // Equivalent to (b >= 128) since b is in [0..255].
              result  = (b & 0x7f) << shift;
              shift  = 7;
            } else {
              result  = b << shift; // Last byte.
              break;
            }
          }
          return result;
        }
    

    尽管HPACK的整数表示方法可以表示无限大的数,但实际的实现中并不会将整数当做无限大的整数来处理。

    动态索引表

    动态表是一个由先进先出的队列维护的有空间限制的表,里面同样维护的是头部与对应的索引。

    每个动态表只针对一个连接,每个连接的压缩解压缩的上下文有且仅有一个动态表。

    什么是连接,抽象的说是HTTP依赖的可靠的传输层的连接,一般来说指的是一个TCP连接。 HTTP/2 中引入了多路复用的概念,对于同一个域名的多个请求,会复用同一个连接。

    那么动态表就是,当一个头部没有出现过的时候,会把他插入动态表中,下次同名的值就可能会在表中查到到索引并替换掉头部。为什么我说是可能呢,因为动态表是有最大空间限制的。

    动态表的大小 = (每个 Header 的字节数的和 32) * 键值对个数

    为什么要加32呢,32是为了头所占用的额外空间和计算头被引用次数而估计的值。

    而动态表的最大字节数由 HTTP/2 的 SETTING 帧中的 SETTINGS_HEADER_TABLE_SIZE 来控制。

    同时压缩时,可以插入一个字节来动态的修改动态表的大小,但是不可以超过上面预设的值。这个下面会介绍。

    那么动态表是如何管理大小呢,2种情况下,动态表会被修改:

    1. 压缩方用上述方式要求动态修改动态表的大小。在这种情况下,如果新的值更小,并且当前大小超过了新值,就会从旧至新,不断的删除头,直到小于等于新的大小。
    2. 收到或发出一个新的头部,会触发插入和可能的删除操作。 RFC 里面说的比较复杂,我用等价的语义解释一下。新的值被插到队首,一样从旧到新删除直到空间占用小于等于最大值。那么在这种情况下,如果新来的头比最大值还要大,就等于变相的清除了动态表。

    动态索引表中最的值是索引值最的,最的值是索引值最的。
    动态表与静态表共同组成了索引表的索引空间。

    字符串字面量的编码

    头部字段名和头部字段值可使用字符串字面量表示。字符串字面量有两种表示方式,一种是直接用UTF-8这样的字符串编码方式表示,另一种是将字符串编码用Huffman 码表示。 字符串表示的格式如下:

      0   1   2   3   4   5   6   7
     --- --- --- --- --- --- --- --- 
    | H |    String Length (7 )     |
     --- --------------------------- 
    |  String Data (Length octets)  |
     ------------------------------- 
    

    先是标记位 H 字符串长度,然后是字符串的实际数据。各部分说明如下:

    • H: 一位的标记,指示字符串的字节是否为Huffman编码。
    • 字符串长度: 编码字符串字面量的字节数,一个整数,编码方式可以参考前面 整数的表示 的部分,一个7位前缀的整数编码。
    • 字符串数据: 字符串的实际数据。如果H是'0',则数据是字符串字面量的原始字节。如果H是'1',则数据是字符串字面量的Huffman编码。

    在OkHttp3中,总是会使用直接的字符串编码,而不是Huffman编码, okhttp3.internal.http2.Hpack.Writer 中编码字符串的过程如下:

        void writeByteString(ByteString data) throws IOException {
          writeInt(data.size(), PREFIX_7_BITS, 0);
          out.write(data);
        }
    

    OkHttp中,解码字符串在 okhttp3.internal.http2.Hpack.Reader 中实现:

        /** Reads a potentially Huffman encoded byte string. */
        ByteString readByteString() throws IOException {
          int firstByte = readByte();
          boolean huffmanDecode = (firstByte & 0x80) == 0x80; // 1NNNNNNN
          int length = readInt(firstByte, PREFIX_7_BITS);
    
          if (huffmanDecode) {
            return ByteString.of(Huffman.get().decode(source.readByteArray(length)));
          } else {
            return source.readByteString(length);
          }
        }
    

    字符串编码没有使用Huffman编码时,解码过程比较简单,而使用了Huffman编码时会借助于Huffman类来解码。

    Huffman编码是一种变长字节编码,对于使用频率高的字节,使用更少的位数,对于使用频率低的字节则使用更多的位数。每个字节的Huffman码是根据统计经验值分配的。为每个字节分配Huffman码的方法可以参考 哈夫曼(huffman)树和哈夫曼编码 。

    索引空间

    索引空间就是动态表和静态表组成的头部与索引的映射关系。这个看起来很高深,实际上很简单。

    静态表的大小现在是固定的 61, 因此静态表就是从1到61的索引,然后动态表从新到旧,依次从62开始递增。这样就共同的组成了一个索引空间,且互不冲突。

    如果以后静态表扩充了,依次往后推即可。

    哈夫曼树的构造

    Huffman 类被设计为一个单例类。对象在创建时构造一个哈夫曼树以用于编码和解码操作。

      private static final Huffman INSTANCE = new Huffman();
    
      public static Huffman get() {
        return INSTANCE;
      }
    
      private final Node root = new Node();
    
      private Huffman() {
        buildTree();
      }
    ......
    
      private void buildTree() {
        for (int i = 0; i < CODE_LENGTHS.length; i  ) {
          addCode(i, CODES[i], CODE_LENGTHS[i]);
        }
      }
    
      private void addCode(int sym, int code, byte len) {
        Node terminal = new Node(sym, len);
    
        Node current = root;
        while (len > 8) {
          len -= 8;
          int i = ((code >>> len) & 0xFF);
          if (current.children == null) {
            throw new IllegalStateException("invalid dictionary: prefix not unique");
          }
          if (current.children[i] == null) {
            current.children[i] = new Node();
          }
          current = current.children[i];
        }
    
        int shift = 8 - len;
        int start = (code << shift) & 0xFF;
        int end = 1 << shift;
        for (int i = start; i < start   end; i  ) {
          current.children[i] = terminal;
        }
      }
    ......
    
      private static final class Node {
    
        // Null if terminal.
        private final Node[] children;
    
        // Terminal nodes have a symbol.
        private final int symbol;
    
        // Number of bits represented in the terminal node.
        private final int terminalBits;
    
        /** Construct an internal node. */
        Node() {
          this.children = new Node[256];
          this.symbol = 0; // Not read.
          this.terminalBits = 0; // Not read.
        }
    
        /**
         * Construct a terminal node.
         *
         * @param symbol symbol the node represents
         * @param bits length of Huffman code in bits
         */
        Node(int symbol, int bits) {
          this.children = null;
          this.symbol = symbol;
          int b = bits & 0x07;
          this.terminalBits = b == 0 ? 8 : b;
        }
      }
    

    OkHttp3中的 哈夫曼树 并不是一个二叉树,它的每个节点最多都可以有256个字节点。OkHttp3用这种方式来优化Huffman编码解码的效率。用一个图来表示,将是下面这个样子的:

    图片 27

    Huffman Tree

    编码解码

    Huffman 编码

      void encode(byte[] data, OutputStream out) throws IOException {
        long current = 0;
        int n = 0;
    
        for (int i = 0; i < data.length; i  ) {
          int b = data[i] & 0xFF;
          int code = CODES[b];
          int nbits = CODE_LENGTHS[b];
    
          current <<= nbits;
          current |= code;
          n  = nbits;
    
          while (n >= 8) {
            n -= 8;
            out.write(((int) (current >> n)));
          }
        }
    
        if (n > 0) {
          current <<= (8 - n);
          current |= (0xFF >>> n);
          out.write((int) current);
        }
      }
    

    逐个字节地编码数据。编码的最后一个字节没有字节对齐时,会在低位填充1。

    无符号整数编码

    在 HPACK 中,经常会用一个或者多个字节表示无符号整数。在 HPACK 中一个无符号整数,并不总是在一个字节的开始,但是总是在一个字节的末尾结束。
    这么说有些抽象,什么叫不是一个字节的开始。如下所示:

      0   1   2   3   4   5   6   7
     --- --- --- --- --- --- --- --- 
    | ? | ? | ? |       Value       |
     --- --- --- ------------------- 
    

    0-2 bit可能会用于其他的标识, 那么表达数值只占了5个 bit , 因此只能表示2^5-1,因此当需要表达的数值小于32时,一个字节足够表达了,如果超过了2^n-1以后,剩下的字节是如何编码的呢:

        0     1   2   3   4   5   6   7
     ------- --- --- --- --- --- --- ---
    | (0/1) | ? | ? | ? | ? | ? | ? | ? |
     ------- --- --- --------------- ---
    

    第一个字节的 n 个 bit 全部置1,然后假设这个数为 i,
    那么remain = i - (2^n - 1);然后用剩余的字节表示这个 remain 值,然后首 bit 标识是否是最后一个字节,1表示不是,0表示是。

    去掉首字节,就类似于用7个 bit 的字节的小端法表示无符号整数 remain 。

    一个整数0x12345678用标准的 byte 数组 buffer 用小端法表示就是:

    buffer[0] = 0x78; 
    buffer[1] = 0x56; 
    buffer[3] = 0x34;
    buffer[3] = 0x12;
    

    那么我们整体的字节表示无符号数 i 的伪代码如下:

    if I < 2^N - 1, encode I on N bits
    else
        encode (2^N - 1) on N bits
        I = I - (2^N - 1)
        while I >= 0x7f
             encode (I & 0x7f | 1 << 7) on 8 bits
             I = I >> 7
        encode I on 8 bits
    

    同样,解码的伪代码如下:

    decode I from the next N bits
    if I < 2^N - 1, return I
    else
        M = 0
        repeat
            B = next octet
            I = I   (B & 0x7f) * (1 << M)
            M = M   7
        while (B >> 7) & 1 
        return I
    

    那么举例如果我们用 3 个 bit 作为前缀编码,

    5 = ?????101
    (101b = 5)
    8 = ?????111 00000001
    (111b   1 = 8)
    135 = 7   128 = ?????111 10000000 00000001
    (111b   0   128 * 1 = 135)
    

    Huffman 解码

      byte[] decode(byte[] buf) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        Node node = root;
        int current = 0;
        int nbits = 0;
        for (int i = 0; i < buf.length; i  ) {
          int b = buf[i] & 0xFF;
          current = (current << 8) | b;
          nbits  = 8;
          while (nbits >= 8) {
            int c = (current >>> (nbits - 8)) & 0xFF;
            node = node.children[c];
            if (node.children == null) {
              // terminal node
              baos.write(node.symbol);
              nbits -= node.terminalBits;
              node = root;
            } else {
              // non-terminal node
              nbits -= 8;
            }
          }
        }
    
        while (nbits > 0) {
          int c = (current << (8 - nbits)) & 0xFF;
          node = node.children[c];
          if (node.children != null || node.terminalBits > nbits) {
            break;
          }
          baos.write(node.symbol);
          nbits -= node.terminalBits;
          node = root;
        }
    
        return baos.toByteArray();
      }
    

    配合Huffman树的构造过程,分几种情况来看。Huffman码自己对齐时;Huffman码没有字节对齐,最后一个字节的最低有效位包含了数据流中下一个Huffman码的最高有效位;Huffman码没有字节对齐,最后一个字节的最低有效位包含了填充的1。

    有兴趣的可以参考其它文档对Huffman编码算法做更多了解。

    字面字符串编码

    有了无符号整数编码的基础,我们可以对字符串进行编码,如下所示:

      0   1   2   3   4   5   6   7
     --- --- --- --- --- --- --- --- 
    | H |    String Length (7 )     |
     --- --------------------------- 
    |  String Data (Length octets)  |
     ------------------------------- 
    

    H : 表示是否是 huffman 编码,1 是 0 不是
    StringLength : 表示随后跟随的字符串的长度,用上述的整数编码方式编码
    StringData: 如果是 huffman 编码,则使用 huffman 编码后的字符串,否则就是原始串。

    首部字段及首部块的表示

    首部字段主要有两种表示方法,分别是索引表示和字面量表示。字面量表示又分为首部字段的名字用索引表示值用字面量表示和名字及值都用字面量表示等方法。

    说到用索引表示首部字段,就不能不提一下HPACK的动态表和静态表。

    HPACK使用两个表将 头部字段 与 索引 关联起来。 静态表 是预定义的,它包含了常见的头部字段(其中的大多数值为空)。 动态表 是动态的,它可被编码器用于编码重复的头部字段。

    静态表由一个预定义的头部字段静态列表组成。它的条目在 HPACK规范的 附录 A 中定义。

    动态表由以先进先出顺序维护的 头部字段列表 组成。动态表中第一个且最新的条目索引值最低,动态表最旧的条目索引值最高。

    动态表最初是空的。条目随着每个头部块的解压而添加。

    2首部裁减,完全剖判。静态表和动态表被组合为统一的索引地址空间。

    在 (1 ~ 静态表的长度(包含)) 之间的索引值指向静态表中的元素。

    2首部裁减,完全剖判。大于静态表长度的索引值指向动态表中的元素。通过将头部字段的索引减去静态表的长度来查找指向动态表的索引。

    对于静态表大小为 s,动态表大小为 k 的情况,下图展示了完整的有效索引地址空间。

            <----------  Index Address Space ---------->
            <-- Static  Table -->  <-- Dynamic Table -->
             --- ----------- ---    --- ----------- --- 
            | 1 |    ...    | s |  |s 1|    ...    |s k|
             --- ----------- ---    --- ----------- --- 
                                   ^                   |
                                   |                   V
                            Insertion Point      Dropping Point
    
    静态HUFFMAN编码

    先简单介绍一下 huffman 编码,huffman 编码是一个根据字符出现的概率重新编排字符的二进制代码,从而压缩概率高的字符串,进而压缩整个串的长度。如果不了解的话,建议先去学习一下,这里不再赘述。

    这里的 huffman 编码是静态的,是根据过去大量的 Http 头的数据从而选出的编码方案。整个静态表在这里 http://httpwg.org/specs/rfc7541.html#huffman.code

    用索引表示头部字段

    当一个头部字段的名-值已经包含在了静态表或动态表中时,就可以用一个指向静态表或动态表的索引来表示它了。表示方法如下:

      0   1   2   3   4   5   6   7
     --- --- --- --- --- --- --- --- 
    | 1 |        Index (7 )         |
     --- --------------------------- 
    

    头部字段表示的最高有效位置1,然后用前面看到的表示整数的方法表示索引,即索引是一个7位前缀编码的整数。

    二进制编码

    有了上述所有的准备,我们就可以真正的进行二进制编码压缩了。
    有以下几种类型的字节流:

    用字面量表示头部字段

    在这种表示法中,头部字段的值是用字面量表示的,但头部字段的名字则不一定。根据名字的表示方法的差异,以及是否将头部字段加进动态表等,而分为多种情况。

    1 已被索引的头部
      0   1   2   3   4   5   6   7
     --- --- --- --- --- --- --- --- 
    | 1 |        Index (7 )         |
     --- --------------------------- 
    

    已被索引的头部,会被替换成如上格式:
    第一个 bit 为1, 随后紧跟一个 无整数的编码表示 Index,即为静态表或者是动态表中的索引值。

    增量索引的字面量表示

    以这种方法表示的头部字段需要被 加进动态表中。在这种表示方法下,头部字段的值用索引表示时,头部字段的表示如下:

      0   1   2   3   4   5   6   7
     --- --- --- --- --- --- --- --- 
    | 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)  |
     ------------------------------- 
    

    增量索引的字面量头部字段表示以'01' 的2位模式开始。

    如果头部字段名与静态表或动态表中存储的条目的头部字段名匹配,则头部字段名称可用那个条目的索引表示。在这种情况下,条目的索引以一个具有6位前缀的整数 表示。这个值总是非0。否则,头部字段名由一个字符串字面量 表示,使用0值代替6位索引,其后是头部字段名。

    两种形式的 头部字段名表示 之后是字符串字面量表示的头部字段值。

    2.1 name在索引 value不在索引且允许保存
      0   1   2   3   4   5   6   7
     --- --- --- --- --- --- --- --- 
    | 0 | 1 |      Index (6 )       |
     --- --- ----------------------- 
    | H |     Value Length (7 )     |
     --- --------------------------- 
    | Value String (Length octets)  |
     ------------------------------- 
    

    第一个字节的前两个 bit 为 01,随后 无符号整数编码 Index 表示 name 的索引。

    下面紧随一个字面字符串的编码,表示 value 。

    这个 Header 会被两端都加入动态表中。

    无索引的字面量头部字段

    这种表示方法不改变动态表。头部字段名用索引表示时的头部字段表示如下:

      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 | 0 |       0       |
     --- --- ----------------------- 
    | H |     Name Length (7 )      |
     --- --------------------------- 
    |  Name String (Length octets)  |
     --- --------------------------- 
    | H |     Value Length (7 )     |
     --- --------------------------- 
    | Value String (Length octets)  |
     ------------------------------- 
    

    无索引的字面量头部字段表示以'0000' 的4位模式开始,其它方面与 增量索引的字面量表示 类似。

    2.2 name, value都没被索引且允许保存
      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)  |
     ------------------------------- 
    

    第一个字节为01000000, 然后紧随2个字面字符串的编码表示。

    这个 Header 会被两端都加入动态表中。

    从不索引的字面量头部字段

    这种表示方法与 无索引的字面量头部字段 类似,但它主要影响网络中的中间节点。头部字段名用索引表示时的头部字段如:

      0   1   2   3   4   5   6   7
     --- --- --- --- --- --- --- --- 
    | 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)  |
     ------------------------------- 
    
    3.1 name被索引, value未索引且不保存
      0   1   2   3   4   5   6   7
     --- --- --- --- --- --- --- --- 
    | 0 | 0 | 0 | 0 |  Index (4 )   |
     --- --- ----------------------- 
    | H |     Value Length (7 )     |
     --- --------------------------- 
    | Value String (Length octets)  |
     ------------------------------- 
    

    第一个字节前四个 bit 为 0000, 随后是一个 无符号整数编码的 Index 表示 name ,然后跟随一个字面字符串编码 value 。

    这个 Header 不用加入动态表。

    首部列表的表示

    各个首部字段表示合并起来形成首部列表。在 okhttp3.internal.framed.Hpack.Writer 的writeHeaders() 中完成编码首部块的动作:

        /** This does not use "never indexed" semantics for sensitive headers. */
        // http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-12#section-6.2.3
        void writeHeaders(List<Header> headerBlock) throws IOException {
          if (emitDynamicTableSizeUpdate) {
            if (smallestHeaderTableSizeSetting < maxDynamicTableByteCount) {
              // Multiple dynamic table size updates!
              writeInt(smallestHeaderTableSizeSetting, PREFIX_5_BITS, 0x20);
            }
            emitDynamicTableSizeUpdate = false;
            smallestHeaderTableSizeSetting = Integer.MAX_VALUE;
            writeInt(maxDynamicTableByteCount, PREFIX_5_BITS, 0x20);
          }
          // TODO: implement index tracking
          for (int i = 0, size = headerBlock.size(); i < size; i  ) {
            Header header = headerBlock.get(i);
            ByteString name = header.name.toAsciiLowercase();
            ByteString value = header.value;
            Integer staticIndex = NAME_TO_FIRST_INDEX.get(name);
            if (staticIndex != null) {
              // Literal Header Field without Indexing - Indexed Name.
              writeInt(staticIndex   1, PREFIX_4_BITS, 0);
              writeByteString(value);
            } else {
              int dynamicIndex = Util.indexOf(dynamicTable, header);
              if (dynamicIndex != -1) {
                // Indexed Header.
                writeInt(dynamicIndex - nextHeaderIndex   STATIC_HEADER_TABLE.length, PREFIX_7_BITS,
                    0x80);
              } else {
                // Literal Header Field with Incremental Indexing - New Name
                out.writeByte(0x40);
                writeByteString(name);
                writeByteString(value);
                insertIntoDynamicTable(header);
              }
            }
          }
        }
    

    HPACK的规范描述了多种头部字段的表示方法,但并没有指明各个表示方法的适用场景。

    在OkHttp3中,实现了3种表示头部字段的表示方法:

    1. 头部字段名在静态表中,头部字段名用指向静态表的索引表示,值用字面量表示。头部字段无需加入动态表。
    2. 头部字段的 名-值 对在动态表中,用指向动态表的索引表示头部字段。
    3. 其它情况,用字面量表示头部字段名和值,头部字段需要加入动态表。

    如果头部字段的 名-值 对在静态表中,OkHttp3也不会用索引表示。

    3.2 name, value都未被索引且不保存
        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,紧跟2个字面字符串编码的 name 和 value 。

    这个 Header 不用加入动态表。

    请求间首部字段内容的复用

    HPACK中,最重要的优化就是消除请求间冗余的首部字段。在实现上,主要有两个方面,一是前面看到的首部字段的索引表示,另一方面则是动态表的维护。

    HTTP/2中数据发送方向和数据接收方向各有一个动态表。通信的双方,一端发送方向的动态表需要与另一端接收方向的动态表保持一致,反之亦然。

    HTTP/2的连接复用及请求并发执行指的是逻辑上的并发。由于底层传输还是用的TCP协议,因而,发送方发送数据的顺序,与接收方接收数据的顺序是一致的。

    数据发送方在发送一个请求的首部数据时会顺便维护自己的动态表,接收方在收到首部数据时,也需要立马维护自己接收方向的动态表,然后将解码之后的首部字段列表dispatch出去。

    如果通信双方同时在进行2个HTTP请求,分别称为Req1和Req2,假设在发送方Req1的头部字段列表先发送,Req2的头部字段后发送。接收方必然先收到Req1的头部字段列表,然后是Req2的。如果接收方在收到Req1的头部字段列表后,没有立即解码,而是等Req2的首部字段列表接收并处理完成之后,再来处理Req1的,则两端的动态表必然是不一致的。

    这里来看一下OkHttp3中的动态表维护。

    发送方向的动态表,在 okhttp3.internal.framed.Hpack.Writer 中维护。在HTTP/2中,动态表的最大大小在连接建立的初期会进行协商,后面在数据收发过程中也会进行更新。

    在编码头部字段列表的 writeHeaders(List<Header> headerBlock) 中,会在需要的时候,将头部字段插入动态表,具体来说,就是在头部字段的名字不在静态表中,同时 名-值对不在动态表中的情况。

    将头部字段插入动态表的过程如下:

        private void clearDynamicTable() {
          Arrays.fill(dynamicTable, null);
          nextHeaderIndex = dynamicTable.length - 1;
          headerCount = 0;
          dynamicTableByteCount = 0;
        }
    
        /** Returns the count of entries evicted. */
        private int evictToRecoverBytes(int bytesToRecover) {
          int entriesToEvict = 0;
          if (bytesToRecover > 0) {
            // determine how many headers need to be evicted.
            for (int j = dynamicTable.length - 1; j >= nextHeaderIndex && bytesToRecover > 0; j--) {
              bytesToRecover -= dynamicTable[j].hpackSize;
              dynamicTableByteCount -= dynamicTable[j].hpackSize;
              headerCount--;
              entriesToEvict  ;
            }
            System.arraycopy(dynamicTable, nextHeaderIndex   1, dynamicTable,
                nextHeaderIndex   1   entriesToEvict, headerCount);
            Arrays.fill(dynamicTable, nextHeaderIndex   1, nextHeaderIndex   1   entriesToEvict, null);
            nextHeaderIndex  = entriesToEvict;
          }
          return entriesToEvict;
        }
    
        private void insertIntoDynamicTable(Header entry) {
          int delta = entry.hpackSize;
    
          // if the new or replacement header is too big, drop all entries.
          if (delta > maxDynamicTableByteCount) {
            clearDynamicTable();
            return;
          }
    
          // Evict headers to the required length.
          int bytesToRecover = (dynamicTableByteCount   delta) - maxDynamicTableByteCount;
          evictToRecoverBytes(bytesToRecover);
    
          if (headerCount   1 > dynamicTable.length) { // Need to grow the dynamic table.
            Header[] doubled = new Header[dynamicTable.length * 2];
            System.arraycopy(dynamicTable, 0, doubled, dynamicTable.length, dynamicTable.length);
            nextHeaderIndex = dynamicTable.length - 1;
            dynamicTable = doubled;
          }
          int index = nextHeaderIndex--;
          dynamicTable[index] = entry;
          headerCount  ;
          dynamicTableByteCount  = delta;
        }
    

    动态表占用的空间超出限制时,老的头部字段将被移除。在OkHttp3中,动态表是一个自后向前生长的表。

    在数据的接收防线,okhttp3.internal.http2.Http2Reader 的 nextFrame(Handler handler) 会不停从网络读取一帧帧的数据:

      public boolean nextFrame(Handler handler) throws IOException {
        try {
          source.require(9); // Frame header size
        } catch (IOException e) {
          return false; // This might be a normal socket close.
        }
    
          /*  0                   1                   2                   3
           *  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
           *  - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
           * |                 Length (24)                   |
           *  --------------- --------------- --------------- 
           * |   Type (8)    |   Flags (8)   |
           *  - - ----------- --------------- ------------------------------- 
           * |R|                 Stream Identifier (31)                      |
           *  = ============================================================= 
           * |                   Frame Payload (0...)                      ...
           *  --------------------------------------------------------------- 
           */
        int length = readMedium(source);
        if (length < 0 || length > INITIAL_MAX_FRAME_SIZE) {
          throw ioException("FRAME_SIZE_ERROR: %s", length);
        }
        byte type = (byte) (source.readByte() & 0xff);
        byte flags = (byte) (source.readByte() & 0xff);
        int streamId = (source.readInt() & 0x7fffffff); // Ignore reserved bit.
        if (logger.isLoggable(FINE)) logger.fine(frameLog(true, streamId, length, type, flags));
    
        switch (type) {
          case TYPE_DATA:
            readData(handler, length, flags, streamId);
            break;
    
          case TYPE_HEADERS:
            readHeaders(handler, length, flags, streamId);
            break;
    

    读到头部块时,会立即维护本地接收方向的动态表:

      private void readHeaders(Handler handler, int length, byte flags, int streamId)
          throws IOException {
        if (streamId == 0) throw ioException("PROTOCOL_ERROR: TYPE_HEADERS streamId == 0");
    
        boolean endStream = (flags & FLAG_END_STREAM) != 0;
    
        short padding = (flags & FLAG_PADDED) != 0 ? (short) (source.readByte() & 0xff) : 0;
    
        if ((flags & FLAG_PRIORITY) != 0) {
          readPriority(handler, streamId);
          length -= 5; // account for above read.
        }
    
        length = lengthWithoutPadding(length, flags, padding);
    
        List<Header> headerBlock = readHeaderBlock(length, padding, flags, streamId);
    
        handler.headers(endStream, streamId, -1, headerBlock);
      }
    
      private List<Header> readHeaderBlock(int length, short padding, byte flags, int streamId)
          throws IOException {
        continuation.length = continuation.left = length;
        continuation.padding = padding;
        continuation.flags = flags;
        continuation.streamId = streamId;
    
        // TODO: Concat multi-value headers with 0x0, except COOKIE, which uses 0x3B, 0x20.
        // http://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-8.1.2.5
        hpackReader.readHeaders();
        return hpackReader.getAndResetHeaderList();
      }
    

    okhttp3.internal.http2.Hpack.Reader的readHeaders()如下:

      static final class Reader {
    
        private final List<Header> headerList = new ArrayList<>();
        private final BufferedSource source;
    
        private final int headerTableSizeSetting;
        private int maxDynamicTableByteCount;
    
        // Visible for testing.
        Header[] dynamicTable = new Header[8];
        // Array is populated back to front, so new entries always have lowest index.
        int nextHeaderIndex = dynamicTable.length - 1;
        int headerCount = 0;
        int dynamicTableByteCount = 0;
    
        Reader(int headerTableSizeSetting, Source source) {
          this(headerTableSizeSetting, headerTableSizeSetting, source);
        }
    
        Reader(int headerTableSizeSetting, int maxDynamicTableByteCount, Source source) {
          this.headerTableSizeSetting = headerTableSizeSetting;
          this.maxDynamicTableByteCount = maxDynamicTableByteCount;
          this.source = Okio.buffer(source);
        }
    
        int maxDynamicTableByteCount() {
          return maxDynamicTableByteCount;
        }
    
        private void adjustDynamicTableByteCount() {
          if (maxDynamicTableByteCount < dynamicTableByteCount) {
            if (maxDynamicTableByteCount == 0) {
              clearDynamicTable();
            } else {
              evictToRecoverBytes(dynamicTableByteCount - maxDynamicTableByteCount);
            }
          }
        }
    
        private void clearDynamicTable() {
          headerList.clear();
          Arrays.fill(dynamicTable, null);
          nextHeaderIndex = dynamicTable.length - 1;
          headerCount = 0;
          dynamicTableByteCount = 0;
        }
    
        /** Returns the count of entries evicted. */
        private int evictToRecoverBytes(int bytesToRecover) {
          int entriesToEvict = 0;
          if (bytesToRecover > 0) {
            // determine how many headers need to be evicted.
            for (int j = dynamicTable.length - 1; j >= nextHeaderIndex && bytesToRecover > 0; j--) {
              bytesToRecover -= dynamicTable[j].hpackSize;
              dynamicTableByteCount -= dynamicTable[j].hpackSize;
              headerCount--;
              entriesToEvict  ;
            }
            System.arraycopy(dynamicTable, nextHeaderIndex   1, dynamicTable,
                nextHeaderIndex   1   entriesToEvict, headerCount);
            nextHeaderIndex  = entriesToEvict;
          }
          return entriesToEvict;
        }
    
        /**
         * Read {@code byteCount} bytes of headers from the source stream. This implementation does not
         * propagate the never indexed flag of a header.
         */
        void readHeaders() throws IOException {
          while (!source.exhausted()) {
            int b = source.readByte() & 0xff;
            if (b == 0x80) { // 10000000
              throw new IOException("index == 0");
            } else if ((b & 0x80) == 0x80) { // 1NNNNNNN
              int index = readInt(b, PREFIX_7_BITS);
              readIndexedHeader(index - 1);
            } else if (b == 0x40) { // 01000000
              readLiteralHeaderWithIncrementalIndexingNewName();
            } else if ((b & 0x40) == 0x40) {  // 01NNNNNN
              int index = readInt(b, PREFIX_6_BITS);
              readLiteralHeaderWithIncrementalIndexingIndexedName(index - 1);
            } else if ((b & 0x20) == 0x20) {  // 001NNNNN
              maxDynamicTableByteCount = readInt(b, PREFIX_5_BITS);
              if (maxDynamicTableByteCount < 0
                  || maxDynamicTableByteCount > headerTableSizeSetting) {
                throw new IOException("Invalid dynamic table size update "   maxDynamicTableByteCount);
              }
              adjustDynamicTableByteCount();
            } else if (b == 0x10 || b == 0) { // 000?0000 - Ignore never indexed bit.
              readLiteralHeaderWithoutIndexingNewName();
            } else { // 000?NNNN - Ignore never indexed bit.
              int index = readInt(b, PREFIX_4_BITS);
              readLiteralHeaderWithoutIndexingIndexedName(index - 1);
            }
          }
        }
    
        public List<Header> getAndResetHeaderList() {
          List<Header> result = new ArrayList<>(headerList);
          headerList.clear();
          return result;
        }
    
        private void readIndexedHeader(int index) throws IOException {
          if (isStaticHeader(index)) {
            Header staticEntry = STATIC_HEADER_TABLE[index];
            headerList.add(staticEntry);
          } else {
            int dynamicTableIndex = dynamicTableIndex(index - STATIC_HEADER_TABLE.length);
            if (dynamicTableIndex < 0 || dynamicTableIndex > dynamicTable.length - 1) {
              throw new IOException("Header index too large "   (index   1));
            }
            headerList.add(dynamicTable[dynamicTableIndex]);
          }
        }
    
        // referencedHeaders is relative to nextHeaderIndex   1.
        private int dynamicTableIndex(int index) {
          return nextHeaderIndex   1   index;
        }
    
        private void readLiteralHeaderWithoutIndexingIndexedName(int index) throws IOException {
          ByteString name = getName(index);
          ByteString value = readByteString();
          headerList.add(new Header(name, value));
        }
    
        private void readLiteralHeaderWithoutIndexingNewName() throws IOException {
          ByteString name = checkLowercase(readByteString());
          ByteString value = readByteString();
          headerList.add(new Header(name, value));
        }
    
        private void readLiteralHeaderWithIncrementalIndexingIndexedName(int nameIndex)
            throws IOException {
          ByteString name = getName(nameIndex);
          ByteString value = readByteString();
          insertIntoDynamicTable(-1, new Header(name, value));
        }
    
        private void readLiteralHeaderWithIncrementalIndexingNewName() throws IOException {
          ByteString name = checkLowercase(readByteString());
          ByteString value = readByteString();
          insertIntoDynamicTable(-1, new Header(name, value));
        }
    
        private ByteString getName(int index) {
          if (isStaticHeader(index)) {
            return STATIC_HEADER_TABLE[index].name;
          } else {
            return dynamicTable[dynamicTableIndex(index - STATIC_HEADER_TABLE.length)].name;
          }
        }
    
        private boolean isStaticHeader(int index) {
          return index >= 0 && index <= STATIC_HEADER_TABLE.length - 1;
        }
    
        /** index == -1 when new. */
        private void insertIntoDynamicTable(int index, Header entry) {
          headerList.add(entry);
    
          int delta = entry.hpackSize;
          if (index != -1) { // Index -1 == new header.
            delta -= dynamicTable[dynamicTableIndex(index)].hpackSize;
          }
    
          // if the new or replacement header is too big, drop all entries.
          if (delta > maxDynamicTableByteCount) {
            clearDynamicTable();
            return;
          }
    
          // Evict headers to the required length.
          int bytesToRecover = (dynamicTableByteCount   delta) - maxDynamicTableByteCount;
          int entriesEvicted = evictToRecoverBytes(bytesToRecover);
    
          if (index == -1) { // Adding a value to the dynamic table.
            if (headerCount   1 > dynamicTable.length) { // Need to grow the dynamic table.
              Header[] doubled = new Header[dynamicTable.length * 2];
              System.arraycopy(dynamicTable, 0, doubled, dynamicTable.length, dynamicTable.length);
              nextHeaderIndex = dynamicTable.length - 1;
              dynamicTable = doubled;
            }
            index = nextHeaderIndex--;
            dynamicTable[index] = entry;
            headerCount  ;
          } else { // Replace value at same position.
            index  = dynamicTableIndex(index)   entriesEvicted;
            dynamicTable[index] = entry;
          }
          dynamicTableByteCount  = delta;
        }
    

    HTTP/2中数据收发两端的动态表一致性主要是依赖TCP来实现的。

    Done。

    4.1 name在索引表中, value不在,且绝对不允许被索引
    0   1   2   3   4   5   6   7
     --- --- --- --- --- --- --- --- 
    | 0 | 0 | 0 | 1 |  Index (4 )   |
     --- --- ----------------------- 
    | H |     Value Length (7 )     |
     --- --------------------------- 
    | Value String (Length octets)  |
     ------------------------------- 
    

    和3.1是类似的,只是第一个字节第四个 bit 变成了1, 其他是一样的。

    这个和3.1的区别仅仅在于,中间是否通过了代理。如果没有代理那么表现是一致的。如果中间通过了代理,协议要求代理必须原样转发这个 Header 的编码,不允许做任何修改,这个暗示中间的代理这个字面值是故意不压缩的,比如为了敏感数据的安全等。而3.1则允许代理重新编码等。

    4.2 name 和 value 都不在索引表中,且绝对不允许被索引
     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)  |
     ------------------------------- 
    

    和3.2类似,只是第一个字节的第4个 bit 修改为1。
    对此的解释同4.1。

    5 修改动态表的大小
      0   1   2   3   4   5   6   7
     --- --- --- --- --- --- --- --- 
    | 0 | 0 | 1 |   Max size (5 )   |
     --- --------------------------- 
    

    和前面的不一样,之前的都是传送数据,这个是用来做控制动态表大小的。

    第一个字节前三个 bit 为001, 随后跟上一个无符号整数的编码 表示动态表的大小。上文有提过,这个数值是不允许超过 SETTINGS_HEADER_TABLE_SIZE 的, 否则会被认为是解码错误。

    解码状态机

    我们都知道,想要正确无误的解码,每个编码都要满足一个条件,就是每种编码方式,都不是另外一个编码的前缀。这里的 HPACK 的编码的最小单位是字节。我们看一下整个二进制流解码的状态机:

    图片 28

    HPACK 解码状态机

    图例的根据对应规则解码就是上面介绍的编码规则。

    实战举例

    以下是要被编码的 Headers:

    :method: GET
    :scheme: http
    :path: /
    :authority: www.example.com
    

    这里大概说一下, :xxxx 为 name 的 header, 实际上是 HTTP/2 所谓的伪头的概念。就是把HTTP1.X的请求头替换成伪头对应的 name 和 value,然后再编码传输,完全的定义在这里 http://httpwg.org/specs/rfc7540.html#PseudoHeaderFields

    好的,过掉整个话题,我们看整个 Headers 编码后的16进制字节如下:

    8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4 ff     
    

    其实解析很简单,就按照上面我画的解码状态机来就好了:

    82 = 10000010 -> 静态表Index = 2 -> :method: GET

    86 = 10000110 -> 静态表Index = 6 -> :scheme: http

    84 = 10000100 -> 静态表Index = 4 -> :path: /

    41 = 01000001 -> name = 静态表1 = :authority

    接着是一个字面字符串的解码,表示 header :authority 对应的 value

    8c = 10001100 -> 第一个 bit 为1,表示 huffman 编码,字符串的长度为 1100b = 12

    接着解析12个字节为 huffman 编码后的字符
    f1e3 c2e5 f23a 6ba0 ab90 f4ff, 查表可知为www.example.com

    从而得到最后一个头部 :authority: www.example.com

    小结

    好的,至此我们的 HPACK 完全解析已经结束了,希望大家能通过本文对 HPACK 有更深入的了解。后面我会继续填坑,给大家带来 HTTP/2 与 OkHttp 对应的实现。

    这里是笔者的个人博客地址: dieyidezui.com

    也欢迎关注笔者的微信公众号,会不定期的分享一些内容给大家

    图片 29

    参考文献

    RFC 7541

    本文由新葡亰496net发布于新葡亰官网,转载请注明出处:2首部裁减,完全剖判

    关键词: