HTTP/2 标准于 2015 年 5 月以 RFC 7540 正式发表,它保留了 HTTP/1.1 的大部分语义,例如请求方法、状态码乃至 URI 和绝大多数 HTTP 头字段一致。同时,HTTP/2 采用了新的方法来编码和传输数据,可以有效减少网络延迟,提高浏览器加载页面的速度。
新特性:
-
头部使用 HPACK 算法压缩。
-
将一个 TCP 连接分为若干个流( Stream ),每个流中可以传输若干消息( Message ),每个消息由若干最小的二进制帧( Frame )组成。每个用户的操作行为被分配了一个流编号(Stream ID),这意味着用户与服务端之间建立了一个 TCP 通道。
-
引入了服务器推送,即服务端向客户端发送比客户端请求更多的数据。这允许服务器直接提供浏览器渲染页面所需资源,而无须浏览器在收到、解析页面后再提起一轮请求,节约了加载时间。
头部压缩
HTTP 报文可以分为 Header
和 Body
两部分,在 HTTP/1.1 中可以通过 Content-Encoding
头指定 Body
的压缩方式,比如 gzip 压缩。而在 HTTP/2 中,引入了 HPACK 算法来对 Header
部分进行压缩,进一步节约带宽。
HPACK
静态字典
客户端与服务端根据 RFC 7541 的附录 A,维护一份共同的静态字典(Static Table)
如上图所示,字典中预设了 61 组数据,包括常见的 HTTP 头、请求方法等等。而且有些数据中包含键值对,有些数据只有键没有值,因为这部分值并不是固定的,这些值会经过 Huffman 编码后再发送
下面以一个 server 头为例子,它在 HTTP/1.1 的形式如下:
算上冒号空格和末尾的 \r\n
,共占用了 17 字节;而使用了静态字典和 Huffman 编码,可以将它压缩成 8 字节,压缩率大概 47%
![](/ox-hugo/_20240705_223251screenshot.png” caption=“<span class=“figure-number”>Figure 1: HTTP/2 网络包)
上图中,第一个字节表示 server 头在字典中的索引为 54( 110110 ,前面的 01 为固定值);第二个字节的首个比特位表示 Value 是否经过 Huffman 编码,后 7 位表示 Value 编码后的长度;剩下 6 个字节就是 nghttpx
经过 Huffman 编码后的值(末尾用 1 填充)
![](/ox-hugo/_20240705_224113screenshot.png” caption=“<span class=“figure-number”>Figure 2: 头部二进制格式)
动态字典
客户端和服务端根据先入先出的原则,维护一份可动态添加内容的共同动态字典(Dynamic Table),当 HTTP 头不在静态字典的 61 个预设数据中时,可以从 62 开始动态更新字典
由于客户端和服务端需要 同时更新字典 ,才能使后续请求用索引代替具体的数据, 所以动态表必须作用在同一个连接上,而且要重复传输完全相同的头部 (如果数据变化了字典也会更新)
当动态字典越大,占用的内存也就越大,可能会影响服务器性能,因此 Web 服务器都会提供类似 http2_max_requests
的配置,用于限制一个连接上能够传输的请求数量
二进制帧
有别于 HTTP/1.1 在连接中的明文请求,HTTP/2 将一个 TCP 连接分为若干个流( Stream ),每个流中可以传输若干消息( Message ),每个消息由若干二进制帧( Frame )组成
HTTP/2 把报文数据划分成两类帧, Headers Frame
和 Data Frame
,分别对应 HTTP/1.1 中的请求头和请求体
单个二进制帧的结构如下:
在帧头中有一个字节用来表示帧的类型,分为数据帧和控制帧两大类:
多路复用
前面提到过 HTTP/2 中的 Stream、 Message 、Frame 间的关系,具体如下图所示:
从图中可以看到,客户端和服务端的同一个请求和响应会在一个 Stream 中完成,而在一个 TCP 连接中可以存在多个 Stream,实现数据传输的多路复用。而且 HTTP/2 为每个用户的操作行为分配了一个 Stream ID,因此不同的 Stream 在一个 TCP 连接中不需要按照顺序来收发,只要根据 ID 就能正确处理各个 Stream 的数据(同一 Stream 内的帧 必须是严格有序的 )
由于不同的 Stream 不需要按照顺序传输,解决了 HTTP/1.x 中的队头阻塞问题
客户端和服务器双方都可以建立 Stream,因为服务端可以主动推送资源给客户端,客户端建立的 Stream ID 必须是奇数,而服务器建立的 Stream ID 必须是偶数
同一个 TCP 连接中的 Stream ID 是不能复用的,只能顺序递增,所以当 Stream ID 耗尽时,需要发一个控制帧 GOAWAY
用来关闭 TCP 连接
服务器推送
HTTP/1.1 不支持服务器主动推送资源给客户端,所有资源都需要由客户端发起请求来获取,比如客户端访问 HTML 页面,页面中使用的所有 CSS、JavaScript 等资源都需要浏览器来请求获取。而在 HTTP/2 中,服务器可以将这些页面使用到的资源主动推送给客户端,节省发起请求带来的消耗
在 Nginx 中,如果希望客户端访问 /test.html
时,服务器直接推送 /test.css
,可以这样配置: