ch12-http2

HTTP/2

HTTP/2 让我们的应用更快、更简单、更健壮——这三者兼得——它允许我们将许多原本在应用层实现的 HTTP/1.1 变通方案转移到传输层本身来处理。更好的是,它还开启了一系列全新的优化机会,让我们能够进一步提升应用性能!

HTTP/2 的核心目标是通过实现完整的请求与响应多路复用来降低延迟,通过高效压缩 HTTP 头部字段来最小化协议开销,并添加对请求优先级和服务器推送的支持。为了实现这些需求,协议还包含大量其他增强功能,如新的流量控制、错误处理和升级机制,但上述特性是每位 Web 开发者都应当理解并在应用中利用的最重要的功能。

HTTP/2 并未以任何方式修改 HTTP 的应用语义。所有核心概念,如 HTTP 方法、状态码、URI 和头部字段,都保持不变。相反,HTTP/2 修改的是数据在客户端与服务器之间的格式化(分帧)和传输方式,双方共同管理整个过程,并将所有复杂性隐藏在新的分帧层之后。因此,所有现有应用都可以无需修改直接交付。这是好消息。

然而,我们不仅仅满足于交付一个能工作的应用;我们的目标是交付最佳性能!HTTP/2 开启了许多此前不可能实现的新优化手段,我们的任务就是充分利用它们。让我们深入探究其内部机制。

§ 为何不是 HTTP/1.2?

为了实现 HTTP 工作组设定的性能目标,HTTP/2 引入了一种新的二进制分帧层,它与之前的 HTTP/1.x 服务器和客户端不向后兼容——因此协议主版本号递增为 HTTP/2。

也就是说,除非你正在通过原始 TCP 套接字实现 Web 服务器(或自定义客户端),否则你不会看到任何差异:所有新的底层分帧工作都由客户端和服务器代你完成。唯一可观察到的差异将是性能的提升,以及请求优先级、流量控制和服务器推送等新功能的可用性!

SPDY 与 HTTP/2 的简史

SPDY 是 Google 开发的一个实验性协议,于 2009 年中期宣布,其主要目标是尝试通过解决 HTTP/1.1 的一些众所周知的性能限制来降低网页加载延迟。具体而言,项目目标设定如下:

  • 将页面加载时间(PLT)减少 50%。
  • 避免网站作者对内容进行任何更改。
  • 最小化部署复杂性,避免网络基础设施变更。
  • 与开源社区合作开发这一新协议。
  • 收集真实性能数据以(不)验证实验性协议。

为了实现 50%的 PLT 提升,SPDY 旨在通过引入新的二进制分帧层来更高效地利用底层 TCP 连接,实现请求与响应的多路复用、优先级排序和头部压缩;参见《延迟作为性能瓶颈》。

在最初宣布后不久,Google 的软件工程师 Mike Belshe 和 Roberto Peon 分享了他们对新 SPDY 协议实验性实现的初步成果、文档和源代码:

“到目前为止,我们仅在实验室条件下测试了 SPDY。初步结果非常令人鼓舞:当我们在模拟的家庭网络连接上下载前 25 个网站时,我们看到了显著的性能提升——页面加载速度提高了 55%。”
——《速度提升两倍的 Web》,Chromium 博客

时间快进到 2012 年,这一新的实验性协议已获 Chrome、Firefox 和 Opera 支持,越来越多的网站(包括 Google、Twitter、Facebook 等大型网站和众多小型网站)开始在其基础设施中部署 SPDY。实际上,SPDY 正通过不断增长的行业采用成为事实标准。

观察到上述趋势,HTTP 工作组(HTTP-WG)启动了一项新工作,汲取 SPDY 的经验教训,在此基础上构建和改进,并交付官方的”HTTP/2”标准:起草了新章程,公开征集 HTTP/2 提案,经过工作组的大量讨论后,SPDY 规范被采纳为新 HTTP/2 协议的起点。

在接下来的几年里,SPDY 和 HTTP/2 并行协同发展,SPDY 充当实验分支,用于测试 HTTP/2 标准的新功能和提案:纸上看起来不错的方案在实践中未必可行,反之亦然,SPDY 提供了一条在纳入 HTTP/2 标准之前测试和评估每个提案的途径。最终,这一过程历时三年,产生了十几个中间草案:

  • 2012 年 3 月:征集 HTTP/2 提案
  • 2012 年 11 月:HTTP/2 首个草案(基于 SPDY)
  • 2014 年 8 月:HTTP/2 草案-17 和 HPACK 草案-12 发布
  • 2014 年 8 月:HTTP/2 工作组最后征集意见
  • 2015 年 2 月:IESG 批准 HTTP/2 和 HPACK 草案
  • 2015 年 5 月:RFC 7540(HTTP/2)和 RFC 7541(HPACK)发布
  • 2022 年 6 月:RFC 9113 发布,作为 HTTP/2 的修订版,纳入了多年来的勘误和改进

2015 年初,IESG 审查并批准了新的 HTTP/2 标准予以发布。此后不久,Google Chrome 团队宣布了弃用 SPDY 和 TLS 的 NPN 扩展的时间表:

