【译文】HTTP/3的前世今生(强烈建议收藏阅读)

2,727 阅读23分钟

总结

经过近五年的发展,新的 HTTP/3 协议接近最终形式。早期的迭代已经作为一项实验性功能可用,可以预期 HTTP/3 的可用性和使用将在 2022 年大幅提升。

那么 HTTP/3 到底是什么?为什么在 HTTP/2 之后这么快就需要它?是否可以使用它?它在性能方面有哪些提升?让我们来了解一下。

你可能听过看或者看过一些说法,https3比http2在丢包场景下速度会快的多http3链接具有更少的延迟和连接时间http3可以更快的发送数据并可以并行发送资源。他们常常让 HTTP/3 看起来像是一场性能革命,但它实际上是一个更温和(可用性高!)的进化。因为新协议在实践中可能无法满足这些高期望。我担心这会导致许多人最终感到失望,并让初学者被大量长期存在的盲目、错误信息所迷惑。

我很害怕这一点,因为我们已经看到 HTTP/2 发生了完全相同的情况。它被誉为令人惊叹的性能改进,具有令人兴奋的新功能,例如服务器推送、并行流和优先级。可以停止捆绑资源,停止在多个服务器之间分片我们的资源,并大大简化页面加载过程。只需轻轻一按,网站速度就会神奇地提高 50%!

五年后,我们知道,服务器推送并没有真正的工作在实践中,数据流和优先级往往是不好实现的,并且,因此(降低)资源、 捆绑甚至分片在某些情况下都还是不错的做法

同样,其他调整协议行为的机制,例如预加载提示,通常包含隐藏的 深度错误,使它们难以正确使用。

因此,我认为防止此类错误信息和这些不切实际的期望也传播到 HTTP/3 非常重要。

TCP 是主要协议,它提供关键服务,如可靠性和向其他协议(如 HTTP)的有序交付。这也是我们可以继续与许多并发用户一起使用 Internet 的原因之一,因为它巧妙地将每个用户的带宽使用限制为他们的公平份额。

为什么我们需要 HTTP/3

“为什么我们在 2015 年才标准化的 HTTP/2 之后这么快就需要 HTTP/3?” 这确实很奇怪,直到您意识到我们实际上并不需要一个新的 HTTP 版本,而是底层传输控制协议(TCP)的升级。

TCP 是主要协议,它提供关键服务,如可靠性和向其他协议(如 HTTP)的有序交付。这也是我们可以继续与许多并发用户一起使用 Internet 的原因之一,因为它巧妙地将每个用户的带宽使用限制为他们的公平份额。

你可知道?

使用 HTTP(S) 时,您实际上是在同时使用除 HTTP 之外的多种协议。这个“堆栈”中的每个协议都有自己的特性和职责(见下图)。例如,当 HTTP 处理 URL 和数据解释时,传输层安全 (TLS) 通过加密确保安全,TCP 通过重新传输丢失的数据包来实现可靠的数据传输,而互联网协议 (IP) 将数据包从一个端点路由到另一个跨不同设备的端点之间(中间盒)。

这种在协议之上的“分层”是为了允许轻松重用它们的功能。高层协议(如 HTTP)不必重新实现复杂的功能(如加密),因为低层协议(如 TLS)已经为它们做这些了。再举一个例子,Internet 上的大多数应用程序在内部使用 TCP 来确保它们的所有数据都得到完整传输。因此,TCP 是 Internet 上使用和部署最广泛的协议之一。

HTTP/2 与 HTTP/3 协议栈比较

image.png

HTTP/2 与 HTTP/3 协议栈比较

什么是QUIC?

QUIC 与 TLS 深度集成

TLS(传输层安全协议)负责保护和加密通过 Internet 发送的数据。当您使用 HTTPS 时,您的明文 HTTP 数据首先由 TLS 加密,然后由 TCP 传输。 image.png

