3.3 HTTP/2 牛逼在哪?
HTTP/1.1 协议的性能问题
现在的站点相比以前变化太多了,比如:
- 消息的大小变大了,从几 KB 大小的消息,到几 MB 大小的消息;
- 页面资源变多了,从每个页面不到 10 个的资源,到每页超 100 多个资源;
- 内容形式变多样了,从单纯到文本内容,到图片、视频、音频等内容;
- 实时性要求变高了,对页面的实时性要求的应用越来越多;
这些变化带来的最大性能问题就是 HTTP/1.1 的高延迟
兼容 HTTP/1.1
HTTP/2 使用的依旧是http域名
第二点,只在应用层做了改变,还是基于 TCP 协议传输,应用层方面为了保持功能上的兼容,HTTP/2 把 HTTP 分解成了「语义」和「语法」两个部分,「语义」层不做改动,与 HTTP/1.1 完全一致,比如请求方法、状态码、头字段等规则保留不变。
但是,HTTP/2 在「语法」层面做了很多改造,基本改变了 HTTP 报文的传输格式。
头部压缩
HTTP 协议的报文是由「Header + Body」构成的,对于 Body 部分,HTTP/1.1 协议实现了如 gzip 压缩。
HTTP/1.1 报文中 Header 部分存在的问题:
- 含很多固定的字段,比如 Cookie、User Agent、Accept 等;
- 大量的请求和响应的报文里有很多字段值都是重复的,可以避免;
- 字段是 ASCII 编码的,虽然易于人类观察,但效率低;
HTTP/2 开发了 HPACK 算法,包含三个组成部分:
- 静态字典;
- 动态字典;
- Huffman 编码(压缩算法);
客户端和服务器两端都会建立和维护「字典」,用长度较小的索引号表示重复的字符串,再用 Huffman 编码压缩数据,可达到 50%~90% 的高压缩率。
静态表编码
HTTP/2 为高频出现在头部的字符串和字段建立了一张静态表,它是写入到 HTTP/2 框架里的,不会变化的,静态表里共有 61 组,如下图:

表中的 Index 表示索引(Key),Header Value 表示索引对应的 Value,Header Name 表示字段的名字,比如 Index 为 2 代表 GET,Index 为 8 代表状态码 200。
你可能注意到,表中有的 Index 没有对应的 Header Value,这是因为这些 Value 并不是固定的而是变化的,这些 Value 都会经过 Huffman 编码(通过字符出现的概率进行编码,出现概率高的字符编码短)后,才会发送出去。
动态表编码
不在静态表范围内的头部字符串要自行构建动态表,它的 Index 从 62 起步,会在编码解码的时候随时更新。
使得动态表生效有一个前提:必须同一个连接上,重复传输完全相同的 HTTP 头部。如果消息字段在 1 个连接上只发送了 1 次,或者重复传输时,字段总是略有变化,动态表就无法被充分利用了。
因此,随着在同一 HTTP/2 连接上发送的报文越来越多,客户端和服务器双方的「字典」积累的越来越多,理论上最终每个头部字段都会变成 1 个字节的 Index,这样便避免了大量的冗余数据的传输,大大节约了带宽。但是传输越多,表会越大,内存占用过大,所以HTTP/2会限制单词连接请求数。(每次断开就是重置动态表)
综上,HTTP/2 头部的编码通过「静态表、动态表、Huffman 编码」共同完成的。
二进制帧
HTTP/2 厉害的地方在于将 HTTP/1 的文本格式改成二进制格式传输数据,极大提高了 HTTP 传输效率,而且二进制数据使用位运算能高效解析。
HTTP/2 把响应报文划分成了两类帧(Frame),一条 HTTP 响应,划分成了两类帧(数据帧和头部帧)来传输,并且采用二进制来编码。
这样虽然对人不友好,但是对计算机非常友好,因为计算机只懂二进制,那么收到报文后,无需再将明文的报文转成二进制,而是直接解析二进制报文,这增加了数据传输的效率。
比如状态码 200,在 HTTP/1.1 是用 '2''0''0' 三个字符来表示(二进制:00110010 00110000 00110000)在 HTTP/2 对于状态码 200 的二进制编码是 10001000(1表示存在静态表中,8表示静态表中序号是8),只用了 1 字节就能表示,相比于 HTTP/1.1 节省了 2 个字节。
HTTP/2 二进制帧的结构如下图:

