从 HTTP/1.1 到 HTTP/2,HTTP 协议一直都是使用 TCP 作为传输协议。

然而,就在最新的 HTTP/3,HTTP 就直接把 TCP 抛弃了,向孤立无援的 UDP 伸出了援手,基于 UDP 协议的基础上,在应用层实现了一个可靠的传输协议 —— QUIC。

很多同学可能就好奇了,HTTP 都用 TCP 都用了几十年了,而且 TCP 已经是那么完善的可靠传输协议了,又有超时重传、按序接收、流量控制、拥塞控制这些特性,怎么突然就把 TCP 抛弃了?到底是 TCP 哪里做的不够好?是不是鸡蛋里挑骨头了?

所以,今天就跟大家聊聊,TCP 那些不够“好”的原因。

TCP 存在队头阻塞问题

TCP 队头阻塞的问题要从两个角度看,一个是发送窗口的队头阻塞,另外一个是接收窗口的队头阻塞

1、发送窗口的队头阻塞。

TCP 发送出去的数据,都是需要按序确认的,只有在数据都被按顺序确认完后,发送窗口才会往前滑动。举个例子,比如下图的发送方把发送窗口内的数据全部都发出去了,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了。

接着,当发送方收到对第 32~36 字节的 ACK 确认应答后,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来第 52~56 字节又变成了可用窗口,那么后续也就可以发送 52~56 这 5 个字节的数据了。

但是如果某个数据报文丢失或者其对应的 ACK 报文在网络中丢失,会导致发送方无法移动发送窗口,这时就无法再发送新的数据,只能超时重传这个数据报文,直到收到这个重传报文的 ACK,发送窗口才会移动,继续后面的发送行为。

举个例子,比如下图,客户端是发送方,服务器是接收方。

客户端发送了第 5~9 字节的数据,但是第 5 字节的 ACK 确认报文在网络中丢失了,那么即使客户端收到第 6~9 字节的 ACK 确认报文,发送窗口也不会往前移动。

此时的第 5 字节相当于“队头”,因为没有收到“队头”的 ACK 确认报文,导致发送窗口无法往前移动,此时发送方就无法继续发送后面的数据,相当于按下了发送行为的暂停键,这就是发送窗口的队头阻塞问题

2、接收窗口的队头阻塞。

接收方收到的数据范围必须在接收窗口范围内,如果收到超过接收窗口范围的数据,就会丢弃该数据,比如下图接收窗口的范围是 32 ~ 51 字节,如果收到第 52 字节以上数据都会被丢弃。

接收窗口什么时候才能滑动?当接收窗口收到有序数据时,接收窗口才能往前滑动,然后那些已经接收并且被确认的「有序」数据就可以被应用层读取。

但是,当接收窗口收到的数据不是有序的,比如收到第 33~40 字节的数据,由于第 32 字节数据没有收到, 接收窗口无法向前滑动,那么即使先收到第 33~40 字节的数据,这些数据也无法被应用层读取的。只有当发送方重传了第 32 字节数据并且被接收方收到后,接收窗口才会往前滑动,然后应用层才能从内核读取第 32~40 字节的数据。

好了,至此发送窗口和接收窗口的队头阻塞问题都说完了,这两个问题的原因都是因为 TCP 必须按序处理数据,也就是 TCP 层为了保证数据的有序性,只有在处理完有序的数据后,滑动窗口才能往前滑动,否则就停留。

  • 停留「发送窗口」会使得发送方无法继续发送数据。

  • 停留「接收窗口」会使得应用层无法读取新的数据。

其实也不能怪 TCP 协议,它本来设计目的就是为了保证数据的有序性。

HTTP/2 的队头阻塞

HTTP/2 通过抽象出 Stream 的概念,实现了 HTTP 并发传输,一个 Stream 就代表 HTTP/1.1 里的请求和响应。

在 HTTP/2 连接上,不同 Stream 的帧是可以乱序发送的(因此可以并发不同的 Stream ),因为每个帧的头部会携带 Stream ID 信息,所以接收端可以通过 Stream ID 有序组装成 HTTP 消息,而同一 Stream 内部的帧必须是严格有序的。

但是 HTTP/2 多个 Stream 请求都是在一条 TCP 连接上传输,这意味着多个 Stream 共用同一个 TCP 滑动窗口,那么当发生数据丢失,滑动窗口是无法往前移动的,此时就会阻塞住所有的 HTTP 请求,这属于 TCP 层队头阻塞

没有队头阻塞的 QUIC

QUIC 也借鉴 HTTP/2 里的 Stream 的概念,在一条 QUIC 连接上可以并发发送多个 HTTP 请求 (Stream)。

但是 QUIC 给每一个 Stream 都分配了一个独立的滑动窗口,这样使得一个连接上的多个 Stream 之间没有依赖关系,都是相互独立的,各自控制的滑动窗口