在互联网的早期,加密流量在处理方面的成本非常高。此外,也并非所有用例都需要它。从历史上看,TLS 是一个完全独立的协议,可以选择性地在 TCP 之上使用。这就是我们区分 HTTP(无 TLS)和 HTTPS(有 TLS)的原因。

随着时间的推移,我们对互联网安全的态度当然已经转变为“默认安全”。因此,虽然理论上 HTTP/2 可以在没有 TLS 的情况下直接通过 TCP 运行(这甚至在 RFC 规范中定义为明文 HTTP/2),但实际上没有(流行的)Web 浏览器支持这种模式。在某种程度上,浏览器供应商有意识地以牺牲性能为代价来获得更高的安全性。

鉴于这种向永远在线的 TLS(尤其是网络流量)的明显演变,QUIC 的设计者决定将这一趋势提升到一个新的水平也就不足为奇了。他们没有简单地为 HTTP/3 定义明文模式,而是选择将加密深深地嵌入 QUIC 本身。虽然第一个谷歌特定版本的 QUIC 为此使用了自定义设置,但标准化的 QUIC 直接使用现有的 TLS 1.3 本身。

为此,它打破了协议栈中协议之间典型的清晰分离,正如我们在上图中所见。虽然 TLS 1.3 仍然可以在 TCP 之上独立运行,但 QUIC 反而封装了 TLS 1.3。换句话说,没有 TLS 就无法使用 QUIC;QUIC(以及扩展为 HTTP/3)始终是完全加密的。此外,QUIC 还加密了几乎所有的数据包头字段;QUIC 中的中介无法再读取传输层信息(例如数据包编号,这些数据从未为 TCP 加密)(甚至某些数据包标头标志已加密)。

与 TCP + TLS 不同,QUIC 还在数据包头和有效载荷中加密其传输层元数据。(注意:字段大小不按比例缩放。) image.png

这种方法为 QUIC 提供了几个好处:

  1. QUIC 对其用户更安全。
    没有办法运行明文 QUIC,因此攻击者和窃听者监听的选项也更少。(最近的研究表明HTTP/2 的明文选项多么危险。)
  2. QUIC 的连接建立速度更快。
    对于 TLS-over-TCP,两种协议都需要自己单独的握手,而 QUIC 将传输和加密握手合二为一,节省了往返(见上图)。我们将在第 2 部分中更详细地讨论这一点。
  3. QUIC 可以更容易地进化。
    因为它是完全加密的,网络中的中间件不能再像使用 TCP 那样观察和解释其内部工作原理。因此,它们也不会因为更新失败而在较新版本的 QUIC 中(意外地)中断。如果我们想在未来为 QUIC 添加新功能,我们“只需要”更新终端设备,而不是所有的中间件。

然而,除了这些好处之外,广泛加密也有一些潜在的缺点:

  1. 许多网络会犹豫是否允许 QUIC。
    公司可能希望在他们的防火墙上阻止它,因为检测不需要的流量变得更加困难。ISP 和中间网络可能会阻止它,因为不再容易获得诸如平均延迟和丢包率之类的指标,从而使检测和诊断问题变得更加困难。这一切都意味着 QUIC 可能永远不会普遍可用,我们将在第 3 部分详细讨论。

  2. QUIC 具有更高的加密开销。
    QUIC 使用 TLS 加密每个单独的数据包,而 TLS-over-TCP 可以同时加密多个数据包。对于高吞吐量场景,这可能会使 QUIC 变慢(我们将在第 2 部分中看到)。

  3. QUIC 使网络更加集中。
    我经常遇到的抱怨是,“谷歌正在推动 QUIC,因为它让他们可以完全访问数据,同时不与其他人共享任何数据”。我大多不同意这一点。首先,与 TLS-over-TCP 相比,QUIC 不会向外部观察者隐藏更多(或更少!)用户级信息(例如,您正在访问哪些 URL)(QUIC 保持现状)。

    其次,虽然谷歌发起了 QUIC 项目,但我们今天讨论的最终协议是由互联网工程任务组 (IETF) 中更广泛的团队设计的。IETF 的 QUIC 在技术上与谷歌的 QUIC 非常不同。尽管如此,IETF 的成员大多来自谷歌和 Facebook 等大公司以及 Cloudflare 和 Fastly 等 CDN,这是事实。由于 QUIC 的复杂性,主要是那些拥有正确和高效部署所需专业知识的公司,例如,实践中的 HTTP/3。这可能会导致这些公司更加集中,这一个真正的问题。

