简介
HTTP/2
可以让我们的应用更快、更简单、更稳定。
HTTP/2
的目的是通过支持完整的请求与响应复用来减少延迟,通过有效压缩 HTTP
标头字段将协议开销降至最低,同时增加对请求优先级和服务器推送的支持。为达成这些目标,HTTP/2
还给我们带来了大量其他协议层面的辅助实现,例如新的流控制、错误处理和升级机制。
需要注意的是,HTTP/2
仍是对之前 HTTP
标准的扩展,而非替代。HTTP
的应用语义不变,提供的功能不变,HTTP
方法、状态代码、URI
和标头字段等这些核心概念也不变。
二进制分帧层
HTTP/2
所有性能增强的核心在于新的二进制分帧层,它定义了如何封装 HTTP
消息并在客户端与服务器之间传输。
数据流、消息和帧
- 数据流:已建立的连接内的双向字节流,可以承载一条或多条消息。
- 消息:与逻辑请求或响应消息对应的完整的一系列帧。
- 帧:
HTTP/2
通信的最小单位,每个帧都包含帧头,至少也会标识出当前帧所属的数据流。
这些概念的关系总结如下:
- 所有通信都在一个
TCP
连接上完成,此连接可以承载任意数量的双向数据流。 - 每个数据流都有一个唯一的标识符和可选的优先级信息,用于承载双向消息。
- 每条消息都是一条逻辑
HTTP
消息(例如请求或响应),包含一个或多个帧。 - 帧是最小的通信单位,承载着特定类型的数据,例如
HTTP
标头、消息负载,等等。来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。
请求与响应复用
在 HTTP/1.x
中,如果客户端要想发起多个并行请求以提升性能,则必须使用多个 TCP
连接。这是 HTTP/1.x
交付模型的直接结果,该模型可以保证每个连接每次只交付一个响应(响应排队)。更糟糕的是,这种模型也会导致队首阻塞,从而造成底层 TCP
连接的效率低下。这就是需要对请求资源的个数做优化的原因(CSS Sprite、合并 js 和 css、内联小资源、利用 HTTP 管道、域名分片)
队首阻塞: > 1.
http1.0
的队首阻塞
对于同一个tcp
连接,所有的http1.0
请求放入队列中,只有前一个请求的响应收到了,然后才能发送下一个请求。
可见,http1.0
的队首组塞发生在客户端。
2.http1.1
的队首阻塞
对于同一个tcp
连接,http1.1
允许一次发送多个http1.1
请求,也就是说,不必等前一个响应收到,就可以发送下一个请求,这样就解决了http1.0
的客户端的队首阻塞。但是,http1.1
规定,服务器端的响应的发送要根据请求被接收的顺序排队,也就是说,先接收到的请求的响应也要先发送。这样造成的问题是,如果最先收到的请求的处理时间长的话,响应生成也慢,就会阻塞已经生成了的响应的发送,也会造成队首阻塞。
HTTP/2
中新的二进制分帧层突破了这些限制,实现了完整的请求和响应复用:客户端和服务器可以将 HTTP
消息分解为互不依赖的帧,然后交错发送,最后再在另一端把它们重新组装起来。就是请求或者响应可以不一起发送,而是分帧发送,哪一个消息的帧准备好了就可以发,而不用等待。
数据流优先级
前面提到每个数据流都有一个唯一的标识符和可选的优先级消息,这个优先级消息就包括关联的权重和依赖关系:
- 可以向每个数据流分配一个介于 1 至 256 之间的整数。
- 每个数据流与其他数据流之间可以存在显式依赖关系。
总体的规则就是父项优先级更高,也就是 ”根数据流“ 的子项优先级最高,当同级时按照权重分配资源。
注意着不能保证顺序,只是要求,即客户端无法强制服务器通过数据流优先级以特定顺序处理数据流。
每个来源一个连接
每一个 origin
只需要一个连接,因为每个数据流都拆分成很多帧,而这些帧可以交错,还可以分别设定优先级。因此,所有 HTTP/2
连接都是永久的,而且仅需要每个来源一个连接,随之带来诸多性能优势。
连接数量减少对提升
HTTPS
部署的性能来说是一项特别重要的功能:可以减少开销较大的TLS
连接数、提升会话重用率,以及从整体上减少所需的客户端和服务器资源。
流控制
流控制是一种阻止发送方向接收方发送大量数据的机制,以免超出后者的需求或处理能力。
HTTP/2
的流量控制具有以下特征:
1.流量控制是特定于一个连接的。每种类型的流量控制都是在单独的一跳的两个端点之间的,并不是在整个端到端的路径上的。(这里的一跳指的是 HTTP
连接的一跳,而不是 IP
路由的一跳)
2.流量控制是基于 WINDOW_UPDATE
帧的。接收方公布自己打算在每个流以及整个连接上分别接收多少字节。这是一个以信用为基础的方案。
3.流量控制是有方向的,由接收者全面控制。接收方可以为每个流和整个连接设置任意的窗口大小。发送方必须尊重接收方设置的流量控制限制。客户方、服务端和中间代理作为接收方时都独立地公布各自的流量控制窗口,作为发送方时都遵守对端的流量控制设置。
4.无论是新流还是整个连接,流量控制窗口的初始值是 65535 字节。
5.帧的类型决定了流量控制是否适用于帧。目前,只有 DATA
帧服从流量控制,所有其它类型的帧并不消耗流量控制窗口的空间。这保证了重要的控制帧不会被流量控制阻塞。
6.流量控制不能被禁用。建立 HTTP/2
连接后,客户端将与服务器交换 SETTINGS
帧,这会在两个方向上设置流控制窗口。流控制窗口的默认值设为 65535(2^16 - 1) 字节,但是接收方可以设置一个较大的最大窗口大小(2^31-1 字节),并在接收到任意数据时通过发送 WINDOW_UPDATE
帧来维持这一大小。
7.HTTP/2
只定义了 WINDOW_UPDATE
帧的格式和语义,并没有规定接收方如何决定何时发送帧、发送什么样的值,也没有规定发送方如何选择发送包。具体实现可以选择任何满足需求的算法。
服务器推送
HTTP/2
新增的另一个强大的新功能是,服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以向客户端推送额外资源,而无需客户端明确地请求。
PUSH_PROMISE
所有服务器推送数据流都由 PUSH_PROMISE
帧发起,表明了服务器向客户端推送所述资源的意图,并且需要 先于请求推送资源的响应数据传输。这种传输顺序非常重要:客户端需要了解服务器打算推送哪些资源,以免为这些资源创建重复请求。满足此要求的最简单策略是先于父响应(即,DATA
帧)发送所有 PUSH_PROMISE
帧,其中包含所承诺资源的 HTTP
标头。
在客户端接收到 PUSH_PROMISE
帧后,它可以根据自身情况选择拒绝数据流(通过 RST_STREAM
帧)。 (如果资源已经位于缓存中,可能会发生这种情况。)这是一个相对于 HTTP/1.x
的重要提升。 相比之下,使用资源内联(一种受欢迎的 HTTP/1.x
“优化”)等同于“强制推送”:客户端无法选择拒绝、取消或单独处理内联的资源。
标头压缩
传统的 HTTP/1.x
传递报文时需要传输标头,有时候会很大(带上 cookie
的时候),开销过大了。而 HTTP/2
使用 HPACK
压缩格式压缩请求和响应标头元数据,这种格式通过两种方式压缩:
- 这种格式支持通过静态
Huffman
代码对传输的标头字段进行编码,从而减小了各个传输的大小。 - 这种格式要求客户端和服务器同时维护和更新一个包含之前见过的标头字段的索引列表(换句话说,它可以建立一个共享的压缩上下文),此列表随后会用作参考,对之前传输的值进行有效编码。
利用 Huffman
编码,可以在传输时对各个值进行压缩(比如还没建立索引时,传输的 User-Agent
的内容也就是值可以使用霍夫曼编码压缩),而利用之前传输值的索引列表,我们可以通过传输索引值的方式对重复值进行编码,索引值可用于有效查询和重构完整的标头键值对(比如之前传递过 MyHeader:test
的头部,建立索引放在动态表中,索引为 62,那么下次传输只用传递索引值 62即可)。
作为一种进一步优化方式,HPACK
压缩上下文包含一个静态表和一个动态表:静态表在规范中定义,并提供了一个包含所有连接都可能使用的常用 HTTP
标头字段(例如,有效标头名称)的列表;动态表最初为空,将根据在特定连接内交换的值进行更新。因此,为之前未见过的值采用静态 Huffman
编码,并替换每一侧静态表或动态表中已存在值的索引,可以减小每个请求的大小。
下面是预定义的头字段静态映射表:
在
HTTP/2
中,请求和响应标头字段的定义保持不变,仅有一些微小的差异:所有标头字段名称均为小写,请求行现在拆分成各个:method
、:scheme
、:authority
和:path
伪标头字段。
HPACK 的安全性和性能
早期版本的
HTTP/2
和SPDY
使用zlib
(带有一个自定义字典)压缩所有HTTP
标头。然而,2012 年夏天,出现了针对TLS
和SPDY
压缩算法的“犯罪”安全攻击,此攻击会导致会话被劫持。 于是,zlib
压缩算法被HPACK
替代,后者经过专门设计,可以解决发现的安全问题、实现起来也更高效和简单,当然,可以对HTTP
标头元数据进行良好压缩。
关于 HPACK
压缩算法的完成详情查看 https://tools.ietf.org/html/draft-ietf-httpbis-header-compression。