“HTTP/2 相对于 HTTP/1.1 的主要变化集中在性能提升上。多路复用、头部压缩、优先级排序和协议协商等关键功能源自一个名为 SPDY 的早期开放但非标准协议的工作。Chrome 从第 6 版开始就支持 SPDY,但由于 HTTP/2 已经包含了大部分优势,是时候说再见了。我们计划在 2016 年初移除对 SPDY 的支持,同时也将移除对名为 NPN 的 TLS 扩展的支持,转而支持 ALPN。强烈建议服务器开发者迁移到 HTTP/2 和 ALPN。

我们很高兴为 HTTP/2 的开放标准进程做出了贡献,并希望在标准化和实施方面的广泛行业参与下看到广泛的采用。”
——《你好 HTTP/2,再见 SPDY》,Chromium 博客

SPDY 和 HTTP/2 的协同演进使服务器、浏览器和网站开发者能够在协议开发过程中获得真实世界的经验。因此,HTTP/2 标准是有史以来最好、经过最广泛测试的标准之一。当 HTTP/2 获得 IESG 批准时,已有数十个经过充分测试且可用于生产的客户端和服务器实现。事实上,在最终协议获批仅几周后,许多用户就已经享受到了其好处,因为几款主流浏览器(和众多网站)部署了完整的 HTTP/2 支持。

设计与技术目标

HTTP 协议的首个版本有意为简化实现而设计:HTTP/0.9 是一个单行的协议,用于启动万维网;HTTP/1.0 在一个信息性标准中记录了 HTTP/0.9 的流行扩展;HTTP/1.1 引入了正式的 IETF 标准;参见《HTTP 简史》。因此,HTTP/0.9-1.x 恰好完成了其设定目标:HTTP 是互联网上最普遍、采用最广泛的应用协议之一。

不幸的是,实现简单性也带来了应用性能的代价:HTTP/1.x 客户端需要使用多个连接来实现并发和降低延迟;HTTP/1.x 不压缩请求和响应头部,造成不必要的网络流量;HTTP/1.x 不允许有效的资源优先级排序,导致底层 TCP 连接利用不佳;等等。

这些限制并非致命,但随着 Web 应用在我们的日常生活中不断增长其范围、复杂性和重要性,它们给 Web 的开发者和用户带来了日益沉重的负担,这正是 HTTP/2 旨在解决的差距:

“HTTP/2 通过引入头部字段压缩并允许在同一连接上进行多个并发交换,实现了网络资源的更高效利用和延迟感知的降低……具体而言,它允许在同一连接上交错请求和响应消息,并对 HTTP 头部字段使用高效编码。它还允许请求优先级排序,让更重要的请求更快完成,进一步提升性能。

由此产生的协议对网络更友好,因为与 HTTP/1.x 相比可以使用更少的 TCP 连接。这意味着与其他流的竞争更少,连接生命周期更长,进而带来可用网络容量的更好利用。最后,HTTP/2 还通过使用二进制消息分帧实现了消息更高效的处理。”
——《超文本传输协议版本 2》,草案 17

需要注意的是,HTTP/2 是在扩展而非取代之前的 HTTP 标准。HTTP 的应用语义保持不变,提供的功能或核心概念(如 HTTP 方法、状态码、URI 和头部字段)均未更改——这些变更明确不在 HTTP/2 工作范围内。话虽如此,虽然高层 API 保持不变,但理解底层变更如何解决先前协议的性能限制仍然很重要。让我们简要了解一下二进制分帧层及其特性。

二进制分帧层

HTTP/2 所有性能增强的核心是新的二进制分帧层(图 12-1),它规定了 HTTP 消息在客户端与服务器之间的封装和传输方式。

Figure 12-1. HTTP/2 binary framing layer

图 12-1. HTTP/2 二进制分帧层

“层”指的是在套接字接口和向应用暴露的高层 HTTP API 之间引入新的优化编码机制的设计选择:HTTP 语义(如动词、方法和头部)不受影响,但它们在传输过程中的编码方式有所不同。与换行符分隔的明文 HTTP/1.x 协议不同,所有 HTTP/2 通信被分割成更小的消息和帧,每个都以二进制格式编码。

因此,客户端和服务器都必须使用新的二进制编码机制来相互理解:HTTP/1.x 客户端无法理解仅支持 HTTP/2 的服务器,反之亦然。值得庆幸的是,我们的应用对所有这些变化浑然不觉,因为客户端和服务器代我们完成了所有必要的分帧工作。

§ 二进制协议的优缺点

ASCII 协议易于检查且上手简单。然而,它们效率不高,通常更难正确实现:可选的空白字符、变化的终止序列和其他怪癖使得区分协议与负载变得困难,并导致解析和安全错误。相比之下,虽然二进制协议可能需要更多努力才能上手,但它们往往能带来更高性能、更健壮且可证明正确的实现。

HTTP/2 使用二进制分帧。因此,你将需要能够识别它的工具来检查和调试协议——例如 Wireshark 或同等工具。实际上,这并不像看起来那么麻烦,因为你本来就需要使用相同的工具来检查加密的 TLS 流——它们也依赖二进制分帧(参见 TLS 记录协议)——承载 HTTP/1.x 和 HTTP/2 数据。

