【简要回答】

1、首先引出UDP的可靠性方案QUIC和TCP的4大缺点

2、介绍机制——QUIC 是如何实现可靠传输的?

3、分点阐述(根据TCP缺点)——QUIC是如何解决上面 TCP 协议四个方面的缺陷的?

①QUIC 是如何解决 TCP 队头阻塞问题的?

②QUIC 是如何做流量控制的?

③QUIC 对拥塞控制改进?

④QUIC 更快的连接建立?

⑤QUIC 是如何迁移连接的?


【详细回答】
1、引出UDP的可靠性方案QUIC和TCP的4大缺点

UDP 协议实现的可靠传输协议的成熟方案是 QUIC 协议,已经应用在了 HTTP3.0。HTTP3.0将底层由TCP换成了UDP,原因是TCP连接固然可靠,但是仍然有缺点:

  • 升级 TCP 的工作很困难;
  • TCP 建立连接的延迟;
  • TCP 存在队头阻塞问题;
  • 网络迁移需要重新建立 TCP 连接;

2、QUIC 是如何实现可靠传输的?

QUIC 是一种基于 UDP 的新一代传输协议,虽然 UDP 本身是不可靠的,但 QUIC 在协议层自己设计了可靠传输机制,等于是“用应用层的手段补上了 TCP 的功能”。它通过设计精巧的包头结构,实现了数据传输的有序性、确认机制和丢包重传,甚至比 TCP 做得更好。

首先,QUIC 的每个数据包都有一个 Packet Number,这个编号是严格递增的。哪怕是重传的数据包,也不会使用原来的编号,而是重新分配一个新的、更大的编号。这样做的好处是,在 ACK 确认时不会像 TCP 那样出现歧义。因为 TCP 的重传包和原包用的是一样的序号,所以当收到 ACK 时,很难判断是确认了原包,还是重传包,这会影响 RTT 的计算。而 QUIC 的编号不会重复,就可以精确判断 ACK 对应的是哪一包,从而准确计算出 RTT,提高超时时间估算的准确性。

其次,QUIC 的这个 Packet Number 机制还带来了另一个好处,就是可以实现乱序确认,也就是不需要像 TCP 那样“必须等前面的包都收到了,后面的包才算有效”。在 QUIC 中,如果中间某个数据包丢了,但后续的数据包先到了,也可以先处理,窗口可以继续向后滑动,不会被“卡死”在原地。这样就避免了 TCP 中常见的“队头阻塞”问题,大大提高了传输效率,尤其在丢包率高的场景下优势更明显。

为了确保重传数据内容的一致性,QUIC 还设计了 Frame 结构。每个数据包里可以包含一个或多个 Frame,比如 Stream Frame 是用来承载业务数据的。每个 Frame 都有自己的 Stream ID、Offset 和 Length,用于标识它属于哪条流、在这条流中的偏移位置以及数据长度。这样即使某个数据包被重传了,接收方也能通过 Stream ID 和 Offset 判断出这是不是同一段数据,从而正确地把数据组装起来,确保传输的完整性和顺序。

整体来说,QUIC 把 TCP 的可靠性机制做了“重构升级”,它不仅能保证数据的可靠传输,还解决了 TCP 中因为重传带来的 RTT 计算问题和顺序确认限制,可以说是为了现代网络环境量身打造的传输协议。


3、QUIC是如何解决上面 TCP 协议四个方面的缺陷的?
①QUIC 是如何解决 TCP 队头阻塞问题的?

TCP 队头阻塞的问题,本质上是接收方必须按序处理数据。如果中间某个数据包丢了,后面的数据即使收到了,也不能被应用层读取,直到丢失的数据被重传回来。这是因为 TCP 的滑动窗口只能在收到连续的有序数据后才能向前滑动,所以一旦最前面的数据没到,整个窗口就卡住了,这就是典型的“队头阻塞”。

这个问题在 HTTP/2 中依然存在。虽然 HTTP/2 把一条 TCP 连接划分成多个逻辑上的 Stream,允许多请求并发,但所有 Stream 底层都还是复用同一个 TCP 连接。当某个请求的数据丢了,即使是别的请求的数据到了,也会因为 TCP 层窗口没滑动,导致整体阻塞。这种阻塞不是应用层造成的,而是 TCP 本身机制导致的。

QUIC 针对这个问题,做了两个关键改进。第一,它和 HTTP/2 一样也使用了 Stream 的概念;第二,它给每一个 Stream 都分配了独立的流控机制和滑动窗口。这意味着,每个 Stream 都是“自己的小世界”,不会因为其他 Stream 的丢包而受到影响。

