HTTP/2.x
- 队头压缩
- 多路复用
- 服务端推送
- 二进制传输
- 优先级
- 长连接
二进制成帧层#
HTTP/2 的所有性能增强的核心是新的二进制帧层,它规定了 HTTP 消息如何在客户端和服务器之间封装和传输。
“层”是指一种设计选择,在套接字接口和暴露给我们的应用程序的更高 HTTP API
之间引入一种新的优化编码机制:HTTP
语义,例如动词、方法和标头,不受影响,但它们的方式在运输过程中被编码是不同的。与换行符分隔的明文HTTP/1.x
协议不同,所有 HTTP/2
通信都被拆分为更小的消息和帧,每个都以二进制格式编码。
因此,客户端和服务器都必须使用新的二进制编码机制来相互理解:HTTP/1.x
客户端无法理解仅 HTTP/2
的服务器,反之亦然。值得庆幸的是,我们的应用程序仍然幸福地没有意识到所有这些变化,因为客户端和服务器代表我们执行所有必要的框架工作。
流、消息和帧#
新的二进制成帧机制的引入改变了客户端和服务器之间的数据交换方式。为了描述这个过程,让我们熟悉一下 HTTP/2 术语:
- 流:已经建立链接中的双向字节流,可以携带一条或者多条消息。
- 消息:映射到逻辑请求和响应消息的完整帧序列
- Frame:
HTTP/2
中最小的通信单元,每个都包含一个帧头,它至少标识了该帧所属的流。
这些术语的关系可以概括如下:
- 所有通信都是通过一个TCP链接执行,该连接可以携带任意数量的双向流。
- 每一个流都有唯一的 标识 和 可选的优先级
- 每一个消息都是一个逻辑的HTTP消息,例如请求和响应,他由一个或多个帧组成。
- 帧是承载特定信息(HTTP标头、payload)的 最小信息单元,来自不同流的帧信息可以相互交织,然后通过嵌入在每个帧头的标识符重新组合。
简而言之,HTTP/2
将 HTTP
协议通信分解成二进制编码帧的交换,然后映射到特点流的消息,所有链接都是在单个TCP
链接中多路复用, 这是启用 HTTP/2
协议 提供所有其他功能和性能优化的基础。
请求和响应复用
对于 HTTP/1.x
如果客户端想要 发送多个并行的请求来提高性能,则必须使用多个TCP连接 。这种行为是 HTTP/1.x
交付模型的 直接结果。 该模型 确保每一次连接只能交付一个响应,响应队列,更糟糕的是,这会导致队头拥塞及底层 TCP 连接
的低效使用。
HTTP/2
中新的二进制帧层消除了这些限制,并通过允许客户端和服务器将 HTTP 消息分解为独立的帧、交织它们,然后在另一端重新组合它们来实现完整的请求和响应多路复用。
快照在同一个连接中 捕获多个并行的流, 客户端正在发送 数据帧5(stream 5),服务器同时向客户端推送Stream 1 和 Stream3 的交错帧,因此有三个并行的流在传输中。
- 并行交错多个请求,而不会阻塞在任何一个请求
- 并行交错多个响应,而不会阻塞在任何一个响应
- 使用单个连接并行多个请求和响应
- 消除不必要的延迟,减少网页加载时间
- 不要在为绕过HTTP/1.x 而做更多的工作
总之,HTTP 2.0 的二进制分帧机制解决了 HTTP 1.x 中存在的队首阻塞问题,也消除了并行处理和发送请求及响应时对多个连接的依赖。结果,就是应用速度更快、开发更简单、部署成本更低。
支持多向请求与响应,可以省掉针对 HTTP 1.x 限制所费的那些脑筋和工作,比如拼接文件、图片精灵、域名分区(参见 13.2 节“针对 HTTP 1.x的优化建议”)。类似地,通过减少 TCP 连接的数量,HTTP 2.0 也会减少客户端和服务器的 CPU 及内存占用。
请求优先级
把 HTTP 消息分解为很多独立的帧之后,就可以通过优化这些帧的交错和传输顺序,进一步提升性能。为了做到这一点,每个流都可以带有一个 31 比特的优先值:
- 0 表示最高优先级;
- 2^31-1 表示最低优先级。
有了这个优先值,客户端和服务器就可以在处理不同的流时采取不同的策略,以最优的方式发送流、消息和帧。具体来讲,服务器可以根据流的优先级,控制资源分配(CPU、内存、带宽),而在响应数据准备好之后,优先将最高优先级的帧发送给客户端。
浏览器请求优先级与 HTTP 2.0
浏览器在渲染页面时,并非所有资源都具有相同的优先级:HTML 文档本身对构建 DOM 不可或缺,CSS 对构建 CSSOM 不可或缺,而 DOM 和 CSSOM 的构建都可能受到 JavaScript 资源的阻塞(参见 10.1 节的附注栏“DOM、CSSOM 和JavaScript”),其他资源(如图片)的优先级都可以降低。
为加快页面加载速度,所有现代浏览器都会基于资源的类型以及它在页面中的位置排定请求的优先次序,甚至通过之前的访问来学习优先级模式——比如,之前的渲染如果被某些资源阻塞了,那么同样的资源在下一次访问时可能就会被赋予更高的优先级。
在 HTTP 1.x 中,浏览器极少能利用上述优先级信息,因为协议本身并不支持多路复用,也没有办法向服务器通告请求的优先级。此时,浏览器只能依赖并行连接,且最多只能同时向一个域名发送 6 个请求。于是,在等连接可用期间,请求只能在客户端排队,从而增加了不必要的网络延迟。理论上,HTTP 管道可以解决这个问题,只是由于缺乏支持而无法付诸实践。
HTTP 2.0 一举解决了所有这些低效的问题:浏览器可以在发现资源时立即分派请求,指定每个流的优先级,让服务器决定最优的响应次序。这样请求就不必排队了,既节省了时间,也最大限度地利用了每个连接。
HTTP 2.0 没有规定处理优先级的具体算法,只是提供了一种赋予数据优先级的机制,而且要求客户端与服务器必须能够交换这些数据。这样一来,优先值作为提示信息,对应的次序排定策略可能因客户端或服务器的实现而不同:客户端应该明确指定优先值,服务器应该根据该值处理和交付数据。
在这个规定之下,尽管你可能无法控制客户端发送的优先值,但或许你可以控制服务器。因此,在选择 HTTP 2.0 服务器时,可以多留点心!为说明这一点,考虑下面几个问题。
- 如果服务器对所有优先值视而不见怎么办?
- 高优先值的流一定优先处理吗?
- 是否存在不同优先级的流应该交错的情况?
如果服务器不理睬所有优先值,那么可能会导致应用响应变慢:浏览器明明在等关键的 CSS 和 JavaScript,服务器却在发送图片,从而造成渲染阻塞。不过,规定严格的优先级次序也可能带来次优的结果,因为这可能又会引入队首阻塞问题,即某个高优先级的慢请求会不必要地阻塞其他资源的交付。
服务器可以而且应该交错发送不同优先级别的帧。只要可能,高优先级流都应该优先,包括分配处理资源和客户端与服务器间的带宽。不过,为了最高效地利用底层连接,不同优先级的混合也是必需的。
每一个源一个连接
有了新的分帧机制后,HTTP 2.0 不再依赖多个 TCP 连接去实现多流并行了。现在,每个数据流都拆分成很多帧,而这些帧可以交错,还可以分别优先级。于是,所有 HTTP 2.0
连接都是持久化的,而且客户端与服务器之间也只需要一个连接即可。
实验表明,客户端使用更少的连接肯定可以降低延迟时间。HTTP 2.0 发送的总分组数量比 HTTP 差不多要少 40%。而服务器处理大量并发连接的情况也变成了可伸缩性问题,因为 HTTP 2.0 减轻了这个负担。
——HT.TP/2.0 Draft 2
每个来源一个连接显著减少了相关的资源占用:连接路径上的套接字管理工作量少了,内存占用少了,连接吞吐量大了。此外,从上到下所有层面上也都获得了相应的好处:
- 所有数据流的优先次序始终如一
- 压缩上下文单一,压缩效果更好
- 由于TCP连接减少而使网络拥堵的状况得以改观
- 慢启动时间减少,拥塞或者掉包恢复速度快
丢包、高 RTT 连接和 HTTP 2.0 性能
等一等,我听你说了一大堆每个来源一个 TCP 连接的好处,难道它就一点坏处都没有吗?有,当然有。
• 虽然消除了 HTTP 队首阻塞现象,但 TCP 层次上仍然存在队首阻塞(“队首阻塞”);
• 如果 TCP 窗口缩放被禁用,那带宽延迟积效应可能会限制连接的吞吐量;
• 丢包时,TCP 拥塞窗口会缩小。
上述每一点都可能对 HTTP 2.0 连接的吞吐量和延迟性能造成不利影响。然而,
除了这些局限性之外,实验表明一个 TCP 连接仍然是 HTTP 2.0 基础上的最佳
部署策略:
目前为止的测试表明,压缩和优先级排定带来的性能提升,已经超过了队首阻塞(特别是丢包情况下)造成的负面效果。——HTTP/2.0 Draft 2
与所有性能优化过程一样,去掉一个性能瓶颈,又会带来新的瓶颈。对 HTTP 2.0 而言,TCP 很可能就是下一个性能瓶颈。这也是为什么服务器端 TCP 配置对HTTP 2.0 至关重要的一个原因。
目前,针对 TCP 性能优化的研究还在进行中:TCP 快速打开、比例降速、增大的初始拥塞窗口,等等不一而足。总之,一定要知道 HTTP 2.0 与之前的版本一样,并不强制使用 TCP。UDP 等其他传输协议也并非不可以。
服务器推送
HTTP 2.0 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源(图 12-4),而无需客户端明确地请求。
建立 HTTP 2.0 连接后,客户端与服务器交换 SETTINGS 帧,借此可以限定双向并发的流的最大数量。因此,客户端可以限定推送流的数量,或者通过把这个值设置为 0 而完全禁用服务器推送。
为什么需要这样一个机制呢?通常的 Web 应用都由几十个资源组成,客户端需要分析服务器提供的文档才能逐个找到它们。那为什么不让服务器提前就把这些资源推送给客户端,从而减少额外的时间延迟呢?服务器已经知道客户端下一步要请求什么资源了,这时候服务器推送即可派上用场。事实上,如果你在网页里嵌入过 CSS、JavaScript,或者通过数据 URI 嵌入过其他资源,那你就已经亲身体验过服务器推送了。
把资源直接插入到文档中,就是把资源直接推送给客户端,而无需客户端请求。在HTTP 2.0 中,唯一的不同就是可以把这个过程从应用中拿出来,放到 HTTP 协议本身来实现,而且还带来了如下好处:
- 客户端可以缓存推送过来的资源
- 客户端可以拒绝推送过来的资源
- 推送的资源可以由不同的页面共享
- 服务器可以按照优先级推送资源
所有推送的资源都遵守同源策略。换句话说,服务器不能随便将第三方资源推送给客户端,而必须是经过双方确认才行。
有了服务器推送后,HTTP 1.x 时代的大多数插入或嵌入资源的做法基本上也就过时了。唯一有必要直接在网页中插入资源的情况,就是该资源只供那一个网页使用,而且编码代价不大;此处仍然可以参考“嵌入资源”。除此之外,所有应用都应该使用 HTTP 2.0 服务器推送。
PUSH_PROMISE
所有服务器推送流都由 PUSH_PROMISE 发端,它是除了对原始请求的响应之外,服务器向客户端发出的有意推送所述资源的信号。PUSH_PROMISE 帧中只包含要约
(promise)资源的 HTTP 首部。
客户端接收到 PUSH_PROMISE 帧之后,可以视自身需求选择拒绝这个流(比如,已经缓存了相应资源),而这是对 HTTP 1.x 的一个重要改进。嵌入资源作为针对HTTP 1.x 的一种流行“优化技巧”,实际上无异于“强制推送”:客户端无法取消这种“推送”,而且也不能个别地缓存嵌入的资源。
最后再说一说服务器推送的几点限制。首先,服务器必须遵循请求 - 响应的循环,只能借着对请求的响应推送资源。也就是说,服务器不能随意发起推送流。其次,PUSH_PROMISE 帧必须在返回响应之前发送,以免客户端出现竞态条件。否则,就可能出现比如这种情况:客户端请求的恰好是服务器打算推送的资源。
实现HTTP2 服务端推送
服务器推送为优化应用的资源交付提供了很多可能。然而,服务器到底如何确定哪些资源可以或应该推送呢?与确定优先级类似,HTTP 2.0 标准也没有就此规定某种算法,所以实现者就拥有了解释权。自然地,也就有可能出现多种策略,每种策略可能会考虑一种应用或服务器使用场景。
- 应用可以在自身的代码中明确发起服务器推送。这种情况要求与
HTTP 2.0
紧密耦合,但开发人员有控制权。 - 应用可以通过额外的 HTTP 首部向服务器发送信号,列出它希望推送的资源。这样可以将应用与 HTTP 服务器 API 分离。比如 Apache 的
mod_spdy
能够识别X-Associated-Content
首部,这个首部中列出了希望服务器推送的资源。 - 服务器可以不依赖应用而自动学习相关资源。服务器可以解析文档,推断出要推送的资源,或者可以分析流量,然后作出适当的决定。比如服务器可以根据Referer 首部收集依赖数据,然后自动向客户端推送关键资源。
当然,以上只是各种可能策略中的几个,但由此也可以知道可能性是很多的:可能是手工调用低级 API,也可能是一种全自动的实现。类似地,服务器应不应该重复推送相同的资源,还是应该实现一个更智能的策略?服务器可以根据自身的模型、客户端 cookie
或其他机制,智能推断出客户端缓存中有什么资源,然后再作出推送决定。简言之,服务器推送领域将爆出各种创新。
最后还有一点,就是推送的资源将直接进入客户端缓存,就像客户端请求了似的。不存在客户端 API 或 JavaScript 回调方法等通知机制,可以用于确定资源何时到达。整个过程对运行在浏览器中的 Web 应用来说好像根本不存在。
首部压缩
HTTP 的每一次通信都会携带一组首部,用于描述传输的资源及其属性。在 HTTP 1.x 中,这些元数据都是以纯文本形式发送的,通常会给每个请求增加 500~800 字节的负荷。如果算上 HTTP cookie,增加的负荷通常会达到上千字节(“HTTP/1.x度量和控制协议开销”)。为减少这些开销并提升性能,HTTP 2.0 会压缩首部元数据:
- HTTP2.0 在客户端和服务端通过
首部表
来更新和存储之前发送的键-值 对,对于相同的数据不在通过每次请求和响应发送 - 首部表 在HTTP2.0 的连接存续期内一直存在,由客户端和服务端共同渐进的更新
- 每一个首部的新值都将添加到首部表的末尾会替换之前的值
于是 HTTP2.0 的客户端与服务端 都知道发送了哪些首部,这些首部的值是什么,从而可以根据之前发送的数据编码差异的数据。
请求与响应首部的定义在 HTTP 2.0 中基本没有改变,只是所有首部键必须全部小写,而且请求行要独立为 :method、:scheme、:host 和 :path 这些键-值对
在前面的例子中,第二个请求只需要发送变化了的路径首部(:path),其他首部没有变化,不用再发送了。这样就可以避免传输冗余的首部,从而显著减少每个请求的开销。通信期间几乎不会改变的通用键-值对(用户代理、可接受的媒体类型,等等)只需发送一次。事实上,如果请求中不包含首部(例如对同一资源的轮询请求),那么首部开销就是零字节。此时所有首部都自动使用之前请求发送的首部!
HTTP 2.0帧数据流分析
有 3 个活动的流:stream 1、stream 3 和 stream 5。
- 3 个流的 ID 都是奇数,说明都是客户端发起的。
- 这里没有服务器发起的流。
- 服务器发送的 stream 1 包含多个 DATA 帧,这是对客户端之前请求的响应数据。这也说明在此之前已经发送过 HEADERS 帧了。
- 服务器在交错发送 stream 1 的 DATA 帧和 stream 3 的 HEADERS 帧,这就是响应的多路复用!
- 客户端正在发送 stream 5 的 DATA 帧,表明 HEADERS 帧之前已经发送过了。
简言之,图 12-9 中连接正在并行传送 3 个数据流,每个流都处于各自处理周期的不同阶段。服务器决定帧的顺序,而我们不用关心每个流的类型或内容。stream 1 携带的数据量可能比较大,也许是视频,但它不会阻塞共享连接中的其他流!