流、消息与帧

新的二进制分帧机制的引入改变了客户端与服务器之间的数据交换方式(图 12-2)。为了描述这一过程,让我们熟悉 HTTP/2 的术语:

流(Stream) : 在已建立连接内的双向字节流,可承载一个或多个消息。

消息(Message) : 映射到逻辑请求或响应消息的完整帧序列。

帧(Frame) : HTTP/2 中最小的通信单位,每个帧包含一个帧头部,至少标识该帧所属的流。

  • 所有通信都在单个 TCP 连接上进行,该连接可承载任意数量的双向流。
  • 每个流具有唯一标识符和可选的优先级信息,用于承载双向消息。
  • 每个消息是一个逻辑 HTTP 消息,如请求或响应,由一个或多个帧组成。
  • 帧是承载特定类型数据(如 HTTP 头部、消息负载等)的最小通信单位。来自不同流的帧可以交错,然后通过每个帧头部中嵌入的流标识符重新组装。

Figure 12-2. HTTP/2 streams, messages, and frames

图 12-2. HTTP/2 的流、消息与帧

简而言之,HTTP/2 将 HTTP 协议通信分解为二进制编码帧的交换,这些帧映射到属于特定流的消息,所有这些都多路复用在单个 TCP 连接内。这是实现 HTTP/2 协议提供的所有其他功能和性能优化的基础。

请求与响应多路复用

在 HTTP/1.x 中,如果客户端想要发出多个并行请求以提升性能,则必须使用多个 TCP 连接;参见《使用多个 TCP 连接》。这一行为是 HTTP/1.x 交付模型的直接后果,它确保每个连接一次只能交付一个响应(响应排队)。更糟糕的是,这还会导致队头阻塞和底层 TCP 连接的低效利用。

HTTP/2 中的新二进制分帧层消除了这些限制,通过允许客户端和服务器将 HTTP 消息分解为独立的帧(图 12-3),交错它们,然后在另一端重新组装,实现了完整的请求与响应多路复用。

Figure 12-3. HTTP/2 request and response multiplexing within a shared connection

图 12-3. 共享连接内的 HTTP/2 请求与响应多路复用

图 12-3 的快照捕捉了同一连接内正在传输的多个流:客户端正在向服务器传输一个 DATA 帧(流 5),同时服务器正在向客户端交错传输流 1 和 3 的帧序列。结果,有三个并行流正在传输!

将 HTTP 消息分解为独立帧、交错它们然后在另一端重新组装的能力,是 HTTP/2 最重要的增强。事实上,它在整个 Web 技术栈中引入了众多性能优势的连锁反应,使我们能够:

  • 并行交错多个请求而不会被任何一个阻塞
  • 并行交错多个响应而不会被任何一个阻塞
  • 使用单个连接并行交付多个请求和响应
  • 移除不必要的 HTTP/1.x 变通方案(参见《为 HTTP/1.x 优化》),如文件拼接、图片精灵和域名分片
  • 通过消除不必要的延迟和提升可用网络容量的利用率来降低页面加载时间
  • 以及更多……

HTTP/2 中的新二进制分帧层解决了 HTTP/1.x 中存在的队头阻塞问题,并消除了使用多个连接来实现请求和响应并行处理和交付的需求。因此,这使我们的应用更快、更简单、部署成本更低。

流优先级

一旦 HTTP 消息可以被分割为多个独立帧,并且我们允许来自多个流的帧被多路复用,帧被交错和交付的顺序就成为客户端和服务器双方关键的性能考量。为了促进这一点,HTTP/2 标准允许每个流具有关联的权重和依赖关系:

  • 每个流可被分配 1 到 256 之间的整数权重。
  • 每个流可被赋予对另一个流的显式依赖。

流依赖关系和权重的组合允许客户端构建和传达一个”优先级树”(图 12-4),表达它希望如何接收响应。反过来,服务器可以使用这些信息来优先处理流,通过控制 CPU、内存和其他资源的分配,一旦响应数据可用,再分配带宽以确保高优先级响应的最佳交付。

Figure 12-4. HTTP/2 stream dependencies and weights

图 12-4. HTTP/2 流依赖关系与权重

HTTP/2 中的流依赖关系通过引用另一个流的唯一标识符作为其父流来声明;如果省略,则称该流依赖于”根流”。声明流依赖关系表明,如果可能,父流应在其依赖流之前被分配资源——例如,请在响应 C 之前处理和交付响应 D。

共享相同父流的流(即兄弟流)应按其权重比例分配资源。例如,如果流 A 权重为 12,其兄弟流 B 权重为 4,则确定每个流应接收的资源比例:

  • 求和所有权重:12 + 4 = 16
  • 将每个流权重除以总权重:A = 12/16 = 3/4,B = 4/16 = 1/4

