HTTP/2 简介

HTTP/2 让我们能够撤消之前在应用中执行的许多 HTTP/1.1 解决方法,并在传输层本身内解决这些问题,这使得我们的应用速度更快、操作更简单、更稳定(这是一种很罕见的组合)。更棒的是,它还为优化应用和提高性能提供了许多全新的机会!

HTTP/2 的主要目标是启用完整的请求和响应多路复用来缩短延迟时间,通过高效压缩 HTTP 标头字段最大限度地减少协议开销,并添加对请求优先级和服务器推送的支持。为了实现这些要求,需要大量支持其他协议增强功能,例如新的流控制、错误处理和升级机制,但这些功能是每个 Web 开发者都应该了解并在其应用中加以利用的最重要的功能。

HTTP/2 不会以任何方式修改 HTTP 的应用语义。HTTP 方法、状态代码、URI 和标头字段等所有核心概念都保持不变。相反,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%。
  • 让网站作者无需对内容进行任何更改。
  • 将部署复杂性降至最低,并避免网络基础架构变更。
  • 与开源社区合作开发此新协议。
  • 收集真实性能数据以验证实验性协议。

首次发布后不久,Google 的两位软件工程师 Mike Belshe 和 Roberto Peon 就分享了他们对新 SPDY 协议的实验性实现的第一个结果、文档和源代码:

到目前为止,我们仅在实验室条件下测试了 SPDY。初步结果非常鼓舞人心:我们通过模拟的家庭网络连接下载了前 25 个网站后,发现性能有了显著的提升,网页加载速度最多可以提高 55%。 (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) 发布

2015 年初,IESG 审核并批准了新的 HTTP/2 标准发布。之后不久,Google Chrome 团队宣布了弃用适用于 TLS 的 SPDY 和 NPN 扩展程序的时间表:

HTTP/2 相对于 HTTP/1.1 的主要变更侧重于提高性能。一些关键功能(如多路复用、标头压缩、优先级和协议协商)是从较早开放但非标准协议 SPDY 中完成的工作发展而来。Chrome 从 Chrome 6 开始就支持 SPDY,但由于大部分优势都是在 HTTP/2 中造成的,现在该说再见了。我们计划在 2016 年初停止对 SPDY 的支持,并同时在 Chrome 中取消对名为 NPN 的 TLS 扩展程序的支持,改为使用 ALPN。强烈建议服务器开发者改用 HTTP/2 和 ALPN。

我们非常高兴地为促成了 HTTP/2 的开放标准流程贡献力量,并希望行业广泛参与标准化和实现。(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 所有性能增强的核心是新的二进制分帧层,它规定了 HTTP 消息的封装方式,并在客户端和服务器之间传输。

HTTP/2 二进制分帧层

“层”是指一种设计选择,用于在套接字接口与提供给应用的更高级别 HTTP API 之间引入一种经过优化的新编码机制:HTTP 语义(例如动词、方法和标头)不受影响,但其在传输过程中的编码方式有所不同。与以换行符分隔的 HTTP/1.x 协议明文形式不同,所有 HTTP/2 通信都会拆分为较小的消息和帧,其中的每个消息和帧都采用二进制格式进行编码。

因此,客户端和服务器都必须使用新的二进制编码机制来相互理解:HTTP/1.x 客户端无法理解仅支持 HTTP/2 的服务器,反之亦然。幸运的是,我们的应用仍可轻松了解所有这些变化,因为客户端和服务器会代表我们执行所有必要的取景工作。

信息流、消息和帧

新的二进制分帧机制改变了客户端与服务器之间的数据交换方式。为了描述此过程,我们需要先熟悉 HTTP/2 术语:

  • 数据流:已建立的连接内的双向字节流,可承载一条或多条消息。
  • 消息:映射到逻辑请求或响应消息的完整帧序列。
  • :HTTP/2 中的最小通信单元,每个单元包含一个帧标头,至少标识该帧所属的数据流。

这些术语之间的关系可总结如下:

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

HTTP/2 流、消息和帧

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

请求和响应多路复用

使用 HTTP/1.x 时,如果客户端想要发出多个并行请求以提高性能,则必须使用多个 TCP 连接(请参阅使用多个 TCP 连接)。这是 HTTP/1.x 传送模型的直接结果,该行为可确保每个连接一次只传送一个响应(响应排队)。更糟糕的是,这也会导致队首阻塞,以及底层 TCP 连接的效率低下。

HTTP/2 中新的二进制分帧层消除了这些限制并实现了完整的请求和响应多路复用,方法是允许客户端和服务器将 HTTP 消息分解为独立的帧,交错发送,然后在另一端重新组装这些帧。

共享连接内的 HTTP/2 请求和响应多路复用

该快照捕捉了同一连接内传输的多个数据流。客户端正在向服务器传输 DATA 帧(流 5),而服务器正在将交错的帧序列发送到客户端,以便流 1 和流 3。因此,正在传输三个并行流。

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

  • 并行交错地发送多个请求,请求之间互不影响。
  • 并行交错地发送多个响应,响应之间互不影响。
  • 使用一个连接并行发送多个请求和响应。
  • 移除了不必要的 HTTP/1.x 权宜解决方法(请参阅针对 HTTP/1.x 进行优化,例如串联文件、image sprites 和网域分片)。
  • 消除不必要的延迟并提高可用网络容量的利用率,从而缩短网页加载时间。
  • 等等...

HTTP/2 中新的二进制分帧层解决了 HTTP/1.x 中存在的队首阻塞问题,并且消除了并行处理及传送请求和响应所需的多个连接。因此,我们的应用部署速度更快、操作更简单、部署成本更低。

数据流优先级

一旦 HTTP 消息可以拆分为多个单独的帧,并且我们允许对来自多个数据流的帧进行多路复用,那么客户端和服务器交错和传送这些帧的顺序就成为关键的性能考虑因素。为了方便起见,HTTP/2 标准允许每个数据流具有关联的权重和依赖关系:

  • 可以为每个数据流分配一个介于 1 到 256 之间的整数。
  • 每个数据流都可以显式依赖于另一个数据流。

通过结合使用数据流依赖关系和权重,客户端可以构建和传递“优先级树”,表明其希望如何接收响应。反过来,服务器可以使用这些信息通过控制 CPU、内存和其他资源的分配来确定流处理的优先级,并在响应数据可用后分配带宽,以确保将高优先级响应以最优方式传递给客户端。

HTTP/2 数据流依赖项和权重

HTTP/2 中的数据流依赖关系通过将另一个数据流的唯一标识符作为父数据流来声明;如果省略标识符,相应数据流将依赖于“根数据流”。声明数据流依赖项表示,如果可能,应在父数据流的依赖项之前为其分配资源。换句话说,“请先处理和传送响应 D,然后再处理和传送响应 C”。

共享同一父项的数据流(即同级数据流)应按其权重比例分配资源。例如,如果数据流 A 的权重为 12,其同级数据流 B 的权重为 4,则要确定每个数据流应接收的资源比例,请执行以下操作:

  1. 将所有权重相加:4 + 12 = 16
  2. 将每个数据流权重除以总权重:A = 12/16, B = 4/16

因此,数据流 A 应获得四分之三的可用资源,数据流 B 应获得四分之一的可用资源;数据流 B 获得分配给数据流 A 的资源的三分之一。我们来看一下上图中的一些实际示例。从左到右:

  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 不再需要多个 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 不仅可以减少网络延迟,还有助于提高吞吐量并降低运营费用。

流控制

流控制是一种机制,用于防止发送方向接收器发送其可能不需要或无法处理的数据,也就是:接收器可能很忙,负载高,或者只愿意为特定流分配固定数量的资源。例如,客户端可能请求了一个高优先级的大型视频流,但用户已暂停视频,而客户端现在想要暂停或限制从服务器的传送,以避免提取和缓冲不必要的数据。或者,代理服务器可能具有快速的下游连接和缓慢的上游连接,并且也希望控制下游传输数据的速度与上游的速度相匹配,以控制其资源用量;等等。

上述要求是否让您想起了 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),而无需客户端明确请求每项资源。