帧头(Frame Header)很小,只有 9 个字节,帧开头的前 3 个字节表示帧数据(Frame Playload)的长度,然后是帧类型(数据帧还是头帧)。
帧数据了,它存放的是通过 HPACK 算法压缩过的 HTTP 头部和包体。
并发传输
我们都知道 HTTP/1.1 的实现是基于请求 - 响应模型的。同一个连接中,HTTP 完成一个事务(请求与响应),才能处理下一个事务,也就是说在发出请求等待响应的过程中,是没办法做其他事情的,如果响应迟迟不来,那么后续的请求是无法发送的,也造成了队头阻塞的问题。
而 HTTP/2 就很牛逼了,通过 Stream 这个设计,多个 Stream 复用一条 TCP 连接,达到并发的效果,解决了 HTTP/1.1 队头阻塞的问题,提高了 HTTP 传输的吞吐量。
- 1 个 TCP 连接包含一个或者多个 Stream
- Stream 里可以包含 1 个或多个 Message,Message 对应 HTTP/1 中的请求或响应,由 HTTP 头部和包体构成;
- Message 里包含一条或者多个 Frame,Frame 是 HTTP/2 最小单位,以二进制压缩格式存放 HTTP/1 中的内容(头部和包体);
在 HTTP/2 连接上,不同 Stream 的帧是可以乱序发送的(因此可以并发不同的 Stream),因为每个帧的头部会携带 Stream ID 信息,所以接收端可以通过 Stream ID 有序组装成 HTTP 消息,而同一 Stream 内部的帧必须是严格有序的。
客户端和服务器双方都可以建立 Stream,因为服务端可以主动推送资源给客户端,客户端建立的 Stream 必须是奇数号,而服务器建立的 Stream 必须是偶数号。
同一个连接中的 Stream ID 是不能复用的,只能顺序递增,所以当 Stream ID 耗尽时,需要发一个控制帧 GOAWAY,用来关闭 TCP 连接。
在 Nginx 中,可以通过 http2_max_concurrent_Streams 配置来设置 Stream 的上限,默认是 128 个。
HTTP/2 还可以对每个 Stream 设置不同优先级,帧头中的「标志位」可以设置优先级,比如客户端访问 HTML/CSS 和图片资源时,希望服务器先传递 HTML/CSS,再传图片,那么就可以通过设置 Stream 的优先级来实现,以此提高用户体验。
服务器主动推送资源
HTTP/1.1 不支持服务器主动推送资源给客户端,都是由客户端向服务器发起请求后,才能获取到服务器响应的资源,请求html、css等文件要请求多次。
如上图右边部分,在 HTTP/2 中,客户端在访问 HTML 时,服务器可以直接主动推送 CSS 文件,减少了消息传递的次数。
在 Nginx 中,如果你希望客户端访问 /test.html 时,服务器直接推送 /test.css,那么可以这么配置:
location /test.html {
http2_push /test.css;
}那 HTTP/2 的推送是怎么实现的?
客户端发起的请求,必须使用的是奇数号 Stream,服务器主动的推送,使用的是偶数号 Stream。服务器在推送资源时,会通过 PUSH_PROMISE 帧传输 HTTP 头部,并通过帧中的 Promised Stream ID 字段告知客户端,接下来会在哪个偶数号 Stream 中发送包体。

如上图,在 Stream 1 中通知客户端 CSS 资源即将到来,然后在 Stream 2 中发送 CSS 资源,注意 Stream 1 和 2 是可以并发的。