拓展

这里的关键是 QUIC默认深度加密的。这不仅提高了其安全性和隐私特性,而且有助于其可部署性和可进化性。它使协议运行起来更重一些,但作为回报,它允许其他优化,例如更快的连接建立。

QUIC 支持多个独立的字节流

TCP 和 QUIC 之间的第二个大区别是技术性的,我们将在第 2 部分中更详细地探讨其影响。不过,就目前而言,我们可以以高层次的方式理解主要方面。

你可知道?

首先考虑即使是一个简单的网页也是由许多独立的文件和资源组成的。有 HTML、CSS、JavaScript、图像等等。这些文件中的每一个都可以看作是一个简单的“二进制 blob”——浏览器以某种方式解释的零和一的集合。当通过网络发送这些文件时,我们不会一次全部传输它们。相反,它们被细分为更小的块(通常每个块大约 1400 字节)并在单独的数据包中发送。因此,我们可以将每个资源视为一个单独的“字节流”,因为数据会随着时间的推移被逐段下载或“流式传输”。

对于 HTTP/1.1,资源加载过程非常简单,因为每个文件都有自己的 TCP 连接并完整下载。例如,如果我们有文件 A、B 和 C,我们将有三个 TCP 连接。第一个将看到 AAAA 的字节流,第二个 BBBB,第三个 CCCC(每个字母重复是一个 TCP 数据包)。这有效但也非常低效,因为每个新连接都有一些开销。

在实践中,浏览器对可以使用的并发连接数量(以及可以并行下载的文件数量)施加了限制——通常,每页加载 6 到 30 个。一旦前一个文件完全传输,连接将被重新使用以下载新文件。这些限制最终开始阻碍现代页面的 Web 性能,这些页面通常加载 30 多个资源。

改善这种情况是 HTTP/2 的主要目标之一。该协议不再为每个文件打开一个新的 TCP 连接,而是通过单个 TCP 连接下载不同的资源。这是通过 “多路复用”不同的字节流来实现的。这是一种奇特的说法,即我们在传输不同文件时混合了不同文件的数据。对于我们的三个示例文件,我们将获得单个 TCP 连接,传入的数据可能看起来像 AABBCCAABBCC(尽管许多其他排序方案也是可能的)。这看起来很简单,实际上工作得很好,使 HTTP/2 通常与 HTTP/1.1 一样快或快一点,但开销要少得多。

让我们仔细看看区别:

image.png

但是,TCP 端存在问题。你看,因为 TCP 是一个更老的协议,而不是仅仅用于加载网页,它不知道 A、B 或 C。在内部,TCP 认为它只传输一个文件X,它并不知道请注意它所视为的 XXXXXXXXXXXX 实际上是 HTTP 级别的 AABBCCAABBCC。在大多数情况下,这无关紧要(它实际上使 TCP 非常灵活!),但是当网络上出现数据包丢失等情况时,情况就会发生变化。

假设第三个 TCP 数据包丢失(包含文件 B 的第一个数据的数据包),但所有其他数据都已传送。TCP 通过在新数据包中重新传输丢失数据的新副本来处理这种丢失。然而,这种重传可能需要一段时间才能到达(至少一个 RTT)。您可能认为这不是什么大问题,因为我们看到资源 A 和 C 没有损失。因此,我们可以在等待 B 丢失数据的同时开始处理它们,对吗?

