# SCTP 流 · 多流复用与队头阻塞

> 一个 association 里跑多条独立的流，流内有序、流间互不阻塞——这是 SCTP 为「一条连接承载多路数据」量身设计的能力。这一章讲清流 id 的奇偶分配、按流的有序性、无序流、流数量协商、流重置，以及一个常被忽略的真相：普通 DATA 其实挡不住大消息造成的流间阻塞，要靠 I-DATA 才行。

- 作者：David（道雾轩）
- 专栏：音视频与 WebRTC 详解（https://daiw.org/manual/webrtc.md）
- 最后更新：2026-08-03
- 原文：https://daiw.org/manual/webrtc/sctp-streams
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# SCTP 流 · 多流复用与队头阻塞

[前面几章](https://daiw.org/manual/webrtc/sctp-reliability)反复提到「流」：[有序性是按流保证的](https://daiw.org/manual/webrtc/sctp-reliability)、[流间不阻塞](https://daiw.org/manual/webrtc/data-channel-overview)、[association 握手要协商流数量](https://daiw.org/manual/webrtc/sctp-association)。这一章把「流」本身讲透——它是 SCTP 区别于 TCP 最根本的一处设计，也是[数据通道](https://daiw.org/manual/webrtc/data-channel-controller)能多路复用一条连接的全部原因。

## 流是什么

一个 [association](https://daiw.org/manual/webrtc/sctp-association) 内部，可以有多条**流（stream）**——彼此独立的子通道。

```text
                    一个 SCTP association
   ┌──────────────────────────────────────────────────┐
   │  流 0   ▸▸▸▸▸▸▸▸▸▸▸▸  chat 消息（有序可靠）        │
   │  流 1   ▸▸▸✕▸▸▸▸▸▸▸▸  文件传输（丢包重传中）       │
   │  流 2   ▸▸▸▸▸▸▸▸▸▸▸▸  游戏状态（无序，照常流动）   │
   └──────────────────────────────────────────────────┘
        流 1 在重传，流 0 与流 2 完全不受影响
```

**这就是 SCTP 和 TCP 的分水岭。** TCP 只有一条字节流，严格全局有序：中间缺了一段，后面所有数据都得在接收缓冲里等——哪怕后面那些数据属于逻辑上毫不相干的业务。这就是[队头阻塞](https://daiw.org/manual/webrtc/data-channel-overview)，也是「在一条 TCP 上多路复用」的死穴。

SCTP 把有序性的粒度从「整条连接」降到「每条流」，于是一条流的重传不再牵连其他流。

<Callout type="info">
  **这个设计后来被 QUIC 完整继承。** QUIC 把「多流 + 流间独立」当作核心卖点，而 SCTP 早在 2000 年（RFC 2960）就这么做了。二者的动机也一样：**HTTP/2 在一条 TCP 上多路复用，结果被队头阻塞反噬**——这正是 QUIC 要换掉 TCP 的原因。SCTP 只是早了十几年遇到同一个问题。
</Callout>

## 流 id 怎么分配

每条流有一个 **流 id（stream identifier，SID）**，16 位，收发共用同一个号。

问题是：两端都能主动开流，怎么保证不撞号？RFC 8832 的办法很简洁——**按 [DTLS](https://daiw.org/manual/webrtc/dtls-handshake) 角色分奇偶**：

| 角色 | 可用的流 id |
| --- | --- |
| DTLS **client** 侧 | **偶数**（0, 2, 4, …） |
| DTLS **server** 侧 | **奇数**（1, 3, 5, …） |

无需任何协商，两端各取各的号，天然不冲突。这和 [ICE 里用 controlling / controlled 角色打破对称](https://daiw.org/manual/webrtc/ice-overview)是同一种手法：**先定一个不对称的角色，后续所有需要「谁说了算」的地方都能直接引用它**。

## 有序性是按流的

[上一层](https://daiw.org/manual/webrtc/sctp-reliability)讲过，[TSN](https://daiw.org/manual/webrtc/sctp-reliability) 是**整个 association** 唯一递增的——它管的是「有没有丢」。而「按什么顺序交给应用」由另一个序号管：

- **SSN（Stream Sequence Number）** —— 每条流独立编号的消息序号。
- 接收方按 SSN 在**流内**重排后交付；**流与流之间没有任何顺序约束**。

所以 A 流的第 5 条消息在重传时，B 流的第 100 条消息照样可以立刻上交——因为它们的 SSN 空间根本不相干。

**无序流**则连 SSN 重排都跳过：消息一到就交付。[数据通道把 `ordered: false` 映射成这种流](https://daiw.org/manual/webrtc/sctp-partial-reliability)，适合游戏状态同步这类「最新的才有价值」的数据。

## 流数量要在握手时协商

[INIT / INIT_ACK 握手](https://daiw.org/manual/webrtc/sctp-association)里带着两个数字：

- **outbound streams** —— 我打算开多少条出流。
- **max inbound streams** —— 我最多能接多少条入流。

双方取**较小值**作为实际可用的流数。默认值通常是 65535（16 位上限），实践中很少成为瓶颈——但如果对端给了个小值，你 `createDataChannel` 开到第 N 条就会失败。

<Callout type="warn">
  **流数量是握手时定死的，中途加不了。** SCTP 有一个 `ADD_STREAMS` 扩展（RFC 6525）可以动态增加，但并非所有实现都支持。所以真实系统一般在握手时就要一个足够大的数，而不是「按需扩容」。
</Callout>

## 流重置：关掉一条流

数据通道关闭时，对应的流要被回收——否则流 id 会一直被占着，长会话里迟早耗尽。

**RFC 6525 的 RE-CONFIG chunk** 干这件事：发一个 **Outgoing SSN Reset Request**，双方把该流的 SSN 归零、状态清空，流 id 就能被重新使用了。

这一步比听上去微妙：重置必须**等该流上所有在途数据都确认完**才能做，否则会把还没送达的消息连同状态一起清掉。dcSCTP 把这个时序处理在 `net/dcsctp/socket/` 的 stream reset 逻辑里。

## 一个常被忽略的真相：大消息仍然会阻塞

到这里为止的说法——「多流所以无队头阻塞」——**在有大消息时并不成立**。

原因在于 SCTP 的分片规则：一条消息如果超过 MTU，会被切成多个 [DATA chunk](https://daiw.org/manual/webrtc/sctp-chunks-packets)，而**这些分片必须在该流上连续发送，中间不能插入其他流的数据**。

```text
用普通 DATA（RFC 9260）:
  流 0 发一个 10 MB 文件  ─────────────────────────────▶  占满发送侧
  流 1 想发一条 20 字节的聊天消息                          ✕ 只能排队等
```

于是「一条连接多路复用」的承诺，在传大文件时就破功了——**流间阻塞变成了实际问题**。

<Callout type="info">
  **解法是 I-DATA（RFC 8260）。** 它把 DATA chunk 换成 I-DATA，多带两个字段：

  - **MID（Message Identifier）** —— 取代 SSN，标识这是该流的第几条消息。
  - **FSN（Fragment Sequence Number）** —— 标识这是该消息的第几个分片。

  有了这两个号，接收方就能把**交错到达**的分片正确重组，于是发送方**可以在一条大消息的分片之间插入其他流的数据**。真正的流间独立，到 I-DATA 才算兑现。

  dcSCTP 实现了 I-DATA（`net/dcsctp/packet/chunk/` 下有对应的 chunk 类型），并在握手时协商是否启用。`net/dcsctp/tx/stream_scheduler` 则负责「下一个包从哪条流取」——有了它，多条流之间才能按策略公平地轮转发送。
</Callout>

这也解释了为什么 SSN 和 MID 在文档里常被并列提及：**它们是同一个角色在两代协议里的两个名字**，普通 DATA 用 SSN，I-DATA 用 MID。

## 小结与下一章

SCTP 的**流**是它区别于 TCP 的根本设计：一个 [association](https://daiw.org/manual/webrtc/sctp-association) 内多条独立流，**流 id 按 [DTLS](https://daiw.org/manual/webrtc/dtls-handshake) 角色分奇偶**避免撞号，**[有序性按流](https://daiw.org/manual/webrtc/sctp-reliability)**保证（SSN 重排，无序流则直接交付），流数量在[握手](https://daiw.org/manual/webrtc/sctp-association)时取双方较小值且中途难加，关流靠 **RE-CONFIG 重置**回收 id。

但要注意那个反直觉的结论：**只有多流还不够**——普通 DATA 的分片必须连续发送，大消息照样会阻塞其他流；**要靠 I-DATA（MID + FSN）允许分片交错**，流间独立才真正成立。

流讲完了，还差最后一块：这些流**怎么和数据通道对应起来、通道又怎么协商开启**？下一章讲 DCEP，收尾第 8 部分。👉 [数据通道建立 · DCEP](https://daiw.org/manual/webrtc/sctp-streams-dcep)。

## 参考资料

- [1] libwebrtc 源码.`net/dcsctp/tx/stream_scheduler`（流调度）、`net/dcsctp/packet/chunk/`（DATA / I-DATA chunk）、`net/dcsctp/socket/`（流重置）。
- [2] IETF RFC 9260「SCTP」§ 6.5（流）、RFC 8260「SCTP 消息交织（I-DATA）」、RFC 6525「流重配置」、RFC 8832 § 6（流 id 奇偶分配）。
- [3] 关联[SCTP 可靠传输](https://daiw.org/manual/webrtc/sctp-reliability)、[部分可靠](https://daiw.org/manual/webrtc/sctp-partial-reliability)、[数据通道总览](https://daiw.org/manual/webrtc/data-channel-overview)。
