简介
QUIC(Quic UDP Internet Connection) 协议是基于 UDP 协议实现的一种支持多路复用及安全传输的协议,基于 QUIC 协议的 HTTP/3 正在规划中,而 QUIC 协议已经应用在了 Chrome 浏览器中,HTTP/2 over QUIC 已经取得了非常成功的成效
QUIC协议 存在的意义在于解决 TCP 协议的一些无法解决的痛点
- 多次握手:TCP 协议需要三次握手建立连接,而如果需要 TLS 证书的交换,那么则需要更多次的握手才能建立可靠连接,这在如今长肥网络的趋势下是一个巨大的痛点
- 队头阻塞:TCP 协议下,如果出现丢包,则一条连接将一直被阻塞等待该包的重传,即使后来的数据包可以被缓存,但也无法被递交给应用层去处理。
- 无法判断一个 ACK 是重传包的 ACK 还是原本包的 ACK:比如 一个包 seq=1, 超时重传的包同样是 seq=1,这样在收到一个 ack=1 之后,我们无法判断这个 ack 是对之前的包的 ack 还是对重传包的 ack,这会导致我们对 RTT 的估计出现误差,无法提供更准确的拥塞控制
- 无法进行连接迁移:一条连接由一个四元组标识,在当今移动互联网的时代,如果一台手机从一个 wifi 环境切换到另一个 wifi 环境,ip 发生变化,那么连接必须重新建立,inflight 的包全部丢失。
现在我们给出一个 QUIC 协议的 Overview
- 更好的连接建立方式
- 更好的拥塞控制
- 没有队头阻塞的多路复用
- 前向纠错
- 连接迁移
QUIC 建立连接的方式
首先要知道一点,QUIC 作者也明确指出当前的 QUIC 加密协议是 destined to die 的,未来会被 TLS 1.3 替代,不过 QUIC 协议提出时,TLS 1.3 还没有问世…下面,我们还是直接讲 TLS 1.3 如何建立连接吧
TLS 1.2 的情况下,一个连接的建立需要经过这样几个过程:
- TCP 三次握手 (1-RTT)
- 建立 TLS 1.2 安全连接 (2-RTT)
- client 发送
client hello
包给 server, 包含了一个随机数 R1, 其支持的加密套件, 其他各种首选项 - server 接收到
client hello
后,发送server hello
,包含了一个随机数 R2, 又发送certificate
证书,包含了 RSA 加密的公钥,可选发送一个ServerKeyExchange
(仅在 Certificate 不足够使 client 交换 预主密钥 时发送),全部发送完之后最后发送一个ServerHelloDone
。 - 到client 接收 server 发送的全部信息,以上过程花费 1-RTT,现在 client 有两个随机数 R1, R2, 证书及公钥,约定的加密算法信息等首选项;server 也有两个随机数 R1, R2,也知道约定的加密算法等信息
- client 验证证书合法性,包括有效期、证书链可信性、域名是否和证书匹配等。验证通过之后使用证书携带的公钥加密一个随机数 R3,形成预主密钥,发送给 server,然后由 R1, R2 和预主密钥,计算出协商的对称加密密钥,用于之后信息交换的加密
- server 通过自身的 RSA 私钥解密出预主密钥,此时也有 R1, R2 和预主密钥,通过同样的加密算法,计算出相同的对称加密密钥,给 client 发送一个
Finished
包 - client 接收到
Finished
后便可以通过对称密钥来加密HTTP请求的消息了,以上过程又花费 1-RTT,此时才开始发送有效载荷
- client 发送
以上的过程如果不算上 TCP 握手,已经有 2-RTT 的延迟
而 TLS 1.3 相对于 TLS 1.2 的巨大改进就在于,它只需要 1-RTT 就可以建立安全连接,而且在 PSK 会话恢复的情况下可以实现 0-RTT 的连接建立
下面我们就介绍一下 TLS 1.3 连接建立的方式
先介绍一下 1-RTT 的连接建立过程:
- 同样,client 需要发送
client hello
, 但这次,它不仅仅告诉 server 它支持的加密套件,还把各个加密套件的 Key Share(譬如 DH 加密参数) 也一并发送了出去 - server 接收到之后,就可以选择一个加密算法,结合 Key Share,就可以计算出密钥,同时发送给 client
Finished
,client 接收到这个消息之后,连接就建立完毕了。接着就可以使用密钥来加密 HTTP 消息
下面是一个直观的对比图:
0-RTT 开启的条件:
- Server 在之前的一次握手中,发送了 Session Ticket,并且 Session Ticket 中存在 max_early_data_size 扩展表示愿意接受 early data
- 在 PSK(预共享密钥) 会话恢复的过程中,ClientHello 的扩展中配置了 early data 扩展,表示 Client 想要开启 0-RTT 模式。
- Server 在 Encrypted Extensions 消息中携带了 early data 扩展表示同意读取 early data。0-RTT 模式开启成功。
当然,TLS 1.3 的改进不仅仅在握手延迟的改善上,还在于删除了不安全的加密算法,提供了更高的安全性
但是,即使是 TLS 1.3,在结合 TCP 协议的情况下,仍然需要 2-RTT 或者 1-RTT 才能建立连接,如果是结合了 QUIC 协议,则又可以减少 1-RTT 的延迟!
TCP 协议是工作在内核态的,我们无法在用户态去修改协议的内容,这使得任何建立在 TCP 协议基础上的连接,都无法避免这 1-RTT 的三次握手
而 QUIC 协议借助 TLS 1.3 来完成握手,使得 QUIC + TLS 1.3 的延迟只有 1-RTT 甚至 0-RTT!
TCP | TCP+TLS1.2 | TCP+TLS 1.3 | QUIC | |
---|---|---|---|---|
第一次连接 | 1-RTT | 3-RTT | 2-RTT | 1-RTT |
重复连接 | 1-RTT | 2-RTT | 1-RTT | 0-RTT |
QUIC 在拥塞控制上的改进
QUIC 协议在拥塞控制算法上其实只是套用了 TCP 的算法,但是它的一些独特的地方使得 QUIC 协议整体的拥塞控制也相对于 TCP 有一些改善,改善主要有以下几点:
- 更精准的 RTT 测量
- 拥塞控制可插拔
- 支持 256 个 NACK 字段
RTT 测量
QUIC 协议的包有单调递增的 packet number,可以更精确的计算 RTT,对于TCP 协议,超时事件发生后,收到了重传的包的 ack,不知道这个ack是重传的包的 ack,还是之前的包的ack,这会造成对 RTT 的错误估计。
你可能会好奇在单调递增的情况下如何保证传输才有序性和可靠性,事实上 QUIC 协议中对于同一个连接的不同流(这关系到 QUIC 的另一个特性,多路复用),有一个专门用于标识顺序的 offset 字段,这个偏移量就可以保证 stream 的有序性和可靠性了。
拥塞控制可插拔
同一台服务器上面对的不同客户端,他们的网络状况千差万别,有的丢包率高,有的 RTT 高,如果是 TCP,则拥塞控制算法是固定的,但如果是quic,可以针对不同的用户用不同的拥塞控制算法。
支持 256 个 NACK 字段
对于 TCP 协议,由于 option 字段的长度限制为 40 字节,所以其 SACK 字段最多只支持 4组 SACK,而算上 SACK 头和 timestamp 字段,TCP 中只能有三组 SACK,这大大限制了高丢包率场景下的 一次 ack 的数量
而 QUIC 协议支持最多 256 个 NACK 字段(NACK 其实相当于是反着的 SACK,SACK 是接收端告诉发送端它接收了哪些字段,NACK则告诉发送端它没有接收到哪些字段)
QUIC 没有队头阻塞的多路复用
在 tcp 协议中,假如中间有丢包,即使是缓存下来之后到达的数据,用户层也无法读取这个数据,整个数据队列就阻塞在丢了的包的位置,但在 QUIC 协议中,通过多路复用在同一条连接上标识不同的流,则当一个包丢失了,只会阻塞该包所在的流,不会影响其他的流。
QUIC 连接迁移
通过 connection ID 来标识连接,使得客户端在网络环境改变的情况下(比如由 wifi 切换到移动数据),连接可以继续维持,不需要重新建立
QUIC FEC 前向纠错
在少量丢包的场景下,前向纠错码的存在使得丢失的包无需重传,直接纠正错误
缺点
QUIC协议建立在 UDP 的基础上,而大多运营商对于 UDP 的 QoS 级别是小于 TCP 的,这使得 TCP 协议下的带宽可能会比 UDP 协议要高