在我们更深入地研究 HTTP/3之前,让我们快速了解一下 HTTP 多年来的演变,以便更好地理解为什么需要HTTP/3。
这一切始于1996 年 HTTP/1.0 规范的发布,该规范定义了我们今天所知道的基本 HTTP连接格式(假装 HTTP/0.9 从未存在过)。在 HTTP/1.0中,为客户机和服务器之间的每个请求/响应交换创建一个新的 TCP 连接,这意味着在每个请求之前完成 TCP 和 TLS 握手会导致所有请求有延迟。
更糟糕的是,一旦连接建立,TCP 没有尽可能快地发送所有未完成的数据,而是强制执行一个称为“slow start”的预热周期,这使得 TCP/拥塞控制算法可以在网络路径出现拥塞之前的任何时刻确定可以传输的数据量,并避免网络中充斥着它无法处理的数据包。但是由于新的连接必须经过 slow start 过程,他们不能立即使用所有可用的网络带宽。
HTTP 规范的 HTTP/1.1 版本试图在几年后通过引入“keep-alive”连接的概念来解决这些问题,这种连接允许客户端重用 TCP 连接,从而分摊初始连接建立和多个请求缓慢启动的成本。但这并不是万能的: 虽然多个请求可以共享同一个连接,但它们仍然需要一个接一个地被初始化,因此客户机和服务器在任何给定时间只能为每个连接执行单个请求/响应交换。
随着网络的发展,浏览器在获取和渲染网页时发现自己需要越来越多的并发性,因为每个网站所需的资源数量(CSS、 JavaScript、图片... ...)逐年增加。但是由于 HTTP/1.1 只允许客户机一次进行一个 HTTP 请求/响应交换,因此在网络层获得并发性的唯一方法是并行地使用到同一源的多个 TCP 连接,从而失去了 keep-alive 连接的大部分好处。虽然连接仍然可以在一定程度上重用(但程度要小一些) ,但我们又回到了起点。
最后,十多年后,SPDY 和 HTTP/2 出现了,它们引入了 HTTP“流”的概念: 这是一个抽象概念,允许 HTTP 实现将不同的 HTTP 交换并发到相同的 TCP 连接上,允许浏览器更有效地重用 TCP 连接。
但是,再一次发生了问题,HTTP2 并不是解决问题的银弹!HTTP/2 解决了上面的问题,即低效地使用单个 TCP 连接,因为多个请求/响应现在可以通过同一个连接同时传输。然而,所有的请求和响应都同样受到数据包丢失的影响(例如由于拥塞控制丢失) ,即使丢失的数据只涉及单个请求。这是因为尽管 HTTP/2 层可以在单独的流上隔离不同的 HTTP 交换,但 TCP 并不了解这种情况,它所看到的只是一个没有特定含义的字节流。
TCP 的作用是以正确的顺序将整个字节流从一个端点传递到另一个端点。当携带这些字节的 TCP 数据包在网络上丢失时,它会在流中创建一个空隙,当检测到丢失时,TCP 需要通过重新发送受影响的数据包来填补这个空隙。这样会导致即使它们本身并没有丢失,并且属于一个完全独立的 HTTP 请求。因此,它们也会出现不必要的延迟,因为 TCP 不能知道应用程序是否能够处理它们而没有丢失的数据包。这个问题被称为“head-of-line blocking”。
HTTP 3 的进化
上面我们谈到到了 head-of-line blocking 的问题,这也是 HTTP3 最大改变的地方。它不使用 TCP 作为会话的传输层,而是使用 QUIC,一种新的互联网传输协议,除其他外,它在传输层引入了流作为最优先的传输。QUIC 流共享相同的 QUIC 连接,所以创建新的 QUIC 流不需要额外的握手和缓慢的启动,但是 QUIC 流是独立交付的,因此在大多数情况下,影响一个流的数据包丢失不会影响其他流,因为 QUIC 数据包封装在 UDP 数据包之上。
与 TCP 相比,使用 UDP 提供了更多的灵活性,并且使 QUIC 实现安装在用户空间(user-space)中ーー对协议实现的更新不像 TCP 那样与操作系统更新绑定在一起。使用 QUIC,HTTP 级别的流可以简单地映射到 QUIC 流之上,从而获得 HTTP/2的所有好处,而不会出现“head-of-line blocking”。
QUIC 还将典型的 3-way TCP 握手与 TLS 1.3 的握手相结合。组合这些步骤意味着默认情况下提供了加密和身份验证,并且还支持更快的连接建立。换句话说,即使 HTTP 会话中的初始请求需要一个新的 QUIC 连接,在数据开始流动之前产生的延迟也低于使用 TLS 的 TCP。
但是为什么不在 QUIC 之上使用 HTTP/2,而是创建一个全新的 HTTP 修订版本呢?毕竟,HTTP/2还提供了流多路复用功能。事实证明,情况要比这复杂得多。
虽然 HTTP/2的一些特性可以很容易地映射到 QUIC 之上,但并不是所有的特性都是这样。尤其是 HTTP/2的 header 压缩方案 HPACK,它在很大程度上取决于不同的 HTTP 请求和响应传递到端点的顺序。QUIC 在单个流中强制执行字节的交付顺序,但不保证不同流之间的顺序。
这就导致需要创建一个新的 HTTP 头部压缩方案,称为 QPACK,它修复了问题,但需要对 HTTP 映射进行更改。此外,HTTP/2提供的一些特性(如每流流量控制)已经由 QUIC 本身提供,因此它们被从 HTTP/3 中删除,以消除协议中不必要的复杂性。