21、数据通道协商:SCTP over DTLS、DataChannel的SDP协商(a=sctp-port)、有序/无序传输

聊到 WebRTC 的数据通道,很多人第一反应就是「哦,就是那个能传文件的」。但说实话,数据通道的底层机制远比表面看到的要复杂。今天我就带你深入扒一扒,数据通道到底是怎么协商出来的,以及 SCTP over DTLS 这个组合到底在干什么。

SCTP over DTLS:为什么是这对组合?

先问一个问题:为什么 WebRTC 不用 TCP 或 UDP 直接传数据?

答案其实很简单——WebRTC 的媒体流走的是 SRTP/SRTCP,这些都是在 UDP 上跑的。如果你单独为数据通道开一个 TCP 连接,那防火墙和 NAT 穿越的复杂度就翻倍了。而且,TCP 的队头阻塞问题在实时场景下很要命。

所以,设计者选择了 SCTP(流控制传输协议)。SCTP 的好处是它天然支持多流、部分可靠传输、以及消息边界保留。但问题来了——SCTP 本身不是为 Web 设计的,浏览器里没有原生的 SCTP 协议栈。

于是就有了 SCTP over DTLS 这个方案。说白了,就是把 SCTP 的包封装在 DTLS 记录层里,再通过 UDP 发出去。DTLS 提供了加密和完整性保护,SCTP 提供了灵活的数据传输能力。

核心要点:数据通道的传输路径是:应用数据 → SCTP 分片 → DTLS 加密 → UDP 发送。反过来就是接收路径。

我在项目中遇到过一个问题:某个客户反馈数据通道偶尔丢消息,但媒体流是正常的。排查了半天,发现是 DTLS 握手超时导致 SCTP 关联重建,中间的消息就丢了。嗯,这个坑后面会细说。

DataChannel 的 SDP 协商:a=sctp-port 的秘密

数据通道不是凭空建立的,它需要和媒体流一样通过 SDP 协商。你抓包看 SDP 的时候,会看到类似这样的东西:

m=application 9 UDP/DTLS/SCTP webrtc-datachannel
c=IN IP4 0.0.0.0
a=mid:data
a=sctp-port:5000
a=max-message-size:262144

这里有几个关键字段:

  • m=application:表明这是一个数据通道媒体行,不是音频也不是视频。
  • UDP/DTLS/SCTP:协议栈的声明,告诉对端我们要用这个组合。
  • a=sctp-port:5000:这是 SCTP 关联使用的端口号。注意,这个端口是 SCTP 层的逻辑端口,不是 UDP 端口。UDP 端口由 ICE 协商决定。
  • a=max-message-size:限制单条消息的最大字节数,防止内存爆炸。

你可能会问:为什么 SCTP 还需要一个端口?其实 SCTP 本身是有端口概念的,类似于 TCP/UDP 的端口。但在 WebRTC 场景下,这个端口是协商出来的,通常双方各选一个空闲端口,然后通过 SDP 交换。

个人经验:我建议在实现时,sctp-port 不要写死。有些浏览器实现会随机分配端口,如果你硬编码 5000,可能和对端冲突。最好在创建 PeerConnection 时让底层自己选。

有序 vs 无序传输:你该怎么选?

数据通道支持两种传输模式:有序(ordered)和无序(unordered)。这个选择直接影响你的应用行为。

特性 有序传输 无序传输
消息到达顺序 保证与发送顺序一致 不保证顺序
可靠性 可配置(可靠/部分可靠) 可配置(可靠/部分可靠)
典型场景 文件传输、聊天消息 游戏状态同步、实时数据流
延迟特性 可能因重传导致延迟抖动 延迟更稳定

举个例子。我在做在线协作白板的时候,光标位置同步用的是无序传输。因为光标位置每秒更新几十次,丢一帧无所谓,但延迟必须低。而文字输入用的是有序传输,因为「你」「好」和「好」「你」完全是两个意思。

注意:无序不代表不可靠。你可以配置无序+可靠,或者无序+部分可靠。SCTP 的灵活性就在这里——每个数据通道可以独立配置自己的传输参数。

数据通道的创建流程

整个协商过程,我习惯把它分成三步:

  1. SDP 协商阶段:双方交换 m=application 行,确认 sctp-port 和 max-message-size。
  2. DTLS 握手阶段:建立加密通道,SCTP 关联在 DTLS 之上初始化。
  3. DataChannel 打开阶段:应用层通过 createDataChannel 或 ondatachannel 回调来创建/接收通道。

这里有个容易被忽略的点:SCTP 关联是在 DTLS 握手完成后才建立的。如果你在 DTLS 完成之前就调用 createDataChannel,消息会先排队,等关联建立后再发送。

我曾经踩过一个坑:在 PeerConnection 的 iceConnectionState 变成 connected 后,我立刻创建数据通道并发送消息。结果发现消息发不出去。后来才意识到,connected 只代表 ICE 通了,DTLS 还没完成。正确的做法是等 dtlsState 变成 connected 再操作数据通道。

SVG 流程图:数据通道协商全貌

数据通道协商流程 ① SDP 协商 交换 m=application 行 确认 sctp-port ② DTLS 握手 建立加密通道 证书验证 ③ SCTP 关联 INIT/ACK 交换 流初始化 ④ DataChannel 打开 DCEP 协议交换 open/ack 消息 传输模式配置 • 有序 + 可靠 • 有序 + 部分可靠 • 无序 + 可靠 • 无序 + 部分可靠 每个 DataChannel 可以独立配置有序/无序和可靠/部分可靠 协议栈: 应用数据 → SCTP → DTLS → UDP → IP

部分可靠传输:什么时候用?

除了有序/无序,SCTP 还支持部分可靠传输。你可以设置最大重传次数或消息生存时间。比如:

// 创建数据通道:无序,最大重传 3 次
const dc = pc.createDataChannel('game-state', {
  ordered: false,
  maxRetransmits: 3
});

// 创建数据通道:有序,消息生存时间 500ms
const dc2 = pc.createDataChannel('cursor', {
  ordered: true,
  maxPacketLifeTime: 500
});

这两个参数不能同时设置,要么用 maxRetransmits,要么用 maxPacketLifeTime。为什么?因为 SCTP 内部实现上,这两个参数对应不同的重传策略,混用会导致行为不确定。

避坑指南:我曾经在项目中同时设置了 maxRetransmits 和 maxPacketLifeTime,结果 Chrome 直接抛异常了。后来查了规范才发现,这两个参数是互斥的。记住:要么限制重传次数,要么限制生存时间,二选一。

总结一下

数据通道的协商,本质上是在回答三个问题:

  • 用什么协议栈?——SCTP over DTLS over UDP
  • 端口和参数怎么定?——通过 SDP 的 a=sctp-port 和 a=max-message-size
  • 数据怎么传?——有序/无序 + 可靠/部分可靠,按需组合

理解了这个流程,你就能在遇到数据通道问题时快速定位。比如消息发不出去,先查 DTLS 状态;消息顺序乱了,检查 ordered 参数;消息丢了,看 maxRetransmits 有没有设对。

数据通道是个好东西,但用对场景才是关键。下次遇到实时数据传输的需求,你知道该怎么选了吧?

公众号:蓝海资料掘金营,微信deep3321