HTTP/2 头部压缩技术介绍

2015/11/03 · HTML5 ·
HTTP/2

原文出处:
imququ(@屈光宇)   

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

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
协议的详细内容请参考RFC2616。HTTP协议采用了请求/响应模型。客户端向服务器发送一个请求,请求头包含请求的方法、URI、协议版本、以及包含请求修饰符、客户信息和内容的类似于MIME的消息结构。服务器以一个状态行作为响应,相应的内容包括消息协议的版本,成功或者错误编码加上包含服务器信息、实体元信息以及可能的实体内容。
通常HTTP消息包括客户机向服务器的请求消息和服务器向客户机的响应消息。这两种类型的消息由一个起始行,一个或者多个头域,一个只是头域结束的空行和可选的消息体组成。HTTP的头域包括通用头,请求头,响应头和实体头四个部分。每个头域由一个域名,冒号(:)和域值三部分组成。域名是大小写无关的,域值前可以添加任何数量的空格符,头域可以被扩展为多行,在每行开始处,使用至少一个空格或制表符。

原作者lcamry的博客

6.1 HTTP报文头部

报文头部由几个字段构成

为什么要压缩

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

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

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

图片 1

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

图片 2

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

为什么要压缩

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

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

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

图片 3

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

图片 4

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

二、通用头域(即通用头)
通用头域包含请求和响应消息都支持的头域,通用头域包含Cache-Control、
Connection、Date、Pragma、Transfer-Encoding、Upgrade、Via。对通用头域的扩展要求通讯双方都支持此扩展,如果存在不支持的通用头域,一般将会作为实体头域处理。下面简单介绍几个在UPnP消息中使用的通用头域。