比如说,在一个 QUIC 连接上,Stream1 和 Stream2 同时在传输数据。如果 Stream2 某个数据包丢了,Stream1 仍然可以继续滑动窗口、继续传输数据,不受影响。只有 Stream2 自己的传输会被暂时中断。这样就彻底摆脱了 TCP 那种“一个丢包,全连接受限”的局面,提升了多路复用时的并发能力和传输效率。

所以可以说,QUIC 通过将 Stream 的控制彻底“去中心化”,让每个流自成体系,最终解决了长期困扰 TCP 和 HTTP/2 的队头阻塞问题。它不是修补 TCP,而是从底层重构了一套传输逻辑。


②QUIC 是如何做流量控制的?

QUIC 是基于 UDP 的协议,而 UDP 本身没有流量控制能力,所以 QUIC 自己在协议层实现了一整套流量控制机制。它的核心思想,其实和 TCP 类似,都是通过“接收方告诉发送方:我现在还能接多少数据”来进行控制的,只不过 QUIC 的控制更加灵活,分得更细。

具体来说,QUIC 实现了两级流量控制:一个是 Stream 级别的,一个是 Connection 级别的。Stream 可以理解为一条独立的数据流,比如一次 HTTP 请求,每条 Stream 都有自己的接收窗口;Connection 是整个连接的总窗口,是所有 Stream 的接收窗口加起来的总和。

在 Stream 级别,接收方会通过专门的窗口更新帧(类似 TCP 的 window_update)告诉发送方自己还能接受多少字节。一开始窗口大小是协商出来的,接收方接收数据后,如果上层应用及时处理了数据,它就可以滑动窗口、扩大接收范围,再通过帧通知发送方继续发送更多数据。如果中途某个数据包丢失,比如 offset=30 这段数据没收到,那这个 Stream 的窗口就暂时不能滑动,但不会影响其他 Stream,因为每个 Stream 的窗口是独立的。

这点和 TCP 是很大的不同。TCP 的所有数据共享一个窗口,一旦中间某一段丢失,整个窗口就被卡住了,后续数据都得等。QUIC 解决了这个问题,不同 Stream 互不影响,真正实现了多路复用中的“并行”。

而在 Connection 级别,QUIC 会对所有 Stream 加起来的数据做总量限制,防止某一个连接中的某个 Stream 独占带宽、影响整体传输。例如,如果一个连接允许最多传120个字节的数据,某三个 Stream 分别用了100、90、110字节,那它们剩下的窗口分别是20、30和10,总共还有60个字节的空间可以发送数据。这个机制确保了连接层面不会被撑爆,同时 Stream 层面也能合理分配。

在数据包传输过程中,还有一个细节值得一提:QUIC 的接收窗口滑动是基于“收到的数据偏移量”来判断的,而不是像 TCP 那样必须前面的数据都收到才滑动。这意味着只要数据被“顺序确认并提交”了一部分,就能开始滑动窗口、扩大接收能力;而 TCP 必须等前面的数据都收到才滑动,这也是 QUIC 更灵活的一个地方。

最后,在实际丢包和重传场景下,QUIC 还设计了重新编号机制,比如数据包33丢了,它会被重传为一个新的编号,比如42,不像 TCP 那样复用旧编号。这也进一步避免了因为丢包导致的窗口滑动延迟或传输阻塞。


③QUIC 对拥塞控制改进?

QUIC 在拥塞控制这一块,其实一开始并没有发明什么全新算法,它默认采用的是 TCP 的 Cubic 算法,也支持像 Reno、BBR、PCC 等其他算法。看上去好像是“照搬”,但最大的突破不是算法本身,而是 部署方式的变化。QUIC 是在应用层做拥塞控制的,不像 TCP 那样依赖操作系统内核。这就意味着,拥塞控制算法的更新和替换变得非常灵活,比如浏览器升级一下就能更新算法,而不用等系统内核发布版本。这对实际部署来说,是一个质的飞跃。更重要的是,QUIC 可以根据不同应用场景选用不同的拥塞控制策略,而不像 TCP 那样一刀切,这就让协议变得更适应各种复杂网络环境。


④QUIC 更快的连接建立?

在传统的 HTTP/1 或 HTTP/2 中,建立连接通常需要多个往返时延(RTT),因为 TCP 和 TLS 是分层实现的,必须先完成 TCP 的三次握手,然后再完成 TLS 的密钥协商,这样才能开始传输应用数据。即使开启了 TLS 会话复用,也至少要两次 RTT。而 QUIC 的做法就非常巧妙:它把 TLS 1.3 整合进了自己的协议帧中,连接建立的同时就完成了 TLS 握手。也就是说,QUIC 用一个 RTT 就搞定了“建连接 + 协商密钥”这两件事,甚至在重连的时候,应用数据和握手信息还能一并发送,实现真正的 0-RTT。这就极大提升了首次访问和重连时的响应速度,用户基本感觉不到延迟。


