天天看点

UDP实现可靠传输:QUIC

UDP实现可靠传输:

把TCP可靠传输的特性(序号,确认应答,超时重传,流量控制)在应用层实现一遍。

为什么不用天然支持可靠传输的TCP:

  • 升级TCP很困难(TCP在运输层,升级TCP需要升级操作系统)。
  • TCP建立连接的延迟。
  • TCP存在窗口阻塞的问题。
  • 网络迁移需要TCP重新建立连接。

现在市面上已经有基于UDP实现可靠传输的成熟方案了,即QUIC协议,已经应用在了HTTP/3。

在HTTP/3中,UDP报头与HTTP消息间有三层头部:

UDP实现可靠传输:QUIC

Packet Header:

Packet Header首次建立连接时和日常传输数据时使用的Header是不同的:

UDP实现可靠传输:QUIC

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 大的值。

UDP实现可靠传输:QUIC

TCP 在重传报文时的序号和原始报文的序号是一样的,这导致了 TCP 重传的歧义问题。

UDP实现可靠传输:QUIC

比如上图,当 TCP 发生超时重传后,客户端发起重传,然后接收到了服务端确认 ACK 。由于客户端原始报文和重传报文序列号都是一样的,那么服务端针对这两个报文回复的都是相同的 ACK,客户端就无法判断出是原始报文的响应还是重传报文的响应,这样在计算 RTT(往返时间) 时就会产生歧义。(应该选择从发送原始报文开始计算,还是重传原始报文开始计算呢?)

RTT 计算不精确的话,RTO(超时时间)也就不精确,因为 RTO 是基于 RTT 来计算的,RTO 计算不准确可能导致重传的概率事件增大。

QUIC 报文中的 Pakcet Number 是严格递增的, 即使是重传报文,它的 Pakcet Number 也是递增的,这样就能更加精确计算出报文的 RTT。

UDP实现可靠传输:QUIC

另外,还有一个好处,QUIC 使用的 Packet Number 单调递增的设计,可以让数据包不再像TCP 那样必须有序确认,QUIC 支持乱序确认,当数据包Packet N 丢失后,只要有新的已接收数据包确认,当前窗口就会继续向右滑动。

待发送端超过一定时间没收到 Packet N 的确认报文后,会将需要重传的数据包放到待发送队列,重新编号比如数据包 Packet N+M 后重新发送给接收端,对重传数据包的处理跟发送新的数据包类似,这样就不会因为丢包重传将当前窗口阻塞在原地,从而解决了窗口阻塞问题。

所以,Packet Number 单调递增的两个好处:

  • 可以更加精确计算 RTT,没有 TCP 重传的歧义性问题;
  • 可以支持乱序确认,防止因为丢包重传将当前窗口阻塞在原地,而 TCP 必须是顺序确认的,丢包时会导致窗口阻塞。

QUIC Frame Header:

一个Packet报文中可以存放多个QUIC Frame:

UDP实现可靠传输:QUIC

每一个 Frame 都有明确的类型,这里只举例 Stream 类型的 Frame 格式,Stream 可以认为就是一条 HTTP 请求,它长这样:

UDP实现可靠传输:QUIC
  • 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 按照顺序组织起来,然后交给应用程序处理。

UDP实现可靠传输:QUIC

总的来说,QUIC 通过单向递增的 Packet Number,配合 Stream ID 与 Offset 字段信息,可以支持乱序确认而不影响数据包的正确组装,摆脱了TCP 必须按顺序确认应答 ACK 的限制,解决了 TCP 因某个数据包重传而阻塞后续所有待发送数据包的问题。

QUIC是如何解决队头阻塞问题的:

TCP窗口阻塞问题:

TCP 发送出去的数据,都是需要按序确认的,只有在前面数据都被确认后,发送窗口才会滑动。如果前面某个数据没有被确认,会导致发送窗口不能继续移动,这时就无法再发送新的数据,只能超时重传这个数据,直到收到这个重传数据的确认,发送窗口才能移动,继续发送后面的数据。

HTTP/2窗口阻塞问题:

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

UDP实现可靠传输:QUIC

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

但是HTTP/2的各个流都是在一条TCP连接上传输,这意味着多个流共用一个TCP滑动窗口,当前面数据没有被确认,滑动窗口就无法向前移动,此时就会阻塞所有的HTTP请求。

UDP实现可靠传输:QUIC

没有窗口阻塞的QUIC:

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

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

假如某一个流丢了一个数据包,也只会阻塞这一个流的窗口,不会影响其他流。

UDP实现可靠传输:QUIC

QUIC流量控制:

TCP流量控制:

接收方告诉发送方自己的接收窗口大小,发送方据此调整自己发送的数据量。

QUIC实现了两种级别的流量控制:

stream级别的流量控制:每个流都有独立的滑动窗口,每个流都可以做流量控制,防止单个流消耗掉连接的全部接收窗口。

connection流量控制:限制连接中所有流的发送数据量,防止其超过各个流接收窗口大小之和。

参考:

​​https://blog.csdn.net/m0_69305074/article/details/124753240​​

继续阅读