可悲的是,事实并非如此,因为重传逻辑发生在 TCP 层,而 TCP 不知道 A、B 和 C!TCP 反而认为单个 X 文件的一部分已经丢失,因此它觉得它必须保持 X 的其余数据不被处理,直到空洞被填满。换句话说,虽然在 HTTP/2 级别,我们知道我们已经可以处理 A 和 C,但 TCP 不知道这一点,导致事情比潜在的。这种低效率是“队头 (HoL) 阻塞”问题的一个例子。

解决传输层的 HoL 阻塞是 QUIC 的主要目标之一。与 TCP 不同,QUIC 非常清楚它正在复用多个独立的字节流。当然,它不知道它正在传输 CSS、JavaScript 和图像;它只知道流是分开的。因此,QUIC 可以在每个流的基础上执行丢包检测和恢复逻辑。

在上面的场景中,它只会阻止流 B 的数据,并且与 TCP 不同,它会尽快将 A 和 C 的任何数据传递到 HTTP/3 层。(这在下面进行了说明。)理论上,这可能会导致性能改进。然而,在实践中,这个故事要微妙得多,我们将在第 2 部分中讨论。

QUIC 允许 HTTP/3 绕过队头阻塞问题

image.png

我们可以看到,我们现在在 TCP 和 QUIC 之间有了根本的区别。顺便说一句,这也是我们不能像在 QUIC 上那样运行 HTTP/2 的主要原因之一。正如我们所说,HTTP/2 还包含在单个 (TCP) 连接上运行多个流的概念。因此,HTTP/2-over-QUIC 将有两个不同且相互竞争的流抽象。

让它们很好地协同工作将非常复杂且容易出错;因此,HTTP/2 和 HTTP/3 之间的主要区别之一是后者删除了 HTTP 流逻辑并重用了 QUIC 流。但是,正如我们将在第 2 部分中看到的那样,这会对如何实现服务器推送、标头压缩和优先级等功能产生其他影响。

拓展

这里的关键点是 TCP 从未设计为通过单个连接传输多个独立文件。因为这正是网页浏览所需要的,所以多年来这导致了许多低效率。QUIC 通过将多字节流作为传输层的核心概念并在每个流的基础上处理数据包丢失来解决这个问题。

QUIC 使用连接 ID

QUIC 的第三个主要改进是连接可以保持更长时间。

你可知道?

在谈论 Web 协议时,我们经常使用“连接”的概念。然而,究竟什么是连接?通常,一旦两个端点(例如,浏览器或客户端和服务器)之间发生握手,人们就会谈论 TCP 连接。这就是为什么 UDP 经常(有些被误导)被称为“无连接”的原因,因为它不进行这样的握手。但是,握手实际上并没有什么特别之处:它只是发送和接收具有特定形式的几个数据包。它有几个目标,其中主要是确保对方有一些东西,并且愿意并且能够与我们交谈。这里值得重申的是,QUIC 也执行握手,即使它运行在 UDP 上,而 UDP 本身并没有。

那么,问题就变成了,这些数据包如何到达正确的目的地?在 Internet 上,IP 地址用于在两台唯一的机器之间路由数据包。然而,仅仅拥有手机和服务器的 IP 是不够的,因为两者都希望能够在每一端同时运行多个联网程序。

这就是为什么每个单独的连接还在两个端点上分配一个端口号以区分连接及其所属的应用程序。服务器应用程序通常根据其功能具有固定的端口号(例如,HTTP(S) 的端口为 80 和 443,DNS 的端口号为 53),而客户端通常为每个连接(半)随机选择其端口号。

因此,要定义跨机器和应用程序的唯一连接,我们需要这四件事,即所谓的四元组:客户端 IP 地址 + 客户端端口 + 服务器 IP 地址 + 服务器端口

在 TCP 中,连接仅由 4 元组标识。因此,如果这四个参数中只有一个发生变化,连接就会失效,需要重新建立(包括新的握手)。要理解这一点,请想象一下停车场问题:您目前正在建筑物内使用带有 Wi-Fi 的智能手机。因此,您在此 Wi-Fi 网络上有一个 IP 地址。