假如 Stream2 丢了一个 UDP 包,也只会影响 Stream2 的处理,不会影响其他 Stream,与 HTTP/2 不同,HTTP/2 只要某个流中的数据包丢失了,其他流也会因此受影响。

frc-cf6ad593b6d851f717bf77b4b05228e9-hjrjhpgg.png

TCP 建立连接的延迟

对于 HTTP/1 和 HTTP/2 协议,TCP 和 TLS 是分层的,分别属于内核实现的传输层、openssl 库实现的表示层,因此它们难以合并在一起,需要分批次来握手,先 TCP 握手(1RTT),再 TLS 握手(2RTT),所以需要 3RTT 的延迟才能传输数据,就算 Session 会话服用,也需要至少 2 个 RTT,这在一定程序上增加了数据传输的延迟。

TCP 三次握手和 TLS 握手延迟,如图:

HTTP/3 在传输数据前虽然需要 QUIC 协议握手,这个握手过程只需要 1 RTT,握手的目的是为确认双方的「连接 ID」,连接迁移就是基于连接 ID 实现的。

但是 HTTP/3 的 QUIC 协议并不是与 TLS 分层,因为 QUIC 也是应用层实现的协议,所以可以将 QUIC 和 TLS 协议握手的过程合并在一起,QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。

如下图右边部分,HTTP/3 当会话恢复时,有效负载数据与第一个数据包一起发送,可以做到 0-RTT(下图的右下角):

升级 TCP 的工作很困难

TCP 协议是诞生在 1973 年,至今 TCP 协议依然还在实现更多的新特性。

但是 TCP 协议是在内核中实现的,应用程序只能使用不能修改,如果要想升级 TCP 协议,那么只能升级内核。

而升级内核这个工作是很麻烦的事情,麻烦的事情不是说升级内核这个操作很麻烦,而是由于内核升级涉及到底层软件和运行库的更新,我们的服务程序就需要回归测试是否兼容新的内核版本,所以服务器的内核升级也比较保守和缓慢。

很多 TCP 协议的新特性,都是需要客户端和服务端同时支持才能生效的,比如 TCP Fast Open 这个特性,虽然在2013 年就被提出了,但是 Windows 很多系统版本依然不支持它,这是因为 PC 端的系统升级滞后很严重,Windows Xp 现在还有大量用户在使用,尽管它已经存在快 20 年。

所以,即使 TCP 有比较好的特性更新,也很难快速推广,用户往往要几年或者十年才能体验到。

相反,QUIC 是处于应用层的,所以如果升级 QUIC 协议的话,其实就是像升级软件一样轻松。而且,QUIC 可以针对不同的应用设置不同的拥塞控制算法,这样灵活性就很高了,这是 TCP 做不到的,因为 TCP 更改拥塞控制算法是对系统中所有应用都生效,无法根据不同应用设定不同的拥塞控制策略。

网络迁移需要重新建立 TCP 连接

基于 TCP 传输协议的 HTTP 协议,由于是通过四元组(源 IP、源端口、目的 IP、目的端口)确定一条 TCP 连接。

那么当移动设备的网络从 4G 切换到 WIFI 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立 TCP 连接

而建立连接的过程包含 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。

QUIC 协议没有用四元组的方式来“绑定”连接,而是通过连接 ID来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。

总结

HTTP/3 抛弃 TCP 后,基于 UDP 实现的可靠传输 QUIC 协议,带来这四点好处:

  1. 降低连接耗时:在客户端有缓存的情况下实现0-RTT建立连接

  2. 更灵活的拥塞控制:在用户态可以为每个请求配置不同的拥塞控制策略

  3. 无队头阻塞的多路复用:每个请求流独立拥有滑动窗口,互不影响

  4. 连接迁移:网络切换不会中断数据传输

不过, HTTP/3 也面临了一些挑战,QUIC 基于 UDP 协议在用户空间实现的可靠传输协议,如果一些网络设备无法识别出 QUIC 协议,那么在这些网络设备的眼里它就是一个 UDP 协议。

而几乎所有的电信运营商都会“歧视” UDP 数据包,原因也很容易理解,毕竟历史上几次臭名昭著的 DDoS 攻击都是基于 UDP 的。国内某城宽带在某些区域更是直接禁止了非 53 端口的UDP数据包,而其他运营商即使没有封禁 UDP,也是对 UDP 进行严格限流的。

自 2013 年 QUIC 被正式公开以来,到 2023 年已经发展了差不多 10 年,目前网上已经有了不少热门开源的项目,除去带头大哥 Google 在完成了对自身搜索引擎的支持,还同时拉上了 Gmail 、YouTube 等站点。但对于国内的绝大部分站点来说,大部分还是 HTTP/2 协议,HTTP/3 之路,似乎还停留在东土大唐。