计算机网络闲谈01——QUIC协议
预备知识
重传机制
RTT 一个连接的往返时间
RTO 重传超时时间
RTT和RTO 的关系是:由于网络波动的不确定性,每个RTT都是动态变化的,所以RTO也应随着RTT动态变化。
流量控制
对发送方发送速率的控制 称之为 流量控制
拥塞控制
- 慢启动
- 拥塞避免
- 快速重传
- 快速回复
QUIC协议
所以使用QUIC的优点就在于减少了系统的延时,适用于可以容忍一些数据丢包的情况,比如在线游戏、广告竞价、在线视频、实时流等地方。
-
低延时 TLS1.3 减少了一次往返时延
-
更快的连接迁移 connection id
-
队头阻塞问题 stream 多路流复用 解决Hot
-
更加灵活的拥塞控制 自定义
01 更方便的连接迁移
一条 TCP 连接是由四元组标识的(源 IP,源端口,目的 IP,目的端口)。
什么叫连接迁移呢?就是当其中任何一个元素发生变化时,这条连接依然维持着,能够保持业务逻辑不中断。当然这里面主要关注的是客户端的变化,因为客户端不可控并且网络环境经常发生变化,而服务端的 IP 和端口一般都是固定的。
比如大家使用手机在 WIFI 和 4G 移动网络切换时,客户端的 IP 肯定会发生变化,需要重新建立和服务端的 TCP 连接。
又比如大家使用公共 NAT 出口时,有些连接竞争时需要重新绑定端口,导致客户端的端口发生变化,同样需要重新建立 TCP 连接。
所以从 TCP 连接的角度来讲,这个问题是无解的。
QUIC解决方法
当用户的地址发生变化时,如 WIFI 切换到 4G 场景,基于 TCP 的 HTTP 协议无法保持连接的存活。QUIC 基于连接 ID 唯一识别连接。当源地址发生改变时,QUIC 仍然可以保证连接存活和数据正常收发。
那 QUIC 是如何做到连接迁移呢?很简单,QUIC是基于UDP协议的,任何一条 QUIC 连接不再以 IP 及端口四元组标识,而是以一个 64 位的随机数作为 ID 来标识,这样就算 IP 或者端口发生变化时,只要 ID 不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连。
02 更快的连接建立
以一次简单的浏览器访问为例,在地址栏中输入https://www.abc.com,实际会产生以下动作:
◎DNS递归查询www.abc.com,获取地址解析的对应IP;
◎TCP握手,我们熟悉的TCP三次握手需要需要1个RTT;
◎TLS握手,以目前应用最广泛的TLS 1.2而言,需要2个RTT。对于非首次建连,可以选择启用会话重用,则可缩小握手时间到1个RTT;
◎HTTP业务数据交互,假设abc.com的数据在一次交互就能取回来。那么业务数据的交互需要1个RTT;经过上面的过程分析可知,要完成一次简短的HTTPS业务数据交互,需要经历:新连接 4RTT + DNS;会话重用 3RTT + DNS。
TCP握手(1个)=》TLS握手(2个)=》数据传输(1个)
所以,对于数据量小的请求而言,单一次的请求握手就占用了大量的时间,对于用户体验的影响非常大。同时,在用户网络不佳的情况下,RTT延时会变得较高,极其影响用户体验。
QUIC解决方法
QUIC 由于基于 UDP,无需 TCP 连接,在最好情况下,短连接下 QUIC 可以做到 0RTT 开启数据传输。而基于 TCP 的 HTTPS,即使在最好的 TLS1.3 的 early data 下仍然需要 1RTT 开启数据传输。而对于目前线上常见的 TLS1.2 完全握手的情况,则需要 3RTT 开启数据传输。对于 RTT 敏感的业务,QUIC 可以有效的降低连接建立延迟。
究其原因一方面是TCP和TLS分层设计导致的:分层的设计需要每个逻辑层次分别建立自己的连接状态。另一方面是TLS的握手阶段复杂的密钥协商机制导致的。要降低建连耗时,需要从这两方面着手。
QUIC具体握手过程如下:
- 客户端判断本地是否已有服务器的全部配置参数(证书配置信息),如果有则直接跳转到(5),否则继续 ;
- 客户端向服务器发送inchoate client hello(CHLO)消息,请求服务器传输配置参数;
- 服务器收到CHLO,回复rejection(REJ)消息,其中包含服务器的部分配置参数;
- 客户端收到REJ,提取并存储服务器配置参数,跳回到(1) ;
- 客户端向服务器发送full client hello消息,开始正式握手,消息中包括客户端选择的公开数。此时客户端根据获取的服务器配置参数和自己选择的公开数,可以计算出初始密钥K1;
- 服务器收到full client hello,如果不同意连接就回复REJ,同(3);如果同意连接,根据客户端的公开数计算出初始密钥K1,回复server hello(SHLO)消息,SHLO用初始密钥K1加密,并且其中包含服务器选择的一个临时公开数;
- 客户端收到服务器的回复,如果是REJ则情况同(4);如果是SHLO,则尝试用初始密钥K1解密,提取出临时公开数;
- 客户端和服务器根据临时公开数和初始密钥K1,各自基于SHA-256算法推导出会话密钥K2
- 双方更换为使用会话密钥K2通信,初始密钥K1此时已无用,QUIC握手过程完毕。之后会话密钥K2更新的流程与以上过程类似,只是数据包中的某些字段略有不同。
加密握手过程
- 客户端向服务器发送一个带有客户端随机数和支持的加密算法列表的INITIAL报文。
- 服务器收到后生成一个自己的随机数,并根据客户端发来的加密算法列表选择一个加密套件,然后将自己的随机数和所选的加密套件作为响应一并发给客户端。
- 客户端收到服务器的响应后,会生成会话密钥,并使用所选的加密套件进行加密,随后向服务器发送一个带有客户端证书、加密数据以及证书验证信息的ENCRYPTED_EXTENSIONS报文,用来表示协商成功。
03 解决了TCP队头阻塞问题
虽然 HTTP2 实现了多路复用,但是因为其基于面向字节流的 TCP,因此一旦丢包,将会影响多路复用下的所有请求流。QUIC 基于 UDP,在设计上就解决了队头阻塞问题。
TCP 队头阻塞的主要原因是数据包超时确认或丢失阻塞了当前窗口向右滑动,我们最容易想到的解决队头阻塞的方案是不让超时确认或丢失的数据包将当前窗口阻塞在原地。QUIC也正是采用上述方案来解决TCP 队头阻塞问题的。
TCP 为了保证可靠性,使用了基于字节序号的 Sequence Number 及 Ack 来确认消息的有序到达。
QUCI解决方法
QUIC 同样是一个可靠的协议,它使用 Packet Number 代替了 TCP 的 Sequence Number,并且每个 Packet Number 都严格递增,也就是说就算 Packet N 丢失了,重传的 Packet N 的 Packet Number 已经不是 N,而是一个比 N 大的值,比如Packet N+M。
QUIC 使用的Packet Number 单调递增的设计,可以让数据包不再像TCP 那样必须有序确认,QUIC 支持乱序确认,当数据包Packet N 丢失后,只要有新的已接收数据包确认,当前窗口就会继续向右滑动。待发送端获知数据包Packet N 丢失后,会将需要重传的数据包放到待发送队列,重新编号比如数据包Packet N+M 后重新发送给接收端,对重传数据包的处理跟发送新的数据包类似,这样就不会因为丢包重传将当前窗口阻塞在原地,从而解决了队头阻塞问题。那么,既然重传数据包的Packet N+M 与丢失数据包的Packet N 编号并不一致,我们怎么确定这两个数据包的内容一样呢?
QUIC使用Stream ID来标识当前数据流属于哪个资源请求,这同时也是数据包多路复用传输到接收端后能正常组装的依据。重传的数据包Packet N+M 和丢失的数据包Packet N 单靠Stream ID 的比对一致仍然不能判断两个数据包内容一致,还需要再新增一个字段Stream Offset,标识当前数据包在当前Stream ID 中的字节偏移量。
有了Stream Offset 字段信息,属于同一个Stream ID 的数据包也可以乱序传输了(HTTP/2 中仅靠Stream ID 标识,要求同属于一个Stream ID 的数据帧必须有序传输),通过两个数据包的Stream ID 与 Stream Offset 都一致,就说明这两个数据包的内容一致。