如果您现在搬到室外,您的手机可能会切换到蜂窝 4G 网络。因为这是一个新网络,它将获得一个全新的 IP 地址,因为这些地址是特定于网络的。现在,服务器将看到来自客户端 IP 的 TCP 数据包,它以前从未见过(当然,两个端口和服务器 IP 可以保持不变)。这如下图所示。

TCP 的停车场问题:一旦客户端获得新 IP,服务器就无法再将其链接到连接。

image.png

但是服务器如何知道这些来自新 IP 的数据包属于“连接”?它如何知道这些数据包不属于蜂窝网络中选择相同(随机)客户端端口(这很容易发生)的另一个客户端的连接?可悲的是,它无法知道这一点。

因为 TCP 是在我们甚至梦想蜂窝网络和智能手机之前发明的,例如,没有机制允许客户端让服务器知道它已经更改了 IP。甚至没有办法“关闭”连接,因为发送到旧 4 元组的 TCP 重置或 fin 命令甚至不会再到达客户端。因此,在实践中,每次网络更改都意味着无法再使用现有的 TCP 连接

必须执行新的 TCP(也可能是 TLS)握手以建立新连接,并且根据应用程序级协议,需要重新启动进程中的操作。例如,如果您通过 HTTP 下载大文件,则可能必须从一开始就重新请求该文件(例如,如果服务器不支持范围请求)。另一个例子是实时视频会议,在切换网络时您可能会遇到短暂的停电。

请注意,4 元组可能会发生变化的其他原因(例如,NAT 重新绑定),我们将在第 2 部分中详细讨论。

因此,重新启动 TCP 连接会产生严重影响(等待新的握手、重新启动下载、重新建立上下文)。为了解决这个问题,QUIC 引入了一个名为连接标识符 (CID) 的新概念。每个连接都在 4 元组的顶部分配了另一个编号,用于在两个端点之间唯一标识它。

至关重要的是,因为这个 CID 是在 QUIC 本身的传输层定义的,所以在网络之间移动时它不会改变!这如下图所示。为了使这成为可能,CID 包含在每个 QUIC 数据包的前面(很像 IP 地址和端口也存在于每个数据包中)。(它实际上是 QUIC 数据包标头中为数不多的未加密的内容之一!)

QUIC 使用连接标识符 (CID) 来允许连接在网络更改后继续存在 image.png

通过这种设置,即使 4 元组中的某一项发生变化,QUIC 服务器和客户端只需查看 CID即可知道它是同一个旧连接,然后他们就可以继续使用它。不需要新的握手,下载状态可以保持完整。此功能通常称为连接迁移。从理论上讲,这对性能更好,但正如我们将在第 2 部分中讨论的那样,当然,这又是一个微妙的故事。

CID 还需要克服其他挑战。例如,如果我们确实只使用一个 CID,那么黑客和窃听者就可以非常容易地跨网络跟踪用户,进而推断出他们的(近似)物理位置。为了防止这种隐私噩梦,每次使用新网络时QUIC 都会更改 CID

不过,这可能会让您感到困惑:我刚才不是说 CID 应该跨网络相同吗?嗯,这过于简单化了。内部真正发生的事情是客户端和服务器就一个共同的(随机生成的)CID 列表达成一致,这些CID都映射到相同的概念“连接”。

例如,他们都知道 CID K、C 和 D 实际上都映射到连接 X。因此,虽然客户端可能在 Wi-Fi 上使用 K 标记数据包,但它可以在 4G 上切换到使用 C。这些公共列表在 QUIC 中协商完全加密,因此潜在的攻击者不会知道 K 和 C 真的是 X,但客户端和服务器会知道这一点,并且他们可以保持连接活跃。

QUIC 使用多个协商连接标识符 (CID) 来防止用户跟踪

image.png

它变得更加复杂,因为客户端和服务器将拥有自己选择的不同 CID 列表(就像它们具有不同的端口号一样)。这主要是为了支持大规模服务器设置中的路由和负载平衡,我们将在第 3 部分中详细介绍。

拓展

