一、SCTP 是什么?为什么 WebRTC 选它?

聊数据通道,绕不开 SCTP。很多同学第一次接触 WebRTC 时,会问:为什么不用 TCP 或 UDP 直接传数据?

嗯,这个问题我当年也纠结过。TCP 可靠但太慢,UDP 快但不可靠。WebRTC 既要实时性,又要保证部分数据的可靠性——比如文件传输不能丢包,游戏指令可以丢但不能等太久。

SCTP(Stream Control Transmission Protocol)正好卡在中间。它是个传输层协议,兼具 TCP 的可靠性和 UDP 的消息边界特性。说白了,它是个「既要又要」的协议。

核心要点: SCTP 不是 WebRTC 发明的,但 WebRTC 把它用在了数据通道上。SCTP 运行在 DTLS 之上,DTLS 又运行在 UDP 之上。所以数据通道的完整协议栈是:
应用层 → SCTP → DTLS → UDP → IP

我个人习惯把 SCTP 比作「智能快递系统」。TCP 像挂号信——必须签收,顺序不能乱。UDP 像扔纸飞机——快,但丢了就丢了。SCTP 呢?它像顺丰——你可以选「必须本人签收」或「放快递柜就行」,还能同时发多个包裹,互不干扰。

二、SCTP 关联:建立连接的那点事

SCTP 里没有「连接」这个词,叫「关联」(Association)。为什么?因为 SCTP 支持多宿主(Multi-homing),一个关联可以绑定多个 IP 地址。这在 WebRTC 里其实用不上,但概念得懂。

2.1 关联建立的四次握手

SCTP 建立关联需要四次握手,比 TCP 多一次。我刚开始学的时候觉得多此一举,后来踩了坑才明白——这是为了防 SYN 攻击。

流程是这样的:

  1. 客户端发 INIT 块(携带验证标签)
  2. 服务端回 INIT-ACK(确认标签,分配资源)
  3. 客户端发 COOKIE-ECHO(携带 Cookie)
  4. 服务端回 COOKIE-ACK(关联建立)

注意第三步的 Cookie。SCTP 用 Cookie 机制防止资源被恶意耗尽。服务端收到 INIT 后不立刻分配资源,而是生成一个加密 Cookie 发回去。客户端必须原样返回这个 Cookie,服务端才真正分配内存。

避坑指南: 我曾经在调试数据通道时,发现 SCTP 关联建立失败。查了半天,原来是 DTLS 握手没完成,SCTP 的 INIT 块发不出去。记住:SCTP 关联建立在 DTLS 加密通道之上,DTLS 没通,SCTP 就别想通。

2.2 验证标签(Verification Tag)

每个 SCTP 关联有两个验证标签——一个给发送方,一个给接收方。所有数据块都必须携带正确的标签,否则直接丢弃。

这玩意儿干嘛用的?防伪造。你想想看,如果攻击者伪造一个数据包塞进来,没有正确的标签,接收端直接扔掉。我在项目中见过有人误以为这是可选项,结果生产环境出了安全漏洞。

三、流(Stream):数据通道的「车道」

SCTP 的流是个好东西。一个关联里可以开多个流,每个流独立编号,互不干扰。

为什么需要流?举个例子:你在视频通话里同时传文件和发聊天消息。文件传输可以容忍延迟,但不能丢包。聊天消息可以丢一两条,但不能延迟。如果用同一个流,文件重传会阻塞聊天消息。

SCTP 的流解决了这个问题。每个流有自己的序号,消息在流内有序,但流之间完全独立。

特性 TCP SCTP 流
有序性 全局有序 流内有序,流间无序
阻塞 队头阻塞 流间无阻塞
数量 1 个 最多 65535 个
WebRTC 中的映射: 每个数据通道对应一个 SCTP 流。你创建 5 个数据通道,底层就是 5 个 SCTP 流。流的 ID 就是数据通道的 ID。

四、消息分片与重组:大消息怎么传?

SCTP 的传输单元叫「块」(Chunk)。一个 SCTP 包可以包含多个块。但问题来了:如果应用层发了一个 64KB 的消息,底层网络 MTU 只有 1500 字节,怎么办?