因此,流 A 应接收可用资源的四分之三,流 B 应接收四分之一;流 B 应接收分配给流 A 的资源的三分之一。让我们通过图 12-4 中的几个实际例子来理解。从左到右:

  1. 流 A 和 B 均未指定父依赖关系,称它们依赖于隐式”根流”;A 权重为 12,B 权重为 4。因此,基于比例权重:流 B 应接收分配给流 A 的资源的三分之一。
  2. D 依赖于根流;C 依赖于 D。因此,D 应在 C 之前接收全部资源分配。权重无关紧要,因为 C 的依赖关系表达了更强的偏好。
  3. D 应在 C 之前接收全部资源分配;C 应在 A 和 B 之前接收全部资源分配;流 B 应接收分配给流 A 的资源的三分之一。
  4. D 应在 E 和 C 之前接收全部资源分配;E 和 C 应在 A 和 B 之前接收相等分配;A 和 B 应基于其权重接收比例分配。

如上述例子所示,流依赖关系和权重的组合为资源优先级排序提供了一种表达性语言,这是提升浏览性能的关键功能,因为我们有许多具有不同依赖关系和权重的资源类型。更好的是,HTTP/2 协议还允许客户端随时更新这些偏好,这实现了浏览器中的进一步优化——例如,我们可以根据用户交互和其他信号改变依赖关系并重新分配权重。

流依赖关系和权重表达的是传输偏好,而非要求,因此不保证特定的处理或传输顺序。也就是说,客户端不能通过流优先级强制服务器按特定顺序处理流。虽然这可能看起来违反直觉,但这实际上是期望的行为:我们不想因为高优先级资源被阻塞而阻止服务器在低优先级资源上取得进展。

§ 浏览器请求优先级与 HTTP/2

在浏览器中渲染页面时,并非所有资源都具有同等优先级:HTML 文档本身对构建 DOM 至关重要;CSS 是构建 CSSOM 所必需的;DOM 和 CSSOM 的构建都可能被 JavaScript 资源阻塞(参见 DOM、CSSOM 和 JavaScript);其余资源(如图片)通常以较低优先级获取。

为了加速页面加载时间,所有现代浏览器都基于资源类型、其在页面上的位置,甚至从先前访问中学到的优先级来优先处理请求——例如,如果渲染在先前访问中被特定资源阻塞,则该资源在未来可能被赋予更高优先级。

使用 HTTP/1.x,浏览器利用上述优先级数据的能力有限:协议不支持多路复用,也没有向服务器传达请求优先级的方法。相反,它必须依赖并行连接的使用,这使得每个源的并行度限制为最多六个请求。结果,请求在客户端排队等待连接可用,增加了不必要的网络延迟。理论上,HTTP 管道尝试部分解决这个问题,但实际上未能获得采用。

HTTP/2 解决了这些低效问题:请求排队和队头阻塞被消除,因为浏览器可以在发现请求的瞬间就分发所有请求,并且浏览器可以通过流依赖关系和权重传达其流优先级偏好,允许服务器进一步优化响应交付。

每个源一个连接

随着新的二进制分帧机制的到位,HTTP/2 不再需要多个 TCP 连接来并行多路复用流;每个流被分割为多个帧,这些帧可以被交错和优先级排序。因此,所有 HTTP/2 连接都是持久的,每个源只需要一个连接,这提供了众多性能优势。

“对于 SPDY 和 HTTP/2,杀手级功能是在单个拥塞控制良好的通道上进行任意多路复用。我对这一点的重要性及其工作效果之好感到惊讶。我喜欢的一个很好的指标是仅承载单个 HTTP 事务(从而使该事务承担所有开销)的连接创建比例。对于 HTTP/1,我们 74%的活动连接仅承载单个事务——持久连接并不像我们希望的那么有用。但在 HTTP/2 中,这一数字骤降至 25%。这对开销减少来说是巨大的胜利。”
——《HTTP/2 在 Firefox 中上线》,Patrick McManus

大多数 HTTP 传输是短促而突发的,而 TCP 针对长寿命的大块数据传输进行了优化。通过重用同一连接,HTTP/2 能够更高效地利用每个 TCP 连接,并显著降低整体协议开销。此外,使用更少的连接减少了整个连接路径(即客户端、中介和源服务器)的内存和处理占用,降低了整体运营成本并改善了网络利用率和容量。因此,转向 HTTP/2 不仅应降低网络延迟,还应有助于提升吞吐量并降低运营成本。

连接数减少对于提升 HTTPS 部署性能尤为重要:这意味着更少的昂贵 TLS 握手、更好的会话重用,以及客户端和服务器所需资源的整体减少。

§ 丢包、高 RTT 链路与 HTTP/2 性能

等等,你说,我们列出了使用每个源一个 TCP 连接的好处,但难道没有潜在的缺点吗?是的,确实存在。

  • 我们消除了 HTTP 的队头阻塞,但 TCP 层面仍然存在队头阻塞(参见《队头阻塞》)。
  • 如果禁用 TCP 窗口缩放,带宽延迟积效应可能会限制连接吞吐量。
  • 当发生丢包时,TCP 拥塞窗口大小会减少(参见《拥塞避免》),从而降低整个连接的最大吞吐量。

列表中的每一项都可能对 HTTP/2 连接的吞吐量和延迟性能产生不利影响。然而,尽管存在这些限制,转向多个连接将导致其自身的性能权衡:

  • 由于不同的压缩上下文,头部压缩效果降低
  • 由于不同的 TCP 流,请求优先级排序效果降低
  • 每个 TCP 流的利用效率降低,由于更多竞争流导致拥塞的可能性更高
  • 由于更多 TCP 流而增加的资源开销

