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 流从生到死,经历三个阶段:创建 → 数据传输 → 关闭。我画了一张图,帮你理清这个流程。

QUIC 流生命周期 创建 MAX_STREAM_DATA 帧 数据传输 STREAM 帧 关闭 RESET_STREAM 帧 关键说明: 1. 创建:客户端/服务端发送 MAX_STREAM_DATA 帧,协商流控参数 2. 传输:通过 STREAM 帧发送数据,支持乱序交付 3. 关闭:发送 RESET_STREAM 或 FIN 标志位,流进入「关闭」状态 特殊状态: • 流被重置(RESET_STREAM)后,未发送的数据直接丢弃 • 流关闭后,流 ID 不可复用(重要!)

具体来说:

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 的基石,理解透了,后面的多路复用、流量控制才能学得轻松。