程序员进阶之路:QUIC篇
QUIC(Quick UDP Internet Connections,快速UDP互联网连接)是一种基于UDP改进的应用层通信协议,与Quick谐音,意思是“快”。它最初由Google设计并在2013年首次提出,QUIC的目标是提供更快的连接建立和更低的延迟,同时提供更好的安全性。QUIC于RFC9000中被正式标准化。
一、协议体
QUIC协议的头部结构比较复杂,它的设计目标是适应高效的数据传输和连接管理。RFC9000中QUIC头部的格式大致如图1所示。
图1
QUIC的头部主要由连接头部(Connection Header)和帧头部(Frame Header)组成。
连接头部包含了一些必要的字段,用于标识和管理连接。它的头部字段包括:
(1)标志位(Flags):占1字节,包括连接ID长度、标志位等信息。第一位是区分长短头部的标识位,设置为1表示长头部,设置为0表示短头部,短头部没有源连接ID。
(2)版本号(Version):占4字节,用于指定QUIC的版本。
(3)目的连接ID长度(DCID Len):占1字节,表示目的连接ID的长度,单位是字节。
(4)目的连接ID(DCID):占变长字节(0~255字节),标识一个QUIC连接,长度取决于DCID Len。
(5)源连接ID长度(SCID Len):占1字节,表示目的连接ID的长度,单位是字节。
(6)源连接ID(SCID):占变长字节(0~255字节),标识一个QUIC连接,长度取决于SCID Len。
(7)报文长度(Packet Length):占2字节,QUIC报文总长度包括头部和数据部分。
(8)报文编号(Packet Number):占变长字节,用于标识数据包在连接中的顺序。
帧头部用于标识和管理QUIC数据包中的帧,每个QUIC报文可以包含一个或多个帧。帧的类型有很多种,每种帧的头部不尽相同,以Stream Frame为例,它的头部字段包括:
(1)帧类型(Frame Type):占1字节,标识帧的类型,如STREAM、ACK、PADDING等。
(2)流ID(Stream ID):占变长字节,标识帧所属的流。
(3)数据偏移(Offset):占变长字节,指示帧的起始数据位置在整个流中的偏移。
(4)帧长度(Length):占变长字节,指示帧的长度。
二、0-RTT
因为QUIC基于UDP,所以不需要先建立连接就可以直接发送数据。TCP在1-RTT之后才能发送数据,即使TCP开启了TFO,也得第二次建立连接时才能做到无须等待就可以发送数据(0-RTT发送数据),而QUIC第一次就可以做到0-RTT发送数据,如图2所示。
图2
三、可靠传输
TCP超时重传的报文的序列号和原始的序列号一致,而QUIC超时重传的报文的Packet Number与原始报文的不一样,Packet Number是单调递增的,如图3所示。
图3
TCP的超时重传的方式有一个问题,它会造成RTT测量得不准确,引入了 TCP 重传歧义的问题。如图4所示,由于重传前后的两个报文的序列号一样,因此响应的报文的确认序列号也是一样的,发送方区分不出来接收的是原始确认报文还是重传确认报文。
-
如果把重传确认报文当成原始确认报文,则会导致计算出来的RTT偏大(图4左侧)。
-
如果把原始确认报文当成重传确认报文,则会导致计算出来的RTT偏小(图4右侧)。
图4
由于RTO的计算依赖RTT,因此也会导致RTO测量得不准确。QUIC就没有这个问题,因为它每次发送的报文的Packet Number都不一样,包括重传的报文,确认报文的序列号也都是不一样的,所以也就没有歧义问题了,如图5所示。
图5
在上面例子中引入的Packet Number单调递增确保了报文的先后顺序,但是引入了报文的不连续性。为了解决不连续问题,QUIC又引入了Stream Offset,即每一个Stream上的Offset是连续的,如图6所示。如果Packet N丢失了,则导致Stream M的Offset X的数据丢失,那么超时重传该报文,但是Packet不是N了,而是该连接上所有Stream单调递增到的N+3。接收方收到Stream M上的Offset X后与之前收到的Offset X+Y拼接到一起交给应用层,通过Stream上的Offset保证了数据的连续性。
图6
四、流量控制
QUIC的流量控制与TCP相似,也是接收方将接收窗口通过报文告知发送方来控制发送方的发送窗口。由于QUIC基于UDP,而UDP没有流量控制,所以QUIC在应用层实现了流量控制机制。
QUIC实现了两个级别的流量控制,分别是Connection级别和Stream级别的流量控制。QUIC通过WINDOW_UPDATE帧告知发送方接收方的接收窗口大小,而通过BLOCKED帧告知发送方停止发送报文,流量被阻塞。
1.Stream级别的流量控制
同一个Connection(连接)上的所有Stream(流)都有各自的滑动窗口,相互独立,互不影响。这么做可以防止一个Stream抢占过多的Connection级别的接收窗口。 接收窗口的初始状态如图7所示,接收窗口等于最大接收窗口。
图7
经过一段时间接收和读取数据后,接收窗口的状态如图8所示。最大接收窗口包含部分已读数据、可读数据、丢失数据和乱序数据的大小及接收窗口大小。接收窗口大小就等于最大接收窗口减去收到的最大Offset。
图8
当已读数据的右边界越过最大接收窗口中线时,将最大接收窗口和接收窗口一并向右移动,其左边界等于已读数据的右边界,最大接收窗口的大小不变,接收窗口的左边界不变,其右边界等于最大接收窗口的右边界,移动过程如图9所示。当接收窗口移动时,QUIC会通过WINDOW_UPDATE帧告知发送方接收方的接收窗口大小。
图9
2.Coneection级别的流量控制
Connection级别的接收窗口等于所有该连接上Stream接收窗口的总和,通常一个连接上最多有100个流。假设一个连接上有3个Stream,每个Stream的最大接收窗口都是1000。每个Stream的已读数据大小和最大Offset如图10所示。
-
Stream 1的接收窗口为1000-720=280。
-
Stream 2的接收窗口为1000-630=370。
-
Stream 3的接收窗口为1000-560=440。
那么这个连接的接收窗口大小为280+370+440=1090。
图10
3.队头阻塞
有了上面知识的铺垫,下面我们来看一下QUIC是如何解决TCP在使用多路复用时遇到的队头阻塞问题的。
多路复用(Multiplexing)指的是一个连接上可以同时多个数据流并行,互不干扰,目的是提高数据传输的效率,多路复用在HTTP/1.2中引入。但是TCP实现的多路复用存在队头阻塞问题。所谓队头阻塞就是当多个Stream中有一个Stream发生丢包时,会导致TCP滑动窗口卡住,阻塞同一连接上的所有Stream,即使其他Stream的数据到达也不会交给应用层处理。如图11所示,由于Stream2的数据(序列号为1003的包)丢失,导致接收窗口无法向右移动,进而导致 Stream1 的数据(序列号为1004的包)到达了也不能交给应用层消费。
图11
而QUIC天然支持多路复用,前面提到一个QUIC报文中可以包含多个Frame,Frame的类型可以是STREAM,每个Stream独自维护一个接收窗口,当一个Stream上发生丢包时,它只会阻止这个Stream 上的应用层读取丢失的数据,而不会影响其他Stream。
五、拥塞控制
QUIC采取了和TCP一样的拥塞控制策略,同样支持Reno、CUBIC和BBR等拥塞控制算法,只不过它是在应用层实现了拥塞控制算法,与Linux TCP一样,也默认使用CUBIC。
相比在内核实现的TCP拥塞控制算法,QUIC在应用层实现拥塞控制算法有2个好处:
-
升级周期快,不需要内核升级。目前很多服务器上的Linux版本还是3.x。很多新的拥塞控制算法还没有应用上,比如BBR是在 Linux 4.6中引入的。
-
使用更灵活,QUIC可以为不同的连接使用不同的拥塞控制算法,而内核修改拥塞控制算法会影响所有连接。
六、连接迁移
所谓连接迁移就是当客户端网络环境发生变化时,连接仍然可以使用,比如当客户端由Wi-Fi切换到4G/5G移动网络时,连接也不会断开。也就是说,客户端的IP地址发生变化时不会影响连接。QUIC不用像TCP那样使用四元组唯一标识一个连接,而是采用一个连接ID唯一标识。因为UDP是无连接的,所以当IP地址发生变化时,UDP不需要重新建立连接,直接发送包含之前连接ID的报文,服务端通过连接ID就能识别这个连接。
七、参考
-
QUIC维基百科
推荐一本《程序员进阶之路:缓存、网络、内存与案例》图书。本书是作者工作十多年来的技术沉淀和总结。非常适合想要进一步提升自己专业力的读者。服务端开发拼到最后才能体会到该书有多香!
转载自:https://juejin.cn/post/7390769366427746331