上述优缺点并非详尽列表,总是可能构造出特定场景,其中一个或多个连接可能证明是有益的。然而,在野外部署 HTTP/2 的实验证据表明,单个连接是首选的部署策略:

“到目前为止的测试中,队头阻塞的负面影响(尤其是在存在丢包的情况下)被压缩和优先级排序的好处所抵消。”
——《超文本传输协议版本 2》,草案 2

与所有性能优化过程一样,当你移除一个性能瓶颈时,你就解锁了下一个。对于 HTTP/2 来说,TCP 可能就是它。这就是为什么,重申一次,服务器上经过良好调优的 TCP 堆栈对于 HTTP/2 是如此关键的优化标准。

目前正在进行研究以解决这些问题并普遍提升 TCP 性能:TCP 快速打开、比例速率降低、增加的初始拥塞窗口,以及更多。话虽如此,重要的是要认识到 HTTP/2 与其前辈一样,并不强制使用 TCP。展望未来,其他传输协议(如 UDP)并非不可能——事实上,HTTP/3 已经基于 QUIC over UDP 实现了这一点,彻底解决了 TCP 队头阻塞问题。

流量控制

流量控制是一种机制,用于防止发送方用接收方不想要或无法处理的数据将其淹没:接收方可能正忙、负载沉重,或可能只愿为特定流分配固定数量的资源。例如,客户端可能以高优先级请求了一个大视频流,但用户暂停了视频,客户端现在想要暂停或限制从服务器到客户端的交付,以避免获取和缓冲不必要的数据。或者,代理服务器可能具有快速的下游和缓慢的上游连接,同样希望调节下游交付数据的速度以匹配上游速度来控制其资源使用;等等。

上述要求是否让你想起了 TCP 流量控制?它们应该让你想起,因为问题实际上完全相同——参见《流量控制》。然而,由于 HTTP/2 流在单个 TCP 连接内被多路复用,TCP 流量控制既不够精细,也没有提供必要的应用层 API 来调节单个流的交付。为了解决这一点,HTTP/2 提供了一组简单的构建块,允许客户端和服务器实现其自己的流级和连接级流量控制:

  • 流量控制是定向的。每个接收方可以为每个流和整个连接选择设置其期望的任意窗口大小。
  • 流量控制是基于信用的。每个接收方通告其初始连接和流流量控制窗口(以字节为单位),每当发送方发出 DATA 帧时减少,并通过接收方发送的 WINDOW_UPDATE 帧增加。
  • 流量控制不能被禁用。当 HTTP/2 连接建立时,客户端和服务器交换 SETTINGS 帧,设置双向的流量控制窗口大小。流量控制窗口的默认值设置为 65,535 字节,但接收方可以设置更大的最大窗口大小(2^31-1 字节),并通过在收到任何数据时发送 WINDOW_UPDATE 帧来维持它。
  • 流量控制是逐跳的,而非端到端的。也就是说,中介可以使用它来控制资源使用,并基于自身标准和启发式实现资源分配机制。

HTTP/2 并未指定实现流量控制的任何特定算法。相反,它提供了简单的构建块,并将实现留给客户端和服务器,它们可以使用它来实现自定义策略来调节资源使用和分配,以及实现可能有助于提升 Web 应用真实和感知性能(参见《速度、性能与人类感知》)的新交付能力。

例如,应用层流量控制允许浏览器仅获取特定资源的一部分,通过将流流量控制窗口减少到零来暂停获取,然后在之后恢复——例如,获取图片的预览或首屏扫描,显示它并允许其他高优先级获取继续进行,然后在更关键的资源加载完成后恢复获取。

服务器推送

HTTP/2 的另一个强大的新功能是服务器能够为单个客户端请求发送多个响应的能力。也就是说,除了对原始请求的响应外,服务器还可以向客户端推送额外资源(图 12-5),而无需客户端明确请求每一个!

Figure 12-5. Server initiates new streams (promises) for push resources

图 12-5. 服务器为推送资源发起新流(承诺)

HTTP/2 突破了严格的请求-响应语义,启用了一对多和服务器发起的推送工作流,为浏览器内外开启了全新的交互可能性世界。这是一个赋能功能,将对我们思考协议的方式以及其使用地点和方式产生重要的长期影响。

为什么我们在浏览器中需要这样的机制?典型的 Web 应用由数十个资源组成,客户端通过检查服务器提供的文档来发现所有这些资源。因此,为何不消除额外的延迟,让服务器提前推送关联资源?服务器已经知道客户端需要哪些资源;这就是服务器推送。

事实上,如果你曾经通过 data URI 内联过 CSS、JavaScript 或任何其他资源(参见《资源内联》),那么你就已经亲手体验过服务器推送!通过手动将资源内联到文档中,我们实际上是在将该资源推送给客户端,而无需等待客户端请求它。使用 HTTP/2,我们可以实现相同的结果,但具有额外的性能优势:

  • 推送的资源可以被客户端缓存
  • 推送的资源可以跨不同页面重用
  • 推送的资源可以与其他资源一起多路复用
  • 推送的资源可以由服务器优先级排序
  • 推送的资源可以被客户端拒绝

