QUIC协议基础教程笔记(第1~3部分)
概览
www.rfc-editor.org/rfc/rfc9000…
QUIC 是一个一安全为总体目标的传输协议。 是一个在服务器和客户端之间有状态化互操作的有连接传输协议。
类似于 TCP。
QUIC 协议的全称是 Quick Udp Internet Connection. 就是快速 UDP 互联网连接的意思。
QUIC 握手绑定加密和数据传输参数。? 握手之后将会尽可能保持数据传输,包括从客户端直接向服务器发送数据的功能(0-RTT) 。不过这一点要求两端事前进行某种形式的协商或者配置。
应用程序数据依靠QUIC流在网络上传输。所谓的流可以分为两个大类,四个小类。两大类分别是 单工(unidirectional)和双工(bidirectional).
和 TCP 有明显不同的一点是 QUIC 不要求一个连接必须在同一条网络层路径上(由 Socket 4 元组表示) 而标志连接的是QUIC连接标志符号,这是一个整数。
本文将会了解:
- 什么是 流
- 流的类型
- 流的状态(机)
术语
www.rfc-editor.org/rfc/rfc9000…
Connection-ID: 在一个endpoint唯一标志一个QUIC连接的标志符。 用一个整数表示,但是这个整数的值对于连接的另一方是透明的。
流
www.rfc-editor.org/rfc/rfc9000…
流的类型和标志符
Bits | 流类型 |
---|---|
(00)b | 客户端初始化的,双工 |
(01)b | 服务器初始化的,双工 |
(10)b | 客户端初始化的,单工 |
(11)b | 服务器初始化的,单工 |
单工流(Unidirectional Stream) 只允许从创建端到对端发送数据.
双工流(Bidirectional Stream) 允许连接两端彼此发送数据.
服务器和客户端都可以主动发起一个连接。 因此不同的两大类流有可以被分成一共四个小类。
流ID 是一个 62 位整数,范围是 在编码的时候,Stream ID 不是定长的。另外在同一个连接中,不允许 endpoint 重复使用一个 Stream ID.
Stream ID 的最低有效位表示流的发起者,如果是0就表示客户端发起。1表示服务器发起。
第二个最低有效位表示流的类型(双工/单工)
每种类型的流空间从最小值开始(分别为 0x00 到 0x03); 每种类型的连续流都是使用数字增加的流 ID 创建的。 乱序使用的流 ID 会导致所有具有较低编号流 ID 的类型的流也被打开。
这句话的意思,我认为:QUIC默认编号小于等于n的流全部都在打开状态。 那如果出现较小流ID流在较大流ID流关闭之前关闭的情况出现, 实现应当怎样处理呢?
发送和接收数据
Stream Frame(流帧) 将应用数据封装在内部。endpoint 通过流帧中的流ID和偏移量(offset) 来将数据按顺序排列。
enpoint 必须保证能够将数据按序提供给应用程序。 当然也可以有将数据乱序提供给应用程序的功能。
endpoint 绝对不可以在不明确的对端设定的流控制极限的情况下在任何流上传输数据。
流的状态
www.rfc-editor.org/rfc/rfc9000…
QUIC 状态机有两种,分别是负责 endpoint 发送数据和接受数据的。 当流的类型是单工,那么endpoint 就使用上述两个状态机的某一个。 如果是双工,连接两端就同时使用两个状态机。
发送流状态机
www.rfc-editor.org/rfc/rfc9000…
o
| Create Stream (Sending)
| Peer Creates Bidirectional Stream
v
+-------+
| Ready | Send RESET_STREAM
| |-----------------------.
+-------+ |
| |
| Send STREAM / |
| STREAM_DATA_BLOCKED |
v |
+-------+ |
| Send | Send RESET_STREAM |
| |---------------------->|
+-------+ |
| |
| Send STREAM + FIN |
v v
+-------+ +-------+
| Data | Send RESET_STREAM | Reset |
| Sent |------------------>| Sent |
+-------+ +-------+
| |
| Recv All ACKs | Recv ACK
v v
+-------+ +-------+
| Data | | Reset |
| Recvd | | Recvd |
+-------+ +-------+
发送endpoint有应用程序调用响应API被初始化进入 Ready 状态. 进入当前状态则表示QUIC设施可以从应用程序接受数据, 并且可以在当前状态将数据进行打包(buffered)方便后期发送
发送第一个 STREAM
或 STREAM_DATA_BLOCKED
帧会导致流的发送部分进入“发送”状态。
实现可能会选择推迟将流 ID 分配给流,
直到它发送第一个 STREAM 帧并进入此状态,这可以支持更好的流优先级。
在 Send
状态,endpoint 使用 STREAM
帧来携带流的数据。
但是 endpoint 接收对方设置的流控制帧的影响。
在达到对方设定的传输极限时,产生 STREAM_DATA_BLOCKED
当应用程序报告,所有数据已经发送,并且带有 FIN
的 STREAM
也发送了之后,流的发送部分进入 Data Sent
状态.
endpoint 可以将 RESET_STREAM
作为当前流发送的第一个帧。
这会使当前流打开后立刻进入 Reset Sent
状态.
一旦接收到包含 RESET_STREAM
的包(packet)收到ACK
则流进入 Reset Sent
状态。
接收流状态机
www.rfc-editor.org/rfc/rfc9000…
o
| Recv STREAM / STREAM_DATA_BLOCKED / RESET_STREAM
| Create Bidirectional Stream (Sending)
| Recv MAX_STREAM_DATA / STOP_SENDING (Bidirectional)
| Create Higher-Numbered Stream
v
+-------+
| Recv | Recv RESET_STREAM
| |-----------------------.
+-------+ |
| |
| Recv STREAM + FIN |
v |
+-------+ |
| Size | Recv RESET_STREAM |
| Known |---------------------->|
+-------+ |
| |
| Recv All Data |
v v
+-------+ Recv RESET_STREAM +-------+
| Data |--- (optional) --->| Reset |
| Recvd | Recv All Data | Recvd |
+-------+<-- (optional) ----+-------+
| |
| App Read All Data | App Read Reset
v v
+-------+ +-------+
| Data | | Reset |
| Read | | Read |
+-------+ +-------+
当接收到该流的第一个 STREAM
、STREAM_DATA_BLOCKED
或 RESET_STREAM
帧时,将创建由对等点启动的流的接收部分(客户端的类型 1 和 3,
或服务器的类型 0 和 2)。 对于由对等方发起的双向流,接收流的发送部分的
MAX_STREAM_DATA
或 STOP_SENDING
帧也会创建接收部分。
流的接收部分的初始状态是Recv
.
对于双向流,若对段(也就是发起端)进入 Ready
状态之后,接收端进入 Recv
状态。
但是上图中是的当前流进入 Recv
状态的条件有些奇怪,
因为网络环境导致的某些帧在 RESET_STREAM
到达之后才能呢给到达。
所以 MAX_STREAM_DATA
和 STOP_SENDING
这种帧可能出现 Recv
状态之前.
在创建流之前,必须创建具有较低编号流 ID 的所有相同类型的流。 这确保了流的创建顺序在两个端点上是一致的。
在 Recv
状态,数据被放在 STREAM
和 STREAM_DATA_BLOCKED
收到这两种帧之后可以将数据缓冲并且重新组装,以备应用程序使用。
当应用程序将数据全部取走,并且缓冲区重新可用之后,发送
MAX_STREAM_DATA
告知对端可以继续发送数据。
接收到带有 FIN
的帧之后,流进入 Size Known
状态。在当前状态,
流不需要发送 MAX_STREAM_DATA
只需要接受重传的数据。
等到所有数据全部接收到,流进入 Data Recvd
状态,
进入当前状态之后,收到的所有 STREAM
和 STREAM_DATA_BLOCKED
帧都可以丢弃掉(discard).
Data Recvd
状态将会持续到应用程序接收所有数据。
一旦应用程序接收完所有数据,流进入 Data Read
状态.
在Recv
或者 Size Known
状态下收到 RESET_STREAM
,
会导致流进入 Reset Recvd
状态.
但是收到流重置帧的时候,对于不同的情况,实现可以自行选择处理方式:
- 丢弃全部收到的数据并告知客户端流已被重置。
- 若所有数据已经接收到,并且准备好给客户端读取,
那么实现可以抑制或者忽略
RESET_STREAM
并在应用程序读取完所有数据之后进入Data Recvd
状态.
当应用程序受到流已被重置的信号时,流进入 Reset Read
状态.
准许的帧类型
www.rfc-editor.org/rfc/rfc9000…
流的发送方只允许发送三种类型的帧可以影响流在发送方和接收方的状态。
他们分别是: STREAM
STREAM_DATA_BLOCKED
和 RESET_STREAM
发送方绝对不可以在 Data Recvd
或 Reset Recvd
状态下发送任何帧。
发送方绝对不可以在 任何终止 状态下(比如 Reset Sent
)发送
STREAM
和 STREAM_DATA_BLOCKED
帧.
接收方可以在任何状态下接收到上述三种帧。
造成这种情况的原因肯能是不良的网络状态。
接收方只能在 Recv
状态下发送 MAX_STREAM_DATA
.
接收方可以在任何收到 RESET_STREAM
之后的状态(Reset Recvd
Reset Read
).
接收方仅在“Recv”状态下发送 MAX_STREAM_DATA
帧。
接收者可以在它没有收到 RESET_STREAM
帧的任何状态下发送一个 STOP_SENDING
帧——也就是说,除了“Reset Recvd”或“Reset Read”之外的状态。
但是,在“Data Recvd”状态下发送 STOP_SENDING
帧没有什么价值,
因为所有流数据都已接收。 由于数据包的延迟交付,
发送方可以在任何状态下接收这两种类型的帧中的任何一种。
双工流状态
www.rfc-editor.org/rfc/rfc9000…
双工流组合接受和发送两部分。因此双工流的状态也可以通过流的不同部分的状态组合出来。 比如可以有像 TCP 中的半开半闭以及 open close 等状态。
发送部分 | 接收部分 | 组合状态 |
---|---|---|
没有流建立/Ready | 没有流建立/Recv | 闲置(idle) |
Ready/Send/Data Sent | Recv/Size Known | 打开(open) |
Ready/Send/Data Sent | Data Recvd/Data Read | 半关闭(远程(remote)) |
Ready/Send/Data Sent | Reset Recvd/Reset Read | 半关闭(远程(remote)) |
Data Recvd | Recv / Size Known | 半关闭 (本地(local)) |
Reset Sent / Reset Recvd | Recv / Size Known | 半关闭 (本地(local)) |
Reset Sent / Reset Recvd | Data Recvd / Data Read | 关闭 |
Reset Sent / Reset Recvd | Reset Recvd / Reset Read | 关闭 |
Data Recvd | Data Recvd / Data Read | 关闭 |
Data Recvd | Reset Recvd / Reset Read | 关闭 |
备注: 若一个流没有被创建,或者他接收方处于 Recv 状态并且没有受到任何帧。
被征求的状态转换 (Solicited Sate Transitions)
www.rfc-editor.org/rfc/rfc9000…
应用程序若对当前流上的数据不感兴趣了,就可以提前终止传输。
这种情况可以对端发送一个 STOP_SENDING
帧。
前提是这个流没有被对段重置(reset)
若流的状态是 Ready 或 Sent 那么 endpoint 收到 STOP_SENDING
帧之后
必须立刻发送 RESET_STREAM
帧。 并且应该在
RESET_STREAM
帧中附上 收到的 STOP_SENDING
帧当中的错误码。
当然一开始发送 STOP_SENDING
帧的一端可以忽略后续到达的 RESET_STREAM
中携带的任何错误码。可见所以不能以来该错误码来做一些事情。
另外,如果想要关闭双工流,可以在发送端和接收端分别发送 RESET_STREAM
和 STOP_SENDING
. 后者用来征求对端关闭。
如果包含 STOP_SENDING
的帧在传输过程中丢失了,
则应该重新发送这个帧。
不过,若接收到所有的流数据或者 RESET_STREAM
帧,那么就不要发送 STOP_SENDING
了。
换句话说,在 Recv
和 Size Known
状态以外发送 STOP_SENDING
帧都是不必要的。
推荐阅读