服务器为推送资源启动新数据流 (promise)

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

事实上,如果您曾经通过数据 URI 内嵌过 CSS、JavaScript 或任何其他素材资源(请参阅资源内嵌),那么您就已经亲身体验过服务器推送了。通过手动将资源内嵌到文档中,我们实际上是在将资源推送到客户端,而不是等待客户端请求。使用 HTTP/2,我们可以实现相同的结果,但会带来额外的性能优势。推送资源可以是:

  • 由客户端缓存
  • 在不同网页中重复使用
  • 与其他资源多路复用
  • 由服务器设定优先级
  • 被客户端拒绝

PUSH_PROMISE 入门指南

所有服务器推送数据流都通过 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 压缩格式压缩请求和响应标头元数据,该格式使用两种简单但强大的技术:

  1. 它允许通过静态霍夫曼代码对传输的标头字段进行编码,从而减小了各个传输的大小。
  2. 它要求客户端和服务器维护并更新之前看到标头字段的索引列表(换句话说,它会建立共享压缩上下文),然后将其用作参考,对之前传输的值进行高效编码。

霍夫曼编码允许在传输各个值时进行压缩,而通过之前传输的值的索引列表,我们可以通过传输索引值对重复值进行编码。索引值可用于高效查找和重建完整的标头键和值。

HPACK:HTTP/2 的标头压缩

作为一项进一步的优化,HPACK 压缩上下文包含一个静态表和一个动态表:静态表在规范中定义,并提供所有连接可能使用的常用 HTTP 标头字段的列表(例如,有效标头名称);动态表最初为空,会根据特定连接中的交换值进行更新。因此,通过对之前未出现过的值使用静态霍夫曼编码,并替换每一侧静态或动态表中已存在的值,从而减小每个请求的大小。

HPACK 的安全性和性能

早期版本的 HTTP/2 和 SPDY 使用 zlib 和自定义字典来压缩所有 HTTP 标头。这样一来,传输的标头数据的大小就减少了 85% 到 88%,页面加载时间延迟也得到了显著的缩短:

在上传链接只有 375 Kbps 的低带宽 DSL 链接中,请求标头压缩特别能显著缩短某些网站(即发出大量资源请求的网站)的网页加载时间。我们发现,由于标头压缩,网页加载时间就减少了 45-1142 毫秒。(SPDY 白皮书,chromium.org)

然而,2012 年夏天,出现了针对 TLS 和 SPDY 压缩算法的“犯罪”安全攻击,可能导致会话盗用。因此,zlib 压缩算法被 HPACK 取代,HPACK 专门用于解决发现的安全问题,正确实现高效且简单,当然,可以对 HTTP 标头元数据进行有效压缩。

如需了解 HPACK 压缩算法的完整详情,请参阅 IETF HPACK - HTTP/2 的标头压缩

补充阅读材料