分片。

4.1 分片规则

SCTP 的分片发生在发送端。它会根据路径 MTU 把大消息切成多个 DATA 块。每个分片携带相同的流 ID 和流序号,但分片序号不同。

接收端收到后,按分片序号重组。如果某个分片丢了,接收端只重传那个分片,而不是整个消息。

嗯,这里要注意:SCTP 的分片对应用层完全透明。你发一个 100KB 的 ArrayBuffer,SCTP 自动帮你切好、传完、拼好。应用层拿到的就是完整消息。

性能陷阱: 虽然 SCTP 支持大消息,但我建议单条消息不要超过 256KB。为什么?因为分片越多,重传概率越大。我曾经在项目中传 1MB 的图片,结果网络抖动时,一个分片丢了,整个消息都得等。后来改成 64KB 分片发送,体验好很多。

4.2 无序传输

SCTP 支持无序传输。你可以在发送时设置无序标志,接收端收到后直接交给应用层,不用等前面的消息。

这有什么用?游戏里的位置更新。玩家位置每秒发 10 次,丢一两次无所谓,但必须立刻拿到最新的。用无序传输,每个位置包独立处理,不排队。

WebRTC 的数据通道默认是有序的。但你可以通过 ordered: false 选项创建无序通道。

// 创建无序数据通道
const channel = peerConnection.createDataChannel('game', {
  ordered: false,
  maxRetransmits: 0  // 不重传
});

五、SCTP 协议栈全景图

下面这张图展示了 SCTP 在 WebRTC 数据通道中的完整位置和内部结构。我画的时候特意把流、分片、关联的关系标清楚了。

应用层:数据通道 API SCTP 层 流 0(数据通道 0) 流 1(数据通道 1) 流 N(数据通道 N) 分片与重组:大消息 → DATA 块 → 分片传输 → 重组 SCTP 关联(Association):四次握手 + 验证标签 + 多宿主 DTLS 加密层 UDP 传输层

六、实际开发中的注意事项

说了这么多理论,最后聊点实战经验。

6.1 流 ID 的管理

WebRTC 规范规定,数据通道的流 ID 从 0 开始递增。但如果你同时创建多个通道,ID 不一定连续。比如你创建了通道 0、1、2,然后关闭通道 1,再创建新通道,新通道的 ID 可能是 3,而不是 1。

我建议不要依赖流 ID 做业务逻辑。用数据通道的 label 属性来标识用途,更靠谱。

6.2 消息大小限制

SCTP 本身支持大消息,但浏览器实现有限制。Chrome 的 SCTP 实现默认最大消息是 256KB。超过这个值,发送会失败。

我的做法: 应用层自己做分片。比如传文件时,切成 16KB 的块,每个块单独发送。接收端收到后按序号重组。这样既绕过了 SCTP 的限制,又能做更精细的进度控制。

6.3 有序 vs 无序的选择

别盲目用无序。我见过有人把所有数据通道都设成无序,结果文件传输乱序了,还得应用层自己排序,得不偿失。

我的建议:

  • 文件传输、二进制数据 → 有序 + 可靠
  • 实时位置、游戏状态 → 无序 + 不可靠
  • 聊天消息 → 有序 + 部分可靠(设置重传次数)

七、总结

SCTP 是数据通道的基石。理解了关联、流、分片这三个概念,你就能明白数据通道为什么既快又可靠。说白了,SCTP 把 TCP 的可靠性和 UDP 的灵活性结合在了一起,再加上流的概念,让 WebRTC 能同时处理多种类型的数据。

下次调试数据通道时,如果遇到消息丢失或顺序错乱,先别急着怀疑应用层代码。看看 SCTP 的流配置对不对,分片大小是否合适,验证标签有没有问题。这些底层细节,往往才是真正的坑。

一句话记住: SCTP 关联是管道,流是车道,分片是货车。管道通了,车道对了,货车大小合适,数据就能顺畅到达。
公众号:蓝海资料掘金营,微信deep3321