Quick, UDP, Internet, Connections
诞生背景
- HTTP/1:每次请求都建立一个TCP连接
- HTTP/1.1:支持长连接,同一个IP对应一个TCP连接
-
HTTP/2:TCP多路复用,同一个TCP并发 多个HTTP请求
- 并发数量在浏览器实现上有限制,以Chrome为例为6,普遍为6~8(可能为滑动窗口大小限制,或者因为更多的并发数量若发生头部拥塞使得总体传输速率下降)
使用HTTP/2所提供的多路复用功能在链路出现丢包时,TCP的按序确认机制使得丢失的数据包需要等待重新发送和确认,滑动窗口停滞,其后的所有数据包都被阻塞,这样一来HTTP/2在这种情形下的表现反而不如HTTP/1。
此外,HTTP/2在建立TCP连接的时,需要和服务器进行三次握手来确认连接成功,会消耗1.5个RTT,如果使用HTTPS的话,还需要使用TLS协议进行加密,而TLS也根据版本需要1~2个RTT(TLS1.2需要1RTT),也就是说,使用HTTP/2在信息得到传输前就需要消耗3~4个RTT(至少2.5RTT)的时间。
TCP的短板问题
- TCP +TSL握手占用时间(至少2.5RTT)
-
TCP巨大的头部浪费带宽(20~60字节)
- TCP 头部拥塞
TCP的按序确认除了导致头部拥塞外,还导致了另一个重传包数量问题:TCP接收方可以将未按序到达的数据包37、38、40先行缓存(并且引入快速重传机制,回送缺失数据包34的ack不断提醒发送方,如果发送方连续收到3次相同的ack,就会重传,防止超时引发窗口缩小),但是由于ack序列号只能确认连续的数据包,所以无法通知发送方37、38、40已经先行到达,只能发送数据包34的ack,而发送方在接收到重传请求后不确定从35~41这些已经发送的数据包要不要同样重传,因为后续的包可能被接收,也可能丢失。如果全部重传,那么会浪费带宽,如果不重传,那么如果这些包丢失,就会浪费时间。(后续又引入了SACK机制:caoziye.top/2019/10/TCP…
- TCP连接无法迁移(源IP + 源Port + 目标IP + 目标Port + 传输层协议)
除了传输层协议是TCP不变以外,剩下的四元组其中任一发生变化,TCP连接就会断开,需要重新和新的ip:port重新握手建立连接。比如移动设备wifi和5g网络的切换,或者是行车过程中导致的移动网络节点的切换都会让TCP的连接断开。
传输层协议带来的问题无法在应用层协议上得到解决,并且TCP因为已经存在了40多年,基于TCP协议的更新非常难以推进(因为被大量内置于操作系统内核、中间件固件以及硬件实现中),因此Google基于UDP协议推出了QUIC协议。
UDP协议相较于TCP,拥有更小的头部,简单而高效,但是不保证可靠交付,因此使用UDP协议同时为了确保数据传输的可靠性,需要自己维护丢包检测、数据确认、拥塞控制、重传等等一系列基础设施。
QUIC主要特性
多路复用
HTTP/2.0使得一个TCP连接能够顺序传输多个文件,再通过SPDY协议实现请求的并发以及优先级控制,但是终归会受到头部拥塞的限制。
而QUIC是基于UDP的,在传输层层面并没有固定的连接,可以根据需要开辟任意逻辑链路。QUIC一次建立一个Connection,一个Connection下包含多个Stream流(每个stream独自维护一个逻辑连接,因为UDP层面上是无连接的),每个流对应一个文件传输,并将不同的Stream中的数据交付给不同的上层应用。QUIC的一个Connection对应多个Stream,Stream之间相互独立,因此任意一条链路断开都不会导致其他数据阻塞。
协议头部
QUIC是基于UDP的,所以最外层是UDP头部(单位为Bit)
内部是QUIC Connection头部和每个Stream的Frame头部(单位为Bit)
具体每个头部字段含义和标志位过于机械和繁杂,有兴趣可以直接查看原文datatracker.ietf.org/doc/html/rf…
- Flags: 用于表示 Connection ID 长度、Packet Number 长度等信息;
- Connection ID:客户端选择的无符号64位统计随机数,该数字是连接的标识符。由于 QUIC 的连接被设计为,即使客户端漫游,连接依然保持建立状态,因而 IP 4元组(源IP,源端口,目标IP,目标端口)可能不足以标识连接。对每个传输方向,当4元组足以标识连接时,连接ID可以省略。
- QUIC Version:QUIC 协议的版本号,32 位的可选字段。
- Diversification nonce:这是服务端用于生成会话密钥的字段,仅存于服务端->客户端的请求中。一旦前向保密连接得到建立,后续就不会再包含这个字段了,简单理解就是只在服务端->客户端的握手请求中才会使用。(因此QUIC工作组也推进TLS的后续标准将这个字段整合进TLS1.3的头部中,而不存在于QUIC中)
- Packet Number:长度取决于 Public Flag 中 Bit4 及 Bit5 两位的值,最大长度 6 字节。发送端在每个普通报文中设置 Packet Number。发送端发送的第一个包的序列号是 1,随后的数据包中的序列号的都大于前一个包中的序列号;
- Stream ID:用于标识当前数据流属于哪个资源请求;
- Offset:标识当前数据包在当前 Stream ID 中的偏移量。
数据流控制
QUIC提供了两种层面上的数据流控制方案:
- Stream 流量控制,通过限制在任何 stream 上可以发送的最大绝对字节偏移量,防止单个 stream 消耗连接(connection)的全部接收缓冲。
- Connection流量控制,通过限制所有
STREAM
帧的数据总字节数,防止发送方超过接收方的连接缓冲容量。
Stream控制
- QUIC 的Stream流基于Stream ID+Offset进行包确认,流量控制需要保证所发送的所有包offset小于最大绝对字节偏移量 ( maximum absolute byte offset ) , 该值是基于当前已经提交的字节偏移量(offset of data consumed) 而进行确定的,QUIC会把连续的已确认的offset数据向上层应用提交。QUIC支持乱序确认,但本身也是按序(offset顺序)发送数据包。
- QUIC利用ack frame来进行数据包的确认,来保证可靠传输。一个ack frame只包含多个确认信息,没有正文。
- 如果数据包N超时,发送端将超时数据包N重新设置编号M(即下一个顺序的数据包编号) 后发送给接收端。
- 在一个数据包发生超时后,其余的已经发送的数据包依旧可以基于Offset得到确认,避免了TCP利用SACK才能解决的重传问题。
💡 其实QUIC的乱序确认设计思想并不新鲜,大量网络视频流就是通过类似的基于UDP的RUDP、RTP、UDT等协议来实现快速可靠传输的。他们同样支持乱序确认,所以就会导致这样的观看体验:明明进度条显示还有一段缓存,但是画面就是卡着不动了,如果跳过的话视频又能够播放了。
- 如图所示,当前缓冲区大小为8,QUIC按序(offset顺序)发送29-36的数据包:
- 31、32、34数据包先到达,基于offset被优先乱序确认,但30数据包没有确认,所以当前已提交的字节偏移量不变,缓存区不变。
- 30到达并确认,缓存区收缩到阈值,接收方发送MAX_STREAM_DATA frame(协商缓存大小的特定帧)给发送方,请求增长最大绝对字节偏移量。
- 协商完毕后最大绝对字节偏移量右移,缓存区变大,同时发送方发现数据包33超时
- 发送方将超时数据包重新编号为42继续发送
以上就是最基本的数据包发送-接收过程,控制数据发送的唯一限制就是最大绝对字节偏移量,该值是接收方基于当前已经提交的偏移量(连续已确认并向上层应用提交的数据包offset)和发送方协商得出。
Connection控制
除了Stream层面的数据流控制之外,QUIC还提供了Connection层面的总体缓存大小控制,Connection具有总体的缓冲区大小限制,并且可以为其中的各个stream动态分配缓冲区大小,在总体缓冲区大小不变的情况下优先向速度更快的stream倾斜(并不是平均分配)。
如图所示,Connection具有传输字节上限,即Stream1、2、3的Maximum Offset之和不得超过该上限,QUIC会根据网络情况为各个Stream分配不同的偏移量,并且随着传输的进行,接收方会发送MAX_DATA frame通知发送方提高Connection总体传输字节分配上限,并在Stream连接中通过MAX_STREAM_DATA frame为各个Stream分配更多的缓存。
快速握手与加密传输
QUIC在握手过程中使用Diffie-Hellman算法协商初始密钥,初始情况下服务器存储的配置参数如下:
- Server Config:一个服务器配置文件,包括服务器端的Diffie-Hellman算法的长期公钥A以及两个固定质数g和p
- Certificate Chain:用来对服务器进行认证的信任链证书
- Signature of the Server Config:Server Config的签名并用信任链的叶子证书的私钥加密
- Source-Address Token:一个经过身份验证的加密块,包含客户端可见的IP地址和服务器的时间戳。
这些参数会周期性的更新。
Diffie-Hellman 算法的基本原理
Diffie-Hellman并不是加密算法,而是密钥的一种交换技术,可以通过该算法在双方互不知情的情况下建立加密通讯
假设Alice为服务器,Bob为客户端
- Alice和 Bob 都知道两个素数(g、p)的存在
- Alice随机选择a作为private key,Bob随机选择b作为private key
于是,双方都有了一个共享密钥 (初始密钥)K。简单理解,a、b就相当于密钥,A、B就相当于公钥。
随后再利用这个初始密钥商定会话密钥,之后就一直用会话密钥沟通了。
密钥交换过程
QUIC 首次连接需要1RTT,具体过程如下:
step1: 客户端发送Inchoate Client Hello消息(CHLO)请求建立连接。
step2: 服务器根据一组质数p以及其原根g和a(长期私钥)算出A(长期公钥),将Apg(通过CA证书私钥加密后)放在serverConfig里面,发到Rejection消息(REJ)到客户端;
服务器一开始不直接使用随机生成的短期密钥的原因就是因为客户端可以缓存下服务端的长期公钥,这样在下一次连接的时候客户端就可以直接使用这个长期公钥实现0-RTT握手并直接发送加密数据
setp3&4: 客户端在接收到REJ消息后,会随机选择一个数b(短期密钥),并用CA证书获取的公钥解密出serverConfig里面的p、A和b就可以算出初始密钥K,并将B(Complete client hello消息)和用初始密钥K加密的Data数据发到服务器。
step5: 服务器收到客户端发来的公开数B,再利用p、g计算得到同样的初始秘钥K,来解密客户端发来的数据。这时会利用其他加密算法随机生成此次会话密钥K' ,再通过初始密钥K加密K'发送给客户端(SHLO)(每次会话都是用随机密钥,并且服务器会定期更新a和A,实际上这就是为了保证前向安全性)
在密码学中,前向保密(Forward Secrecy)是密码学中通讯协议的安全属性,指的是当前使用的主密钥泄漏不会导致过去的会话密钥泄漏。
step6: 客户端收到SHLO后利用初始密钥K解出会话密钥K',二者后续的会话都使用K'加密。
连接迁移
TCP 的连接标识是通过 “源IP + 源Port + 目标IP + 目标Port + 传输层协议(TCP)” 组成的唯一五元组,一旦其中一个参数发生变化,则需要重新创建新的 TCP 连接。
- 比如wifi和5g网络切换
- 服务数据节点切换
都会造成TCP断线,需要客户端上层应用重新发送请求建立连接(又一次进行握手)
QUIC 连接不再以 IP 及端口四元组标识,而是以一个服务端产生的 64 位的随机数作为 ID 来标识,这样就算 IP 或者端口发生变化时,只要 ID 不变,这条连接依然维持着,上层业务逻辑感知不到变化,不会中断,也就不需要重连。(当然如果UDP和IP协议所包含的源IP + 源Port + 目标IP + 目标Port四元组已经能够标识链接的唯一性的话,connection头部是可忽略的)
连接迁移的简化流程(实际情况更为复杂):
- 连接迁移之前,客户端的IP 1,使用非探测包(Non-probing Packet)和服务端进行通信。
- 客户端的IP变成 2,它继续发送非探测包维持通信,将连接迁移到新的地址。
-
服务端收到包后在新路径启动路径验证,验证新路径的可达性,以及客户端对其新IP地址的所有权。
- 服务端发送包含
PATH_CHALLENGE
帧的探测包(Probing Packet),PATH_CHALLENGE
帧里面包含一个不可预测的随机值。 - 客户端在
PATH_RESPONSE
帧里面包含前一步PATH_CHALLENGE
接收到的随机值,响应探测包(Probing Packet)。 - 服务端接收到客户端发送的的
PATH_RESPONSE
,验证 payload 里面的值是否正确。
- 服务端发送包含
- 随后客户端也会对服务端进行路径验证保证双向通信。
丢包检测
TCP 传输的数据只包括校验码,并没有增加纠错码等冗余数据,如果出现部分数据丢失或损坏,只能重新发送该数据包。
QUIC 引入了前向冗余纠错码(FEC: Fowrard Error Correcting),如果接收端出现少量(不超过FEC的纠错能力)的丢包或错包,可以借助冗余纠错码恢复丢失或损坏的数据包,这就不需要再重传该数据包了,降低了丢包重传概率,自然就减少了拥塞控制机制的触发次数,可以维持较高的网络利用效率。因此需要根据当前网络状况设置一定比率的冗余数据,就可以带来网络利用率的提升。
此外由于QUIC 采用单向递增的Packet Number 来标识数据包,所以不像TCP会因为超时重传的同样序列的数据包而和原数据包重叠,造成RTT测量的不准确,进而导致RTO(Retransmission Time Out:重传超时时间)的不准确。
TCP的RTT计算
TCP对于此问题也是非常头疼,于是也不断进行改进,比如
- 忽略重传,不把重传的 RTT 做采样, 但是当网络波动产生大延时,所有的包都需要重传而此时RTO又不会被更新,导致数据包超时时间估算不准确。
-
通过各种参数修正的计算方法:
- 首先计算平滑平滑RTT(Smooth RTT)
- 计算平滑RTT和真实的差距(加权移动平均)
- 再经过各种修正最终得出RTO:
- 在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4 —— nobody knows why, it just works…
QUIC的RTT计算
QUIC的包号不会重复,重传的包采用了新的Packet Number,因此不会产生RTT歧义问题
因此QUIC对于RTT的计算更为准确,预估的超时时间能够有效防止更多的重传请求被错误地发送回发送端。同时也给予了QUIC网络更为快速的反应时间,及时通知发送方重传数据包。
自定义拥塞控制
QUIC 的传输控制不再依赖内核的拥塞控制算法,而是实现在应用层上,这意味着我们根据不同的业务场景,实现和配置不同的拥塞控制算法以及参数。比如BRR或者Cubic,如果有兴趣可以自行查阅相关算法资料。
在HTTP/3上的应用
- wifi和移动网络无缝切换
- 更强的网络安全性(前向安全+全载荷加密)
- 在慢网情况下更高的传输速率
QUIC离我们并不遥远
QUIC早在2012年就已经开始试验性部署,关于其详细草案在2015年向IETF提出,终于在2021年五月被接受并于RFC9000中标准化。
chrome://flags/#enable-quic
在chrome浏览器中可以选择是否开启QUIC实验性功能,如果服务端支持QUIC协议,就会启用该协议(大部分都是Google的服务器)。
推荐一个插件可以查看当前网页支持的连接类型:HTTP/2 and SPDY indicator:
chrome.google.com/webstore/de…
性能参考(数据来源:腾讯PCG研发部)
60kb主页面资源加载速度(单位:毫秒)
弱网环境下的表现
不同丢包率下的下载耗时
从总体上来看,QUIC在网络环境良好的情况下对于当前HTTP2的提升有限,尤其是首次1-RTT握手的总体时间消耗提升只有15%左右,但是在后续有缓存的情况下建立连接的速度就会快很多,首次响应时间将会大大缩小。
此外在弱网环境下,尤其是丢包率高的情况下QUIC对于性能提升十分惊人,良好的RTO估算机制使得超时重发的估算变得更为精确。同时多个逻辑连接使得文件与文件之间的传输互不干扰阻塞,加上更加轻量的头部和简单高效的握手方式,因此能够在弱网环境下取得更为强大的表现。
总结
随着网络基础设施的提升,UDP的传输准确率也得到了很大的提升,而TCP却因为20~60字节的头部以及可能的头部拥塞导致一定的效率降低,但是TCP协议已经被大量内置于操作系统内核中,因此只能利用UDP进行定制化。虽然QUIC可能会在小页面的性能不如TCP,但随着前端日益复杂化,资源量不断增大的情况下,使用QUIC替换TCP将能够显著提升传输速率。
放弃TCP而使用基于UDP的QUIC,有点类似早期x86cpu内置的tss硬件切换不好用,linux系统内核直接使用软件控制进程上下文切换。
参考文献
【HTTP/2与HTTP/3 的新特性】blog.csdn.net/howgod/arti…
【QUIC 协议原理浅解】www.163.com/dy/article/…
【QUIC加密握手中共享密钥算法】blog.csdn.net/chuanglan/a…
【QUIC流量控制】:zhuanlan.zhihu.com/p/337175711
【QUIC加密传输和握手】zhuanlan.zhihu.com/p/301505712
【TCP乱序缓存和重传的改进方式】blog.csdn.net/cws1214/art…
【科普:QUIC协议原理分析】zhuanlan.zhihu.com/p/32553477
【rfc9000】datatracker.ietf.org/doc/html/rf…