本章节主要是介绍谷歌推出的新传输协议 --QUIC,全称 quick udp internet connection,“快速 UDP 互联网连接”,是基于UDP协议的可靠数据传输协议。
UDP本身是不可靠的,无连接的,所以要在此基础上实现可靠传输,就需要做到前面提到的有关可靠数据传输的相关条件:
- 数据流控制:包括连接建立机制、可靠传输控制(数据编号,确认机制,重传机制) 以及 性能优化
- 拥塞控制:用来使这个协议达的性能达到可用的程度。
数据流控制
连接的建立与断开
首先是连接的建立,所谓连接建立,无非就是保存一个状态而已。与传统的TCP三次握手不同,QUIC只需要一次握手。并提供了快速重连机制。
流程大概能简化成这样:
这里主要是利用了 QUIC 对于连接的标识,和对连接断开的处理。QUIC使用一个64bit的connection id来表示一个连接,即使地址和端口号变了,只要 connection id 不变,就认为是同一个连接。
所以这个握手过程可以简单理解成客户端发送自己的 connection id ,服务端同意连接的话也返回自己的 id。至于后续连接的维持,就由其他机制进行。从而替代了TCP协议的三次握手。
快速重连机制也是基于这一点,如图所示:
客户端处缓存了服务端的信息,直接利用原有信息发送数据。服务端确认信息与自己的相符,说明曾经建立过连接,于是免去了再次握手。
这种简化的握手形式与其连接断开的方式相辅相成:
首先QUIC不提供任何优雅断开的机制,当一个连接断开的时候,发送方将立即停止任何数据的发送,接收方可以选择对迟到数据进行丢弃,并丢弃所有的状态。即将优雅断开交给应用层协议去完成。这个设计其实特别合理,相较TCP的HALF-CLOSE状态,应用层反倒要依靠TCP的 HALF-CLOSE 来确保数据完成传输,不如直接在应用层确认所有数据都传完了再关闭。而出现连接意外断开的情况,这个HALF-CLOSE不起任何作用。
如果发现数据不完整,则可以直接发起快速重连,重传丢失的数据。
QUIC总共提供三种断开机制:
- 超时断开 Deferring Idle Timeout
- 立即断开 Immediate Close
- 无状态重置 Stateless Reset
具体可以去查看文档。
总结下来就是QUIC通过以下几点
- Connection id标记连接
- 快速重连
- 不提供优雅断开
最终提供了简化的握手和挥手机制。当然握手机制中还包括了密钥交换等等细节,这里不展开叙述。
可靠传输控制(数据编号,确认机制,重传机制)
QUIC使用了严格的单调递增数据包编号机制。在TCP中,当重传发生的时候,重传的数据包编号就对应了这个包本身的顺序,接收端发回的ACK报文编号也是这个包原来的编号,并不能确定这个ACK的响应是对重传包的还是延迟包的(可以通过D-SACK解决)。
而QUIC中,重传的数据包仍然会被打上新的编号。并在数据中添加了offset字段,表示这个包的实际对应位置,保证接收端有序。而响应报文同样是针对编号的,发送端就可以直接通过ACK报文的编号来判断这个响应的是重传包还是延时包。从而免去了对D-SACK的解析。
QUIC同样使用了SACK进行选择性重传。并同样支持快速重传等重传特性。这里的SACK传递的就是 offset 信息,而非ACK编号。
性能优化机制
QUIC同样是基于滑动窗口做的性能优化,与TCP稍有不同的是,这里的滑动窗口拥有两个级别 -- 连接级和流级
这是由于QUIC引入了流(stream)的概念,在一个QUIC连接上可以创建多个流,从而为并发传输提供支持,免去TCP下同时传输多个文件需要创建多个连接。
这里稍微介绍一下QUIC的流,这里流的引入主要是用来解决队头阻塞的。
我们在访问一个网页的时候,服务器经常需要传输多个文件:html,css,js以及一些图片资源。在 HTTP1 里,客户端按顺序请求html、css、js。为了提高请求的效率,http使用了管道化传输,允许在前一次请求还未响应的时候就继续发送请求。此时就可能会出现后发送的请求完成了处理,但由于前一个请求还在处理,无法先发送下一个,造成阻塞。这就是 HTTP1 协议的队头阻塞。
原先的解决方法很简单,既然放在一起传会阻塞,那索性就多开几个tcp连接,各自传各自的,互不影响。但tcp的连接过程实在是太占资源了,又要三次握手,如果用上ssl的话还有ssl握手。
到了 HTTP2 ,HTTP2不使用管道化的方式,而是引入了帧、消息和数据流等概念,每个请求/响应被称为消息,每个消息都被拆分成若干个帧进行传输,每个帧都分配一个序号。每个帧在传输是属于一个数据流,而一个连接上可以存在多个流,各个帧在流和连接上独立传输,到达之后在组装成消息,这样就避免了请求/响应阻塞。
虽然这样解决了HTTP的队头阻塞,由于HTTP2还是基于TCP协议的,还是可能出现TCP队头阻塞,即在TCP的传输过程中,由于滑动窗口的限制,前序的某一个报文迟迟未到达,则无法发送后续报文,导致队头阻塞的出现。反映到HTTP2协议上就是所有的数据流都因为某个帧迟迟未到达而全部阻塞。
现在QUIC直接在传输层提供了流,可以是看作轻量化的连接,通过对数据包打上tag,对不同的流的数据进行分开组装。每一个流都有自己的滑动窗口,整体连接的滑动窗口是由所有流的滑动窗口计算而来。因此每个流互不影响。队头阻塞仅出现在各自的流中。
而要做到连接的滑动窗口不影响流的滑动窗口,就需要引入新的滑动窗口判断机制:
上面的是TCP连接的滑动窗口,由于其ACK响应与包顺序编号相关,即使接收到了后面来的包,也不能直接发送ACK,而是继续发送未收到的包序号。最后导致窗口无法滑动。
但QUIC的ACK响应与数据包的实际顺序无关,所以只要接收到了包就可以发送ACK响应,前面未得到响应的部分划入待响应区域,窗口滑动到已经收到响应的后面。基于这个机制,整个连接的滑动窗口只需要对各流的滑动窗口进行简单拼接即可,不会因为某个迟迟未响应的包而导致连接的滑动窗口阻塞,进而导致所有流的阻塞。
拥塞控制
QUIC很好的实现了数据流控制和拥塞控制的解耦,因此QUIC的拥塞控制是模块化可插拔的。支持多种拥塞控制算法,这里就不做过多介绍控制算法本身。
重点就是在于这个解耦的实现。QUIC实现了一个数据发送状态机,拥塞控制算法只负责指导发包数量和发包间隔,至于发包的内容(重传or正常发包)由数据流控制部分自行解决。大概就是数据流控制部分负责维护一个发送队列。数据发送状态机根据拥塞控制部分下发的发包间隔和发包数量从队列中取数据发送,真正解耦。
QUIC的其他特性
- 连接迁移:由于QUIC根据Connection id来维护连接信息,所以不关心连接的ip和端口,可以在不进行重连的情况下动态切换ip和端口,底层原理就是udp的无连接性。
- 高安全性:QUIC的传输过程均是加密的,在连接握手的时候就会通过加密机制交换密钥,具体查看文档
- FEC前向纠正拥塞控制,QUIC会传输N+1冗余数据包,基于异或校验算法,出现丢包时,在某些情况可以直接通过冗余数据包恢复,避免部分重传。
- 更加精确的RTT计算:QUIC的ACK报文会携带上从接收到发送该ACK报文时所使用的时间,提供了缓存延迟的信息。
参考资料
https://quicwg.org/ops-drafts/draft-ietf-quic-applicability.html
https://www.rfc-editor.org/rfc/rfc9000.html
https://zhuanlan.zhihu.com/p/405387352
https://blog.csdn.net/chenhaifeng2016/article/details/79011059
叨叨几句... NOTHING