HTTP2.0简介

156 阅读13分钟

概述

Http2.0升级的一个主要目标就是“快”,没有改动Http1.0的语义,包括方法、状态代码、URI、标头字段等,都没有任何改变。Http2.0支持完整的多路复用能力来解决队头阻塞、并行请求问题,降低延迟;通过有效压缩HTTP标头字段,减小请求负载;支持服务器推送,打破严格请求-响应语义,减少额外的延迟时间;同时增加了对请求优先级的控制,让优先级更高的请求可以更快地获取。

HTTP/2: the Future of the Internet | Akamai这是 Akamai 公司建立的一个官方的演示,用以说明 HTTP/2 相比于之前的 HTTP/1.1 在性能上的大幅度提升。 同时请求 379 张图片,从Load time 的对比可以看出 HTTP/2 在速度上的优势。

SPDY协议

Http2.0脱胎于SPDY协议,SPDY 是 Google 开发的一个实验性协议,于 2009 年年中发布,其主要目标是通过解决 HTTP/1.1 中广为人知的一些性能限制来减少网页的加载延迟。具体来说,这个项目设定的目标如下:

  • 页面加载时间 (PLT) 减少 50%。
  • 无需网站作者修改任何内容。
  • 将部署复杂性降至最低,无需变更网络基础设施。
  • 与开源社区合作开发此新协议。
  • 收集真实性能数据,验证实验性协议是否有效。

为了达到减少 50% 页面加载时间的目标,SPDY 引入一个新的二进制分帧层,以实现请求和响应多路复用、优先级和标头压缩,目的是更有效地利用底层 TCP 连接。到了 2012 年,这个新的实验性协议得到 Chrome、Firefox 和 Opera 的支持,而且越来越多的大型网站(如 Google、Twitter、Facebook)和小型网站开始在其基础设施内部署 SPDY。 事实上,在被行业越来越多的采用之后,SPDY 已经具备了成为一个标准的条件。观察到这一趋势后,HTTP 工作组 (HTTP-WG) 将这一工作提上议事日程,吸取 SPDY 的经验教训,并在此基础上制定了官方“HTTP/2”标准。在接下来几年中,SPDY 和 HTTP/2 继续共同演化,其中 SPDY 作为实验性分支,用于为 HTTP/2 标准测试新功能和建议。

二进制分帧层

HTTP/2 所有性能增强的核心在于新的二进制分帧层,它定义了如何封装 HTTP 消息并在客户端与服务器之间传输。二进制分帧层指的是一种新的信息编码和传输机制,有别于Http1.x的依靠换行符分割明文的文本传输方式,而是将所有传输的信息分割为更小的消息和帧,并采用二进制格式对他们进行编码,并以此为基础进行信息传输机制的设计和优化。

流、消息和帧

二进制分帧层改变了客户端和服务端之间数据传输方式,为了说明新的传输机制,先了解Http2.0的三个概念

  • 数据流:已建立TCP连接中一个双向的字节流,可以承载一个或者多个消息
  • 消息:与逻辑请求或响应消息对应的完整的一系列帧
  • 帧:最小的通信单位,每个帧包含帧头,帧头包含标识位标识所属的流

这三者的关系和通信的机制总结如下:

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

二进制分帧层将一个逻辑完整的http消息拆分配在不同流上的数据帧,不同的数据帧分布在不同的流上,从而达到下图多路传送的效果。这是 Http2.0 协议所有其他功能和性能优化的基础。当然数据拆分之后,接收端需要按照规则进行重组。

多路复用

在Http1.x中,请求的响应是一个串行排队的交付模型,该模型可以保证每个连接每次只交付一个响应(响应排队)。想要实现并行请求,需要同时打开多个TCP连接,而TCP建立连接是个成本很高的事情,会造成网络延迟和服务器负担。同时这种排队模型也造成队头阻塞问题,简而言之就是前一个请求(慢)会阻塞后一个请求。为了解决Http1.1的问题,有各种优化方式,比如image spirts、域名分片(同一个域名6个TCP连接限制)、内联文件等。