每个推送的资源都是一个流,与内联资源不同,它允许被客户端单独多路复用、优先级排序和处理。唯一的安全限制(由浏览器强制执行)是推送的资源必须遵守同源策略:服务器必须对所提供内容具有权威性。

§ PUSH_PROMISE 101

所有服务器推送流都通过 PUSH_PROMISE 帧发起,该帧信号化服务器向客户端推送所述资源的意图,并需要在请求推送资源的响应数据之前交付。这一交付顺序至关重要:客户端需要知道服务器打算推送哪些资源,以避免为这些资源创建自己的重复请求。满足这一要求的最简单策略是在父响应(即 DATA 帧)之前发送所有 PUSH_PROMISE 帧,其中只包含承诺资源的 HTTP 头部。

一旦客户端收到 PUSH_PROMISE 帧,它可以选择拒绝该流(通过 RST_STREAM 帧),如果它愿意的话(例如,资源已在缓存中),这是相对于 HTTP/1.x 的重要改进。相比之下,资源内联的使用(HTTP/1.x 的一个流行”优化”)等同于”强制推送”:客户端无法选择退出、取消它或单独处理内联资源。

使用 HTTP/2,客户端对服务器推送的使用保持完全控制。客户端可以限制并发推送流的数量;调整初始流量控制窗口以控制流首次打开时推送的数据量;完全禁用服务器推送。这些偏好通过 HTTP/2 连接开始时的 SETTINGS 帧传达,并可以随时更新。

头部压缩

每次 HTTP 传输都携带一组描述传输资源及其属性的头部。在 HTTP/1.x 中,这些元数据始终以明文发送,每次传输增加 500-800 字节的开销,如果使用 HTTP cookie,有时甚至增加数千字节;参见《测量和控制协议开销》。为了减少这一开销并提升性能,HTTP/2 使用 HPACK 压缩格式压缩请求和响应头部元数据,该格式使用两种简单但强大的技术:

  • 它允许传输的头部字段通过静态霍夫曼编码进行编码,减少其单个传输大小。
  • 它要求客户端和服务器维护并更新一个先前见过的头部字段的索引列表(即建立共享压缩上下文),然后将其用作引用以高效编码先前传输的值。

霍夫曼编码允许单个值在传输时被压缩,而先前传输值的索引列表允许我们通过传输可用于高效查找和重建完整头部键和值的索引值来编码重复值(图 12-6)。

Figure 12-6. HPACK: Header Compression for HTTP/2

图 12-6. HPACK:HTTP/2 的头部压缩

作为进一步优化,HPACK 压缩上下文由静态表和动态表组成:静态表在规范中定义,提供所有连接都可能使用的常见 HTTP 头部字段列表(如有效的头部名称);动态表初始为空,基于特定连接内交换的值更新。因此,每个请求的大小通过使用静态霍夫曼编码处理先前未见过的值,以及对每侧静态或动态表中已存在的值进行索引替换来减少。

HTTP/2 中请求和响应头部字段的定义保持不变,仅有少数例外:所有头部字段名称均为小写,请求行现在被分割为单独的:method、:scheme、:authority 和:path 伪头部字段。

§ HPACK 的安全性与性能

HTTP/2 和 SPDY 的早期版本使用 zlib(带有自定义字典)压缩所有 HTTP 头部,这实现了传输头部数据大小 85%-88%的减少,以及页面加载时间延迟的显著改善:

“在较低带宽的 DSL 链路上,其上联链路仅为 375 Kbps,请求头部压缩尤其为某些网站(即发出大量资源请求的网站)带来了显著的页面加载时间改善。我们发现仅由于头部压缩就减少了 45-1142 毫秒的页面加载时间。”
——《SPDY 白皮书》,chromium.org

然而,2012 年夏季,一种针对 TLS 和 SPDY 压缩算法的”CRIME”安全攻击被公布,可能导致会话劫持。因此,zlib 压缩算法被 HPACK 取代,后者专门设计用于:解决发现的安全问题、高效且易于正确实现,以及当然实现对 HTTP 头部元数据的良好压缩。

有关 HPACK 压缩算法的完整细节,参见RFC 7541——该规范在 2022 年的 RFC 9113 修订中仍然有效且基本保持不变。

升级到 HTTP/2

向 HTTP/2 的切换不可能一夜之间完成:数百万服务器必须更新以使用新的二进制分帧,数十亿客户端也必须同样更新其网络库、浏览器和其他应用。

好消息是,所有现代浏览器都已承诺支持 HTTP/2 多年,且大多数浏览器已通过各种自动更新机制在后台启用了 HTTP/2 支持,覆盖了绝大部分现有用户。话虽如此,一些用户可能仍停留在遗留浏览器上,服务器和中介也必须更新以支持 HTTP/2,这是一个更长(且耗费人力和资本)的过程。

