HTTP 简史
HTTP ( HyperText Transfer Protocol ,超文本传输协议)是互联网上最普遍采用的一种应用协议,也是客户端与服务器之间的共用语言,是现代 Web 的基础。从最初的一个关键字和文档路径开始, HTTP 最终不仅成为了浏览器的协议,而且也几乎成为了所有互联网软件和硬件应用的协议。
HTTP 0.9 :只有一行的协议
Tim Berners-Lee 最初的 HTTP 建议是以简洁为出发点设计的,目的是推动他的另一个刚刚萌芽的思想——万维网的应用。事实证明,这个策略非常有效。这个经验也非常值得有抱负的协议设计者汲取。 Tim Berners-Lee 概述了这个新协议的动机,并罗列了几条宏观的设计目标:支持文件传输、能够请求对超文本文档的索引搜索、格式化协商机制,以及能够把客户端引导至不同的服务器。为了实际验证这个理论,他构建了一个简单的原型,实现了建议的部分功能:
- 客户端请求是一个 ASCII 字符串;
- 客户端请求由一个回车符( CRLF )结尾;
- 服务器响应是一个 ASCII 字符流;
- 服务器响应的是一种超文本标记语言( HTML );
- 连接在文档传输完毕后断开。
HTTP 1.0 :迅速发展及参考性 RFC
HTTP 1.0的变化:
- 请求可以由于多行首部字段构成;
- 响应对象前面添加了一个响应状态行;
- 响应对象也有自己的由换行符分隔的首部字段;
- 响应对象不局限于超文本;
- 服务器与客户端之间的连接在每次请求之后都会关闭。 请求和响应首部都使用ASCII编码,但响应对象本身可以是任何类型:HTML文件、纯文本文件、图片,或其他内容类型。除了媒体类型协商, RFC 还解释了很多已经被实现的其他功能:内容编码、字符集支持、多部分类型、认证、缓存、代理行为、日期格式,等等。
HTTP 1.1 :互联网标准
HTTP 1.1 标准厘清了之前版本中很多有歧义的地方,而且还加入了很多重要的性能优化:持久连接、分块编码传输、字节范围请求、增强的缓存机制、传输编码及请求管道。此外, HTTP 1.1 协议添加了内容、编码、字符集,甚至语言的协商机制,还添加了传输编码、缓存指令、客户端 cookie 等十几个可以每次请求都协商的字段。
HTTP 2.0 :改进传输性能
曾经以简单的理念开始,只有一行的用于取得超文本的协议,迅速发展为通用的超媒体传输机制。十几年后的今天,HTTP已经成为可以在任何领域使用的核心协议。无所不在的支持这个协议的服务器,以及随处可见的访问这些服务器的客户端,都意味着在 HTTP 之上,人们正在设计和部署更多的应用。
Web 性能要点
宏观上的 Web 性能优化:
- 延迟和带宽对 Web 性能的影响;
- 传输协议( TCP )对 HTTP 的限制;
- HTTP 协议自身的功能和缺陷;
- Web 应用的发展趋势及性能需求;
- 浏览器局限性和优化思路。 HTTP 0.9 会话由一个文档请求构成,这对于取得超文本内容完全够用了:一个文档、一个 TCP 连接,然后关闭连接。因此,提升性能就是围绕短期 TCP 连接优化一次 HTTP 请求。富媒体网页的出现改变了这个局面,因为一个简单的文档,变成了文档加依赖资源。因此, HTTP 1.0 引入了 HTTP 元数据的表示法(首部), HTTP 1.1 又加入了各种旨在提升性能的机制,如缓存、持久连接,等等。事实上,多 TCP 连接目前仍然存在,性能的关键指标已经从文档加载时间,变成了页面加载时间,常简写为 PLT ( Page Load Time )。 PLT 的简单定义就是:“浏览器中的加载旋转图标停止旋转的时间。”更技术的定义则是浏览器中的 onload 事件,这个事件由浏览器在文档及其所有依赖资源( JavaScript 、图片,等等)下载完毕时触发。 小资源受限于延迟,大资源(如视频)受限于带宽。 大多数 HTTP 数据流都是小型突发性数据流,而TCP则是为持久连接和大块数据传输而进行过优化的。网络往返时间在大多数情况下都是 TCP 吞吐量和性能的限制因素。
HTTP 1.x
改进 HTTP 的性能是 HTTP 1.1 工作组的一个重要目标,后来这个版本也引入了大量增强性能的重要特性,其中一些大家比较熟知的有:
- 持久化连接以支持连接重用;
- 分块传输编码以支持流式响应;
- 请求管道以支持并行请求处理;
- 字节服务以支持基于范围的资源请求;
- 改进的更好的缓存机制。 优化规则:
- 减少 DNS 查询每次域名解析都需要一次网络往返,增加请求的延迟,在查询期间会阻塞请求。 减少 HTTP 请求任何请求都不如没有请求更快,因此要去掉页面上没有必要的资源。
- 使用 CDN 从地理上把数据放到接近客户端的地方,可以显著减少每次 TCP 连接的网络延迟,增加吞吐量。
- 添加 Expires 首部并配置 ETag 标签相关资源应该缓存,以避免重复请求每个页面中相同的资源。 Expires 首部可用于指定缓存时间,在这个时间内可以直接从缓存取得资源,完全避免 HTTP 请求。 ETag 及 Last-Modified 首部提供了一个与缓存相关的机制,相当于最后一次更新的指纹或时间戳。
- Gzip 资源,所有文本资源都应该使用 Gzip 压缩,然后再在客户端与服务器间传输。一般来说, Gzip 可以减少 60%~80% 的文件大小,也是一个相对简单(只要在服务器上配置一个选项),但优化效果较好的举措。
- 避免 HTTP 重定向, HTTP重定向极其耗时,特别是把客户端定向到一个完全不同的域名的情况下,还会导致额外的 DNS 查询、 TCP 连接延迟,等等。 两个根本的方面:
- 消除和减少不必要的网络延迟;
- 把传输的字节数降到最少。
持久链接的优点
HTTP 1.1 的一个主要改进就是引入了持久 HTTP 连接。由一个新 TCP 连接发送的 HTTP 请求所花的总时间,最少等于两次网络往返的时间:一次用于握手,一次用于请求和响应。这是所有非持久 HTTP 会话都要付出的固定时间成本。添加对 HTTP 持久连接的支持,就可以避免第二次 TCP 连接时的三次握手、消除另一次 TCP 慢启动的往返,节约整整一次网络延迟。
TCP 连接要发送 N 次 HTTP 请求,这时:没有持久连接,每次请求都会导致两次往返延迟;有持久连接,只有第一次请求会导致两次往返延迟,后续请求只会导致一次往返延迟。在启用持久连接的情况下, N 次请求节省的总延迟时间就是( N-1 ) × RTT 。
HTTP 管道
持久 HTTP 可以让我们重用已有的连接来完成多次应用请求,但多次请求必须严格满足先进先出( FIFO )的队列顺序:发送请求,等待响应完成,再发送客户端队列中的下一个请求。 HTTP 管道是一个很小但对上述工作流却非常重要的一次优化。管道可以让我们把 FIFO 队列从客户端(请求队列)迁移到服务器(响应队列)。 通过尽早分派请求,不被每次响应阻塞,可以再次消除额外的网络往返。这样,就从非持久连接状态下的每个请求两次往返,变成了整个请求队列只需要两次网络往返。
回顾一下上面提到的优化:
- 复用 TCP 链接,减少握手往返;
- 使用 HTTP 管道,减少两次请求间的一次往返。
当延迟更高,请求更多时,上面的优化会带来更多的性能提升,节省的时间更多。因为延迟越高,往返时间更长,减少往返时间带来的收益就更高了。 即使客户端同时发送了两个请求,而且 CSS 资源先准备就绪,服务器也会先发送 HTML 响应,然后再交付 CSS 。这种情况通常被称作队首阻塞,并经常导致次优化交付:不能充分利用网络连接,造成服务器缓冲开销,最终导致无法预测的客户端延迟。假如第一个请求无限期挂起,或者要花很长时间才能处理完,在 HTTP1.1 中,所有后续的请求都将被阻塞,等待它完成。 由于不可能实现多路复用, HTTP 管道会导致 HTTP 服务器、代理和客户端出现很多微妙的,不见文档记载的问题:
- 一个慢响应就会阻塞所有后续请求;
- 并行处理请求时,服务器必须缓冲管道中的响应,从而占用服务器资源,如果有个响应非常大,则很容易形成服务器的受攻击面;
- 响应失败可能终止 TCP 连接,从而强迫客户端重新发送对所有后续资源的请求,导致重复处理;
- 由于可能存在中间代理,因此检测管道兼容性,确保可靠性很重要;
- 如果中间代理不支持管道,那它可能会中断连接,也可能会把所有请求串联起来。
因为浏览器无法对客户端和服务器有完整的权限,所以支持管道的浏览器通常都将其作为一个高级配置选项,但大多数浏览器都会禁用它。换句话说,如果浏览器是 Web 应用的主要交付工具,那还是很难指望通过 HTTP 管道来提升性能。 如果说对客户端和服务器拥有完全控制的权限,那么还是可以使用它的, iTunes 就曾经通过启用持久 HTTP 连接,以及在服务器和 iTunes 客户端内启用 HTTP 管道来达到 3 倍以上的性能提升。 要在你自己的应用中启用管道,要注意如下事项:
- 确保 HTTP 客户端支持管道;
- 确保 HTTP 服务器支持管道;
- 应用必须处理中断的连接并恢复;应用必须处理中断请求的幂等问题;
- 应用必须保护自身不受出问题的代理的影响。 实践中部署 HTTP 管道的最佳途径,就是在客户端和服务器间使用安全通道( HTTPS )。这样,就能可靠地避免那些不理解或不支持管道的中间代理的干扰。
使用多个 TCP 连接
由于 HTTP 1.x 不支持多路复用,浏览器只能在客户端排队所有 HTTP 请求,然后通过一个持久连接,一个接一个地发送这些请求。然而,这种方式在实践中太慢。 使用多个 TCP 连接有优点也有缺点,假设每个主机最多可以打开 6 个连接:
- 客户端可以并行分派最多 6 个请求;
- 服务器可以并行处理最多 6 个请求;
- 第一次往返可以发送的累计分组数量( TCP cwnd )增长为原来的 6 倍。 在没有管道的情况下,最大的请求数与打开的连接数相同。相应地, TCP 拥塞窗口也要乘以打开的连接数量,从而允许客户端绕开由 TCP 慢启动规定的分组限制。这好像是一个方便的解决方案。我们再看看这样做的代价:
- 更多的套接字会占用客户端、服务器以及代理的资源,包括内存缓冲区和 CPU 时钟周期;
- 并行 TCP 流之间竞争共享的带宽;
- 由于处理多个套接字,实现复杂性更高;
- 即使并行 TCP 流,应用的并行能力也受限制。 实践中, CPU 和内存占用并非微不足道,由此会导致客户端和服务器端的资源占用量上升,运维成本提高。类似地,由于客户端实现的复杂性提高,开发成本也会提高。最后,说到应用的并行性,这种方式提供的好处还是非常有限的。这不是一个长期的方案。这不是一个完美的解决方案,今天之所以使用它,主要有三个原因:
- 作为绕过应用协议( HTTP )限制的一个权宜之计;
- 作为绕过 TCP 中低起始拥塞窗口的一个权宜之计;
- 作为让客户端绕过不能使用 TCP 窗口缩放的一个权宜之计。 后面两个针对 TCP 问题可以通过升级服务器的 OS 内核来解决,但是我们没有别的办法来绕开 HTTP 1.x 的多路复用问题。所以只能采取多 TCP 流的方法。 用足客户端连接的限制似乎是一个可以接受的安全问题,但对于需要实时交付数据的应用而言,这样做越来越容易造成部署上的问题。比如 WebSocket 、 Server Sent Event 和挂起 XHR ,这些会话都会占用整整一个 TCP 流,而不管有无数据传输——记住,没有多路复用一说。实际上,如果你不注意,那很可能自己对自己的应用施加 DoS 攻击。
域名分区
通过手工将所有资源分散到多个子域名,就可以突破浏览器的连接限制,实现更高的并行能力,域名分区使用得越多,并行能力就越强。但是每个新主机名都要求有一次额外的 DNS 查询,每多一个套接字都会多消耗两端的一些资源,而更糟糕的是,站点作者必须手工分离这些资源,并分别把它们托管到多个主机上。如果说多出来的 TCP 流得不到充分利用,那么有可能会降低性能。域名分区是一种合理但又不完美的优化手段。请大家一定先从最小分区数目(不分区)开始,然后逐个增加分区并度量分区后对应用的影响。
度量和控制协议开销
每个 HTTP 请求都会携带额外 500~800 字节的 HTTP 元数据:用户代理字符串、很少改变的接收和传输首部、缓存指令,等等。这里面还没有包含 HTTP cookie 。现代应用经常通过 cookie 进行会话管理、记录个性选项或者完成分析。综合到一起,所有这些未经压缩的 HTTP 元数据经常会给每个 HTTP 请求增加几千字节的协议开销。减少要传输的首部数据(高度重复且未压缩),可以节省相当于一次往返的延迟时间,显著提升很多 Web 应用的性能。
连接与拼合
最快的请求是不用请求。不管使用什么协议,也不管是什么类型的应用,减少请求次数总是最好的性能优化手段。
- 减少协议开销。通过把文件组合成一个资源,可以消除与文件相关的协议开销。如前所述,每个文件很容易招致 KB 级未压缩数据的开销。
- 应用层管道,说到传输的字节,这两种技术的效果都好像是启用了 HTTP 管道:来自多个响应的数据前后相继地连接在一起,消除了额外的网络延迟。实际上,就是把管道提高了一层,置入了应用中。 连接和拼合技术都属于以内容为中心的应用层优化,它们通过减少网络往返开销,可以获得明显的性能提升。可是,实现这些技术也要求额外的处理、部署和编码。 连接和拼合是在 HTTP 1.x 协议限制(管道没有得到普遍支持,多请求开销大)的现实之下可行的应用层优化。使用得当的话,这两种技术可以带来明显的性能提升,代价则是增加应用的复杂度,以及导致缓存、更新、执行速度,甚至渲染页面的问题。应用这两种优化时,要注意度量结果,根据实际情况考虑如下问题。
HTTP 2.0
HTTP 2.0 的目的就是通过支持请求与响应的多路复用来减少延迟,通过压缩 HTTP 首部字段将协议开销降至最低,同时增加对请求优先级和服务器端推送的支持。为达成这些目标, HTTP 2.0 还会给我们带来大量其他协议层面的辅助实现,比如新的流量控制、错误处理和更新机制。 HTTP 2.0 不会改动 HTTP 的语义。 HTTP 方法、状态码、 URI 及首部字段,等等这些核心概念一如往常。但是, HTTP 2.0 修改了格式化数据(分帧)的方式,以及客户端与服务器间传输这些数据的方式。
历史及其与 SPDY 的渊源
SPDY 是谷歌开发的一个实验性协议,于 2009 年年中发布,其主要目标是通过解决 HTTP 1.1 中广为人知的一些性能限制,来减少网页的加载延迟。 HTTP-WG ( HTTP Working Group )在 2012 年初把 HTTP 2.0 提到了议事日程,吸取 SPDY 的经验教训,并在此基础上制定官方标准。
走向 HTTP 2.0
SPDY 是 HTTP 2.0 的催化剂,它是 HTTP 2.0 的基础。之所以递增一个大版本到 2.0 ,主要是因为它改变了客户端与服务器之间交换的方式。增加了新的二进制分帧数据层,这一层不兼容之前的 HTTP 1.x 服务器及客户端——是谓 2.0 。 SPDY 其实可以算作是 HTTP 2.0 的实验室,为 HTTP 2.0 制定标准提供了事前的测试和评估手段。
设计和技术目标
HTTP 是应用最广泛、采用最多的一个互联网应用协议。但是简单是以牺牲应用性能为代价的, HTTP 2.0 致力于提高应用性能。 HTTP/2.0 通过支持首部字段压缩和在同一连接上发送多个并发消息,让应用更有效地利用网络资源,减少感知的延迟时间。而且,它还支持服务器到客户端的主动推送机制。
二进制分帧层
HTTP 2.0 性能增强的核心,全在于新增的二进制分帧层,它定义了如何封装 HTTP 消息并在客户端与服务器之间传输。 这里所谓的“层”,指的是位于套接字接口与应用可见的高层 HTTP API 之间的一个新机制: HTTP 的语义,包括各种动词、方法、首部,都不受影响,不同的是传输期间对它们的编码方式变了。 HTTP 1.x 以换行符作为纯文本的分隔符,而 HTTP 2.0 将所有传输的信息分割为更小的消息和帧,并对它们采用二进制格式的编码。
流,消息和帧
- 流,已建立的连接上的双向字节流。
- 消息,与逻辑消息对应的完整的一系列数据帧。
- 帧, HTTP 2.0 通信的最小单位,每个帧包含帧首部,至少也会标识出当前帧所属的流。
要理解 HTTP 2.0 ,就必须理解流、消息和帧这几个基本概念。所有通信都在一个 TCP 连接上完成。流是连接中的一个虚拟信道,可以承载双向的消息;每个流都有一个唯一的整数标识符( 1、2…N )。消息是指逻辑上的 HTTP 消息,比如请求、响应等,由一或多个帧组成。帧是最小的通信单位,承载着特定类型的数据,如 HTTP 首部、负荷,等等。简言之, HTTP 2.0 把 HTTP 协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。相应地,很多流可以并行地在同一个 TCP 连接上交换消息。
多向请求与响应
在。HTTP 1.x 中,如果客户端想发送多个并行的请求以及改进性能,那么必须使用多个 TCP 连接。这是 HTTP1.x 交付模型的直接结果,该模型会保证每个连接每次只交付一个响应(多个响应必须排队)。更糟糕的是,这种模型也会导致队首阻塞,从而造成底层 TCP 连接的效率低下。 HTTP 2.0 中新的二进制分帧层突破了这些限制,实现了多向请求和响应:客户端和服务器可以把 HTTP 消息分解为互不依赖的帧,然后乱序发送,最后再在另一端把它们重新组合起来。
图中包含了同一个连接上多个传输中的数据流:客户端正在向服务器传输一个 DATA 帧( stream 5 ),与此同时,服务器正向客户端乱序发送 stream 1 和 stream 3 的一系列帧。此时,一个连接上有 3 个请求/响应并行交换。把 HTTP 消息分解为独立的帧,交错发送,然后在另一端重新组装是 HTTP 2.0 最重要的一项增强。 分帧机制会在整个 Web 技术栈中引起 一系列连锁反应,从而带来巨大的性能提升:
- 可以并行交错地发送请求,请求之间互不影响;
- 可以并行交错地发送响应,响应之间互不干扰;
- 只使用一个连接即可并行发送多个请求和响应;
- 消除不必要的延迟,从而减少页面加载的时间;
- 不必再为绕过 HTTP 1.x 限制而多做很多工作。
- …… 总之, HTTP 2.0 的二进制分帧机制解决了 HTTP1.x 中存在的队首阻塞问题,也消除了并行处理和发送请求及响应时对多个连接的依赖。结果,就是应用速度更快、开发更简单、部署成本更低。
请求优先级
把 HTTP 消息分解为很多独立的帧之后,就可以通过优化这些帧的交错和传输顺序,进一步提升性能。为了做到这一点,每个流都可以带有一个 31 比特的优先值: 0 表示最高优先级。 2^31-1 表示最低优先级。 服务器可以而且应该交错发送不同优先级别的帧。只要可能,高优先级流都应该优先,包括分配处理资源和客户端与服务器间的带宽。不过,为了最高效地利用底层连接,不同优先级的混合也是必需的。
每个来源一个连接
有了新的分帧机制后, HTTP 2.0 不再依赖多个 TCP 连接去实现多流并行了。现在,每个数据流都拆分成很多帧,而这些帧可以交错,还可以分别优先级。于是,所有 HTTP 2.0 连接都是持久化的,而且客户端与服务器之间也只需要一个连接即可。
流量控制
在同一个 TCP 连接上传输多个数据流,就意味着要共享带宽。标定数据流的优先级有助于按序交付,但只有优先级还不足以确定多个数据流或多个连接间的资源分配。为解决这个问题, HTTP2.0 为数据流和连接的流量控制提供了一个简单的机制:流量控制基于每一跳进行,而非端到端的控制;流量控制基于窗口更新帧进行,即接收方广播自己准备接收某个数据流的多少字节,以及对整个连接要接收多少字节;流量控制窗口大小通过 WINDOW_UPDATE 帧更新,这个字段指定了流 ID 和窗口大小递增值;流量控制有方向性,即接收方可能根据自己的情况为每个流乃至整个连接设置任意窗口大小;流量控制可以由接收方禁用,包括针对个别的流和针对整个连接。
服务器推送
HTTP 2.0 新增的一个强大的新功能,就是服务器可以对一个客户端请求发送多个响应。换句话说,除了对最初请求的响应外,服务器还可以额外向客户端推送资源,而无需客户端明确地请求。
- 客户端可以缓存推送过来的资源;
- 客户端可以拒绝推送过来的资源;
- 推送资源可以由不同的页面共享;
- 服务器可以按照优先级推送资源。 首先,服务器必须遵循请求-响应的循环,只能借着对请求的响应推送资源。也就是说,服务器不能随意发起推送流。其次, PUSH_PROMISE 帧必须在返回响应之前发送,以免客户端出现竞态条件。否则,就可能出现比如这种情况:客户端请求的恰好是服务器打算推送的资源。
首部压缩
HTTP的每一次通信都会携带一组首部,用于描述传输的资源及其属性。在 HTTP 1.x 中,这些元数据都是以纯文本形式发送的,通常会给每个请求增加 500~800 字节的负荷。如果算上 HTTP cookie ,增加的负荷通常会达到上千字节。为减少这些开销并提升性能, HTTP 2.0 会压缩首部元数据:
- HTTP 2.0 在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送;
- 首部表在 HTTP 2.0 的连接存续期内始终存在,由客户端和服务器共同渐进地更新;
- 每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值。
在前面的例子中,第二个请求只需要发送变化了的路径首部( :path ),其他首部没有变化,不用再发送了。这样就可以避免传输冗余的首部,从而显著减少每个请求的开销。
有效的 HTTP 2.0 升级与发现
支持 HTTP 2.0 的客户端在发起新请求之前,必须能发现服务器及所有中间设备是否支持 HTTP 2.0 协议。有三种可能的情况:
- 通过 TLS 和 ALPN 发起新的 HTTPS 连接;
- 根据之前的信息发起新的 HTTP 连接;
- 没有之前的信息而发起新的 HTTP 连接。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
GET /page HTTP/1.1
Host: server.example.com
Connection: Upgrade, HTTP2-Settings
Upgrade: h2c // 1
HTTP2-Settings: (SETTINGS payload) // 2
HTTP/1.1 200 OK // 3
Content-length: 243
Content-type: text/html
(... HTTP/1.1 response ...)
(or)
HTTP/1.1 101 Switching Protocols // 4
Connection: Upgrade
Upgrade: h2c
(... HTTP/2 response ...)
➊ 发起带有 HTTP 2.0 Upgrade 首部的HTTP 1.1请求 ➋ HTTP/2.0 SETTINGS 净荷的 Base64 URL 编码 ➌ 服务器拒绝升级,通过 HTTP 1.1 返回响应 ➍ 服务器接受 HTTP 2.0 升级,切换到新分帧 使用这种 Upgrade 流,如果服务器不支持HTTP2.0,就立即返回 HTTP 1.1 响应。否则,服务器就会以 HTTP 1.1 格式返回 101 SwitchingProtocols 响应,然后立即切换到HTTP 2.0并使用新的二进制分帧协议返回响应。
如果客户端因为自己保存有或通过其他手段(如 DNS 记录、手工配置等)获得了关于 HTTP 2.0 的支持信息,它也可以直接发送 HTTP2.0 分帧,而不必依赖 Upgrade 机制。有了这些信息,客户端可以一上来就通过非加密信道发送 HTTP 2.0 分帧,其他就不管了。最坏的情况,就是无法建立连接,客户端再回退一步,重新使用 Upgrade 首部,或者切换到带 ALPN 协商的 TLS 信道。
二进制分帧简介
HTTP 2.0 的根本改进还是新增的长度前置的二进制分帧层。与 HTTP 1.x 使用换行符分隔纯文本不同,二进制分帧层更加简洁,通过代码处理起来更简单也更有效。 所有帧都共享一个 8 字节的首部,其中包含帧的长度、类型、标志,还有一个保留位和一个 31 位的流标识符。
16 位的长度前缀意味着一帧大约可以携带 64 KB 数据,不包括 8 字节首部。 8 位的类型字段决定如何解释帧其余部分的内容。 8 位的标志字段允许不同的帧类型定义特定于帧的消息标志。 1 位的保留字段始终置为 0 。 31 位的流标识符唯一标识 HTTP 2.0 的流。
HTTP 2.0 规定了如下帧类型: DATA :用于传输 HTTP 消息体。 HEADERS :用于传输关于流的额外的首部字段。 PRIORITY :用于指定或重新指定引用资源的优先级。 RST_STREAM :用于通知流的非正常终止。 SETTINGS :用于通知两端通信方式的配置数据。 PUSH_PROMISE :用于发出创建流和服务器引用资源的要约。 PING :用于计算往返时间,执行“活性”检查。 GOAWAY :用于通知对端停止在当前连接中创建流。 WINDOW_UPDATE :用于针对个别流或个别连接实现流量控制。 CONTINUATION :用于继续一系列首部块片段。
发起新流
在发送应用数据之前,必须创建一个新流并随之发送相应的元数据,比如流优先级、 HTTP 首部等。 HTTP 2.0 协议规定客户端和服务器都可以发起新流,因此有两种可能:客户端通过发送 HEADERS 帧来发起新流,这个帧里包含带有新流 ID 的公用首部、可选的 31 位优先值,以及一组 HTTP 键-值对首部;服务器通过发送 PUSH_PROMISE 帧来发起推送流,这个帧与 HEADERS 帧等效,但它包含“要约流 ID ”,没有优先值。由于两端都可以发起新流,流计数器偏置:客户端发起的流具有偶数 ID ,服务器发起的流具有奇数 ID 。这样,两端的流 ID 不会冲突,而且各自持有一个简单的计数器,每次发起新流时递增 ID 即可。
发送应用数据
分为多个 DATA 帧,最后一帧翻转帧首部的 END_STREAM 字段。
HTTP 2.0 帧数据流分析
- 有 3 个活动的流: stream 1 、 stream 3 和 stream5 。
- 3个流的ID都是奇数,说明都是客户端发起的。这里没有服务器发起的流。
- 服务器发送的 stream 1 包含多个 DATA 帧,这是对客户端之前请求的响应数据。这也说明在此之前已经发送过 HEADERS 帧了。
- 服务器在交错发送 stream 1 的 DATA 帧和 stream 3 的HEADERS 帧,这就是响应的多路复用。
- 客户端正在发送 stream 5 的DATA帧,表明 HEADERS 帧之前已经发送过了。
优化应用的交付
我们无法控制客户端与服务器之间的网络环境,也不能控制客户的硬件或者其手持设备的配置,但除此之外的一切就掌握在我们手里了,包括服务器上的 TCP 和 TLS 优化,以及针对不同物理层特性、不同 HTTP 协议版本和通用最佳实践的数十项应用优化。
事实上,影响绝大多数Web应用性能的并非带宽,而是延迟。网速虽然越来越快,但不幸的是,延迟似乎并没有缩短: 1.2 延迟的构成; 1.7 目标:高宽带和低延迟; 10.3.2 延迟是性能瓶颈; 既然我们不能让信息跑得更快,那么关键就在于对传输层和应用层采取各种可能的优化手段,消除不必要的往返、请求,把每个分组的传输距离缩到最短——比如把服务器放到离客户更近的地方。 只要对何时以及如何下载资源、信标进行简单的优化,就能显著改善用户感觉到的延迟、电池使用时间和应用的整体用户体验: 6.4 针对 Wi-Fi 的优化建议; 8 移动网络的优化建议; 自物理层向上,接下来就是要保证任何一台服务器都要按照最新的 TCP 和 TLS 最佳实践进行配置。针对底层协议的优化能保证每个客户端在与服务器通信时,都可以获取最佳性能——高吞吐量和低延迟: 2.5 针对TCP的优化建议; 4.7 针对TLS的优化建议; HTTP 需要注意的地方:
- 我们必须想方设法地绕过 HTTP 1.x 的种种限制;
- 我们必须掌握利用 HTTP 2.0 性能增强的方法;
- 我们必须在应用经典的性能最佳实践时保持警惕。
性能优化的最佳实践
无论什么网络,也不管所用网络协议是什么版本,所有应用都应该致力于消除或减少不必要的网络延迟,将需要传输的数据压缩至最少。
- 减少 DNS 查找,每一次主机名解析都需要一次网络往返,从而增加请求的延迟时间,同时还会阻塞后续请求。
- 重用 TCP 连接尽可能使用持久连接,以消除 TCP 握手和慢启动延迟;参见2.2.2 “慢启动”。
- 减少 HTTP 重定向, HTTP 重定向极费时间,特别是不同域名之间的重定向,更加费时;这里面既有额外的 DNS 查询、 TCP 握手,还有其他延迟。最佳的重定向次数为零。
- 使用 CDN 把数据放到离用户地理位置更近的地方,可以显著减少每次TCP连接的网络延迟,增大吞吐量。这一条既适用于静态内容,也适用于动态内容;参见4.7.2节中的“不缓存的原始获取”。
- 去掉不必要的资源,任何请求都不如没有请求快。说到这,所有建议都无需解释。延迟是瓶颈,最快的速度莫过于什么也不传输。然而 HTTP 也提供了很多额外的机制,比如缓存和压缩,还有与其版本对应的一些性能技巧。
- 在客户端缓存资源应该缓存应用资源,从而避免每次请求都发送相同的内容。
- 传输压缩过的内容,传输前应该压缩应用资源,把要传输的字节减至最少:确保对每种要传输的资源采用最好的压缩手段。
- 消除不必要的请求开销减少请求的 HTTP 首部数据(比如 HTTP cookie ),节省的时间相当于几次往返的延迟时间。并行处理请求和响应请求和响应的排队都会导致延迟时间。
- 并行处理请求和响应,请求和响应的排队都会导致延迟,无论是客户端还是服务器端。这一点经常被忽视,但却会无谓地导致很长延迟。针对协议版本采取优化措施 HTTP 1.x 支持有限的并行机制,要求打包资源、跨域分散资源,等等。相对而言, HTTP 2.0 只要建立一个连接就能实现最优性能,同时无需针对 HTTP 1.x 的那些优化方法。
在客户端缓存资源
对于通过 HTTP 传输的资源,要保证首部包含适当的缓存字段: Cache-Control 首部用于指定缓存时间; Last-Modified 和 ETag 首部提供验证机制。 HTTP Cache 本书的作者也写了篇文章来详细讲述 HTTP Cache 。
压缩传输的数据
对于必须要传输的资源,为保证传输的字节数最少,需要保证对它们进行最有效的压缩。文本资源经过 gzip 压缩平均可以减少 60%~80% 大小,而图片则有不同的规则:
- 图片一般会占到一个网页需要传输的总字节数的一半;
- 通过去掉不必要的元数据可以把图片文件变小;
- 要调整大小就在服务器上调整,避免传输不必要的字节;
- 应该根据图像选择最优的图片格式;
- 尽可能使用有损压缩;
- 选择正确的格式: JPEG , WebP , PNG ;
- 确定必需的大小,不要让图片超过它需要的大小。
消除不必要的请求字节
• 浏览器会在每个请求中自动附加关联的 cookie 数据; • 在 HTTP 1.x 中,包括 cookie 在内的所有 HTTP 首部都会在不压缩的状态下传输; • 在 HTTP 2.0 中,这些元数据经过压缩了,但开销依然不小; • 最坏的情况下,过大的 HTTP cookie 会超过初始的 TCP 拥塞窗口,从而导致多余的网络往返。 应该认真对待和监控 cookie 的大小,确保只传输最低数量的元数据,比如安全会话令牌。同时,还应该利用服务器上共享的会话缓存,从中查询缓存的元数据。更好的结果,则是完全不用 cookie 。
并行处理请求和响应
如果不使用持久连接,则每个 HTTP 请求都要建立一个 TCP 连接。由于 TCP 握手和慢启动,多个 TCP 会造成明显的延迟。在使用 HTTP 1.1 的情况下,最好尽可能重用已有连接。如果碰上能使用 HTTP 管道的机会,不要放过。更好的选择,则是升级到 HTTP 2.0 ,从而获得最佳性能。
针对 HTTP 1.x 的优化建议
针对 HTTP 1.x 的优化次序很重要:首先要配置服务器以最大限度地保证 TCP 和 TLS 的性能最优,然后再谨慎地选择和采用移动及经典的应用最佳实践,之后再度量,迭代。 • 利用 HTTP 管道,如果你的应用可以控制客户端和服务器这两端,那么使用管道可以显著减少网络延迟。 • 采用域名分区,如果你的应用性能受限于默认的每来源 6 个连接,可以考虑将资源分散到多个来源。 • 打包资源以减少 HTTP 请求,拼接和精灵图等技巧有助于降低协议开销,又能达成类似管道的性能提升。 • 嵌入小资源,考虑直接在父文档中嵌入小资源,从而减少请求数量。
针对 HTTP 2.0 的优化建议
• 服务器的初始 cwnd 应该是 10 个分组; • 服务器应该通过 ALPN (针对 SPDY 则为 NPN )协商支持 TLS ; • 服务器应该支持 TLS 恢复以最小化握手延迟。 少发数据、削减请求,根据无线网络情况调整资源供给。不管使用什么版本的协议,减少传输的数据量和消除不必要的网络延迟,对任何应用都是最有效的优化手段。 最后,杜绝和忘记域名分区、文件拼接、图片精灵等不良的习惯,这些做法在 HTTP 2.0 之上完全没有必要。事实上,继续使用这些手段反而有害。可以利用 HTTP 2.0 内置的多路分发以及服务器推送等新功能。 记得去掉针对 HTTP 1.x 的优化。
双协议应用策略
如果应用可以同时控制服务器和客户端,那倒简单了,因为它可以决定使用什么协议。但大多数应用不能也无法控制客户端,只有采用一种混合或自动策略,以适应两种协议并存的现实。
- 相同的应用代码,双协议部署。
- 分离应用代码,双协议部署。
- 动态 HTTP 1.x 和 HTTP 2.0 优化。
- HTTP 2.0 ,单协议部署。
评估服务器质量与性能
HTTP 2.0 服务器实现的质量对客户端性能影响很大。 HTTP 服务器的配置当然是一个重要因素,但服务器实现逻辑的质量同样与优先级、服务器推送、多路复用等性能机制的发挥紧密相关。
- HTTP 2.0 服务器必须理解流优先级;
- HTTP 2.0 服务器必须根据优先级处理响应和交付资源;
- HTTP 2.0 服务器必须支持服务器推送;
- HTTP 2.0 服务器应该提供不同推送策略的实现。
Comments powered by Disqus.