头部 解释
Accept 告诉服务器自己接受何种介质类型type/sub-type:*/*表示所有类型;type/*表示该类型下的所有子类型
Accept-Charset 声明自己接收的字符集
Accept-Encoding 声明自己接受的编码方式,通常指定压缩方法
Accept-Language 声明自己接收的语言
Accept-Ranges 服务器表明自己是否接受获取其某个实体的一部分的请求
Age 当代理服务器用自己缓存的实体去响应请求时,通过该头部表明该实体从产生到现在经过了多长时间
Authorization 当客户端收到服务器的WWW-Authorization响应时,用该头部回应自己的身份验证信息给服务器
Connection close:告诉服务器在完成本次响应后断开连接;<br />keepalive:告诉服务器本次响应后保持连接<br />服务器响应:close——已关闭;keepalive——连接保持着,等待后续请求;Keep-Alive:希望保持的时间
Content-Encoding 服务器表明自己使用何种压缩方法
Content-Language 服务器表明自己响应的对象的语言
Content-Length 服务器表明自己响应的长度
Content-Range 服务器表明该响应包含的部分对象为整体对象的哪个部分
Content-Type 服务器表明自己响应对象的类型
ETag 对象的标记,服务器用来判断对象是否改变
Expired 服务器表明实体将在何时过期
Host 客户端制定自己想要访问的服务器
If-Match 如果对象的ETag没改变,才请求执行的动作
If-None-Match 如果ETag改变了,请求执行的动作
If-Modified-Since 如果对象在该头部指定的时间之后修改了,才执行请求的动作,否则返回304
If-UNmodified-Since 对象在该头部指定的时间之后没有修改,才执行请求的操作
If-Range 浏览器告诉 WEB 服务器,如果我请求的对象没有改变,就把我缺少的部分给我,如果对象改变了,就把整个对象给我。浏览器通过发送请求对象的 ETag 或者 自己所知道的最后修改时间给 WEB 服务器,让其判断对象是否改变了。总是跟 Range 头部一起使用。
Last-Modified 服务器认为对象最后的修改时间
Location 服务器告诉浏览器访问对象的位置
Pramga 和Cache-Control类似
Proxy-Authenticate 代理服务器响应,要求提供代理身份验证信息;浏览器响应:提供自己的身份信息
Range 浏览器告诉服务器自己想取对象的哪部分
Referer 浏览器告知服务器,表明自己从哪个URL请求当前的URL的
Server 服务器表明自己的软件信息
User-Agent 浏览器表明自己的身份
Transfer-Encoding 服务器表明自己对本响应的消息体做了怎样的编码
Vary WEB服务器用该头部的内容告诉 Cache 服务器,在什么条件下才能用本响应所返回的对象响应后续的请求。假如源WEB服务器在接到第一个请求消息时,其响应消息的头部为:Content- Encoding: gzip; Vary: Content-Encoding那么 Cache 服务器会分析后续请求消息的头部,检查其 Accept-Encoding,是否跟先前响应的 Vary 头部值一致,即是否使用相同的内容编码方法,这样就可以防止 Cache 服务器用自己 Cache 里面压缩后的实体响应给不具备解压能力的浏览器。例如:Vary:Accept-Encoding
Via 列出从客户端到 OCS 或者相反方向的响应经过了哪些代理服务器,他们用什么协议发送的请求。当客户端请求到达第一个代理服务器时,该服务器会在自己发出的请求里面添 加 Via 头部,并填上自己的相关信息,当下一个代理服务器收到第一个代理服务器的请求时,会在自己发出的请求里面复制前一个代理服务器的请求的Via 头部,并把自己的相关信息加到后面,以此类推,当 OCS 收到最后一个代理服务器的请求时,检查 Via 头部,就知道该请求所经过的路由。例如:Via:1.0 236.D0707195.sina.com.cn:80 (squid/2.6.STABLE13)

HTTP请求报文

由方法,URI,HTTP版本,HTTP头部字段等部分构成;

压缩后的效果

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

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

图片 5

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

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

图片 6

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

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

图片 7

图片 8

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

压缩后的效果

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

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

图片 9

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

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

图片 10

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

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

图片 11

图片 12

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

1.Cache-Control头域
Cache -Control指定请求和响应遵循的缓存机制。在请求消息或响应消息中设置
Cache-Control并不会修改另一个消息处理过程中的缓存处理过程。请求时的缓存指令包括no-cache、no-store、max-age、
max-stale、min-fresh、only-if-cached,响应消息中的指令包括public、private、no-cache、no-
store、no-transform、must-revalidate、proxy-revalidate、max-age。各个消息中的指令含义如下:Public指示响应可被任何缓存区缓存;Private指示对于单个用户的整个或部分响应消息,不能被共享缓存处理。这允许服务器仅仅描述当用户的部分响应消息,此响应消息对于其他用户的请求无效;no-cache指示请求或响应消息不能缓存;no-store用于防止重要的信息被无意的发布。在请求消息中发送将使得请求和响应消息都不使用缓存;max-age指示客户机可以接收生存期不大于指定时间(以秒为单位)的响应;min-fresh指示客户机可以接收响应时间小于当前时间加上指定时间的响应;max-stale指示客户机可以接收超出超时期间的响应消息。如果指定max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。

另外还有Cache-Control:

  • 请求包中:
    • no-cache:不要缓存的实体,要求从Web服务器中取
    • max-age: 只接受Age小于max-age的值
    • max-stale:可以接胡搜过去的对象,但过期时间要小于max-stale
    • min-fresh:接收其新鲜生命周期大于当前Age和min-fresh值之间的缓存对象
  • 响应包中:
    • public:可以用Cached内容回应任何永远忽
    • private:只能用缓存内容回应先前请求那个内容的用户
    • no-cache:可以缓存,但只有在跟Web服务器验证了其有效后才返回给客户端
    • max-age:本响应包含的对象的过期时间
  • no-store:不允许缓存

HTTP响应报文

由HTTP版本,状态码,HTTP头部字段构成

技术原理

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

图片 13

我再用通俗的语言解释下,头部压缩需要在支持 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
中所有头部名称必须小写。

技术原理

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

图片 14

我再用通俗的语言解释下,头部压缩需要在支持 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
中所有头部名称必须小写。

2.Date头域
date头域表示消息发送的时间,时间的描述格式由rfc822定义。例如,Date:Mon,31Dec200104:25:57GMT。Date描述的时间表示世界标准时,换算成本地时间,需要知道用户所在的时区。

6.2 HTTP头部字段

实现细节

了解了 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

图片 15

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 的值,将其进行哈夫曼解码就能得到具体内容。

图片 16

客户端或服务端看到这种格式的头部键值对,会将其添加到自己的动态字典中。后续传输这样的内容,就符合第
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 。

图片 17

客户端或服务端看到这种格式的头部键值对,会将其添加到自己的动态字典中。后续传输这样的内容,就符合第
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,只剩下四位来存放索引了,如下图:

图片 18

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

Python

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

对于上图中的数据,按照这个规则算出索引值为 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
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]);
});

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

{ ‘: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
{ ‘: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)。

实现细节

了解了 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

图片 19

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 的值,将其进行哈夫曼解码就能得到具体内容。

图片 20

客户端或服务端看到这种格式的头部键值对,会将其添加到自己的动态字典中。后续传输这样的内容,就符合第
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

图片 21

客户端或服务端看到这种格式的头部键值对,会将其添加到自己的动态字典中。后续传输这样的内容,就符合第
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,只剩下四位来存放索引了,如下图:

图片 22

这里需要介绍另外一个知识点:对整数的解码。上图中第一个字节为
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)。

3.Pragma头域
Pragma头域用来包含实现特定的指令,最常用的是Pragma:no-cache。在HTTP/1.1协议中,它的含义和Cache-
Control:no-cache相同。
Connection表示连接状态
请求:close(告诉WEB服务器或者代理服务器,在完成本次请求的响应后,断开连接,不要等待本次连接的后续请求了)。
keepalive(告诉WEB服务器或者代理服务器,在完成本次请求的响应后,保持连接,等待本次连接的后续请求)。
响应:close(连接已经关闭)。
keepalive(连接保持着,在等待本次连接的后续请求)。
Keep-Alive:如果浏览器请求保持连接,则该头部表明希望 WEB
服务器保持连接多长时间(秒)。例如:Keep-Alive:300

HTTP头部字段结构

头部字段名: 字段值

总结

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

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

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

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

$ 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
$ 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
完全普及前也是一个不错的选择。

1 赞 收藏
评论

图片 23

总结

在进行 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
专题」。

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

打赏作者

三、请求消息(请求头)
请求消息的第一行为下面的格式:
Method Request-URI HTTP-Version
Method
表示对于Request-URI完成的方法,这个字段是大小写敏感的,包括OPTIONS、GET、HEAD、POST、PUT、DELETE、
TRACE。方法GET和HEAD应该被所有的通用WEB服务器支持,其他所有方法的实现是可选的,GET方法取回由Request-URI标识的信息,
HEAD方法也是取回由Request-URI标识的信息,只是可以在响应时,不返回消息体;POST方法可以请求服务器接收包含在请求中的实体信息,可以用于提交表单,向新闻组、BBS、邮件群组和数据库发送消息。Request-URI表示请求的URL。Request-URI遵循URI格式,在此字段为星号(*)时,说明请求并不用于某个特定的资源地址,而是用于服务器本身。HTTP-
Version表示支持的HTTP版本,例如为HTTP/1.1。

4种HTTP头部字段类型

  1. 通用头部字段(General Header Fields)
    请求报文和响应报文都会使用的头部
  2. 请求头部字段(Request Header Fields)
    补充请求的附加内容、客户端信息、响应内容相关优先级等;
  3. 响应头部字段(Response Header Fields)
    补充响应的附加内容
  4. 实体首部字段(Entity Header Fields)
    针对实体部分使用的头部;补充资源内容的更新事件等;

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

任选一种支付方式

图片 24
图片 25

1 赞 3 收藏
评论

请求头域允许客户端向服务器传递关于请求或者关于客户机的附加信息。请求头域可能包含下列字段Accept、Accept-Charset、Accept-
Encoding、Accept-Language、Authorization、From、Host、If-Modified-Since、If-
Match、If-None-Match、If-Range、If-Range、If-Unmodified-Since、Max-Forwards、
Proxy-Authorization、Range、Referer、User-Agent。对请求头域的扩展要求通讯双方都支持,如果存在不支持的请求头域,一般将会作为实体头域处理。

HTTP/1.1头部字段一览

关于作者:JerryQu

图片 26

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

图片 23

典型的请求消息:
GET
http://download.microtool.de:80/somedata.exe
Host: download.microtool.de
Accept:/
Pragma: no-cache
Cache-Control: no-cache
Referer:
http://download.microtool.de/
User-Agent:Mozilla/4.04en
Range:bytes=554554-
上例第一行表示HTTP客户端(可能是浏览器、下载程序)通过GET方法获得指定URL下的文件。棕色的部分表示请求头域的信息,绿色的部分表示通用头部分。
Host头域指定请求资源的Intenet主机和端口号,必须表示请求url的原始服务器或网关的位置。HTTP/1.1请求必须包含主机头域,否则系统会以400状态码返回;Accept:告诉WEB服务器自己接受什么介质类型,/
表示任何类型,type/*
表示该类型下的所有子类型,type/sub-type。Accept-Charset:
浏览器申明自己接收的字符集。Authorization:当客户端接收到来自WEB服务器的
WWW-Authenticate
响应时,用该头部来回应自己的身份验证信息给WEB服务器。User-Agent头域的内容包含发出请求的用户信息。Referer
头域允许客户端指定请求uri的源资源地址,这可以允许服务器生成回退链表,可用来登陆、优化cache等。他也允许废除的或错误的连接由于维护的目的被追踪。如果请求的uri没有自己的uri地址,Referer不能被发送。如果指定的是部分uri地址,则此地址应该是一个相对地址;
Range头域可以请求实体的一个或者多个子范围。例如
表示头500个字节:bytes=0-499
表示第二个500字节:bytes=500-999
表示最后500个字节:bytes=-500
表示500字节以后的范围:bytes=500-
第一个和最后一个字节:bytes=0-0,-1
同时指定几个范围:bytes=500-600,601-999
但是服务器可以忽略此请求头,如果无条件GET包含Range请求头,响应会以状态码206(PartialContent)返回而不是以200
(OK)

6.3 HTTP/1.1通用头部字段

四、响应消息(响应头)
响应消息的第一行为下面的格式:
HTTP-Version Status-Code Reason-Phrase
HTTP -Version表示支持的HTTP版本,例如为HTTP/1.1。Status-
Code是一个三个数字的结果代码。Reason-Phrase给Status-Code提供一个简单的文本描述。Status-Code主要用于机器自
动识别,Reason-Phrase主要用于帮助用户理解。Status-Code的第一个数字定义响应的类别,后两个数字没有分类的作用。第一个数字可能取5个不同的值:
1xx:信息响应类,表示接收到请求并且继续处理
2xx:处理成功响应类,表示动作被成功接收、理解和接受
3xx:重定向响应类,为了完成指定的动作,必须接受进一步处理
4xx:客户端错误,客户请求包含语法错误或者是不能正确执行
5xx:服务端错误,服务器不能正确执行一个正确的请求
响应头域允许服务器传递不能放在状态行的附加信息,这些域主要描述服务器的信息和
Request-URI进一步的信息。响应头域包含Age、Location、Proxy-Authenticate、Public、Retry-
After、Server、Vary、Warning、WWW-Authenticate。对响应头域的扩展要求通讯双方都支持,如果存在不支持的响应头
域,一般将会作为实体头域处理。

cache-control:操作缓存的工作机制

分为缓存请求指令 / 缓存响应指令
表示能否缓存的指令

  1. public:(响应)表明其他用户也可以使用缓存;
  2. private:(响应)只对特定用户返回;
  3. no-cache:不缓存过期的资源
    (请求)客户端不会接受缓存过的响应;
    (响应)缓存前必须确定其有效性;如果对no-cache字段名具体指定参数值,客户端接收后就不能使用缓存

控制可执行缓存的对象的指令

  1. no-store:真正的不进行缓存,请求或响应中含机密信息;

指定缓存期限和认证的指令

  1. s-maxage:与max-age相同,区别于公共服务器
  2. max-age:必须有值,单位为秒;
    (请求)判定资源缓存的时间比指定时间的数值小,客户端就接收缓存的资源;
    (响应)数值代表资源保存为缓存的最长时间;
  3. min-fresh:(请求)要求缓存服务器返回至少还未过指定时间的缓存资源,必须有值,单位为秒;
  4. max-stale:(请求)指示缓存资源,即使过期也照常接受,不指定数值无论过多久,客户端都会接受;置顶数职,即使过期,只要处于max-stale指定时间内,仍会被接收;
  5. only-if-cached:(请求)只从缓存获取资源,要求缓存服务器不重新加载响应,也不再次确认资源的有效性;
  6. must-revalidate:(响应)代理会向源服务器再次验证即将返回的响应缓存目前是否仍然有效;
  7. proxy-revalidate:(响应)要求缓存服务器接收到客户端带有该指令的请求返回响应之前,必须再次验证缓存的有效性;
  8. no-transform:(请求)(响应)代理不能改变实体的媒体类型

Cache-Control扩展

  1. cache-extension token:

典型的响应消息:
HTTP/1.0 200 OK
Date:Mon,31Dec200104:25:57GMT
Server:Apache/1.3.14(Unix)
Content-type:text/html
Last-modified:Tue,17Apr200106:46:28GMT
Etag:”a030f020ac7c01:1e9f”
Content-length:39725426
Content-range:bytes554554-40279979/40279980
上例第一行表示HTTP服务端响应一个GET方法。棕色的部分表示响应头域的信息,绿色的部分表示通用头部分,红色的部分表示实体头域的信息。
Location响应头用于重定向接收者到一个新URI地址。Server响应头包含处理请求的原始服务器的软件信息。此域能包含多个产品标识和注释,产品标识一般按照重要性排序
实体消息(实体头和实体)
请求消息和响应消息都可以包含实体信息,实体信息一般由实体头域和实体组成。
实体头域包含关于实体的原信息,实体头包括Allow、Content-
Base、Content-Encoding、Content-Language、Content-Length、Content-Location、Content-MD5、Content-Range、Content-Type、
Etag、Expires、Last-Modified、extension-header。extension-header允许客户端定义新的实体头,但是这些域可能无法未接受方识别。

Connection

Connection头部字段的作用:控制不再转发给代理的头部字段;管理持久连接;

GET / HTTP/1.1
Upgrade: HTTP/1.1
Connection: Upgrade
Connection: 不再转发的头部字段名

Connection: Keep-Alive
Connection: Close

Content-Type实体头用于向接收方指示实体的介质类型,指定HEAD方法送到接收方的实体介质类型,或GET方法发送的请求介质类型,表示后面的文档属于什么MIME类型。Content-Length表示实际传送的字节数。Allow
实体头至服务器支持哪些请求方法(如GET、POST等)。Content-Range表示传送的范围,用于指定整个实体中的一部分的插入位置,他也指示了整个实体的长度。在服务器向客户返回一个部分响应,它必须描述响应覆盖的范围和整个实体长度。一般格式:
Content-Range:bytes-unitSPfirst-byte-pos-last-byte-pos/entity-legth
例 如,传送头500个字节次字段的形式:Content-Range:bytes0-
499/1234如果一个http消息包含此节(例如,对范围请求的响应或对一系列范围的重叠请求)。Content-Encoding
指文档的编码(Encode)方法。
实体可以是一个经过编码的字节流,它的编码方式由Content-Encoding或Content-Type定
义,它的长度由Content-Length或Content-Range定义。

Date

创建报文的日期和时间

五、附录:
1、 Accept:告诉WEB服务器自己接受什么介质类型,/ 表示任何类型,type/*
表示该类型下的所有子类型,type/sub-type。
2、 Accept-Charset: 浏览器申明自己接收的字符集
Accept-Encoding:
浏览器申明自己接收的编码方法,通常指定压缩方法,是否支持压缩,支持什么压缩方法(gzip,deflate)
Accept-Language::浏览器申明自己接收的语言
语言跟字符集的区别:中文是语言,中文有多种字符集,比如big5,gb2312,gbk等等。
3、
Accept-Ranges:WEB服务器表明自己是否接受获取其某个实体的一部分(比如文件的一部分)的请求。bytes:表示接受,none:表示不接受。
4、
Age:当代理服务器用自己缓存的实体去响应请求时,用该头部表明该实体从产生到现在经过多长时间了。
5、 Authorization:当客户端接收到来自WEB服务器的 WWW-Authenticate
响应时,用该头部来回应自己的身份验证信息给WEB服务器。
6、
Cache-Control:请求:no-cache(不要缓存的实体,要求现在从WEB服务器去取)
max-age:(只接受 Age 值小于 max-age 值,并且没有过期的对象)
max-stale:(可以接受过去的对象,但是过期时间必须小于 max-stale 值)
min-fresh:(接受其新鲜生命期大于其当前 Age 跟 min-fresh
值之和的缓存对象)
响应:public(可以用 Cached 内容回应任何用户)
private(只能用缓存内容回应先前请求该内容的那个用户)
no-cache(可以缓存,但是只有在跟WEB服务器验证了其有效后,才能返回给客户端)
max-age:(本响应包含的对象的过期时间)
ALL: no-store(不允许缓存)
7、
Connection:请求:close(告诉WEB服务器或者代理服务器,在完成本次请求的响应后,断开连接,不要等待本次连接的后续请求了)。
keepalive(告诉WEB服务器或者代理服务器,在完成本次请求的响应后,保持连接,等待本次连接的后续请求)。
响应:close(连接已经关闭)。
keepalive(连接保持着,在等待本次连接的后续请求)。
Keep-Alive:如果浏览器请求保持连接,则该头部表明希望 WEB
服务器保持连接多长时间(秒)。例如:Keep-Alive:300
8、
Content-Encoding:WEB服务器表明自己使用了什么压缩方法(gzip,deflate)压缩响应中的对象。例如:Content-Encoding:gzip
9、Content-Language:WEB 服务器告诉浏览器自己响应的对象的语言。
10、Content-Length: WEB
服务器告诉浏览器自己响应的对象的长度。例如:Content-Length: 26012
11、Content-Range: WEB
服务器表明该响应包含的部分对象为整个对象的哪个部分。例如:Content-Range:
bytes 21010-47021/47022
12、Content-Type: WEB
服务器告诉浏览器自己响应的对象的类型。例如:Content-Type:application/xml
13、 ETag:就是一个对象(比如URL)的标志值,就一个对象而言,比如一个
html 文件,如果被修改了,其 Etag 也会别修改,所以ETag 的作用跟
Last-Modified 的作用差不多,主要供 WEB
服务器判断一个对象是否改变了。比如前一次请求某个 html 文件时,获得了其
ETag,当这次又请求这个文件时,浏览器就会把先前获得的 ETag 值发送给WEB
服务器,然后 WEB 服务器会把这个 ETag 跟该文件的当前 ETag
进行对比,然后就知道这个文件有没有改变了。
14、
Expired:WEB服务器表明该实体将在什么时候过期,对于过期了的对象,只有在跟WEB服务器验证了其有效性后,才能用来响应客户请求。是
HTTP/1.0 的头部。例如:Expires:Sat, 23 May 2009 10:02:12 GMT
15、 Host:客户端指定自己想访问的WEB服务器的域名/IP
地址和端口号。例如:Host:rss.sina.com.cn
16、 If-Match:如果对象的 ETag
没有改变,其实也就意味著对象没有改变,才执行请求的动作。
17、If-None-Match:如果对象的 ETag
改变了,其实也就意味著对象也改变了,才执行请求的动作。
18、
If-Modified-Since:如果请求的对象在该头部指定的时间之后修改了,才执行请求的动作(比如返回对象),否则返回代码304,告诉浏览器该对象没有修改。例如:If-Modified-Since:Thu,
10 Apr 2008 09:14:42 GMT
19、If-Unmodified-Since:如果请求的对象在该头部指定的时间之后没修改过,才执行请求的动作(比如返回对象)。
20、 If-Range:浏览器告诉 WEB
服务器,如果我请求的对象没有改变,就把我缺少的部分给我,如果对象改变了,就把整个对象给我。浏览器通过发送请求对象的
ETag 或者 自己所知道的最后修改时间给 WEB
服务器,让其判断对象是否改变了。总是跟 Range 头部一起使用。
21、 Last-Modified:WEB
服务器认为对象的最后修改时间,比如文件的最后修改时间,动态页面的最后产生时间等等。例如:Last-Modified:Tue,
06 May 2008 02:42:43 GMT
22、 Location:WEB
服务器告诉浏览器,试图访问的对象已经被移到别的位置了,到该头部指定的位置去取。例如:Location:http://i0.sinaimg.cn/dy/deco/2008/0528/sinahome\_0803\_ws\_005\_text\_0.gif
23、 Pramga:主要使用 Pramga: no-cache,相当于 Cache-Control:
no-cache。例如:Pragma:no-cache
24、 Proxy-Authenticate:
代理服务器响应浏览器,要求其提供代理身份验证信息。Proxy-Authorization:浏览器响应代理服务器的身份验证请求,提供自己的身份信息。
25、 Range:浏览器(比如 Flashget 多线程下载时)告诉 WEB
服务器自己想取对象的哪部分。例如:Range: bytes=1173546-
26、 Referer:浏览器向 WEB 服务器表明自己是从哪个 网页/URL 获得/点击
当前请求中的网址/URL。例如:Referer:http://www.sina.com/
27、 Server: WEB
服务器表明自己是什么软件及版本等信息。例如:Server:Apache/2.0.61
(Unix)
28、 User-Agent:
浏览器表明自己的身份(是哪种浏览器)。例如:User-Agent:Mozilla/5.0
(Windows; U; Windows NT 5.1; zh-CN; rv:1.8.1.14) Gecko/20080404
Firefox/2、0、0、14
29、 Transfer-Encoding: WEB
服务器表明自己对本响应消息体(不是消息体里面的对象)作了怎样的编码,比如是否分块(chunked)。例如:Transfer-Encoding:
chunked
30、 Vary: WEB服务器用该头部的内容告诉 Cache
服务器,在什么条件下才能用本响应所返回的对象响应后续的请求。假如源WEB服务器在接到第一个请求消息时,其响应消息的头部为:Content-Encoding:
gzip; Vary: Content-Encoding那么 Cache
服务器会分析后续请求消息的头部,检查其 Accept-Encoding,是否跟先前响应的
Vary 头部值一致,即是否使用相同的内容编码方法,这样就可以防止 Cache
服务器用自己 Cache
里面压缩后的实体响应给不具备解压能力的浏览器。例如:Vary:Accept-Encoding
31、 Via: 列出从客户端到 OCS
或者相反方向的响应经过了哪些代理服务器,他们用什么协议(和版本)发送的请求。当客户端请求到达第一个代理服务器时,该服务器会在自己发出的请求里面添加
Via
头部,并填上自己的相关信息,当下一个代理服务器收到第一个代理服务器的请求时,会在自己发出的请求里面复制前一个代理服务器的请求的Via
头部,并把自己的相关信息加到后面,以此类推,当 OCS
收到最后一个代理服务器的请求时,检查 Via
头部,就知道该请求所经过的路由。

Pragma:历史遗留字段

Pragma: no-cache
要求所有中间服务器不返回缓存的资源;

Trailer

说明在报文主体后记录了哪些首部字段。

Transfer-Encoding

HTTP/1.1中的传输编码方式仅对分块传输编码有效
Transfer-encoding: chunked

Upgrade

检测HTTP协议和其他协议是否可以使用更高的版本进行通信,其数值可以用来指定一个完全不同的通信协议;
Upgrade头部字段仅限于客户端和邻接服务器之间,因此,使用Upgrade还要额外制定Connection:
Upgrade;

Via

追踪客户端和服务器哦之间的请求和响应报文的传输路径;
经过代理或网关时,会首先在头部字段Via中附加该服务器的信息,然后转发

Warning:告知用户一些缓存相关问题的警告

HTTP/1.1 警告码

警告码 警告内容 说明
110 Response is stale 代理返回已过期的资源
111 Revalidation failed 代理验证资源有效性失败
112 Disconnection operation 代理与互联网连接被故意切断
113 Heuristic expiration 响应的使用期超过有效缓存的设定时间
199 Miscellaneous warning 任意的警告内容
214 Transformation applied 代理对内容编码或媒体类型执行了处理
299 Miscellaneous persistent warning 任意警告内容(持久)

6.4 请求头部字段

Accept

通知服务器用户代理能处理的媒体类型及媒体类型的相对优先级,type/subtype,可以一次指定多种媒体类型;

Accept: text/html, application/xhtml+xml;q=0.9

一些媒体类型的例子
文本文件
text/html,text/plain,text/css
application/xhtml+xml,application/xml
图片文件
image/jpeg,image/gif,image/png
视频文件
video/mpeg,video/quicktime

Accept-Charset

通知服务器用户代理支持的字符集和字符集相对优先级

Accept-Encoding

通知服务器用户代理支持的内容编码和内容编码的优先级;
几个内容编码的例子
gzip compress deflate
identify:不执行压缩或不会变化的默认编码样式

Accept-Language

告知服务器用户代理能够处理的自然语言集和优先级;

Authorization

通知服务器用户代理的认证信息(证书值)。通常,用户代理会在接受返回的401状态码响应后,把头部字段Authorization加入请求中;

Expect:告知服务器,期望出现的特定行为,HTTP/1.1只定义了一种

Expect: 100-continue

From

告知服务器使用用户代理的用户的电子邮件地址;例如显示搜索引擎等用户代理的负责人的邮件;

Host

HTTP/1.1规范中唯一一个必须被包含在请求内的头部字段;
告知服务器请求的资源所处的互联网主机名和端口号;因为单台服务器可能分配多个域名

If-Match

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图