图书 Http2 in Action

Review

  1. 2024-10-23 06:38

[!Summary] 《HTTP 2 in Action 中文版 2020》郑维智 封面:Habit of a Russian Market Woman in 1768

一、Introduction #

Web技术、性能调优、安全及技术实践

第1章 万维网与HTTP #

因特网(Internet)和万维网(World Wide Web或Web)

因特网是使用IP(Internet Protocol,因特网协议)连接在一起实现消息传递的计算机构成的网络。因特网上有很多服务,包括万维网,以及电子邮件、文件共享、因特网电话等。

Tim Berners-Lee 当初发明万维网时,一共创造了三项核心技术

  1. HTTP
  2. URL(Uniform Resource Locator,统一资源定位符)
  3. HTML(Hypertext Markup Language,超文本标记语言)

服务器可以提供多种服务(比如电子邮件、FTP、HTTP和HTTPS),而端口可以让不同服务共享同一个IP地址

IETF(Internet Engineering Task Force,因特网工程任务组)

从本质上来讲,HTTP是一个请求-响应协议。Web浏览器使用HTTP协议,向服务器发送一个请求。服务器响应一个消息,这个消息包含了浏览器所请求的资源。

尽管不是正式标准,HTTP/1.0 还是新增了一些关键特性,包含:

  • 更多的请求方法。除了先前定义的GET方法,新增了HEAD和POST方法。
  • 为所有的消息添加HTTP版本号字段。此字段是可选的,为了向后兼容,默认情况下使用HTTP/0.9。
  • HTTP首部。它可以与请求和响应一起发送,以提供与正在执行的请求或发送的响应相关的更多信息。
  • 一个三位整数的响应状态码,(例如)用来表示响应是否成功。此状态码还可以用来表示重定向请求、条件请求和错误状态(404 - Not Found是其中最著名的错误状态之一)。

可以发送具有相同名称的多个首部,在语义上这与发送以逗号分隔的版本完全相同。

HTTP/1.1做了进一步的改进,以便充分利用HTTP协议(例如,持久连接、强制响应首部、更好的缓存选项和分块编码)