HTTP/1.x 至少还会存在十年,大多数服务器和客户端必须同时支持 HTTP/1.x 和 HTTP/2 标准。因此,HTTP/2 客户端和服务器必须能够在交换应用数据之前发现和协商将使用哪种协议。为了解决这一点,HTTP/2 协议定义了以下机制:

  • 通过 TLS 和 ALPN 协商 HTTP/2
  • 无需先验知识将明文连接升级到 HTTP/2
  • 通过先验知识发起明文 HTTP/2 连接

HTTP/2 标准并不要求使用 TLS,但在存在大量现有中介的情况下,实践中它是部署新协议最可靠的方式;参见《代理、中介、TLS 和 Web 上的新协议》。因此,使用 TLS 和 ALPN 是部署和协商 HTTP/2 的推荐机制:客户端和服务器在 TLS 握手过程中协商期望的协议,不增加任何额外延迟或往返;参见《TLS 握手和应用层协议协商(ALPN)》。此外,作为额外约束,虽然所有主流浏览器都承诺支持 HTTP/2 over TLS,但大多数现代浏览器实际上仅通过 HTTPS 支持 HTTP/2——例如 Firefox 和 Google Chrome 已明确表态,Safari 和 Edge 也遵循此实践。因此,TLS with ALPN 协商已成为在浏览器中启用 HTTP/2 的事实要求

通过常规的非加密通道建立 HTTP/2 连接仍然是可能的,尽管可能无法通过主流浏览器实现,且存在一些额外的复杂性。由于 HTTP/1.x 和 HTTP/2 都运行在同一端口(80),在缺乏关于服务器 HTTP/2 支持的其他信息时,客户端必须使用 HTTP 升级机制来协商适当的协议:

GET /page HTTP/1.1
Host: server.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c
HTTP2-Settings: (SETTINGS payload)
HTTP/1.1 200 OK
Content-length: 243
Content-type: text/html

(... HTTP/1.1 响应 ...)

HTTP/1.1 101 Switching Protocols
Connection: Upgrade
Upgrade: h2c

(... HTTP/2 响应 ...)
  1. 带有 HTTP/2 升级头部的初始 HTTP/1.1 请求
  2. HTTP/2 SETTINGS 负载的 Base64 URL 编码
  3. 服务器拒绝升级,通过 HTTP/1.1 返回响应
  4. 服务器接受 HTTP/2 升级,切换到新分帧

使用前述升级流程,如果服务器不支持 HTTP/2,它可以立即通过 HTTP/1.1 响应请求。或者,它可以通过返回 HTTP/1.1 格式的 101 Switching Protocols 响应来确认 HTTP/2 升级,然后立即切换到 HTTP/2 并使用新的二进制分帧协议返回响应。无论哪种情况,都不会产生额外的往返。

最后,如果客户端选择,它也可以通过其他方式记住或获取关于 HTTP/2 支持的信息——例如 DNS 记录、手动配置等——而无需依赖升级工作流。掌握这些知识后,它可以选择从一开始就通过非加密通道发送 HTTP/2 帧,并希望一切顺利。在最坏的情况下,连接将失败,客户端将回退到升级工作流或切换到带有 [[ALPN]] 协商的 TLS 隧道。

客户端与服务器之间、服务器与服务器之间以及所有其他排列的安全通信是安全最佳实践:所有传输中的数据都应加密、认证并检查篡改。简而言之,使用带有 ALPN 协商的 TLS 来部署 HTTP/2

二进制分帧简介

HTTP/2 所有改进的核心是新的二进制、长度前缀分帧层。与换行符分隔的明文 HTTP/1.x 协议相比,二进制分帧提供了更紧凑的表示,既更高效处理也更易于正确实现。

一旦 HTTP/2 连接建立,客户端和服务器通过交换帧进行通信,帧作为协议内最小的通信单位。所有帧共享一个通用的 9 字节头部(图 12-7),包含帧的长度、类型、标志位字段和 31 位流标识符。

Figure 12-7. Common 9-byte frame header

图 12-7. 通用的 9 字节帧头部

  • 24 位长度字段允许单个帧承载最多 2^24-1 字节(约 16MB)的数据。
  • 8 位类型字段决定帧的格式和语义。
  • 8 位标志字段传达特定于帧类型的布尔标志。
  • 1 位保留字段始终设置为 0。
  • 31 位流标识符唯一标识 HTTP/2 流。

技术上,长度字段允许每帧最多 2^24-1 字节(约 16MB)的负载。然而,HTTP/2 标准将 DATA 帧的默认最大负载大小设置为 2^14 字节(约 16KB),并允许客户端和服务器协商更高的值。 bigger 并非总是更好:较小的帧大小实现高效的多路复用并最小化队头阻塞。

鉴于对共享 HTTP/2 帧头部的这些了解,我们现在可以编写一个简单的解析器,能够检查任何 HTTP/2 字节流并识别不同的帧类型、报告它们的标志、并通过检查每帧的前九个字节来报告每个帧的长度。此外,由于每个帧都是长度前缀的,解析器可以快速高效地跳到下一帧的开头——相对于 HTTP/1.x 这是巨大的性能提升。

一旦知道帧类型,帧的其余部分就可以被解析器解释。HTTP/2 标准定义了以下类型:

DATA : 用于传输 HTTP 消息体

HEADERS : 用于通信流的头部字段

