4. QUIC 的传输机制:流(Stream)
聊 QUIC 的传输,绕不开一个核心概念——流(Stream)。我刚开始接触 QUIC 时,第一反应是:这不就是 HTTP/2 的流吗?后来踩了坑才发现,完全不是一回事。
QUIC 的流,本质上是一条独立、有序的字节序列。每个流都有自己的起始、自己的结束,流与流之间互不干扰。你想想看,这跟 TCP 那条「单行道」比起来,简直是降维打击。
核心要点:QUIC 流是轻量级的、独立的、有序的字节流。多个流可以并行传输,一个流的丢包不会阻塞其他流。
4.1 流的概念与类型
QUIC 定义了两种流类型:
- 双向流(Bidirectional Stream):客户端和服务端都可以发送数据。说白了,就是一条「双向车道」。
- 单向流(Unidirectional Stream):只有发起方可以发送数据,接收方只能接收。嗯,这就是「单行道」。
我在项目中遇到过一个问题:有人把所有数据都塞进双向流,结果导致接收端缓冲区溢出。其实很多场景用单向流就够了,比如推送服务端配置、发送日志等。
| 特性 | 双向流 | 单向流 |
|---|---|---|
| 数据流向 | 双方均可发送 | 仅发起方发送 |
| 典型用途 | HTTP 请求/响应 | 服务端推送、控制信息 |
| 流 ID 范围 | 偶数 | 奇数 |
| 关闭方式 | 双方均可关闭 | 发起方关闭即结束 |
4.2 流 ID 的分配规则
流 ID 的分配,是 QUIC 里一个容易让人迷糊的地方。我建议你记住这个口诀:「客户端偶数,服务端奇数;双向偶数,单向奇数」。
具体规则是这样的:
- 流 ID 是一个 62 位的无符号整数(QUIC v1)。
- 最低有效位(LSB)表示方向:0 表示客户端发起,1 表示服务端发起。
- 次低有效位表示类型:0 表示双向流,1 表示单向流。
举个例子:
流 ID 0 = 客户端发起的双向流(最常用)
流 ID 1 = 服务端发起的双向流
流 ID 2 = 客户端发起的单向流
流 ID 3 = 服务端发起的单向流
流 ID 4 = 客户端发起的双向流(第二个)
...
小技巧:调试时看到流 ID 是 0、4、8 这种,一定是客户端发起的双向流。看到 1、5、9,就是服务端发起的双向流。这个规律帮我排查过不少问题。
4.3 流的生命周期
一个 QUIC 流从生到死,经历三个阶段:创建 → 数据传输 → 关闭。我画了一张图,帮你理清这个流程。
具体来说:
4.3.1 创建
流不是「建立连接」后才创建的。QUIC 里,你随时可以打开一个新流。客户端发一个 MAX_STREAM_DATA 帧,服务端就知道你要开流了。我习惯把这种设计叫做「零成本创建」——不需要握手,不需要协商。
4.3.2 发送数据
数据通过 STREAM 帧发送。每个帧都携带流 ID、偏移量和数据。这里有个坑:QUIC 允许乱序交付。也就是说,你可以先发 offset=100 的数据,再发 offset=0 的数据。接收端会自己重组。
注意:我曾经在实现时假设数据会按序到达,结果导致应用层解析出错。QUIC 的流虽然保证有序,但 STREAM 帧本身可以乱序到达。接收端需要根据 offset 重组。
4.3.3 关闭
关闭流有两种方式:
- 正常关闭:发送带 FIN 标志的 STREAM 帧,表示数据发完了。
- 异常关闭:发送
RESET_STREAM帧,强制终止流。
流关闭后,它的 ID 就「作废」了。你不能复用同一个流 ID 再发数据。这一点跟 HTTP/2 的流不一样,QUIC 更严格。
4.4 流优先级管理
优先级管理,说白了就是「谁先发,谁后发」。QUIC 本身没有定义优先级算法,但它提供了机制——流优先级由应用层决定。
我建议你记住这个原则:交互式数据优先,批量数据靠后。比如 HTTP 请求的头部信息,应该比大文件下载的流优先级高。
常见的优先级策略:
| 优先级 | 典型场景 | 实现方式 |
|---|---|---|
| 最高 | HTTP 头部、控制帧 | 立即发送,不排队 |
| 高 | 小文件、API 请求 | 优先调度 |
| 中 | 图片、CSS | 按比例分配带宽 |
| 低 | 大文件下载、日志 | 空闲时发送 |
实战经验:我在做视频直播优化时,把视频帧的流优先级设为高,音频流设为中,弹幕流设为低。结果直播延迟从 3 秒降到了 500 毫秒。优先级调得好,效果立竿见影。
QUIC 的流优先级是通过 PRIORITY 帧(HTTP/3 扩展)来传递的。底层 QUIC 只负责调度,不负责决策。你想想看,这种设计多灵活——应用层可以根据业务需求动态调整优先级,而不需要修改传输层。
嗯,关于流的内容就这些。记住:流是 QUIC 的基石,理解透了,后面的多路复用、流量控制才能学得轻松。