QUIC 学习笔记
本文参考了以下资料:
IETF | Innovative New Technology for Sending Data Over the Internet Published as Open Standard
RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport (rfc-editor.org)
基础概念
Quick UDP Internet Connections
RFC9000
HTTP/3 将是第一个基于 QUIC 的应用层协议
While QUIC is a general transport protocol, the IETF will also soon release HTTP/3, the first application protocol designed for use over QUIC.
术语:
名词 | 解释 |
---|---|
QUIC | QUIC is a name, not an acronym. |
Endpoint 端点 | 可以通过生成、接收、处理 QUIC 包参与 QUIC 连接的实体,分为客户端和服务端 |
Client 客户端 | 发起连接的 endpoint |
Server 服务端 | 接受连接的 endpoint |
QUIC Packet 包 | QUIC 可将其封装至 UDP 数据报的单位,一个 UDP 数据报可以容纳多个 QUIC Packet |
Frame 帧 | 结构化的协议信息单元,有多种类型可以携带不同的信息。包含在 Packet 中 |
Connection ID | 用于在 endpoint 中标识一个 QUIC 连接 |
Stream 流 | 单向或双向的有序字节通道。一个连接中可以有多个 Stream |
流
简介
流是 QUIC 提供的轻量级、有序的字节流抽象,流可以是单向的和双向的。流可以是持久的(long-linved)而且可以在整个连接中持续。所有关于流的操作(ending, canceling, and managing flow control)的设计目标都是最小化开销。
不同流之间可以同时传送数据,但是 QUIC 不保证不同流之间的数据的有序性。
流使用 stream ID 来标识(0~),任何情况下都不应该在一个连接中重用一个 stream ID
流的类型
stream ID 中的最低有效位(0x01)标明流的创建人,客户端发起的为 0,服务端发起的为 1,故客户端的 stream ID 永远是偶数,而服务端的永远是奇数。
最低的第二个有效位表示流是单向的还是双向的,双向为 0,单向为 1
将最低的四位看做一个 16 进制数,可以从这个数推断流的类型
Bits | Stream Type |
---|---|
0x00 | Client-Initiated, Bidirectional |
0x01 | Server-Initiated, Bidirectional |
0x02 | Client-Initiated, Unidirectional |
0x03 | Server-Initiated, Unidirectional |
流的优先级
如果能够正确地在多个流中配置资源,那会大幅提高应用的性能。
但是 QUIC 不提供交换优先级信息的机制,它依赖于应用提供的优先级信息。QUIC 的实现应该(should)为应用提供指明流优先级的方式,并用这些信息来决定怎么为正在工作的流分配资源。
Stream Frame
Stream Frame 隐含地创建一个流并传送数据,可用于流操作(见下一小节)
关于各字段的具体含义见:RFC 9000: QUIC: A UDP-Based Multiplexed and Secure Transport (rfc-editor.org)
1 | STREAM Frame { |
流操作
发送端:
-
写数据(write data)
-
终止流(end the stream,clean termination),以 FIN 位置 1 的 Stream Frame 结束
-
强制终止流(reset the stream ,abrupt termination),以 REST_STREAM 帧结束,收到 RESET_FRAME 的流可以丢弃其已经收到的所有数据。若仅仅发送数据的端收到了 REST_STREAM 则必须终止链接并抛出错误 STREAM_STATE_ERROR(Section 19.4)
1
2
3
4
5
6RESET_STREAM Frame {
Type (i) = 0x04,
Stream ID (i),
Application Protocol Error Code (i),
Final Size (i),
}
接收端:
-
读数据(read_data)
-
中断读取并请求停止发送数据,使用 STOP_ENDING 帧
1
2
3
4
5STOP_SENDING Frame {
Type (i) = 0x05,
Stream ID (i),
Application Protocol Error Code (i),
}
流状态
发送端
1 | o |
Ready:新建的流且已准备后接收来自应用的信息,在这个状态下可缓存即将发送的数据
Send:发送第一个 Stream 帧或 STREAM_DATA_BLOCKED 帧后即进入 Send 态。在实现上可以直到流进入该状态后再分配 Stream ID,便于确定流的优先级
连接建立
TCP 三次握手需要 2 个 RTT,TLS 1.3 握手需要 1 个 RTT,QUIC 基于 TLS 1.3 实现的,首次建立连接时需要 1 RTT,但之后连接时可以实现 0-RTT
0-RTT:客户端缓存了服务端在 TLS 1.3 中发送的 B(b*G%P),在再次连接时直接使用缓存,但又引发了前向安全问题(静态密钥泄露出去,是否会泄漏之前的通讯内容)
- Client Hello:客户端生成随机数 a,计算 ,随后将 A 和 G 发送给服务器
- 客户端使用缓存计算初试密钥 ,加密发送应用数据 1
- 服务器根据 ClientHello 信息计算 (A 是 Client Hello 发的,b 是服务器静态配置的,此 initKey 与之前客户端生成的 initKey 一样)
- Server Hello:服务器生成随机数 C,使用初始密钥加密 C 发送至客户端
- 客户端收到随机数 C 后计算会话密钥 ,发送应用数据 2
- 服务器同样计算会话密钥,获取应用数据 2
使用会话密钥通信能保证即便 b 泄露了,攻击者也无法解密之前发送的数据包
客户端缓存并不会泄露 b,因为客户端存的是 ,难以计算出 b
1 RTT图例
- 带星号代表可以在握手过程中携带应用数据,但存在风险
- 0-RTT 表示不经过握手就可以发送
- 1-RTT 表示要经过一次握手才能发送
- CPYPTO 表示是加密帧
1 | Client Server |
可靠传输
可靠性:完整、有序
完整:选择重传。给每个数据包一个递增的包号(PKN),然后服务器通过 SACK 告知客户端已经收到的包号(一次可确认多个,如 SACK = (1, 3)
),没有收到 SACK 的包则重传,重传的包的 PKN 仍然是递增的,而不是和之前的相同,故不能通过包号来组装数据。
有序:偏移(offset),每个数据包设置一个偏移。
流量控制
和 TCP 一样使用滑动窗口机制,但其将滑动窗口分为了 Connection 和 Stream 两种级别
- Stream 每个流的窗口大小
- Connection 整个连接的总窗口(一个连接由很多个流组成,即为每个流窗口的综合)
拥塞控制
拥塞控制类似于 TCP 的拥塞控制,但是 QUIC 是在用户空间实现的拥塞控制,可以灵活地设置拥塞控制算法:
-
和 TCP 一样发送窗口的大小由拥塞窗口和接收窗口的最小值决定,即
swnd = min(cwnd, rwnd)
-
慢开始、拥塞避免、与 TCP 拥塞控制相同
-
拥塞发生:
- 若发生超时重传,则慢启动门限设为当前拥塞窗口的一半(ssthresh = cwnd / 2),且把拥塞窗口设为 1(cwnd = 1),重新执行慢开始用拥塞避免。
- 若发生快重传(3 次相同ACK),则
ssthresh = cwnd = cwnd/2
,进入快恢复
-
快恢复:cwnd = ssthresh + 3(虽然资料 1 说是因为收到 3 个ACK所以加 3,但还是不理解为什么),进入拥塞避免
多路复用
HTTP/2 时已经实现了基于一个 TCP 连接发起多个 HTTP 请求,因为其采用了二进制帧格式的数据结构,一个请求对应一条流,通过帧中的 Stream ID 来判断数据属于哪条流。
存在队头阻塞问题
HTTP 2 协议基于 TCP 有序字节流实现,因此应用层的多路复用并不能做到无序地并发,在丢包场景下会出现队头阻塞问题。如下面的动态图片所示,服务器返回的绿色响应由 5 个 TCP 报文组成,而黄色响应由 4 个 TCP 报文组成,当第 2 个黄色报文丢失后,即使客户端接收到完整的 5 个绿色报文,但 TCP 层不会允许应用进程的 read 函数读取到最后 5 个报文,并发成了一纸空谈:
![]()
QUIC 解决队头阻塞:为每条流分配一个独立的滑动窗口,但对于同一个流上的请求仍存在着队头阻塞问题
连接迁移
- QUIC 是基于无连接的 UDP 协议,为连接迁移创造了前提
- QUIC 使用 Connection ID 来分辨客户端而不像TCP 依赖于四元组,当源 IP 改变时,仍然可以分辨该数据包来自的客户端是同一个(目的 IP 没有改变,仍可以转发到目的主机,且目的主机认得这个数据包来自哪个客户端)
连接迁移只能发生在握手成功之后,QUIC 握手期间终端必须保持稳定的地址