HTTP/2(曾用名:HTTP/2.01),改进的目标是使得应用更快,更简单,更稳定,它把以前针对 HTTP/1.1 作出的优化方案内置于[[TCP 协议#传输层]]中,使得进行更多的优化成为可能。
HTTP/2 的主要改进方向是通过支持请求、响应复用以减少延迟。为了减少协议开销,引入了“二进制分帧层”,增加了对请求的优先级和服务器推送的支持。
HTTP/2 没有对 HTTP/1.1 的概念进行改动,它改动的是数据格式化(分帧)以及客户端与服务端传输的方式。通过这两项改动,现有的应用不需要做修改,就可以在新的协议下运行。
为什么不叫 HTTP1.2 ?因为HTTP/2 引入的“二进制分帧层”与以前的 HTTP 传输的内容的实现不兼容,所以更改了主版本号。
SPDY 与 HTTP/2
SPDY 是 2009 年,由 Google 开发的一个实验性协议,目标是不更改现有应用的情况下,减少 HTTP/1.1 的加载延迟。2012 年,该协议开始被主流的浏览器支持,越来越多的使用使得它具备成为标准的条件。因此,HTTP 工作组在吸取 SPDY 的优点之后,以 SPDY 为基础设立了新的 HTTP 协议,也就是 HTTP/2。
在之后的三年,SPDY 作为 HTTP/2 的实验分支,并行开发,直至 2015 年初,IESG 批准发布 HTTP/2,Google 决定于 2016 年初停止对 SPDY 的支持。
HTTP/2 的设计
HTTP/1.x 很简单,但是由于它的简单,在应用性能方面就会有一些牺牲,例如以下的一些缺陷,
- 客户端需要建立多个连接实现并发
- 请求头、响应头是纯文本的,且没进行压缩
- 不支持请求优先级
- …
为了解决这些问题的同时,不影响现有的应用,HTTP/2 没有对 HTTP 的核心概念进行变更,所以,HTTP/2 仍然是 HTTP 的拓展,而不是替代。
二进制分帧层
以下的图简单的说明了 HTTP 消息的变化
“层”指的是给套接字与应用使用的,经过优化的新编码机制。这种新的编码方式在不改变原来的 HTTP 语义的前提下,把原来 HTTP/1.x 使用的纯文本分割成多个二进制帧,因此,只使用 HTTP/1.x 的客户端/服务端无法与只使用 HTTP/2 的客户端/服务端通讯。
二进制分帧层的实现是 HTTP/2 所有其它功能和性能优化的基础。
数据流、消息、帧
新的机制带来了三个新的概念:
- 数据流 / 流 (stream):双向字节流,可以承载多条消息
- 消息:与请求/响应对应的完整的一些列的帧
- 帧:HTTP/2 的最小通信单位,至少包含帧头(Headers Frame)
相互的关系:
- 只需要一个 TCP 连接,此连接可以承载任意数量的数据流
- 数据流用于承载双向信息,每个数据流都包含一个唯一的标识符以及可选的优先级信息
- 每一条消息都是一条逻辑 HTTP 请求(如请求/响应),包含一个或者多个帧
- 来自不同的帧可以交错发送,然后根据每个帧头的标识符重新组装(与[[TCP 数据包]]类似)
帧的结构
╔═════════════════════════╦════════════╗
║ Length (24) ║ Type (8) ║
╠═══════════╦═════════════╩════════════╣
║ Flags (8) ║ ║
╠═══════╦═══╩══════════════════════════╣
║ R (1) ║ Stream Identifier (31) ║
╠═══════╩══════════════════════════════╣
║ Frame payload (N) ║
╚══════════════════════════════════════╝
- Length,3 字节,表示帧 payload 的长度。(范围2^ 14~2^24-1 字节)默认帧大小为 2^14 字节,可在
SETTINGS
帧中设置。 - Type,1 字节,帧类型
- Flags,1 字节,帧的相关配置
- R,1 位,保留位
- Stream Identifier,31 位, 数据流的唯一 ID
- Frame Payload,长度由 Length 定义
相对于 HTTP/1.x,HTTP/2 在一开始就知道分配多少的内存,只需要提前分配好内存即可。而前者,需要不断地读取请求流,根据内容开辟新的内存(不连续的内存带来的性能影响可以谷歌一下),而且前者在处理完一个请求流之前,不能停止解析。
请求/响应 复用
在 HTTP/1.x 中,客户端需要发起多个并行的连接的时候,会有多个 TCP 连接被建立。这是 HTTP/1.x 的模型,每个 TCP 连接只处理一个请求+响应,而这种模型会导致队首阻塞(Head-of-line-blocking, AKA. HOL) 问题。2
二进制分帧层解决了这个问题:客户端与服务端之间的 HTTP 消息被拆分成多个互不依赖的帧,交错发送,在另一端重新按序列顺序组装起来。
在这种模式下, 一个 TCP 连接就能同时传输多个 HTTP请求/响应,这个加强带来了巨大的性能提升。
从左到右,效率越来越高。
- 一个请求紧接一个响应,较为低效。
- 在 HTTP/1.1,应用了 Pipeling 技术,请求一次性全部发送,客户端等待响应。
- Pipelineing 技术还是有个问题,如果第一个请求的处理有问题,后续的请求即使处理完了,还是需要等待第一个请求处理完成,即 HOL Blocking。
- HTTP/2 提出的多路复用技术,解决了这个问题。
使用这种多路复用技术带来了以下的特性使得我们的应用速度更快,开发更简单,部署成本更低。
- 并行交错地发送/接收请求/响应,互不影响
- 只使用一个 TCP 连接处理多个请求/响应,减轻了服务器/客户端的负担
- 免除了很多针对 HTTP/1.x 所做的优化
- 免除不必要的延迟、等待,从而减少了页面加载时间
- 等等···
数据流优先级
HTTP/2 赋予了每个数据流一个介于 1~256 整数的优先级,以及显式的依赖关系。
通过这两项构建的”优先级树“被用于系统决定资源的分配(CPU、内存等)。
以上图为例,每个节点代表一个数据流,数据流处理的优先级依照”先处理和传输响应 D,然后再处理和传输响应 C“。在资源分配方面,则是根据权重占比分配,如:针对从左到右的第一张图,A 获得的资源为 12/(12+4)
,B 获得的资源为 4/(12+4)
。
同样是以上图为例,从左到右依次为:
- A、B 没有指定父依赖,那它们会依赖于一个隐式的”根数据流“
- C 依赖于 D,D 依赖于根数据流,所以,D 会有更高的优先级获取到完整的资源分配
- 优先级 D > C > A / B,A 获得的资源为
12/(12+4)
,B 获得的资源为4/(12+4)
。 - 优先级 D > C / E > A / B,···
通过上述的权重及依赖关系,我们可以明确地表达资源的优先级,另外,HTTP/2 还允许客户端随时更新这些优先级,更进一步地优化客户端的性能。
需要注意的是,优先级不是要求,不能被用于强制处理顺序或者传输顺序,因为,我们不希望优先级较高的资源受到阻止的时候,还阻止低优先级的资源的处理。
一个源(Origin)一个连接
在有了新的二进制分帧层机制后,HTTP/2 不再依赖多个 TCP 连接去并行复用数据流,因此,所有 HTTP/2 的连接都是持久的,而且每个源只需一个 TCP 连接。
HTTP 的请求大都是短暂的,而 TCP 协议却是针对长时间的批量数据做了优化,在使用 HTTP/2 之后,可以很好地化解这个矛盾,更少的连接可以减少使用的资源,在同等的配置下,使用了 HTTP/2 的服务器能有效的减少网络延迟,提高通量(连接吞吐量),从而降低成本。3
流控制
因为 HTTP 1.X 一个连接只使用一个 TCP 连接,直接使用 TCP 的 流控制
4无法精准地控制,也无法提供相应的 API 对单个 TCP 内的数据流进行调节。为了解决这个问题,HTTP/2 提供了一系列的概念,从而使得客户端和服务端有能力实现自己的数据流级别的和连接级别的 流控制
:
流控制
是定向的。每个连接接收方都可以根据自身需要选择为每个数据流或者整个连接设置任意的窗口大小。流控制
基于信用。每个接收方都可以公布其初始连接和数据流流控制窗口(以字节为单位),每当发送方发出DATA
帧时都会减小,在接收方发出WINDOW_UPDATE
帧时增大。流控制
无法停用。建立 HTTP/2 连接后,客户端将与服务器交换SETTINGS
帧,这会在两个方向上设置流控制窗口。 流控制窗口的默认值设为 65535 字节,但是接收方可以设置一个较大的最大窗口大小(2^31-1 字节),并在接收到任意数据时通过发送WINDOW_UPDATE
帧来维持这一大小。流控制
是逐跃点控制,而非端到端控制。即,可信中介/代理可以使用它来控制资源使用,以及基于自身条件和启发式算法实现资源分配机制。
HTTP/2 未指定任何特定算法来实现流控制,但是,它提供了简单的相关概念,推迟了客户端和服务端对此的实现,通过实现自定义的策略,服务端、客户端可以实现更好的传输规则,以提高体验。如:通过让浏览器获取低分辨率的缩略图之后,把控制窗口减少为零,等其它关键项加载完毕之后,再重新恢复控制窗口。
服务器推送
HTTP/2 还新增了一个功能,服务端可以对客户端的一个请求发送多个响应。
一个典型的 Web App 会包含多种资源,客户端需要获得完整的文档(如:index.html)之后,才知道需要什么其它的内容。那么如果可以在第一次请求服务端时,就把其它资源都一并发送回客户端,这额外的延迟等待就可以被省去了。
除了提前推送需要用到的资源这个特性之外,服务器推送还有以下的特性:
- 推送的资源由客户端缓存
- 推送的资源可以在不同的页面之间重用
- 与其它资源的请求组成响应复用
- 由服务端设定优先级
- 可以被客户端拒绝
PUSH_PROMISE
101
如果服务端决定推送一个对象,则一个PUSH_PROMISE
帧会被构建,并比DATA
帧更早一步发送到客户端。在客户端收到 PUSH_PROMISE
帧后,它可以根据情况通过发送RST_STREAM
帧选择拒绝数据流。(如:资源已经在本地缓存)
一个PUSH_PROMISE
帧有以下属性:
- 包含所需要推送的资源的 HTTP Headers
- 被发送的对象必须确保是可被缓存的
PUSH_PROMISE
帧使用对应的请求的流的 ID 构造响应的PUSH_PROMISE
帧PUSH_PROMISE
帧会声明接下来发送的响应(推送的资源)所使用的的流的 ID (都为偶数)5- 方法必须是幂等的,即请求不能为 POST 这种,可能改变服务端资源状态的方法
标头(Header)压缩
每个 HTTP 请求都有一组标头,指明了传输的资源及其属性,在 HTTP/1.x 中,标头都是纯文本,以回车换行符(CRLF
)分割,这些元数据通常会给使得实际传输的数据增加 500-800 字节,如果包含了 cookies,则有时候会增加上千字节。
为了减少这个开销,HTTP/2 使用了 HPACK
6压缩格式对请求 / 响应的标头进行压缩,这种压缩格式采用了两种简单但是强大的技术手段:
- 通过静态[[霍夫曼编码(huffman coding)]]对标头进行编码,从而减少了数据量
- 通过要求客户端、服务端同时维护一个包含了之前使用过的标头字段的索引列表,减少了冗余的标头字段传输
如上图,在一个新的数据流中,只需要传输 path
字段的内容,减少了大约六分之五的数据量。
为了做进一步的优化,HPACK 压缩包含一个静态表以及一个动态表
- 静态表包含了常用的标头
- 动态表初始为空,将根据情况在采用了静态的[[霍夫曼编码(huffman coding)]]之后对列表进行更新。
注意,在 HTTP/2 的标头中,字段的含义不变,但会有些许差异
- 所有的标头字段名都为小写
- 请求行从
Method Request-URI HTTP-Version
拆分成:method
,:scheme
,:authority
,:path
确认服务器支持 HTTP/2
协议发现
- 非加密通讯时,使用
Upgrade
请求显示表明期望使用 HTTP/2,如果服务器支持,会返回101 Switching Protocols
- 如果是 TLS 加密的通讯,客户端在 [[TCP 三次握手,四次挥手(TCP Handshake)#ClientHello]] 消息中设置 ALPN(Application-Layer Protocol Negotiation,应用层协议协商)来显示表明期望使用 HTTP/2
Double Confirm
客户端会发送一个特殊字节流 Connection Preface
作为第一个数据。Connection Preface
是一个纯文本的数据:0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a
,解析为 [[ASCII 编码]]:PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n
。这是 HTTP/1.x 的格式,PRI 是请求方法,如果不支持 HTTP/2,则服务端会返回方法错误;而支持 HTTP/2 的服务端会返回一个SETTING
帧来回应紧接着 Connection Preface
的来自于客户端的SETTING
帧,以表明自己支持 HTTP/2,这样客户端就知道服务端支不支持 HTTP/2 了。
参考资料
- Google - HTTP/2 简介: https://developers.google.com/web/fundamentals/performance/http2?hl=zh-cn 🔗
- https://juejin.im/post/5da16e9ef265da5b76373d0e 🔗
Footnotes
-
HTTP/2.0:根据 wikipedia 🔗 的记录,HTTP/2 在 2012 年初,曾用 HTTP/2.0 作为名字,在 2015 年 1 月之后,正式命名为 HTTP/2。 ↩
-
一个要求按顺序传送数据的队列的第一个数据包(队头)受阻而导致后续的所有数据包受阻。一种解决办法是,使用虚拟的输出队列。 ↩
-
维持连接需要资源,更少的连接,代表了更少的内存占用;网络拥塞情况减轻,[[慢启动(slow start)]] 时间减少,拥塞和丢包恢复速度加快;避免了频繁的 [[TCP 三次握手,四次挥手(TCP Handshake)]]。 ↩
-
流控制
是一种阻止发送方向接收方发送大量数据的机制,以免超出后者的需求或处理能力:发送方可能非常繁忙、处于较高的负载之下,也可能仅仅希望为特定数据流分配固定量的资源。 例如,客户端可能请求了一个具有较高优先级的大型视频流,但是用户已经暂停视频,客户端现在希望暂停或限制从服务器的传输,以免提取和缓冲不必要的数据。更多内容,请参阅 [[流控制(Flow Control)]] ↩ -
客户端的流从 1 开始,之后每个新的流都使用加 2 之后的 ID,即都是奇数。而服务端主动推送的流的 ID 从 2 开始,之后每个新的推送的流都使用加 2 之后的 ID,即都是偶数。这样的处理可以让双方判断哪些流是由服务端推送的。0 是保留的流 ID,用于连接级的控制消息。 ↩
-
HPACK 是种表查找压缩方案,它利用霍夫曼编码获得接近
GZIP
的压缩率。 ↩