⑤QUIC 是如何迁移连接的?

传统的 TCP 连接是靠 IP 和端口这四元组来绑定的,如果你换了网络,比如手机从 4G 切到 Wi-Fi,IP 一变,TCP 连接就断了,后续只能重新建连接。这种情况下不仅需要再次握手,而且还要经历 TCP 慢启动,体验上就会有明显的卡顿。而 QUIC 彻底绕过了这个限制,它不靠四元组维持连接状态,而是通过一个独立的连接 ID 来标识这段会话。只要连接 ID 和 TLS 上下文还在,即使 IP 换了,端口变了,连接还是可以继续用。这种“无感切网”能力,特别适合移动设备或者弱网环境下的应用,极大提升了连接的稳定性和用户体验。


【知识拓展】

TCP 和 UDP 可以同时绑定相同的端口吗?

答案:可以的
TCP 和 UDP 传输协议,在内核中是由两个完全独立的软件模块实现的。

当主机收到数据包后,可以在 IP 包头的「协议号」字段知道该数据包是 TCP/UDP,所以可以根据这个信息确定送给哪个模块(TCP/UDP)处理,送给 TCP/UDP 模块的报文根据「端口号」确定送给哪个应用程序处理。

因此, TCP/UDP 各自的端口号也相互独立,互不影响。


**多个 TCP 服务进程可以绑定同一个端口吗?
**

答案:看情况。

如果两个 TCP 服务进程同时绑定的 IP 地址和端口都相同,那么执行 bind() 时候就会出错,错误是“Address already in use”。

如果两个 TCP 服务进程绑定的端口都相同,而 IP 地址不同,那么执行 bind() 不会出错。


如何解决服务端重启时,报错“Address already in use”的问题?

当我们重启 TCP 服务进程的时候,意味着通过服务器端发起了关闭连接操作,于是就会经过四次挥手,而对于主动关闭方,会在 TIME_WAIT 这个状态里停留一段时间,这个时间大约为 2MSL。

当 TCP 服务进程重启时,服务端会出现 TIME_WAIT 状态的连接,TIME_WAIT 状态的连接使用的 IP+PORT 仍然被认为是一个有效的 IP+PORT 组合,相同机器上不能够在该 IP+PORT 组合上进行绑定,那么执行 bind() 函数的时候,就会返回了 Address already in use 的错误。

要解决这个问题,我们可以对 socket 设置 SO_REUSEADDR 属性。

这样即使存在一个和绑定 IP+PORT 一样的 TIME_WAIT 状态的连接,依然可以正常绑定成功,因此可以正常重启成功。


客户端的端口可以重复使用吗?

在客户端执行 connect 函数的时候,只要客户端连接的服务器不是同一个,内核允许端口重复使用。

TCP 连接是由四元组(源IP地址,源端口,目的IP地址,目的端口)唯一确认的,那么只要四元组中其中一个元素发生了变化,那么就表示不同的 TCP 连接的。

所以,如果客户端已使用端口 64992 与服务端 A 建立了连接,那么客户端要与服务端 B 建立连接,还是可以使用端口 64992 的,因为内核是通过四元祖信息来定位一个 TCP 连接的,并不会因为客户端的端口号相同,而导致连接冲突的问题。


**客户端 TCP 连接 TIME_WAIT 状态过多,会导致端口资源耗尽而无法建立新的连接吗?

要看客户端是否都是与同一个服务器(目标地址和目标端口一样)建立连接。

如果客户端都是与同一个服务器(目标地址和目标端口一样)建立连接,那么如果客户端 TIME_WAIT 状态的连接过多,当端口资源被耗尽,就无法与这个服务器再建立连接了。

即使在这种状态下,还是可以与其他服务器建立连接的,只要客户端连接的服务器不是同一个,那么端口是重复使用的。


如何解决客户端 TCP 连接 TIME_WAIT 过多,导致无法与同一个服务器建立连接的问题?

打开 net.ipv4.tcp_tw_reuse 这个内核参数。

因为开启了这个内核参数后,客户端调用 connect 函数时,如果选择到的端口,已经被相同四元组的连接占用的时候,就会判断该连接是否处于 TIME_WAIT 状态。

如果该连接处于 TIME_WAIT 状态并且 TIME_WAIT 状态持续的时间超过了 1 秒,那么就会重用这个连接,然后就可以正常使用该端口了。


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