一、QUIC简介
QUIC介绍
QUIC是Google开发的基于UDP的传输协议,用于提升网络加载速度。
QUIC发展:2012年部署上线,2013年提交IETF,2021年推出标准RFC9000.
QUIC 协议非常复杂,因为它做了太多事情:
为了实现传输的可靠性,它基本上实现并且改进了整个 TCP 协议的功能,包括序列号,重传,拥塞控制,流量控制等。
为了实现传输的安全性,它又彻底重构了 TLS 协议,包括证书压缩,握手消息,0RTT 等。虽然后续可能会采用 TLS1.3 协议,但是事实上是 QUIC 推动了 TLS1.3 的发展。
为了实现传输的并发性,它又实现了 HTTP2 的大部分特性,包括多路复用,流量控制等。
Google现网的QUIC数据
- PC端:搜索和视频的传输延迟降低了8%
- 移动端:搜索降低了6%,视频降低了5.3%。
还能发现,在弱网环境下,提升更加明显。
QUIC是如何提升速度的呢?
二、QUIC的数据包格式
QUIC的数据包分为Header和Data部分,其中Header是明文传输,包括Flags是标志位,Connection ID是连接ID,可用于连接迁移,QUIC Version是QUIC的版本号,Packet Number是包序号,用于保证可靠传输;Data部分是密文传输,是一些数据帧,有很多数据帧类型:Stream、ACK、Padding、Blocked等,其中Stream帧传输应用数据。
Stream
Frame Type: Bit7~Bit0
- Bit7:必须设置为1,表示Stream帧
- Bit6:如果设置为1,表示发送端在这个stream上已经结束发送数据,流将处于半关闭状态
- Bit5:如果设置为1,表示Stream头中包含Data Length字段
- Bit432:表示offset的长度。000表示0字节,001表示1字节,002表示2字节,以此类推
- Bit10:表示Stream ID的长度。00表示1字节,01表示2字节,10表示3字节,11表示4字节
三、QUIC的实现原理
1. 建立连接
建立https连接
先分析一样https的握手过程,包括TCP握手和TLS(Transport Layer Security)握手
https连接耗时3个RTT
QUIC基于TLS建立连接
右边展示的是ECDH算法,一个RTT就可以协商好通信秘钥。
0-RTT建立连接
还可以0-RTT建立连接,原理在于:客户端缓存服务端配置ServerConfig(\(B = b*G%P\))
ClientHello: \(A = c*G%P\)
ServerHello: \(B = b*G%P\)
通信秘钥:\(key = B*c = A*b = b*c*G%P\)
客户端使用\(key\)发送数据,无需建立连接
0RTT 能实现的关键是 ServerConfig。就像 TLS session resume 实现的关键是 session id 或者 session ticket 一样。
ServerConfig 到达服务端后,我们根据 ServerConfig ID 查找本地内存,如果找到了,即认为这个数据是可信的,能够完成 0RTT 握手。
但是会有两个问题:
- 进程间 ID 数据无法共享。
- 多台服务器间的 ID 数据无法共享。
明确了问题,那工程层面就需要实现多进程共享及分布式多集群的 ID 共享。
SeverConfig Cache 集群:Stgw 在生成 ServerConfig ID 和内容时,会存储到全局的 Cache 集群。用户握手请求落到任意一台 STGW 机器,从全局 Cache 集群都能找到相应的内容,实现 0RTT 握手。
前向安全
QUIC能保证前向安全,什么是前向安全呢?
问题:假设攻击者记录所有的通信数据和公开参数,一旦服务器的私钥\(b\)泄露,则计算计算出通信秘钥,这样就可以破解之前的通信数据了
前向安全就是指用来产生会话秘钥的长期秘钥泄露出去,不会泄露以前的通讯内容
总结起来就是,通过ServerConfig实现0-RTT握手,通过会话秘钥保证数据的前向安全。
2. 可靠传输
QUIC是基于UDP的协议,而UDP是不可靠传输,QUIC如何实现可靠传输呢?
可靠传输必须满足两个条件:
- 数据完整性:发送端发出的数据包,接收端都能收到
- 数据有序性:接收端能按序组装数据包,解码得到原始数据
可靠传输:数据完整性
实现方案:基于包号PKN和确认应答SACK的丢包重传机制
PKN是单调递增的,即使是重传,PKN也和之前的不一样。那么接收端怎么保证数据的有序性呢?
通过添加数据包在原始数据中的偏移量offset,接收端根据offset字段对异步到达的数据包进行排序
3. 流量控制
就是说发送端发出的包,接收端要有足够的缓冲空间来接收。
和TCP一样,QUIC也是利用滑动窗口机制实现流量控制,也就是连续ARQ协议
如果发送端收到了接收端的ACK帧,窗口就会向右移动,可用窗口就会变大,然后发送新的数据包。
虽然都是采用滑动窗口机制,和TCP不同的是,QUIC的滑动窗口分为Connect和Stream两种级别。
- Connnect流量控制:规定了所有数据流的总窗口大小
- Stream流量控制:规定了每个流的窗口大小
因为QUIC中每个连接上可以发送多个请求,每个请求对应一条流,每个流有自己的滑动窗口,整个连接也有一个滑动窗口,其大小是所有流的可用窗口之和。
4. 拥塞控制
目的:通过拥塞窗口限制发送方的数据量,避免整个网络拥塞。
它们的目的是不同的,流量控制是为了接防止发送太快,接收方接受不过来,拥塞控制控制是为了防止发送太快,整个网络拥塞。
发送窗口的大小等于min(接受窗口,拥塞窗口),即 \(swnd = min(rwnd, cwnd)\)
前面讲了接收窗口的大小是接收端告诉发送端的,那拥塞窗口的大小怎么得到的呢?
很显然,拥塞窗口是为了避免网络拥塞,应该根据网络拥塞情况动态调整。那怎么判断网络的拥塞程度呢?例如发生超时,或连续收到三次相同的ACK等,分为基于丢包的和基于网络带宽的。
拥塞控制:慢启动
初始化:cwnd=1,也就是一个MDS(Max Datagram Size)数据包大小
对于UDP而言:MDS = 1500(MTU)- 20(IP头部) - 8(UDP头部) = 1472字节,MTU是指网卡允许的最大包长度。
通过查看QUIC的源码,发现初始阈值是2000倍的MDS。每收到2倍的ACK,拥塞窗口就增加到2倍。
拥塞控制:拥塞避免
每收到1个ACK,拥塞窗口就增加1。线性增长,一直增长,直达发生丢包。
拥塞控制:拥塞发生
超时重传,是有一个定时器,在指定时间内没有收到ACK,就会触发超时重传,并进入慢启动阶段;
快速重传,是指发送端连续收到3个相同的ACK,就会触发快速重传,并进入快速恢复阶段。
图中有点小错误,cwnd是ssthresh+3,如果是收到n个相同的ACK,则是ssthreash+n。
5. 多路复用
前面有提到QUIC的一大优势就是解决了队头阻塞的问题,什么叫队头阻塞,先回顾一个HTTP2协议。
HTTP2
HTTP2首次提出了多路复用,多路复用是指单个HTTP连接上可以同时发送多个HTTP请求,解决了HTTP1.1中单个连接只能发送一个请求的性能瓶颈。
之所以能多路复用,是因为HTTP2和HTTP1.1的帧格式是不同的,其中StreamID就是流的ID,每个请求都是自己的流ID。
- 1个请求对应1条流
- 通过Stream ID判断数据帧属于哪个请求
假设有A和B两个请求,对应的Stream ID分别为1和2
在同一个TCP连接中,可以存在两个流。但是对于TCP连接来说,仍然只有1个滑动窗口来发送这些数据包。所以这就会有队头阻塞问题。什么是队头阻塞呢?
队头阻塞
客户端将56789发送出去,服务端返回了6789的ACK,但是ACK 5丢失了,会导致发送窗口无法向前移动。也就是说队头5阻塞了后面数据包的发送。
多路复用:无队头阻塞
实现原理:给每个流都分配一个滑动窗口
虽然单条连接上无队头阻塞,但是单条流仍然存在队头阻塞。这也很容易理解,单条流还是一个滑动窗口。
6. 连接迁移
QUIC还有一个独特的优势:连接迁移,其他协议都没有这个特性。
定义:当客户端切换网络时,和服务端的连接并不会断开(逻辑上),仍然可以正常通信。
实现原理:QUIC的连接是基于64位的Connection ID,网络切换时并不会改变Connection ID,连接在逻辑上仍然是通的。
TCP是不行的,TCP是基于四元组(客户端IP,客户端Port,服务端IP,服务端端口Port),只要有一个变化连接就会断开,必须要重新建立连接。
四、总结
问题:QUIC是如何提升网络加载速度的?
- 降低连接耗时:在客户端有缓存的情况下实现0-RTT建立连接
- 更灵活的拥塞控制:在用户态可以为每个请求配置不同的拥塞控制策略
- 无队头阻塞的多路复用:每个请求流独立拥有滑动窗口,互不影响
- 连接迁移:网络切换不会中断数据传输