这里的关键要点是,在 TCP 中,连接由四个参数定义,当端点改变网络时,这些参数会改变。因此,有时需要重新启动这些连接,从而导致一些停机时间。QUIC 向混合中添加了另一个参数,称为连接 ID。QUIC 客户端和服务器都知道哪些连接 ID 映射到哪些连接,因此对网络变化更加健壮。

QUIC 使用框架

QUIC 的最后一个方面是它特别易于发展。这是通过几种不同的方式实现的。首先,正如所讨论的,QUIC 几乎完全加密的事实意味着,如果我们想要部署更新版本的 QUIC,我们只需要更新端点(客户端和服务器),而不是所有中间件。这仍然需要时间,但通常需要几个月,而不是几年。

其次,与 TCP 不同,QUIC 不使用单个固定的数据包头来发送所有协议元数据。相反,QUIC 具有较短的数据包标头,并在数据包有效载荷内使用各种“帧”(类似于微型专用数据包)来传达额外信息。例如,有一个ACK框架(用于确认)、一个NEW_CONNECTION_ID框架(用于帮助建立连接迁移)和一个STREAM框架(用于携带数据),如下图所示。

这主要是作为优化完成的,因为并非每个数据包都携带所有可能的元数据(因此 TCP 数据包标头通常会浪费相当多的字节——另见上图)。然而,使用框架的一个非常有用的副作用是,将来定义新的框架类型作为 QUIC 的扩展将非常容易。例如,一个非常重要的就是DATAGRAMframe,它允许通过加密的 QUIC 连接发送不可靠的数据。 QUIC 使用单独的帧来发送元数据,而不是一个大的固定数据包头

image.png

第三,QUIC 使用自定义 TLS 扩展来携带所谓的传输参数。这些允许客户端和服务器为 QUIC 连接选择配置。这意味着他们可以协商启用哪些功能(例如,是否允许连接迁移、支持哪些扩展等)并为某些机制传达合理的默认值(例如,支持的最大数据包大小、流量控制限制)。虽然 QUIC 标准定义了很长的列表,但它也允许扩展来定义新的,再次使协议更加灵活。

最后,虽然 QUIC 本身并不是真正的要求,但大多数实现目前是在“用户空间”中完成的(与 TCP 不同,后者通常在“内核空间”中完成)。详细信息在第 2 部分中讨论,但这主要意味着与 TCP 相比,试验和部署 QUIC 实现变体和扩展要容易得多。

拓展

虽然 QUIC 现在已经标准化,但它确实应该被视为QUIC 版本 1(在征求意见稿(RFC) 中也有明确说明),并且有明确的意图来创建版本 2 和更快的版本。最重要的是,QUIC 允许轻松定义扩展,因此可以实现更多用例。

总结

让我们总结一下我们在这部分学到的东西。我们主要讨论了无所不在的 TCP 协议,以及它是如何在当今许多挑战未知的时代设计的。当我们试图发展 TCP 以跟上步伐时,很明显这在实践中会很困难,因为几乎每个设备都有自己的 TCP 实现,需要更新。

为了在改进 TCP 的同时绕过这个问题,我们创建了新的 QUIC 协议(实际上是 TCP 2.0)。为了使 QUIC 更容易部署,它运行在 UDP 协议之上(大多数网络设备也支持),并确保它可以在未来发展,默认情况下几乎完全加密并使用灵活的帧机制。

除此之外,QUIC 主要反映了已知的 TCP 特性,例如握手、可靠性和拥塞控制。除了加密和成帧之外的两个主要变化是多字节流的感知和连接 ID 的引入。然而,这些变化足以阻止我们直接在 QUIC 之上运行 HTTP/2,因此需要创建 HTTP/3(这实际上是引擎盖下的 HTTP/2-over-QUIC)。

QUIC 的新方法让位于许多性能改进,但它们的潜在收益比关于 QUIC 和 HTTP/3 的文章中通常传达的更微妙。现在我们了解了基础知识,我们可以在本系列的下一部分中更深入地讨论这些细微差别。

原文地址