Garaguru's Blog Just A Normal Student

QUIC 协议详解

2019-09-11
Garaguru

简介

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 的情况下,一个连接的建立需要经过这样几个过程:

  1. TCP 三次握手 (1-RTT)
  2. 建立 TLS 1.2 安全连接 (2-RTT)
    1. client 发送 client hello 包给 server, 包含了一个随机数 R1, 其支持的加密套件, 其他各种首选项
    2. server 接收到 client hello 后,发送 server hello,包含了一个随机数 R2, 又发送 certificate 证书,包含了 RSA 加密的公钥,可选发送一个 ServerKeyExchange (仅在 Certificate 不足够使 client 交换 预主密钥 时发送),全部发送完之后最后发送一个 ServerHelloDone
    3. 到client 接收 server 发送的全部信息,以上过程花费 1-RTT,现在 client 有两个随机数 R1, R2, 证书及公钥,约定的加密算法信息等首选项;server 也有两个随机数 R1, R2,也知道约定的加密算法等信息
    4. client 验证证书合法性,包括有效期、证书链可信性、域名是否和证书匹配等。验证通过之后使用证书携带的公钥加密一个随机数 R3,形成预主密钥,发送给 server,然后由 R1, R2 和预主密钥,计算出协商的对称加密密钥,用于之后信息交换的加密
    5. server 通过自身的 RSA 私钥解密出预主密钥,此时也有 R1, R2 和预主密钥,通过同样的加密算法,计算出相同的对称加密密钥,给 client 发送一个 Finished
    6. client 接收到 Finished 后便可以通过对称密钥来加密HTTP请求的消息了,以上过程又花费 1-RTT,此时才开始发送有效载荷

以上的过程如果不算上 TCP 握手,已经有 2-RTT 的延迟

而 TLS 1.3 相对于 TLS 1.2 的巨大改进就在于,它只需要 1-RTT 就可以建立安全连接,而且在 PSK 会话恢复的情况下可以实现 0-RTT 的连接建立

下面我们就介绍一下 TLS 1.3 连接建立的方式

先介绍一下 1-RTT 的连接建立过程:

  1. 同样,client 需要发送 client hello, 但这次,它不仅仅告诉 server 它支持的加密套件,还把各个加密套件的 Key Share(譬如 DH 加密参数) 也一并发送了出去
  2. server 接收到之后,就可以选择一个加密算法,结合 Key Share,就可以计算出密钥,同时发送给 client Finished,client 接收到这个消息之后,连接就建立完毕了。接着就可以使用密钥来加密 HTTP 消息

下面是一个直观的对比图:

0-RTT 开启的条件:

  1. Server 在之前的一次握手中,发送了 Session Ticket,并且 Session Ticket 中存在 max_early_data_size 扩展表示愿意接受 early data
  2. 在 PSK(预共享密钥) 会话恢复的过程中,ClientHello 的扩展中配置了 early data 扩展,表示 Client 想要开启 0-RTT 模式。
  3. 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 协议要高


上一篇 Zookeeper 简介

Content