HTTP请求行(如GET指令)中的URL不是一个包含绝对路径的URL(如http://www.example.com/section/page.html),而是一个只包含相对路径的URL(如 /section/page.html

Host 作为必选项是HTTP/1.1的重要改进(这个首部在HTTP/1.0中是可选的),这使得服务器能够充分利用虚拟主机托管技术,无须顾及因为新增网站而新加独立主机所带来的复杂性,从而使得网络繁荣发展。

HTTP/1.1规范声明,“为了支持在某些未来版本的HTTP中,转为使用绝对URL形式,服务器必须接受请求中的绝对URL形式,即使HTTP/1.1客户端只会在使用代理的请求中发送它们。”但是,稍后我们会看到,HTTP/2并未彻底解决此问题,而是使用 :authority 伪首部字段替换 Host 首部

对于非持久连接,关闭连接是一个表明服务器已经完成了发送的好信号!所以,必须使用 Content-Length 首部来定义消息响应体的长度,以便当整个消息体传输完成后,客户端可以发送一个新请求。

可以假定任何HTTP/1.1连接都使用持久连接。如果服务器确实想要关闭连接,无论出于何种原因,则它必须在响应中显式包含 Connection:close HTTP首部

HTTP消息以未加密的方式通过互联网发送,因此在消息被路由到目的地的过程中,任何一方都可以读取到消息。

HTTPS是HTTP的安全版本,它使用TLS(Transport Layer Security,传输层加密)协议对传输中的消息进行加密

SSL是由Netscape发明的,由于SSL由Netscape拥有,因此它不是正式的互联网标准,尽管它随后由IETF作为历史文档发布。SSL被标准化为TLS(传输层加密)

HTTPS使用公钥加密,服务器在用户首次连接时以数字证书的形式提供公钥。浏览器使用此公钥加密消息,只有服务器可以解密,因为只有它拥有配对的私钥。该系统允许你安全地与网站进行通信,而无须事先知道共享密钥。

公钥加密很慢,因此公钥仅用于协商共享密钥。为了更好的性能,可用共享密钥加密后续的消息

Advanced REST Client

第2章 通向HTTP/2之路 #

在第1章,我们了解到HTTP是一种请求-响应协议,最初设计用于请求单个纯文本内容,在请求完成后会终止连接。HTTP/1.0引入了其他媒体类型,例如支持图像。HTTP/1.1确保在默认情况下开启持久连接(假设网页需要更多请求)。

网站的增长主要是因为媒体越来越丰富,例如,图像和视频在大多数网站上都很常见。

网站变得越来越复杂,需要多个框架和依赖项才能正确显示其内容。

现代互联网最大的问题之一是延迟而不是带宽。延迟是将单个消息发送到服务器所需的时间,而带宽是指用户可以在这些消息中下载多少内容。

队头(HOL)阻塞问题

HTTP/1.1不是一种高效的协议,因为它为等待响应会阻塞发送。导致在当前请求完成之前,无法发送另一个请求。

突破HTTP/1.1的性能限制的技术,这些技术分为以下3类:

  1. 使用多个HTTP连接:域名分片
  2. 合并HTTP请求:精灵图、合并JS、合并CSS、行内SVG、Base64编码图片
  3. HTTP缓存

其他的和HTTP关联不大的性能优化技术,包括:

  1. 优化用户请求资源的方式(比如先请求关键CSS)
  2. 减小下载资源的大小(压缩和使用响应式图片)
  3. 减少浏览器的渲染任务(更高效的CSS和JavaScript)

打开多个连接是解决HTTP/1.1阻塞问题的最简单方法,这样可以同时开启多个HTTP请求,大多数浏览器可以为每个域名打开6个连接。

域名分片

当开启多个HTTP连接时,客户端和服务器都有额外的开销:打开TCP连接需要时间,维护连接需要更多的内存和CPU资源。

TCP在开启连接时比较小心,在确认网络不拥堵之前只会发送比较少的数据包。CWND(Congestion Window,拥塞窗口)随着时间的推移逐渐增加,只要连接没发现丢包,就可以处理更大的流量。TCP拥塞窗口的大小受TCP慢启动算法控制。

慢启动算法是在创建连接时是给它限流了,不管它所处环境的网络有多快,带宽有多大。

使用多个独立的连接也可能导致带宽问题。例如,如果所有带宽都用掉了,就会导致TCP超时,和其他的连接上的重传。在这些独立的连接之间,没有优先级的概念,这就无法更高效地利用带宽。

另外一个常见的优化技术是发送更少的请求,包括:减少不必要的请求(比如在浏览器中缓存静态资源),以更少的HTTP请求获取同样的资源。

精灵图

内联资源到其他文件。比如,Critical CSS经常直接被内联在HTML的<style>标签中。图片可以包含在CSS中,通过行内SVG,或者转换为Base64编码,也能减少HTTP请求数。

HTTP/1.1是一个简单的文本协议。这种简单性也带来一些问题。尽管HTTP消息体可以包含二进制数据(比如图片,以及客户端和服务器能理解的任何格式),但请求和首部需要是文本的形式

向HTTP首部中添加换行符,可以进行一些HTTP攻击

HTTP使用文本格式带来的另外一个问题是,HTTP消息较大,这是因为不能高效编码数据(比如使用数字来表达Date首部,而不是使用人类可读的完整文本),而且首部内容也有重复

域名分片,如本章前面讲过的,主要用来提供更多连接数,但它也用来创建所谓的无Cookie域名。出于性能和安全的考虑,浏览器不会向这些域名发送Cookie。

性能问题是HTTP/1.1需要改善的其中一个问题。除此之外,它还存在纯文本协议的安全和隐私问题(HTTPS加密很好地解决了这个问题),以及缺少状态的问题(cookie在一定程度上解决了这个问题)

设置 --disable-http2 来禁用HTTP/2(Advanced Settings → Chrome → Command-Line Options)

出于安全上的考虑(参考浏览器如何处理跨域请求),Amazon在对JavaScript文件的一部分请求中使用了setAttribute (crossorigin:anonymous),即不带认证信息,这意味着不使用现有的连接。所以,浏览器创建了更多的连接。

HTTP-NG尝试解决HTTP/1的多种问题,而SPDY的主要目标是解决HTTP/1.1的性能问题

SPDY变成了一个二进制协议。这个改动使得我们可以在一个连接上处理较小的消息,然后将它们合并为较大的HTTP消息,这跟TCP将HTTP消息拆分为TCP数据包的模式非常像,而且这种拆分对于大多数HTTP实现来说是透明的。

2012年11月,基于SPDY发布了HTTP/2初稿 在2014年底,HTTP/2规范作为互联网的标准被提出 在2015年5月,被正式通过,这就是RFC 7450

加载时间指页面发起 onload 事件的时间 —— 通常指所有的CSS和阻塞式JavaScript加载完成的时间。 首字节时间指从网站收到第一个字节的时间。通常,此响应是第一个真正的响应,不是重定向。 开始渲染时间指页面开始绘制的时间。 视觉完整时间指页面停止变化的时间


第3章 升级到HTTP/2 #

这个要求被排除在HTTP/2的正式规范之外,但是所有浏览器厂商都表示,他们将仅支持基于HTTPS的HTTP/2,这使其成为事实上的标准。

只有服务器支持ALPN,其才支持HTTP2。

ALPN(Application Layer Protocol Negotiation,应用层协议协商)

乐观地看,拦截代理通常用于家庭或公司环境,这时连接通常很快,而HTTP/2带来的收益也没那么高。在移动环境下,使用中间代理的情况要少得多,而低延迟网络(如移动网络)是HTTP/2的主要受益者之一。

一些浏览器(如Chrome和Firefox)会在后台静默更新,不提示用户,我们称之为evergreen浏览器。

让操作系统或软件包管理器处理这个的好处是,只需要周期性地执行更新(可以自动化),就可以应用最新的安全补丁。

Chrome和Opera仅支持基于ALPN的HTTP/2,而不支持NPN(Next Protocol Negotiation,下一代协议协商)

ALPN仅包含在最新版本的OpenSSL(1.0.2及更高版本)中

反向代理很常见,主要用于以下两种场景: • 作为负载均衡器 • 用以卸载一些功能,如HTTPS或者HTTP/2

负载均衡器在两个Web服务器之前,将流量发送到其中之一,转发规则取决于配置(多活模式,还是主备模式)

多活模式负载均衡器使用算法决定如何分发流量(例如,基于源IP分发,或者是使用轮询)。

负载均衡器已经支持HTTP/2(如F5、Citrix Netscaler和HAProxy)

基于反向代理实现HTTP/2时,HTTP/2连接在反向代理处终止,从这时起,使用另外的连接(可能是HTTP/1.1)。此过程类似于使用反向代理卸载HTTPS,HTTPS在反向代理处被卸载,然后反向代理使用HTTP与基础架构的其余部分进行通信。

HTTP/2的主要优点是可以提升高延迟、低带宽连接的速度,连接到边缘服务器(在这种情况下为反向代理)的用户通常处在这样的网络环境下。

从反向代理到其他Web基础架构的流量一般处于低延迟、高带宽、短距离的网络环境中(即使不是同一台机器,也通常是相同的数据中心),因此此场景下通常不需要考虑HTTP/1.1的性能问题。

Nginx已经声明,它不会为代理连接实现HTTP/2

CDN是非常有效的反向代理。在HTTP/2出现之前,CDN主要用于解决性能问题,而现在它们还为升级HTTP/2提供了一个简单方法。

CDN能够解密数据流,因此你必须接受第三方能读取你的数据这个事实。

TLS协议用于创建HTTPS会话,ALPN是TLS的扩展,服务器用它来声明对HTTP/2的支持。一些Web浏览器(在撰写本书时,有Safari、Edge和Internet Explorer)允许使用旧的NPN,以及更新的ALPN。其他浏览器(如Chrome、Firefox和Opera)仅支持新的ALPN。

  • SSL-Labs
  • KeyCDN HTTP/2 Test
  • OpenSSL的s_client
  • testssl

操作系统默认的TLS库。在Linux下是OpenSSL,macOS下是LibreSSL,Windows下是SChannel。

Mozilla SSL Configuration Generator

反向代理不应转发Upgrade首部

第4章 HTTP/2协议基础 #

新增了如下概念:

  1. 二进制协议
  2. 多路复用
  3. 流量控制功能
  4. 数据流优先级
  5. 首部压缩
  6. 服务端推送

新版本的变化主要与HTTP/2在网络中传输的方式有关

HTTP/2定义了新版本HTTP的主要部分(二进制、多路复用等),并且未来的任何实现或变更(如果有HTTP/2.1的话),此规范都兼容

HTTP/1这个术语(人们不太使用它,在本书中用来代表HTTP/1.0和HTTP/1.1)也是一样的,它的定义是一个基于文本的协议,首部后面跟着消息体

使用基于文本的协议,要先发完请求,并接收完响应之后,才能开始下一个请求

HTTP/1.0引入了二进制的HTTP消息体

HTTP/1.1引入了管道化(见第2章)和分块编码。

分块编码和管道化都有队头阻塞(HOL)的问题——在队列首部的消息会阻塞后面消息的发送,更不用说,管道化在实际应用中并没有得到很好的支持。

所有的HTTP/2消息都使用分块的编码技术

RFC7230第4.1节中定义的分块传输编码不得在HTTP/2中使用。

HTTP/2允许在单个连接上同时执行多个请求,每个HTTP请求或响应使用不同的流。通过使用二进制分帧层,给每个帧分配一个流标识符,以支持同时发出多个独立请求。当接收到该流的所有帧时,接收方可以将帧组合成完整消息。

帧是同时发送多个消息的关键。每个帧都有标签表明它属于哪个消息(流),这样在一个连接上就可以同时有两个、三个甚至上百个消息。

从严格意义上说请求并不是同时发出去的,因为,帧在HTTP/TCP连接上也需要依次发送。

服务器发送响应的顺序完全取决于服务器,但客户端可以指定优先级。如果可以发送多个响应,则服务器可以进行优先级排序,先发送重要资源(例如CSS和JavaScript),然后是不太重要的资源(例如图像)

因为HTTP/2中流会被丢弃而且不能重用,而HTTP/1.1保持连接打开,并且可以重新用它来发送另一个请求。

为了防止流ID冲突,客户端发起的请求使用奇数流ID

,服务器发起的请求使用偶数流ID。请

ID为0的流(图中未显示出)是客户端和服务器用于管理连接的控制流。

现在HTTP/2对并发的请求数量的限制放宽了很多(在许多实现中,默认情况下允许同时存在100个活跃的流),因此许多请求不再需要浏览器来排队,可以立即发送它们。

流的优先级控制是通过这种方式实现的:当数据帧在排队时,服务器会给高优先级的请求发送更多的帧。

流量控制是在同一个连接上使用多个流的另一种方式。如果接收方处理消息的速度慢于发送方,就会存在积压,需要将数据放入缓冲区。而当缓冲区满时会导致丢包,需要重新发送。在连接层,TCP支持限流,但HTTP/2要在流的层面实现流量控制。

HTTP首部(包括请求首部和响应首部)用于发送与请求和响应相关的额外信息。在这些首部中,有很多信息是重复的,多个资源使用的首部经常相同。

有些特殊的响应首部,如CSP(Content Security Policy)首部,可能会很大,也可能会有重复。

HTTP/1允许压缩HTTP正文内容(Accept-Encoding首部),但是不会压缩HTTP首部

HTTP/2引入了首部压缩的概念,但是它使用了和正文压缩不同的技术。该技术支持跨请求压缩首部,这可以避免正文压缩所使用算法的安全问题。

HTTP/1和HTTP/2之间的另外一个重要不同是,HTTP/2添加了服务端推送的概念,它允许服务端给一个请求返回多个响应。

HTTP/2规范文档[3]提供了三种建立HTTP/2连接的方法(但是目前已经有了第4种方法,见4.2.4节)。 • 使用HTTPS协商。 • 使用HTTP Upgrade首部。 • 和之前的连接保持一致。

浏览器仅支持基于HTTPS(h2)建立HTTP/2连接,所以浏览器使用第一个方法来协商HTTP/2。

公钥私钥加密被称为非对称加密,因为它加密和解密消息时使用不同的密钥。这种类型的加密,在你连接到一个新的服务器时非常有必要,但它比较慢,所以这种加密方式用于协商一个对称加密的密钥,以便在创建连接之后使用对称密钥加密消息。

当HTTPS会话建立完成后,在同一个连接上的HTTP消息就不再需要这个协商过程了。类似地,后续的连接(不管是并发的额外连接,还是后来重新打开的连接)可以跳过其中的某些步骤 —— 如果它复用上次的加密密钥,这个过程就叫作TLS会话恢复

ALPN很简单,它可以在现有的HTTPS协商消息中协商HTTP/2支持,不会引入额外的消息往返、跳转,或者其他的升级延迟

ALPN在2014年7月完成,在HTTP/2完成之前,其RFC[7]只规定了它在HTTP/1.1和SPDY下应用的方法。后来,HTTP/2 ALPN扩展注册了,作为完整的HTTP/2规范的一部分

NPN是ALPN之前的一个实现,两者工作方式类似。尽管被很多浏览器和Web服务器使用,但是它从来没有成为正式的互联网标准(虽然有一个草案在编制中)[9]。ALPN成为正式标准,它在很大程度上是基于NPN实现的,正如HTTP/2是基于SPDY的,而HTTP/2成了一个正式版本。

两者的主要区别是,在使用NPN时,客户端决定最终使用的协议,而在使用ALPN时,服务端决定最终使用的协议。

现在不再推荐使用NPN,应该使用ALPN

ECDHE-ECDSA-AES128-GCM-SHA256

通过发送Upgrade首部,客户端可以请求将现有的HTTP/1.1连接升级为HTTP/2。这个首部应该只用于未加密的HTTP连接(h2c)。基于加密的HTTPS连接的HTTP/2(h2)不得使用此方法进行HTTP/2协商,它必须使用ALPN。

客户端支持并想要使用HTTP/2,发送一个带Upgrade首部的请求:[插图]这样的请求必须包含一个HTTP2-Settings首部,它是一个Base-64编码的HTTP/2 SETTINGS帧,后面会讲。

支持HTTP/2的服务器可以返回一个HTTP/1.1 101响应以表明它将切换协议,而不是忽略升级请求,并返回HTTP/1.1 200响应

发送Upgrade首部的问题

HTTP/2规范描述的第3个(也是最后一个)客户端使用HTTP/2的方法是,看它是否已经知道服务器支持HTTP/2。如果它知道,则可以马上开始使用HTTP/2,不需要任何升级请求。

此方法是风险最高的方法,因为它假设服务器可以支持HTTP/2。使用先验知识的客户端必须注意妥善处理拒绝信息,以防之前的信息有误。

第4种方法是使用HTTP Alternative Services(替代服务)[15],它没有被包含在原来的标准中,在HTTP/2发布之后,将其列为单独的标准。此标准允许服务器使用HTTP/1.1协议(通过Alt-Svc HTTP首部)通知客户端,它所请求的资源在另一个位置(例如,另一个IP或端口),可以使用不同的协议访问它们。该协议可以使用先验知识启用HTTP/2。

HTTP/2 连接前奏消息 #

不管使用哪种方法启用HTTP/2连接,在HTTP/2连接上发送的第一个消息必须是HTTP/2连接前奏,或者说是“魔法”字符串。此消息是客户端在HTTP/2连接上发送的第一个消息。它是一个24个八位字节的序列。

这个消息可能看起来有些奇怪,没错,它是一个HTTP/1样式的消息

该消息说明,HTTP请求方法是PRI(而不是GET或者POST),请求的资源是*,所使用的HTTP版本是 HTTP/2.0。接下来是两个换行符(所以没有请求首部),然后是一个请求体SM

对于支持HTTP/2的服务器,可以根据这个收到的前奏消息推断出客户端支持HTTP/2,它不会拒绝这个神奇的消息,它必须发送SETTINGS帧作为其第一条消息(可以为空)。

有一些工具对于查看HTTP/2帧很有帮助,比如Chrome的net-export页面、nghttp和Wireshark。

nghttp是一个基于nghttp2 C库开发的命令行工具,许多Web服务器和客户端使用它来处理底层的HTTP/2协议

使用Wireshark[24]工具嗅探计算机发送和接收的所有流量。这个工具在做一些底层的调试时非常方便,你可以看到发送和接收的原始消息。

Chrome和Firefox开发人员考虑过这个场景,这些浏览器允许你将HTTPS密钥保存到单独的文件中,这样就可以使用Wireshark等工具进行调试了

启动Wireshark,并指定(Pre)-Master-Secret文件。选择Edit→Preferences→Protocols→SSL

开始是Wireshark自己的基础格式,称为以太帧(这里不是HTTP/2帧)。以太帧是以太网内的消息格式,被封装成IPv4消息,通过TCP发送,通过SSL/TLS加密,最后,是你看到的HTTP/2消息。你可以通过Wireshark查看消息的每一层。

Wireshark提供了最详细的信息,因此如果你想要细致地了解消息结构和格式,没有别的选择。

一个8位字节确定是8位,而一个字节大多数情况下可以认为是8位,但实际上取决于所使用的操作系统。

SETTINGS帧仅定义一个可在公共帧首部中设置的标志

流ID 0是保留数字,用于控制消息(SETTINGS和WINDOW_UPDATE帧),所以服务器使用流ID 0发送此SETTINGS帧是合理的。

每个设置项长度为16位(标识符)+32位(值)。也就是说,每项设置有48位即6字节

WINDOW_UPDATE帧(0x8)用于流量控制,比如限制发送数据的数量,防止接收端处理不完。

初始的数据窗口大小可以通过SETTINGS帧设置,然后使用WINDOW_UPDATE帧来改变它的大小。所以,WINDOW_UPDATE帧是一个简单的帧,没有任何标志位,只有一个值(和一个保留位)

HTTP/2流量控制设置仅应用于DATA帧,所有其他类型的帧(至少目前定义的),就算超出了窗口大小的限制也可以继续发送。这个特性可以防止重要的控制消息(比如WINDOW_UPDATE帧自己)被较大的DATA帧阻塞。

HTTP/2定义了新的伪首部(以冒号开始),以定义HTTP请求中的不同部分

:authority伪首部代替了原来HTTP/1.1的Host首部

HTTP/2强制将HTTP首部名称小写

Header Block Fragment(首部块片段)字段包含所有的首部(和伪首部)

HEADERS首部定义了4个标志位,可以在普通的帧首部中发送它们

END_STREAM

END_HEADERS

PADDED

PRIORITY

要求CONTINUATION帧紧跟在HEADERS帧后面,其中不能插入其他帧,这影响了HTTP/2的多路复用,人们正考虑其他替代方案[32]。实际上CONTINUATION帧很少使用,大多数请求都不会超出一个HEADERS帧的容量。

含END_HEADERS标志位(0x04),它说明整个HTTP响应首部都在当前这一个帧中。

HTTP/1.1引入了尾随首部的概念,可以在正文之后发送它。这些首部可以支持不能提前计算的信息。例如,在以流的形式传输数据时,内容的校验和或者数字签名可以包含在尾随首部中。

实际上,尾随首部的支持很差,很少应用。但是HTTP/2决定继续支持它,所以一个HEADERS帧(或者一个后跟CONTINUATION帧的HEADERS帧)可能出现在流的DATA帧之前或者之后。

HTTP/2的DATA帧比较简单,它包含所需要的任何格式的数据:UTF-8编码、gzip压缩格式、HTML代码、JPEG图片的字节,什么都行。在帧首部中包含了长度,所以DATA帧自己的格式中不需要包含长度字段。

END_STREAM(0x1),当前帧是流中的最后一个。

由于HTTP/2的DATA帧默认支持被分成多个部分,这就没有必要使用分块编码了

HTTP/2规范甚至说,“分块编码不能在HTTP/2中使用。”

GOAWAY帧

用于关闭连接,当连接上没有更多的消息,或发生了严重错误时使用该帧。

客户端发出GOAWAY帧,但不是从服务端接收它。

GOAWAY帧使用最小的8字节的长度发送(1bit + 31bit + 32bit),没有任何标志位,帧使用的流ID为0。

新增了三个新的帧类型——ALTSVC、ORIGIN和CACHE_DIGEST

每一个新的HTTP/2帧类型、HTTP/2的设置项和HTTP/2的错误码,都必须在IANA(Internet Assigned Numbers Authority,互联网数字分配机构)[34]注册。

太大的首部需要使用CONTINUATION帧(0x9),它紧跟在HEADERS帧或者PUSH_PROMISE帧后面。因为在请求可以被处理之前,需要完整的HTTP首部,并且为了应用HPACK的字典(见第8章),所以CONTINUATION帧必须紧跟在HEADERS帧后面。

CONTINUATION帧只定义了一个标志位,就是在普通的帧首部中可以设置的那个。END_HEADERS(0x4),当设置这个标志的时候,表明HTTP首部内容到此帧结束,后续没有别的CONTINUATION帧了。

PING帧(0x6)用以计算发送方的消息往返时间,也可以用来保持一个不使用的连接。当收到这类帧的时候,接收方应当马上回复一个类似的PING帧。两个PING帧都应当在控制流(流ID为0)上发送。

服务器使用PUSH_PROMISE帧(0x5)通知客户端它将推送一个客户端没有明确请求的资源

同样,如果要推送的资源首部比较大,则它后面也可能会跟一个CONTINUATION帧

RST_STREAM(0x3),用于直接取消(重置)一个流。该取消可能是由于一个错误,或者是因为请求已经不需要进行了。可能是客户端已经跳转到其他页面、取消了加载,或者不再需要服务器推送的资源了。

在HTTP/2规范被批准之后,ALTSVC帧(0xa)是第一个追加到HTTP/2中的帧。在一个单独的规范[35]中对其进行了解释,其允许服务端宣告获取资源时可用的其他服务,见4.2.4节。这个帧可以用来进行升级(比如从h2升级到h2c)或者重定义流量到另外一个版本。

ORIGIN帧(0xc)是一个新的帧,于2018年3月标准化[36],服务器使用它来宣告自己可以处理哪些源(比如域名)的请求。当客户端决定是否合并HTTP/2连接的时候,该帧非常有用。

CACHE_DIGEST帧(0xd)是一个新的帧提议[37]。客户端可以使用这个帧来表明自己缓存了哪些资源。例如,它指示服务器不必再推送这些资源,因为客户端已经有了

第5章 实现HTTP/2推送 #

HTTP/2多路复用技术允许在同一连接上并行请求所有资源,因为这样排队会减少,所以它优于HTTP/1。

HTTP/2推送打破了HTTP“一个请求=一个响应”的惯例。它允许服务器使用多个返回来响应一个请求。

如果使用方法正确,HTTP/2推送可以减少加载时间。但如果你多推送了资源(客户端不需要,或者已经在缓存里),则将会延长加载时间。这会浪费带宽,本来应该用这些带宽加载需要的资源。所以,在使用HTTP/2推送时应该小心,经过考虑后再使用。

推送资源是为了响应初始请求而做出的额外响应。

如何推送取决于Web服务器,

一些Web服务器可以通过HTTP link首部或者通过配置来推送。

很多Web服务器(如Apache、Nginx和H2O)和一些CDN(如Cloudflare和Fastly)使用HTTP link首部通知Web服务器推送资源。如果Web服务器看到了这些HTTP link首部,它将推送在首部中引用的资源。

推送link首部经常包含在条件语句中,以支持仅对指定的路径或者文件类型推送资源。

preload link首部的使用早于HTTP/2,开始时被用作客户端暗示。

这个首部允许浏览器直接获取资源,不用等着下载、读取、解析整个页面之后才决定是否下载另一个资源。preload首部允许站长说:“这个资源肯定会用到,所以我建议,如果你没有它的缓存,尽快请求它。”

服务端推送的资源在Chrome开发者工具中以Initiator列显示

Web服务器不能给其他域名推送资源。

通过HTTP link首部直接推送的优势是,服务器不需要在推送前等资源返回以查看link首部。

使用Web服务器的更早推送指令(如H2PushResource)的缺点是,你不能再使用应用来发起这些推送,应用更应该决定是否要推送资源。为了解决这个问题,有了一个新的HTTP状态码——103 Early Hints[6],其允许通过preload HTTP link首部来提前指示是否需要一个资源。

通常会有一个负载均衡器,或者一个Web服务器作为系统接入点(通常叫边缘节点),它将请求代理到后端的应用服务。

中间节点可以从服务端接收推送的资源,并选择不将它们推送到客户端。换句话说,如何利用推送的信息取决于中间节点。同样,中间节点可以选择向客户端推送额外的资源,而不需要后端服务器做任何操作。

服务端如何推送一个资源,浏览器处理推送的方式与你猜想的不太一样。资源不是被直接推到网页中,而是被推到缓存中。跟平常一样处理网页。当页面知道它需要什么资源时,它先查看缓存,如果发现缓存中有,就直接从缓存中加载,而不需要向服务端请求。

细节实现跟浏览器相关,在HTTP/2规范中没有详细说明,但是大多数浏览器都通过一个特殊的HTTP/2推送缓存实现HTTP/2推送,其跟大多数Web开发者都熟悉的普通HTTP缓存不同。

推送缓存不是浏览器查找资源的第一个地方,如果资源在HTTP缓存中,浏览器就不会使用被推送的资源。就算被推送的资源比缓存的资源更新,只要浏览器认为缓存的资源可以用(基于cache-control首部),它就会使用之前缓存的旧的内容。对于使用Service worker的网站来说,浏览器会在推送缓存之前检查Service worker缓存。

图5.15 浏览器和HTTP/2推送的交互

每种缓存的简单解释:

图片缓存是一个短期的内存中的缓存。当页面多次引用一个图片时,它可以防止多次下载图片。当用户离开页面时,缓存被销毁。 preload缓存是另外一种短期的内存中的缓存,它用来缓存预加载的资源(见第6章)。同样,这个缓存是跟页面绑定的。不要给另外一个页面预加载资源,因为它用不到。

Service workers是相当新的后台程序,它独立于网页运行,可以作为网页和网站的中间人。它可以让网站表现得更像原生应用,比如你可以在没有网络的时候运行。它们有自己的缓存和域名绑定。

HTTP缓存是大多数开发者知道的主要缓存,它是一种基于磁盘的持久缓存,多个浏览器可以共享,每个域名使用有限的空间。

HTTP/2推送缓存是一个短期的内存中的缓存,它和连接绑定,最后才使用它。

HTTP/2推送缓存和连接绑定,这意味着如果连接关闭,推送资源也就无法使用了。

最后,当资源从连接的推送缓存中被“认领”并拿出后,就不能再从推送缓存中使用它了。如果HTTP cache-control首部设置了缓存,则可以从浏览器的HTTP缓存中获取。推送缓存还有一点和HTTP缓存不同,那就是不缓存的资源(在HTTP cache-control首部中设置了no-cache和no-store)也可以被推送,并可以从推送缓存中读取它们。

只有在浏览器知道它不需要推送的资源时,RST_STREAM帧才有用。如果HTTP缓存中已经有资源,明显可以利用RST_STREAM帧来停止推送。但是,如果推了一张很大的图片,而页面从来不用,会发生什么?

使用HTTP/2推送有一个较大的风险,那就是可能会推送不需要的资源。有一些风险是本章之前讨论过的实现问题导致的(比如,连接没有被重用),但大多数时候是因为推送的资源浏览器端已经有了。

服务器可以记录它在一个客户端的连接上推送过哪些资源。这项技术的实现取决于服务端,可以基于连接,或者会话ID等。比如,每次推送一个资源时,服务器要记住,在这个连接/会话上不再推送同样的资源,就算被要求推送。

客户端发送一个if-modified-since或者etag首部,而引用这个CSS的页面已经在浏览器的缓存中,但已过期。当你看到类似的首部时,可以选择不推送这个CSS资源,因为这个样式文件可能已经在缓存中了

在客户端记录哪些资源已经被推送。cookie是做这个的理想载体,当然也可以使用LocalStorage和SessionStorage。

当推送资源时,设置一个会话cookie或者和被推送的资源时间相同的cookie。当页面中每个请求到达时,检查是否存在这个cookie。如果cookie不存在,则资源可能就不在浏览器缓存中,那么就推送资源并设置cookie。如果cookie存在,就不推送资源。

缓存摘要是一个提议[18],浏览器用它来告诉服务器缓存里有什么内容。当连接建立时,浏览器发送一个CACHE_DIGEST帧,列出当前域名(或者本连接授权的其他域名,见第6章)的HTTP缓存中的所有资源。

CACHE_DIGEST帧应该在连接建立之后尽快发送

在2019年1月,HTTP工作组声称,他们不会继续进行标准化缓存摘要的工作,安全和隐私问题是停止缓存摘要标准化的原因。

在SETTINGS帧中将SETTINGS_ENABLE_PUSH设置为0,客户端可以禁用推送。此后,服务端不能使用PUSH_PROMISE帧。

推送请求必须使用可以缓存的方法(GET、HEAD和一些POST请求)。

  • 推送请求必须是一些安全请求(通常为GET或者HEAD请求)。
  • 推送请求不能包含请求体(但经常包含响应体)。
  • 只将推送请求发送到权威服务器的域。
  • 客户端不能推送,只有服务端可以。
  • 资源可以在当前请求的响应中推送。如果没有请求,则服务端不可能发起一个推送。

实际上,由于这些规则,只有GET请求会被推送。

有一个叫作Signed HTTP Exchanges[23]的有趣协议(之前叫Web Packaging),通过它你可以在你的域名上提供签名的资源,就像是直接从源站提供的一样,这使得推送其他网站的资源高效了很多。

在理想情况下,应该只推送页面需要的关键资源。

同时,需要考虑客户端缓存里是否已经有资源了(见5.3节)。应该推送很可能缓存里没有的资源。

推送需要的最少资源,来“填充空闲的网络时间,不要做多余的操作”。

Jetty的实现非常有趣,他配合使用一些缓存摘要方法来防止过量推送,这可能就足够了。

字体是最明显的问题,因为必须通过不添加认证信息的连接加载字体,但一些浏览器的问题会导致字体使用不同的连接。

高效使用HTTP/2推送的关键是利用连接未被使用时的空闲带宽。这对于需要花很长时间在服务端生成的网页,性能提升会很大;而对于静态网页,没那么明显。

预加载[32]可以告诉浏览器,页面需要一个资源,而不用等浏览器自己发现它需要这个资源。

还可以使用预加载从其他域加载资源,但使用HTTP/2推送,只能推送自己域名下的资源。

Fastly的Hooman Beheshti分析表明,2018年2月只有0.02%的网站使用HTTP/2推送

给浏览器返回103响应(如果浏览器支持的话)。然后浏览器可以使用预加载的HTTP link首部来预加载资源

也许等到103 Early Hints首部得到更好的支持的时候,过量推送的问题已经可以使用缓存摘要或者类似技术解决了,那时开发者就会有两个选择。

更早地推送关键资源以加速页面加载,取代内联资源

HTTP/2推送是HTTP/2中的一个新概念,它允许为一个请求返回多个响应。 HTTP/2推送被提议时,目的是作为内联关键资源的替代方案。

很多服务器和CDN通过使用HTTP link首部实现HTTP/2推送。

新的103状态码可用来更早提供link首部。

HTTP/2推送在客户端的实现方式可能没有那么显而易见。

  • 很容易就会推送过多的内容,这会降低网站性能。
  • HTTP/2推送带来的性能提升可能没那么大,但是风险很高。
  • 相较于使用推送,配合使用预加载和103状态码可能更好。
  • HTTP/2推送可能有其他应用场景,但有些需要更改协议。

第6章 HTTP/2优化 #

HTTP/2请求依然有开销

图6.1 当需要HTTP资源时,浏览器的处理过程

带有async或者defer属性的JavaScript资源(一般用来解决性能问题,防止阻塞)会被限流,然而不带这两个属性就不会。

HTTP/2不是没有限制

尽管SETTINGS_MAX_CONCURRENT_STREAMS参数默认是无限大的,但很多实现也会为其添加限制

越大的资源压缩越有效

带宽限制和资源竞争

域名分片,当HTTP/2应用更普遍的时候,应减少域名分片的使用。

6.2.6 内联资源

建议,仍然保持同域名的请求不超过100个。如果去掉文件合并会有几百个文件的话,则不要去掉文件合并,但是可以利用新的下载数量限制来更合理地打包代码。

HTTP协议优化并不是提高网站性能的唯一技术,但是由于Web的性质(通常涉及客户端和服务器之间的交互),网络性能优化在很大程度上是优化网络层的应用。

6.3.1 减少要传输的数据量

图像质量

图像宽高

压缩文本数据

gzip仍然是最流行的压缩技术[17],尽管brotli[18]等压缩算法正变得越来越流行。brotli提供更好的压缩率(取决于设置[19]),因此可以带来更多的收益。

不管你使用哪个版本的HTTP,都应该压缩HTTP正文。内容编码会通过content-encoding HTTP首部告诉浏览器,这和在HTTP/1.1中一样。

压缩HTTP首部

最小化代码

gzip压缩(或类似的压缩)是主要的优化方法。最小化代码给你带来的提升比较小。

需要最小化代码的另一种情况是混淆。在限制使用的场景中,我们尝试使用混淆来隐藏一些逻辑,因为反解最小化之后的代码比较烦琐。

6.3.2 使用缓存防止重复发送数据

如果你看到from memory cache,而不是from disk cache,则说明你是从另外一个Wikipedia页面,而不是从其他网站过来的,那这个时候相关的资源已经在最近的内存缓存中了。

使用上次发送页面时的修改时间(if-modified-since)或者eTag值(if-none-match)做比较。”eTag值比日期值更好用。它的值取决于具体实现,比如可以是内容的哈希值。如果同时提供了两个值(见图6.11),则if-none-match首部中的eTag值更优先。

我认为网站应该缓存页面一小段时间,至少在使用HTTP/2时,因为304响应比200响应的开销低很多。

类似地,在服务端使用缓存防止端点服务器频繁请求源站,这会带来显著的性能提升,即使缓存很短的时间

6.3.3 Service Worker可以大幅减少网络加载

Service Worker可以查看、回复,或者更改HTTP请求。

当网站使用Service Worker时,Service Worker可以中断请求,当离线时,其可以返回一个之前缓存的资源版本。这就使得在离线时也能使用缓存的网站,像移动端应用一样。

6.3.4 不发送不需要的内容

6.3.5 HTTP资源暗示

HTTP资源暗示是资源暗示的一部分[30],其可以用来深入优化HTTP的使用

每个暗示使用link HTTP首部或者HTML中的<link>标签来实现。HTTP资源暗示已经存在相当长的时间了,但是最近才被认可、支持。

DNS查询有一个存活时间(TTL),所以网站不能过早地进行DNS查询(比如查询你在下一个页面会用到的域名),因为当TTL过期时,DNS查询可能要再重复一次。通常会有300秒或者更长的TTL,并且理想情况下,你的网页加载时间不会超过5分钟,所以它对于你的页面资源来说是安全的。

preconnect(提前连接)做了进一步延伸。它除了提前做DNS解析以外,还提前创建连接,这可以节省创建新的连接时的TCP和HTTPS开销。

。不要太早使用preconnect,如果有段时间不用它,TCP慢启动算法会介入,这会降低传输速度,更糟的是,连接可能会被断开

prefetch(预取)用来加载低优先级的资源。preload试图让当前页面加载更快,而prefetch通常用来给将来要访问的页面加载内容。

因为它加载的资源的优先级很低,所以直到当前页面加载完成时它才会开始加载。它加载的资源会放在缓存中,方便后续使用

preload(预加载)告诉浏览器使用高优先级给本页加载资源。它是preconnect之后的一个本地步骤,但不像prefetch,它给当前页面加载资源。

Web浏览器非常擅长扫描HTML的前面部分,并加载需要的资源,但preload可以让浏览器先下载页面没有直接包含的资源(比如在CSS文件中引用的字体)

很多人建议,使用preload来代替HTTP/2推送,但要确保使用link首部时添加nopush属性(HTML版本不需要使用该属性,因为Web服务器不会使用HTML中的暗示来推送资源)。

当大家都使用103 Early Hints HTTP响应码(见第5章)时,preload会很有用,因为它可以包含HTTP preload link首部(在HTTP/1.1下也可以)。

prerender是开销最大的资源暗示。使用它可以下载整个页面(包含页面需要的其他资源)并提前渲染。这么做的原因是,如果肯定会访问下一个页面,则可以直接把它加载了。当前只有Chrome和IE11支持这个特性[35],但Chrome正打算把它标记为不推荐使用,以后可能不再支持它[36]。过度使用prerender的风险很高,会浪费客户端的带宽和运算资源。

6.3.6 减少最后1公里的延迟

让服务器尽量离浏览器近一些,对于全球化的网站来说,通常要部署离用户更近的服务器网络。这个网络可以是企业自己管理的网络,或者(更常见的)是CDN。

6.3.7 优化HTTPS

优化HTTPS的设置非常重要,可以减少创建HTTPS连接的时间,也可以增加访客安全等级(防止出现浏览器警告)。

HTTPS优化的建议(对HTTP/1或者HTTP/2都适用):

SSLLabs Server Test

实现TLS会话重用

不要过于关注安全问题而影响性能

ssllabs.com网站

TLSv1.3。该协议在2018年8月成为标准

该版本协议有了很大的性能(及安全)提升。

在Nginx中,可以使用类似的方法,通过$server_protocol环境变量添加协议日志:

HTTP负载均衡器(也叫7层负载均衡器,遵守第1章提到的OSI模型)作为HTTP连接的一端,向实际提供服务的Web服务器发起HTTP请求。

这种情况下,应该在负载均衡器上探查协议,而不是在Web服务器上。

TCP负载均衡器(也叫4层负载均衡器)在TCP层工作,其转发TCP包(HTTP消息)到下游的Web服务器。所以,这时HTTP消息是原始消息,可以在Web服务器上探查协议。

大多数Web服务器都提供各种环境变量[51],可以使用它们判断使用的协议,以及调整服务器配置。

客户端应用可能也想知道你在使用HTTP/1还是HTTP/2。当前没有标准的方法可以获取此信息,但是Resource Timing Level 2 API包含一个nextHopProtocol属性[53],其提供了相关的信息

一般在服务端检测所使用的协议,并将该信息返回给客户端比较好。

HTTP/2规范允许多个域名使用同一个HTTP/2连接,前提是它们是authoritative(官方)的域名[54]。就是说,这些域名被解析到同一个IP地址,并且HTTPS证书中同时包含这些域名

首先,只有在这些域名指向同一台服务器时连接合并才生效。如果服务器不同,就会使用不同的连接。另外一个问题是,浏览器要实现连接重用的功能,而有些浏览器并没有这么做[55]。规范只是说连接可以被重用,但是没说一定要重用。在撰写本书时,Safari和Edge不会做合并,而Chrome和Firefox会。

可以使用新的HTTP状态码421来解决这个问题,服务器可以用它友好地通知浏览器,其使用了错误的连接,并且要再看一下应该向哪个服务器发送请求。

还有一个方法,服务器可以使用ORIGIN帧[57]告诉客户端它是哪些域的官方服务器,不需要客户端再猜。在撰写本书时

简单地说,连接合并比较复杂,所以不建议依赖此项特性。反而,要看是否需要域名分片,看它是否会带来性能提升。如果想要保留域名分片的功能,最好在不同的服务器上使用该功能,以防止连接合并带来复杂问题。

第7章 高级HTTP/2概念 #

每下载一次资源创建一个HTTP/2流,下载完成后这个流会被丢弃,这是HTTP/2流不完全等同于HTTP/1.1连接的一个原因。

因为流不会被重用

在流传输完它的资源之后,流会被关闭。当请求新资源时,会启用一个新的流。流是一个虚拟的概念,它是在每个帧上标示的一个数字,也就是流ID。所以关闭或创建一个流的开销,远小于创建HTTP/1.1连接(包含TCP三次握手,可能在发送请求之前还有HTTPS协议协商)的开销。

HTTP/2的流会经过一些生命周期状态。客户端发送HEADERS帧以开启一个HTTP请求(比如GET请求),服务器响应此请求,然后流结束。这个过程经历如下状态:

空闲。流刚被创建或者引用时的状态。

打开。当流被用以发送HEADERS帧时,就是打开的状态,此时流可以用来做双向的消息传递。只要客户端还在发送数据,流都保持在这个状态。因为大多HTTP/2请求只包含一个HEADERS帧,当这个帧被发送完成时,流就可能进入下一状态:半关闭。

半关闭。当客户端使用END_STREAM标志位,表明请求的HEADERS帧已经包含了请求的所有数据时,流就变成半关闭的状态,此时流只能被用来给客户端发送响应数据,客户端不能使用它再发送数据(除非像WINDOW_UPDATE这种控制帧)。

关闭。当服务器完成数据发送,并在最后一个帧上使用END_STREAM标志时,流就变成关闭状态,此时不可以再使用流。

新的承诺的流会经过一个类似的状态流转过程:

空闲。当承诺(要推送)的流最初被创建,或者被另外一个流上的PUSH_PROMISE帧引用的时候的状态。

保留。推送流直接进入保留状态,直到服务器准备好要推送的资源。你知道流已经存在(所以它起码是空闲的),其将被用于发送指定的资源(此时就不是空闲状态了,所以是保留的状态),但是没有具体资源的详细信息,就像第一个示例中,在接收到HEADERS帧之后的状态。但是因为它只用于推送资源,所以这个流永远不会是打开状态,因为你不会想让客户端在这个流上发送数据。当推送流发送完HEADERS帧之后(在原始的流上发送完PUSH_PROMISE帧之后),它会从保留态变成半关闭状态。明显,半关闭状态是下一个状态。

半关闭。当服务器开始推送响应时,承诺的(推送)流进入半关闭状态,流只能用于发送推送的数据。

关闭。当服务器发送完数据,在最后一个DATA帧上使用END_STREAM标志时,流会变成关闭状态,此时不能再使用流。

 HTTP/2流状态

HTTP/2中的流量控制和TCP方法类似。在连接开始时(使用SETTINGS帧),确定流量控制窗口大小(如果不指定,默认为65 535个8位字节)。然后每次都会从总量中减去发送的数据的大小,而后再将接收到的响应数据(通过WINDOW_UPDATE帧)大小加回去。

流量控制在DATA帧上(将来新的HTTP/2帧也可能会被纳入流量控制)使用。当客户端不再发送确认帧时,还可以发送控制帧(特别是用来控制流量的WINDOW_UPDATE帧)。

这个帧说明,Facebook准备接收10 420 225个8位字节,因为这个帧使用的流ID为0,所以这是会应用到所有流的连接层的限制,其是流本身的流量控制之外的限制。流0不得用于DATA帧,也不需要有自己的流量控制,这也是为什么它可以用来做连接层的流量控制的原因。

具体什么时候发送WINDOW_UPDATE帧(在每个DATA帧被消费完?接近限制的时候?还是周期性的?)取决于客户端。nghttp在消费的数据达到窗口大小的一半时发送此帧

使用nghttp的-w和-W参数,来设置不同的初始窗口大小

流优先级由请求方指定(比如客户端),但由响应方(比如服务器)最终决定发送什么帧。所以,优先级是一种建议或提示,完全由响应方决定要不要忽略优先级,并且以响应方认为的顺序返回数据。

客户端(如浏览器)可能会决定优先级。也可能服务端会覆盖这些配置(见7.3.4节),但大多数情况下,采用客户端的优先级。

HTTP/2定义了两种不同的方法来设置优先级:• 流依赖• 流权重

所有的流都默认依赖于流0(图7.3中未显示),它是控制流,没有依赖。

使用流优先级的目的是尽量高效利用连接,而不是作为一种阻塞机制。

HTTP/2依赖模型不支持多依赖(虽然和权重的方法类似)。

另外一个有助于定义流优先级的概念是流权重,其用于给两个依赖同一个父资源的请求设定优先级。相比于假定同一个依赖水平的资源权重相同,流权重可以支持更复杂的场景

假流仅用于优先级排序,永远不会被用来直接发送请求

nghttp流优先级

该设置最早来自于Firefox依赖树。关键CSS和JavaScript依赖于流3,非关键的JavaScript依赖于流5,其他的依赖于流11。

事实是,优先级问题本身就是复杂的,同时支持依赖和权重,或者两者混合,可以大大增加优先级的灵活性。使用后来增加的仅用于设置优先级的流,我们可以设计出更多实现方案。

对于支持优先级策略的服务器,普遍的思路是让客户端来指定请求的优先级,而不是使用服务端优先级配置。

nghttp是基于Firefox实现

Chromium团队的理由是,大多数请求在完整的资源(除了HTML和渐近式JPEG图片)被接收到之前都不可用,所以通常情况下,在一个连接上发送多个资源没有意义。

第8章 HPACK首部压缩 #

HTTP/1总是支持HTTP正文压缩,但是到了HTTP/2之后才能压缩HTTP首部。

压缩之后再加密小的数据更好一些。

一些压缩是有损压缩,因为用不到,所以数据中的一些细节可以忽略。这类的压缩通常用于媒体内容如音乐文件、图像和视频的压缩,可以对它们进行大量压缩,而不会使数据的整体含义受损。但如果压缩太多,会丢掉细节,这时候像图片就不能再放大

无损压缩的工作方式是,移除那些在解压时可以很容易恢复的重复数据。有三种方法可以实现这个功能:• 查表法• 更高效的编码技术• lookback(反查)压缩

只有表里的值经常重复时查询表才有意义。

静态查询表

动态查询表

变长编码

固定长度的编码。比如ASCII编码,使用7位来表示字符

。Unicode(UTF-8和UTF-16)在某种程度上使用这种方式,针对不同使用频率的字符,分配不同的字符区间(1~4个8位字节)。这种方式复杂的地方在于识别字符之间的边界(因为这个格式的字符的长度不是固定的7位)。

Huffman编码是更进一步的可变长度编码。它的工作原理是根据每个值的使用频率为其分配一个唯一代码,并且保证没有一个代码是其他代码的前缀。

Huffman代码压缩是查询表的扩展。像本章之前讨论的常规的查询表一样,Huffman表可以基于数据可能相似的已知结构(如英文文本)提前定义,也可以基于要加密的数据动态生成,或者可以同时使用两种方式。

反查压缩在当前位置放置引用,指向重复文本

HTTP正文压缩通常用于文本数据。媒体数据一般通过指定的格式提前压缩过了,不需要再压缩。

服务器和浏览器使用的技术(deflate、gzip和brotli)很相似,它们都是基于deflate算法的变种。

基于deflate的压缩算法有一个主要问题:被证实是不安全的。它的问题是,你可以使用数据长度来猜测内容,特别是当你能影响内容中的一部分时。

CRIME(Compression Ratio Info-leak Made Easy)[3]。这种攻击是针对SPDY的,SPDY使用gzip做HTTP首部压缩。

HPACK

它基于查询表和Huffman编码,但(关键)不是基于反查的压缩方法。

首部压缩是HTTP/2的一部分,并且首部压缩是有状态的

HPACK格式被有意保持简单且死板。这两个特征都会减少因为实现错误所带来的互操作风险,或者安全问题。没有定义扩展机制,如要改变当前格式,只能使用一个完整的替代品。

HPACK有一个静态表,包含61个常见的HTTP首部名称

除了静态表以外,HPACK还有一个连接级的动态表,从位置62开始(跟在静态表之后),最大到SETTINGS帧的SETTINGS_HEADER_TABLE_SIZE所定义的大小。如果没有明确指定,默认是4 096个8位字节。当到达表的最大尺寸时,最老的记录会被删除。

所以在HTTP/2中,每个TCP连接有一个唯一的动态表。

带递增索引的字符串首部字段

不索引的字符串首部字段

从不索引的字符串首部字段

这个值一定不能在任何后续的重新编码流程中被添加到动态表

cookie是敏感数据,并且看起来正是最后这种类型适用的对象。不存储的缺点是,对后续请求cookie的压缩减少了。cookie会很大,而且重复,所以理想情况下,应该将它们压缩。

对于一些值,Huffman编码的结果会比ASCII编码更大。

只要编码能够使用更少的字节来表达对应的值,就可以使用。

但一般而言,Huffman编码通常比ASCII编码更高效。有一部分原因是,虽然ASCII编码只需要7位,但它使用完整的8位字节,因此每个ASCII编码都浪费1位。Huffman编码可以是变长的,因此理论上不会浪费。然而,在这种变长编码中,较少使用的字符所占空间多于8位,所以如果都是较少使用的字符,反而使用ASCII编码更高效。按照定义,这些字符应该很少被用到(假设HPACK Huffman编码表反映了实际使用情况)。最后,从静态或动态表中查找总是比Huffman或ASCII格式编码更高效。

有4种首部类型:• 索引首部字段类型(开头是1)• 带递增索引的字符串首部字段(开头是01)• 不索引的字符串首部字段(开头是0000)• 从不索引的字符串首部字段(开头是0001)

首部被添加到了动态表中,使用的索引号为62。因为静态时索引到61,所以62是下一个可用的值。

动态表可以包含重复的条目(有相同的名称和值的条目)。所以解码器不能将重复的条目当作错误处理。

大多数用户的网络连接上传带宽要小于下载带宽,而且请求资源是第一步,因此对请求方来说,收益更高。

HPACK是一种压缩格式,是专门为HTTP/2的HTTP首部压缩实现的。

第4部分 HTTP的未来 #

QUIC是一个新的协议,目标是持续完成HTTP/2的工作,并解决TCP层的性能问题。

第9章 TCP、QUIC和HTTP/3 #

由TCP(Transmission Control Protocol)

TCP在两端(通常指浏览器和Web服务器)之间创建一个连接,然后处理消息传递,并确保消息到达。当消息丢失时处理重传,并确保消息在传递给应用层(如HTTP)之前是有序的。

TCP给每个TCP数据包分配一个序列号,如果数据包在到达时顺序不对,则进行重排;如果丢失了部分数据包,则根据序列号来重新请求。

TCP运行的基本方式会导致5个主要的问题,它们至少影响到了HTTP:• 有一个连接创建的延迟。要在连接开始时协商发送方和接收方可以使用的序列号。• TCP慢启动算法限制了TCP的性能,它小心翼翼地处理发送的数据量,以尽可能防止重传。• 不充分使用连接会导致限流阈值降低。如果连接未被充分使用,TCP会将拥塞窗口的大小减小,因为它不确定在上个最优的拥塞窗口之后网络参数有没有发生变化。• 丢包也会导致TCP的限流阈值降低。TCP认为所有的丢包都是由窗口拥堵造成的,但其实并不是。• 数据包可能被排队。乱序接收到的数据包会被排队,以保证数据是有序的。

首个连接的连接延迟,以及后续使用其他HTTP/2连接的连接延迟

TCP的可靠性特征:确保所有TCP数据包有序到达

TCP慢启动机制可以感知TCP在网络上的最佳吞吐量,以免过大冲垮或者危害到网络。TCP采用很谨慎的算法,它以低速率启动并增大到最大速率,在此过程中它会仔细监控连接和数据发送速度以保证能处理所有数据。

一个TCP连接可以发送的数据量取决于拥塞窗口的大小。拥塞窗口开始时很小,对于现代的PC和服务器,开始时发送10个数据包(相当新的变化,很多服务器还使用之前的4个)[2],使用的最大报文长度(MSS)为1460字节,也就是14KB。在慢启动期间,每次往返,拥塞窗口的大小翻倍。当达到最大容量之后,如果没有发生丢包,TCP拥塞控制就进入拥塞避免阶段,随后拥塞窗口还会持续增长,但会变成慢得多的线性增长(不同于慢启动期间的指数级增长),直到它开始看到丢包,认为到了最大容量

因为TCP慢启动的指数增长特性,所以按大多数定义来说它并不慢。实际上,TCP的拥塞避免阶段的增长更慢。

有一个经常被人吹捧的Web性能优化建议,是将所有的关键资源放到HTML的前14KB中。这个理论来自于,前14KB会在TCP的前10个数据包中加载,这样可以避免TCP确认消息的延迟。

将关键资源放到HTML的开始处还是有必要的,但在我看来,在HTTPS或者HTTP/2下,不需要严格要求14KB以内。

连接闲置降低性能

在连接刚启动时和连接闲置时,TCP慢启动算法会导致延迟。TCP比较小心谨慎,在闲置一段时间后,网络情况可能发生变化,所以TCP将拥塞窗口大小降低,重新进行慢启动流程,以再次找到最佳的拥塞窗口大小。

丢包降低TCP性能

TCP还把丢包当成极端事件。它认为这个事件是由容量限制造成的,因而它会做出激烈反应,直接将拥塞窗口大小减半,也就是说会将容量减半(具体取决于所使用的TCP拥塞控制算法)[3]。然后TCP使用拥塞避免算法再次计算速度,并进入拥塞避免阶段

认为丢包完全是由拥堵造成的,然后大幅降低网速,这是不正确的。

丢包带来的影响在HTTP/2中尤其严重,因为它只使用单个连接。在HTTP/2的世界中,一次丢包会导致所有的资源下载速度变慢。而HTTP/1.1可能有6个独立的连接,一次丢包只会减慢其中一个连接,但是另外5个不受影响。

丢包会导致数据排队

如果没有发生其他的丢包,流7和流9会在重传的数据到来之前被完整接收。但这些响应必须排队,因为TCP要保证顺序,所以尽管已经完整下载,script.js和image.jpg还不能被使用。

记住,HTTP/2在大多数场景下比HTTP/1.1要快。难道你能因为一些非常少见的情况,就放弃使用更好的HTTP/2吗?

感谢多路复用,HTTP队首阻塞在HTTP/2中已经不是一个问题了,但是TCP的队头阻塞却成了问题,特别是在容易丢包的环境下。

提高初始拥塞窗口大小TCP慢启动需要一次往返来提升拥塞窗口大小,最早的时候,初始的拥塞窗口大小是1个TCP数据包,几年后这个初始的设置值变为2,然后提升到4,到了Linux内核2.6.39,这个设置值从4增加到了10。这个设置值通常被写死到内核代码中,所以除非升级操作系统,否则不建议修改。

TODO ——

支持窗口缩放

使用SACK

禁止重启慢启动

使用TFO

TFO(TCP Fast Open,TCP快速打开)允许使用TCP三次握手的初始SYN部分发送初始数据包。可以使用这种方法避免与TCP相关的连接创建延迟(

出于安全的原因,这个数据包只能在TCP重连时使用,而不能在初次连接时使用,它同时需要客户端和服务端的支持。

TFO带来的提升真的很显著。Google曾经表示:“通过对网络流量的分析和网络仿真,我们得出TFO能够将HTTP网络延迟降低15%,网页整页加载时间平均降低10%,有时候能降低40%。”[

PRR(Proportional Rate Reduction,按比例降低,从Linux 3.2版本之后成为默认算法)[15]是对CUBIC的增强,当丢包时它减小拥塞窗口,但减小的值不到一半

BBR(Bottleneck Bandwidth and Round-trip propagation time,瓶颈带宽和往返时间),已经有数据表明它可以大幅度提升性能[17],特别是对于HTTP/2连接[

QUIC(发音同quick)是Google(又是Google)发明的一个基于UDP的协议,目标是替换TCP和HTTP栈中的某些部分,以解决本章中提到的低效率因素。HTTP/2引入了一些类似TCP的概念(比如数据包和流量控制),但是QUIC更进一步,替换掉了TCP。

QUIC开始是Quick UDP Internet Connections的缩写,此协议面世时大多数Google Chromium文档中都是这样写的[20],[21],[22]。在标准化的过程中,QUIC工作组决定丢弃这个缩写[23],随后在QUIC的规范中明确声明,“QUIC是一个名字,不是缩写。”

FEC(Forward Error Correction,前向纠错)试图通过在邻近的数据包中添加一个QUIC数据包的部分数据来减少数据包重传的需求。这个想法是,如果只丢了一个数据包,那应该可以从成功传送的数据包中重新组合出该数据包。这

QUIC最初是一个Google的协议,于2013年6月公开发布[39]。Google在此后两年内改进了此协议。2015年6月,该公司将其作为提议标准提交给IETF

两个版本的QUIC:gQUIC和iQUIC

HTTP/2由两个标准组成(HTTP/2和HPACK)

QUIC的主要部分由多个不同的标准组成: • QUIC Invariants —— QUIC中恒定不变的部分 • QUIC Transport —— 核心传输协议 • QUIC Recovery —— 丢包检测和拥塞控制 • QUIC TLS —— QUIC中如何使用TLS加密 • HTTP/3 —— 主要基于HTTP/2,但有一些不同 • QUIC QPACK —— 使用QUIC的HTTP协议的首部压缩

HTTP/2有多种方法来协商HTTP/2协议,包括使用ALPN、Upgrade首部、前置知识,还有Alt-Svc HTTP首部和HTTP/2帧。

因为QUIC是基于UDP的,连接到Web服务器的浏览器必须先使用TCP连接,然后再升级到QUIC[60]。这个过程就需要依赖基于TCP的HTTP,这就抵消了QUIC带来的一个关键好处(大量减少连接创建时间)。

QUIC旨在消除连接层顺序传输数据包的要求,以允许流独立处理。

QPACK引入了一些其他变化。使用一个比特位指定使用的是静态表还是动态表(而不是像HPACK一样显式地从61计数)。此外,可以更简单、更有效地复制首部,这就可以让关键首部(例如:authority和user-agent)保持在动态表的顶部附近,从而使用较少的数据进行传输。

QUIC有两个版本:Google QUIC(gQUIC),当前有少量应用,但没有被标准化;IETF QUIC(iQUIC),正在标准化过程中。

第10章 HTTP将何去何从 #

HTTP/2规范于2015年5月正式成为标准。

有争论说SPDY不应该用作HTTP/2的基础,并且HTTP的隐私问题没有得到解决。此外,还有关于是否要在协议中强制执行加密的争论。

Microsoft的HTTP Speed and Mobility[13](基于SPDY和WebSockets[14])和Network Friendly HTTP Upgrade[15]。这两个提案在很多方面与SPDY类似(不出意外,考虑了构成HTTP/2基础的一些东西),都重点关注添加二进制分帧层和HTTP首部优化。

人们对所有在某领域占据主导地位的公司都存在不信任

HTTP被设计为无状态协议,就算在HTTP/2下,多数情况下它仍然是无状态的。理论上,你对服务器的任何请求,都与之前或之后的请求无关。

HTTP cookie[17]被认为是解决状态问题的方法。cookie是存储在浏览器中的一小段信息,由浏览器自动给每个请求添加。有了cookie,可以使用会话标识符,或其他适用于HTTP的设置。

认为HTTP cookie不好的原因很多,其中包括: • 它们可以用来做广告跟踪(或者用在其他更糟的场景)。 • 默认情况下它们是不安全的。 • 它们会随着每个请求被发送。

欧盟(EU)实施了所谓的cookie法案,根据该法案,网站必须告知用户他们正在使用cookie

GDPR(General Data Protection Regulations,通用数据保护规则)更严格的规定于2018年生效,其中增加了很多关于cookie的屏幕警告。

在默认情况下HTTP cookie不安全。

Secure

HttpOnly

关cookie前缀[21]的提案,其要求使用诸如__Secure开头的cookie名称,以给cookie默认添加Secure属性,从而防止此问题发生。

SameSite

HTTPS的主要问题是,复杂的初始设置和管理,对第三方证书颁发机构(CA)提供证书的依赖[29],以及它的应用不够普遍

机会加密比HTTP要好,但不如HTTPS,但它可以在协议层部署,无须使用第三方CA,不需要站长付出其他努力。

封闭的内部网络风险较小,因此加密内部站点往往优先级较低。

内部站点通常不能使用商业CA(像Let’s Encrypt这样的是自动免费CA),除非它们将自己暴露给公网或使用通配符证书,这种证书更昂贵且不易自动化运行。运行内部CA通常是解决这种问题的方案,但内部CA通常没有自动颁发和续订证书的自动化支持

如Varnish等HTTP缓存

新协议包含重大更新,因此,从任何合理的定义来讲,它都应与先前版本区分开来。

在大多数情况下,简单性比复杂性更好(Keep It Simple Stupid,KISS原则)

QUIC致力于解决HTTP/2无法解决的一些问题,例如TCP队头阻塞、更完整的加密、改进的连接建立流程和连接迁移。

HTTP/2已经被扩充,一些新的设置和帧类型增强了协议,例如替代服务[39]、ORIGIN帧[40]和基于HTTP/2的WebSockets[41]。其他提议,例如缓存摘要[42],试图进一步扩充HTTP/2,还有更多的提议正在着手中。

并非一切都取得了成功,特别是HTTP/2推送,目前为止未能产生任何特殊影响,主要是由于正确使用它较为复杂(见第5章),而且还缺乏服务端支持。

QUIC的定位是更广泛、更通用的传输层协议,不仅仅适用于HTTP,而HTTP/3只是QUIC的一个用例。

现在已经使用ALTSVC、ORIGIN和(提议中的)CACHE_DIGEST帧对协议进行了扩充。也还有其他的提议,例如二级证书[47],因此存在一种强有力的方法来持续扩展协议:使用新的帧类型

WebDAV(Web Distributed Authoring and Versioning,Web分布式创作和版本控制)[51]引入了一些新方法(包括PROPFIND、COPY和LOCK),有一些RFC引入了其他方法[52],但最后一个方法是在2010年注册的(BIND)。

HTTP/2明确禁止使用以冒号(:method、:scheme、:authority、:path和:status)开头的新伪首部[53],但这些字段可以通过新的规范添加(例如:protocol伪首部是在Bootstrapping Websockets over HTTP/2 的RFC中添加的)

CSP(Content-Security-Policy,内容安全策略)

HSTS(HTTP Strict-Transport-Security,HTTP严格传输安全)

Client Hints规范

当第一次使用103时,在许多HTTP实现(例如Web浏览器)中发现了一些问题,这些实现不能接收多个HTTP响应,因为1XX信息响应的应用不够广泛,只在指定情况下才能使用它。从技术上讲,这个变化是一个非破坏性的变化,不需要更新版本。但由于它与现有的状态码略有不同,许多客户端认为这是一个破坏性的变化,直到它们纠正了自己的实现。

HTTP协议的应用远超出其设计初衷。在这个连接一切的世界中,可以使用HTTP,以标准化、易于理解的方式在不同系统之间进行简单通信。从使用REST API及类似技术的复杂应用程序,到物联网设备,HTTP被广泛地应用于Web和非Web应用程序中。

想要使用HTTP的应用程序有几个选择: • 使用HTTP语义和消息来传递非Web流量。 • 使用HTTP/2二进制分帧层。 • 使用HTTP启动另一个协议。

使用HTTPS接口来实现DNS,开发者们可以解决他们争论了几十年的DNS加密问题。该系统称为基于HTTPS的DNS[64]或DoH(尽量不要想到Homer Simpson)。

IETF发布了一个规范,“On the Use of HTTP as a Substrate”[71],列出了以此种方式使用HTTP的一组推荐和最佳实践

Google新的gRPC协议[72],它没有使用像JSON这种效率较低的格式,而是使用HTTP语义和HTTP/2二进制成帧层[73]来实现基于Protobuf[74]的更高效的API

从HTTP/1.1开始,HTTP就有了CONNECT方法,其可以将HTTP连接用作代理隧道,以连接到其他服务器和端口。通常使用此方法通过HTTP代理进行HTTPS连接

代理的这种使用与中间人代理是不同的,后者创建了两个单独的HTTP连接。在这种情况下,只一个HTTPS连接,但有两个TCP连接,所以在设置成功之后,就像客户端直接连接到终端服务器一样。

HTTP/2不支持此方法,因为它需要升级整个HTTP连接,这对只应该升级流的多路复用连接没有意义。因此,在HTTP/2之上的WebSockets应该使用CONNECT方法。

Reference #