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 套接字来实现网络服务器(或自定义客户端),否则不会发现任何差异:所有新的低级别分帧均由客户端和服务器代表您执行。唯一可观察到的区别将是性能和请求优先级、流控制和服务器推送等新功能的可用性。

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/1.1 相比,HTTP/2 的主要变化侧重于提升性能。一些关键功能(如多路复用、标头压缩、优先级和协议协商)源自于较早开放但非标准且名为 SPDY 的协议。Chrome 从 Chrome 6 开始就支持 SPDY,但由于大多数优势都集中在 HTTP/2 中,是时候向 SPDY 说再见了。我们计划在 2016 年初停止支持 SPDY,并停止支持名为 NPN 的 TLS 扩展程序,同时在 Chrome 中改用 ALPN。强烈建议服务器开发者迁移到 HTTP/2 和 ALPN。

我们很高兴为推动 HTTP/2 的开放式标准流程做出贡献,并希望业界在标准化和实现方面参与广泛,希望能够广泛采用。(Chromium 博客)

SPDY 和 HTTP/2 的共同演化使服务器、浏览器和网站开发者可以在新协议开发过程中获得真实体验。因此,HTTP/2 标准自诞生之日起就成为最好并经过广泛测试的标准之一。截至 IESG 批准 HTTP/2 时,已有许多经过全面测试且可直接用于生产环境的客户端和服务器实现。事实上,在最终协议获批几周后,随着几款热门浏览器(和许多网站)部署了完整的 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/2 最重要的一项增强功能:将 HTTP 消息分解为独立的帧,交错这些帧,然后在另一端重新组装这些帧。事实上,它在所有 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 标头压缩

深入阅读