UDP实现可靠传输:
把TCP可靠传输的特性(序号,确认应答,超时重传,流量控制)在应用层实现一遍。
为什么不用天然支持可靠传输的TCP:
- 升级TCP很困难(TCP在运输层,升级TCP需要升级操作系统)。
- TCP建立连接的延迟。
- TCP存在窗口阻塞的问题。
- 网络迁移需要TCP重新建立连接。
现在市面上已经有基于UDP实现可靠传输的成熟方案了,即QUIC协议,已经应用在了HTTP/3。
在HTTP/3中,UDP报头与HTTP消息间有三层头部:

Packet Header:
Packet Header首次建立连接时和日常传输数据时使用的Header是不同的:
Long Packet Header用于首次建立连接,Short Packet Header用于日常传输数据。
服务器根据客户端的源连接 ID 建立连接,这样后续传输时,双方只需要固定住目的连接ID,从而实现连接迁移功能。所以日常传输数据的 Short Packet Header 不需要在传输源连接 ID 了。
Short Packet Header 中的 Packet Number 是每个报文独一无二的编号,它是严格递增的,也就是说就算 Packet N 丢失了,重传的 Packet N 的 Packet Number 已经不是 N,而是一个比 N 大的值。
TCP 在重传报文时的序号和原始报文的序号是一样的,这导致了 TCP 重传的歧义问题。
比如上图,当 TCP 发生超时重传后,客户端发起重传,然后接收到了服务端确认 ACK 。由于客户端原始报文和重传报文序列号都是一样的,那么服务端针对这两个报文回复的都是相同的 ACK,客户端就无法判断出是原始报文的响应还是重传报文的响应,这样在计算 RTT(往返时间) 时就会产生歧义。(应该选择从发送原始报文开始计算,还是重传原始报文开始计算呢?)
RTT 计算不精确的话,RTO(超时时间)也就不精确,因为 RTO 是基于 RTT 来计算的,RTO 计算不准确可能导致重传的概率事件增大。
QUIC 报文中的 Pakcet Number 是严格递增的, 即使是重传报文,它的 Pakcet Number 也是递增的,这样就能更加精确计算出报文的 RTT。
另外,还有一个好处,QUIC 使用的 Packet Number 单调递增的设计,可以让数据包不再像TCP 那样必须有序确认,QUIC 支持乱序确认,当数据包Packet N 丢失后,只要有新的已接收数据包确认,当前窗口就会继续向右滑动。
待发送端超过一定时间没收到 Packet N 的确认报文后,会将需要重传的数据包放到待发送队列,重新编号比如数据包 Packet N+M 后重新发送给接收端,对重传数据包的处理跟发送新的数据包类似,这样就不会因为丢包重传将当前窗口阻塞在原地,从而解决了窗口阻塞问题。
所以,Packet Number 单调递增的两个好处:
- 可以更加精确计算 RTT,没有 TCP 重传的歧义性问题;
- 可以支持乱序确认,防止因为丢包重传将当前窗口阻塞在原地,而 TCP 必须是顺序确认的,丢包时会导致窗口阻塞。
QUIC Frame Header:
一个Packet报文中可以存放多个QUIC Frame:
每一个 Frame 都有明确的类型,这里只举例 Stream 类型的 Frame 格式,Stream 可以认为就是一条 HTTP 请求,它长这样:
- Stream ID 作用:多个并发传输的 HTTP 消息,通过不同的 Stream ID 加以区别;
- Offset 作用:类似于 TCP 协议中的 Seq 序号,保证数据的顺序性和可靠性;
- Length 作用:指明了 Frame 数据的长度
在前面介绍 Packet Header 时,说到 Packet Number 是严格递增,即使重传报文的 Packet Number 也是递增的,既然重传数据包的 Packet N+M 与丢失数据包的 Packet N 编号并不一致,我们怎么确定这两个数据包的内容一样呢?
所以引入 Frame Header 这一层,通过 Stream ID + Offset 字段信息实现数据的有序性,通过比较两个数据包的 Stream ID 与 Stream Offset ,如果都是一致,就说明这两个数据包的内容一致。
举个例子,下图中,数据包 Packet N 丢失了,后面重传该数据包的编号为 Packet N+2,丢失的数据包和重传的数据包 Stream ID 与 Offset 都一致,说明这两个数据包的内容一致。这些数据包传输到接收端后,接收端能根据 Stream ID 与 Offset 字段信息将 Stream x 和 Stream x+y 按照顺序组织起来,然后交给应用程序处理。
总的来说,QUIC 通过单向递增的 Packet Number,配合 Stream ID 与 Offset 字段信息,可以支持乱序确认而不影响数据包的正确组装,摆脱了TCP 必须按顺序确认应答 ACK 的限制,解决了 TCP 因某个数据包重传而阻塞后续所有待发送数据包的问题。
QUIC是如何解决队头阻塞问题的:
TCP窗口阻塞问题:
TCP 发送出去的数据,都是需要按序确认的,只有在前面数据都被确认后,发送窗口才会滑动。如果前面某个数据没有被确认,会导致发送窗口不能继续移动,这时就无法再发送新的数据,只能超时重传这个数据,直到收到这个重传数据的确认,发送窗口才能移动,继续发送后面的数据。
HTTP/2窗口阻塞问题:
HTTP/2抽象出流的概念,实现HTTP的并发传输,一个流就代表HTTP/1.1里的请求和响应。
在HTTP/2连接上,不同流的帧是可以乱序发送的,因为每个帧的头部会携带stream ID信息,所以接收端可以将同一个stream的帧组装成HTTP消息,而同一stream内部的帧必须是严格有序的。
但是HTTP/2的各个流都是在一条TCP连接上传输,这意味着多个流共用一个TCP滑动窗口,当前面数据没有被确认,滑动窗口就无法向前移动,此时就会阻塞所有的HTTP请求。
没有窗口阻塞的QUIC:
QUIC也借鉴了HTTP/2里流的概念,在一条QUIC连接上并发发送多个HTTP请求(stream)。
但是QUIC给每一个流都分配了一个独立的滑动窗口,这样使得各个流之间没有依赖关系,都是相互独立的,各自控制滑动窗口。
假如某一个流丢了一个数据包,也只会阻塞这一个流的窗口,不会影响其他流。
QUIC流量控制:
TCP流量控制:
接收方告诉发送方自己的接收窗口大小,发送方据此调整自己发送的数据量。
QUIC实现了两种级别的流量控制:
stream级别的流量控制:每个流都有独立的滑动窗口,每个流都可以做流量控制,防止单个流消耗掉连接的全部接收窗口。
connection流量控制:限制连接中所有流的发送数据量,防止其超过各个流接收窗口大小之和。
参考:
https://blog.csdn.net/m0_69305074/article/details/124753240