PRIORITY : 用于通信发送方建议的流优先级

RST_STREAM : 用于信号化流的终止

SETTINGS : 用于通信连接的配置参数

PUSH_PROMISE : 用于信号化服务引用资源的承诺

PING : 用于测量往返时间和执行”活跃度”检查

GOAWAY : 用于通知对等方停止为当前连接创建流

WINDOW_UPDATE : 用于实现流和连接的流量控制

CONTINUATION : 用于继续头部块片段序列

你需要一些工具来检查低层 HTTP/2 帧交换。你最喜欢的十六进制查看器当然是一个选择。或者,为了更人性化的表示,你可以使用 Wireshark 等工具,它理解 HTTP/2 协议并能捕获、解码和分析交换。

好消息是,前述帧分类的确切语义主要只与服务器和客户端实现者相关,他们需要担心流量控制、错误处理、连接终止和其他细节的语义。HTTP 协议的应用层功能和语义保持不变:客户端和服务器处理分帧、多路复用和其他细节,而应用可以享受更快更高效交付的好处。

话虽如此,即使分帧层对我们的应用隐藏,我们再深入一步了解两个最常见的工作流也是有用的:发起新流和交换应用数据。对请求或响应如何被转换为独立帧具有直觉,将为你提供调试和优化 HTTP/2 部署所需的知识。让我们再深入一点。

§ 固定长度与变长字段

HTTP/2使用固定长度字段。HTTP/2 帧的开销很低(DATA 帧为 9 字节头部),变长编码节省的空间无法抵消解析器所需的复杂性,也不会对交换使用的带宽或延迟产生显著影响。

例如,如果变长编码能将开销减少 50%,对于 1400 字节的网络数据包,这仅相当于单个帧节省 4 字节(0.3%)。

§ 发起新流

在发送任何应用数据之前,必须创建新流并发送适当的请求元数据:可选的流依赖关系和权重、可选的标志,以及描述请求的 HPACK 编码 HTTP 请求头部。客户端通过发送包含上述所有内容的 HEADERS 帧(图 12-8)来发起这一过程。

图 12-8. Wireshark 中解码的 HEADERS 帧

Wireshark 以与在线编码相同的顺序解码和显示帧字段——例如,将通用帧头部中的字段与图 12-7 中的帧布局进行比较。

HEADERS 帧用于声明和通信新请求的元数据。应用负载(如果可用)在 DATA 帧内独立交付。这种分离允许协议将”控制流量”的处理与应用数据的交付分开——例如,流量控制仅应用于 DATA 帧,非 DATA 帧始终被高优先级处理。

§ 通过 PUSH_PROMISE 发起服务器端流

HTTP/2 允许客户端和服务器都发起新流。在服务器发起流的情况下,使用 PUSH_PROMISE 帧来声明承诺并通信 HPACK 编码的响应头部。该帧的格式与 HEADERS 相似,只是它省略了可选的流依赖关系和权重,因为服务器对承诺数据的交付方式具有完全控制。

为了消除客户端和服务器发起流之间的流 ID 冲突,计数器是偏移的:客户端发起的流具有奇数编号的流 ID,服务器发起的流具有偶数编号的流 ID。因此,由于图 12-8 中的流 ID 设置为”1”,我们可以推断这是一个客户端发起的流。

§ 发送应用数据

一旦新流创建且 HTTP 头部发送完毕,DATA 帧(图 12-9)被用于发送应用负载(如果存在)。负载可以在多个 DATA 帧之间分割,最后一个帧通过在帧头部切换 END_STREAM 标志来指示消息的结束。

图 12-9. DATA 帧

图 12-9 中”End Stream”标志设置为”false”,表明客户端尚未完成应用负载的传输;还有更多 DATA 帧即将到来。

除了长度和标志字段外,DATA 帧确实没有太多可说的。应用负载可以在多个 DATA 帧之间分割以实现高效的多路复用,但除此之外,它完全按照应用提供的方式交付——即,编码机制的选择(纯文本、gzip 或其他编码格式)留给应用决定。

§ 分析 HTTP/2 帧数据流

掌握了不同帧类型的知识,我们现在可以重温之前在《请求与响应多路复用》中遇到的图表(图 12-10)并分析 HTTP/2 交换:

Figure 12-10. HTTP/2 request and response multiplexing within a shared connection

图 12-10. 共享连接内的 HTTP/2 请求与响应多路复用

  1. 有三个流,ID 设置为 1、3 和 5。
  2. 所有三个流 ID 都是奇数;所有三个都是客户端发起的流。
  3. 此交换中没有服务器发起(“推送”)的流。
  4. 服务器正在为流 1 发送交错的 DATA 帧,携带对客户端先前请求的应用响应。
  5. 服务器在流 1 的 DATA 帧之间交错了流 3 的 HEADERS 和 DATA 帧——响应多路复用正在发挥作用!
  6. 客户端正在为流 5 传输 DATA 帧,这表明 HEADERS 帧此前已被传输。

上述分析当然基于实际 HTTP/2 交换的简化表示,但它仍然说明了新协议的许多优势和特性。至此,你应该具备了成功记录和分析真实世界 HTTP/2 跟踪的必要知识——试试看吧!