一、QUIC协议
QUIC ,即 快速UDP网络连接 ( Quick UDP Internet Connections ), 是由 Google 提出的实验性网络传输协议 ,位于 OSI 模型传输层。 QUIC 旨在解决 TCP 协议的缺陷,并最终替代 TCP 协议, 以减少数据传输,降低连接建立延 迟时间,加快网页传输速度。
标准文档地址:https://quicwg.org/base-drafts/rfc9000.html
1. QUIC框图
1.1 为什么QUIC在应用层实现
- 新的传输层协议通常会经过严格的设计,分析和评估可重复的结果,证明候选协议对 现有协议的正确性和公平性,开发新的传输层协议和它在操作系统进行广泛部署之间 通常需要花费数年的时间。
- 再者,用户与服务器之间要经过许多防火墙、NAT(地址转换)、路由器和其他中间设 备,这些设备很多只认TCP和UDP。如果使用另一种传输层协议,那么就会有可能无法 建立连接或者报文无法转发,这些中间设备会认为除TCP和UDP协议以外的协议都是不 安全或者有问题的。
1.2 QUIC协议术语
QUIC连接:CLient和Server之间的通信关心,Client发起连接,Server接收连接
流(Stream):一个QUIC连接内,单向或者双向的有序字节流。一个QUIC连 接可以同时包含多个Stream
帧(Frame):QUIC连接内的最小通信单元。一个QUIC数据包(packet)中的 数据部分包含一个或多个帧
1.3 QUIC和TCP对比
2. QUIC报文格式
2.1 QUIC数据包格式
- Header是明文的,包含4个字段:Flags、Connection ID、QUIC Version、Packet Number
- Data 是加密的,可以包含 1 个或多个 frame,每个 frame 又分为 type 和 payload, 其中 payload 就是应用数据
2.2 QUIC Stream帧
数据帧有很多类型:Stream、ACK、Padding、Window_Update、Blocked 等,这里重点介 绍下用于传输应用数据的 Stream 帧。
Frame Type:帧类型,占用1个字节
- Bit7:必须设置为 1,表示 Stream 帧
- Bit6:如果设置为 1,表示发送端在这个 stream 上已经结束发送数据,流将处于半关闭状态
- Bit5:如果设置为 1,表示 Stream 头中包含 Data length 字段
- Bit4-2:表示 offset 的长度。000 表示 0 字节,001 表示 2 字节,010 表示 3 字节,以此类推
- Bit1-0:表示 Stream ID 的长度。00 表示 1 字节,01 表示 2 字节,10 表示 3 字节,11 表示 4 字 节
Stream ID:流 ID,用于标识数据包所属的流。后面的流量控制和多路复用会涉及到。
Offset:偏移量,表示该数据包在整个数据中的偏移量,用于数据排序。
Data Length: 数据长度,占用 2 个字节,表示实际应用数据的长度.
Data: 实际的应用数据
3. QUIC的特点
- 连接建立低时延
- 多路复用
- 无队头阻塞
- 灵活的拥塞控制机制
- 连接迁移
- 数据包头和包内数据的身份认证和加密
- FEC前向纠错
- 可靠性传输
- 其他
3.1 连接建立低延时
3.1.1 典型TCP+TLS连接
- 首先,执行三次握手,建立TCP连接(蓝色部分)
- 然后,执行TLS握手,建立TLS连接(黄色)
- 此后开始传输业务数据
注意到,三次握手中的 ACK 包与 handshake 合并在一起发送。 这是 TCP 实现中使用的 延迟确认 技术, 旨在减少协议开销,改善网络性能。
客户端和服务器之间要进行多轮协议交互,才能建立 TLS 连接,延迟相当严重。 平时访问 https 网站明显比 http 网站慢,三次握手和 TLS 握手难辞其咎。
3.1.2 首次连接对比
- 共3RTT:TCP+TLS1.2中: TCP三次握手建立连接需要1个RTT;TLS需要2个RTT完成 身份验证;传输数据
- 共2RTT:TCP+TLS1.3中: TCP三次握手建立连接需要1个RTT;TLS需要1个RTT完成 身份验证;传输数据
- 共1RTT: 首次QUIC连接中,Client 向 Server 发送消息,请求传输配置参数和加 密相关参数;Server 回复其配置参数;传输数据
3.1.2 再次连接对比
- 再次连接的概念:Client已经访问过Server,在本地存放了Cookie。
- 2RTT:TCP+TLS1.2中: TCP三次握手建立连接需要1个RTT;TLS需要1个RTT完成身份 验证(由于缓存的存在,减少1RTT);传输数据
- 1RTT:TCP+TLS1.3 中: TCP三次握手建立连接需要1个RTT;传输数据
- 0RTT:在客户端与服务端的再次QUIC连接中,Client 本地已有 Server 的全部配置 参数(缓存),据此计算出初始密钥,直接发送加密的数据包。
3.2 多路复用
在一个网页里面总是会有多个数据要传输,总是希望多个数据能够同时传输,以此 来提高用户的体验。
3.2.1 HTTP1.1
每个TCP连接同时只能处理一个请求—响应,为了提高响应速度,需要 同时创建多个连接,但是多个连接管理比较复杂。
3.2.2 HTTP2
- 每个TCP连接里面有多个逻辑上独立的多个流(stream)
- 每个流可以传输不同的文件数据
- 解决了HTTP1一个连接无法同时传输多个数据的问题
- 缺点:容易出现队头阻塞问题
3.2.3 QUIC(HTTP3)
- 借鉴了HTTP2中流的概念
- 流之间互相独立,即不同流之间的数据之间交付顺序无关(如果 stream2的数据丢失,只会影响排在stream2后面的数据,stream1和 stream3的数据不会被影响)
- 建立在UDP之上,没有依赖性
3.3 无队头阻塞
HTTP/2会出现队头阻塞问题,而在基于QUIC的HTTP/3中则很好地解决这一问题。
- UDP中的各个数据报独立,基于UDP的QUIC中的各个stream独立
- 即使stream2里有一个包丢失,因为stream之间互相独立无关联,所以不会 阻塞stream3、stream4,依然可以交付给上层
3.4 灵活的拥塞控制和机制
TCP 的拥塞控制实际上包含了四个算法:慢启动,拥塞避免,快速重传,快速恢复。
- New Reno:基于丢包检测
- CUBIC:基于丢包检测
- BBR:基于网络带宽
QUIC协议可以自己设置或实现拥塞控制协议。
QUIC 协议当前默认使用了 TCP 协议的 Cubic 拥塞控制算法。 这个机制还是可插拔的,能够非常灵活地生效,变更和停止,可以根据场景来切换不同的方法。
QUIC协议在用户空间实现,应用程序不需要停机和升级就能实现拥塞控制 的变更,在服务端只需要修改一下配置,reload 一下,完全不需要停止服务 就能实现拥塞控制的切换。甚至可以为每一个请求都设置一种拥塞控制算法。
而TCP在内核态,其拥塞控制难以进行修改和升级。
3.5 连接迁移
连接迁移:当客户端切换网络时,和服务器的连接并不会断开,仍然可以正常通信。
对于 TCP 协议而言,这是不可能做到的。因为 TCP 的连接基于 4 元组:源 IP、源端口、 目的 IP、目的端口,只要其中 1 个发生变化,就需要重新建立连接。
但 QUIC 的连接是 基于 64 位的 Connection ID,网络切换并不会影响 Connection ID 的变化,连接在逻辑上 仍然是通的。IP地址或者端口号发生变化时,只要ID不变,依然能够维持原有连接,上层业务逻辑感 知不到变化,不会中断。
客户端的Connection ID是唯一的。
3.6 数据包头和包内数据的身份认证和加密
相比于TCP,QUIC的安全性是内置的,也就是说是必须的。
在恶意环境下性能与TLS类似,友好环境下优于TLS,因为TLS使用一个会话密钥, 如果这个密钥被截获的话就不能保证之前数据的安全性。
而QUIC使用两个密钥——初始密钥和会话密钥,并且QUIC提供密码保护,TLS不提供。
除此之外,QUIC的包头经过身份认证,包内数据是加密的。这样如果QUIC数据 被恶意修改的话接收端是可以发现的,降低了安全风险。
而且数据加密之后,可以通过像防火墙或者nat这些中间件。
两个密钥的生成可参考:https://blog.csdn.net/chuanglan/article/details/85106706
3.7 FEC前向纠错
FEC是Forward Error Correction前向错误纠正的意思,就是通过多发一些冗余的包, 当有些包丢失时,可以通过冗余的包恢复出来,而不用重传。这个算法在多媒 体网关拥塞控制有重要的地位。QUIC的FEC是使用的XOR的方式,即发N + 1个包, 多发一个冗余的包,在正常数据的N个包里面任意一个包丢了,可以通过这个冗 余的包恢复出来,使用异或可以做到切换网络操持连接。
3.8 可靠性传输
QUIC 是基于 UDP 协议的,而 UDP 是不可靠传输协议,那 QUIC 是如何实现可靠传输的呢?
可靠性传输有2个重要特点:
- 完整性:发送端发出的数据包,接收端都能收到
- 有序性:接收端能按序组装数据包,解码得到有效的数据
3.8.1 有序性设计
QUIC每个Stream帧中都有offset字段和StreamID字段,这使得乱序接收的数据能够有序排列。
3.8.2 完整性设计
发送端通过包号(PKN)和确认应答(SACK)确认发送数据完整性。
- 客户端:发送 3 个数据包给服务器(PKN = 1, 2,3)
- 服务器:通过 SACK 告知客户端已经收到了 1 和 3,没有收到 2
- 客户端:重传第 2 个数据包(PKN=4)
尽管QUIC会重传数据包,但是新的数据包的PKN的继续递增的,即之前发送的数据包(PKN=2)和重传的 数据包(PKN=4),虽然数据一样,但包号不同。这也解决了TCP中,原始包和重传包的序列号一样带来的重传歧义问题。
由于TCP原始包和重传包的序列号是一样的,客户端不知道服 务器返回的 ACK 包到底是原始包的,还是重传包的。但 QUIC 的原始包和重传包的序列号是不同的,也就可以判 断 ACK 包的归属。
4. QUIC开源库
- google的gquic 起源最早, 不过它不是单独项目, 代码在chromium项目里边, 用的 是c++写的, 可能不是很适合。
- 微软的msquic, 用c写的, 跨平台, 不过开始得比较晚。
- facebook的quic 用的是c++写的. 暂不考虑。
- nginx的quic 没有自带client, 但它可与ngtcp2联调。
- litespeed的 lsquic 是基于MIT的, 开始于2017年, 还算比较稳定, 用c语言编写, 各 主流平台都有通过测试, 有server/client/lib, 它用于自家的各种产品, 暂时看上去 是最合适的。
- ngtcp2, 它是一个实验性质的quic client, 很简洁, 实现了几乎每一版ietf draft. 从 代码简洁性上来看, 它无疑是最好的。目前srs流媒体服务器、curl等开源项目有 基于ngtcp2做二次开发。
4.1 ngtcp2
- https://github.com/ngtcp2/ngtcp2
- 采用C语言实现
- 范例client和server使用了c++17的特性,我们需要升级编译器(比如, clang >= 8.0, 或 gcc >= 8.0)
- 编译文档见《QUIC开源库安装和实践.pdf》
参考博客:UDP可靠性传输-QUIC - 幻cat - 博客园