HTTP/2 中新的二进制分帧层突破了这些限制,实现了完整的请求和响应多路复用: 客户端和服务器可以将 HTTP 消息分解为互不依赖的帧,然后交错发送,最后再在另一端把它们重新组装起来。下图中可以看到有三个数据流正在同时进行传送,客户端正在向服务器传输一个 DATA 帧(数据流 5),与此同时,服务器正向客户端交错发送数据流 1 和数据流 3 的一系列帧。这样就将Http1.0的串行模式改为了并行模式,有效降低了TCP连接开销和解决了队头阻塞问题。

头部压缩

每个 HTTP 传输都承载一组标头,这些标头描述了传输的资源及其属性。 在 HTTP/1.x 中,这些元数据始终以纯文本形式,通常会给每个传输增加 500–800 字节的开销。如果使用 HTTP Cookie,增加的开销有时会达到上千字节。 (请参阅测量和控制协议开销。) 为了减少此开销和提升性能,HTTP/2 使用 HPACK 压缩格式压缩请求和响应标头元数据,HPACK格式采用两种简单但是强大的技术:

  1. 这种格式支持通过静态霍夫曼代码对传输的标头字段进行编码,从而减小了各自传输的大小。
  2. 这种格式要求客户端和服务器同时维护和更新一个包含之前见过的标头字段的索引列表(换句话说,它可以建立一个共享的压缩上下文),此列表随后会用作参考,对之前传输的值进行有效编码。

利用霍夫曼编码,可以在传输时对各个独立的值进行压缩,而利用之前传输值的索引列表,我们可以通过传输索引值的方式对重复值进行编码,索引值可用于有效查询和重构完整的标头键值对。

作为一种进一步优化方式,HPACK 压缩上下文包含一个静态表和一个动态表: 静态表在规范中定义,并提供了一个包含所有连接都可能使用的常用 HTTP 标头字段(例如,有效标头名称)的列表;动态表最初为空,将根据在特定连接内交换的值进行更新。 因此,为之前未见过的值采用静态 Huffman 编码,并替换每一侧静态表或动态表中已存在值的索引,可以减小每个请求的大小。

注: 在 HTTP/2 中,请求和响应标头字段的定义保持不变,仅有一些微小的差异: 所有标头字段名称均为小写,请求行现在拆分成各个 :method:scheme:authority:path 伪标头字段。

服务端推送

HTTP/2 新增的另一个强大的新功能是,服务器可以对一个客户端请求发送多个响应。 换句话说,除了对最初请求的响应外,服务器还可以向客户端推送额外资源,而无需客户端明确地请求。这打破了严格的请求-响应语义,在浏览器内外开启了全新的互动可能性。

为什么在浏览器中需要一种此类机制呢?一个典型的网络应用(比如HTML文件)包含多种资源,客户端需要检查服务器响应的文档(HTML)才能逐个找到并且请求它们。这是一个串行的过程, 这种场景下,服务器其实已经知道客户端下一步要请求什么资源, 那为什么不让服务器提前推送这些资源,从而减少额外的延迟时间呢?

事实上,如果您在网页中内联过 CSS、JavaScript,或者通过数据 URI 内联过其他资源,那么您就已经亲身体验过服务器推送的效果了。 对于将资源内联到文档中的过程,我们实际上是在手动将资源强制推送给客户端,而不是等待客户端请求。 使用 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/2 标准允许每个数据流都有一个关联的权重和依赖关系:

  • 每个数据流都被分配一个介于 1 至 256 之间的整数
  • 每个数据流与其他数据流之间可以存在显式依赖关系

数据流依赖关系和权重的组合让客户端可以构建和传递“优先级树”,表明它倾向于如何接收响应。 同时,服务器可以使用此信息通过控制 CPU、内存和其他资源的分配设定数据流处理的优先级,在资源数据可用之后,分配带宽确保将高优先级响应以最优方式传输至客户端。

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 简介

HTTP/2 is here, let's optimize! - Velocity SC 2015

队头阻塞

RFC 7541 - HPACK: Header Compression for HTTP/2

半小时搞懂 HTTP、HTTPS和HTTP2

RFC7541