5.3 数据通道实战 本节摘要:某天凌晨的游戏联调群里,有人发出灵魂拷问:同步玩家坐标走 WebSocket 要绕服务器一圈,能不能像媒体一样直连?答案就是 RTCDataChannel——长在 RTCPeerConnection 上的任意数据通道,复用 DTLS 加密与 ICE 通路,可靠性还能逐条消息定制。本节讲透它的协议栈、两种传输模式与背压处理,聊天与状态同步两类典型实现一并给出。 目标清单 阅读完本节,你应当能够: 解释数据通道的协议栈:SCTP over DTLS,以及它与媒体通道的关系; 区分可靠有序、不可靠无序、部分可靠三种模式,并按业务选型; 用 maxRetransmits 与 maxPacketLifeTime 定制消息级可靠性;
本节摘要:某天凌晨的游戏联调群里,有人发出灵魂拷问:同步玩家坐标走 WebSocket 要绕服务器一圈,能不能像媒体一样直连?答案就是 RTCDataChannel——长在 RTCPeerConnection 上的任意数据通道,复用 DTLS 加密与 ICE 通路,可靠性还能逐条消息定制。本节讲透它的协议栈、两种传输模式与背压处理,聊天与状态同步两类典型实现一并给出。
阅读完本节,你应当能够:
数据通道的本体是 SCTP(一种流控制传输协议),架设在 DTLS 加密层之上、ICE 通路之上。这个位置决定了它的三重性格:加密与媒体同级(同一条 DTLS 隧道,密钥同一把握手机制导出)、复用同一条网络通路(不必再打洞,ICE 通了它就通)、可靠性可谈(SCTP 原生支持按流控制重传,WebRTC 把这个能力直接暴露给开发者)。
「可靠性可谈」是它与 WebSocket 的本质差异。WebSocket 背着 TCP,每条消息都必须可靠有序——代价是队头阻塞:网络一抖,前面的消息卡住,后面全部排队。数据通道可以声明「这条消息丢了就算了」:坐标同步丢一帧,下一帧马上就来,比苦等重传好得多。选型的直觉是:命令与文本用可靠有序,高频状态用不可靠无序。
创建通道的完整选项与两端对齐方式:
// 发起端 A:创建通道并配置模式 const dc = pc.createDataChannel('chat', { ordered: true, // 有序 maxRetransmits: 3, // 最多重传三次:部分可靠 // 注意:maxRetransmits 与 maxPacketLifeTime 二选一,都写则通道创建报错 }); dc.binaryType = 'arraybuffer'; // 要传二进制(如文件分片)先声明 // 应答端 B:不重复创建,监听对端建好的通道 pc.ondatachannel = (event) => { const dc = event.channel; // 与 A 的 'chat' 是同一条 dc.onopen = () => console.log('通道就绪'); dc.onmessage = (e) => handleMessage(e.data); };
三种模式对号入座。可靠有序(默认):像 TCP,聊天、指令、文件适合。不可靠无序(ordered: false 且设 maxRetransmits: 0 或 maxPacketLifeTime):像 UDP,高频遥测、指针位置适合。部分可靠:设重传上限或时限,介于两者之间,「尽力送达但要争取」的消息(如白笔迹的补发)适合。
背压是数据通道最大的实践陷阱。send() 把消息压进内核缓冲就返回,网络慢时缓冲越积越大,内存涨上去、消息延迟也涨上去——你却毫无察觉。标准解法是盯着 bufferedAmount:
// 背压控制:缓冲超限暂停发送,水位回落再继续 const CHUNK = 16 * 1024; // 每块 16KB,传大文件的惯例尺寸 dc.bufferedAmountLowThreshold = 1024 * 1024; // 低水位 1MB function sendBuffered(file) { let offset = 0; const total = file.size; function pump() { while (offset < total && dc.bufferedAmount < 4 * 1024 * 1024) { // 高水位 4MB const slice = file.slice(offset, offset + CHUNK); dc.send(slice); offset += slice.size; } if (offset < total) { // 高水位已满:等低水位事件再继续泵 dc.addEventListener('bufferedamountlow', pump, { once: true }); } } pump(); }
这段「高低水位泵」是文件传输、批量同步的标准骨架:把 send 的调用冲动装进笼子,用缓冲水位当节拍器。

聊天消息用可靠有序,但要注意「通道未开就发」:dc.send 在 open 之前调用会抛异常,把「待发队列」接在 open 事件后面是标准姿势。状态同步用不可靠无序,但要带上时间戳与序号让接收方判断新旧——乱序到达时旧状态覆盖新状态是这类实现的经典 bug,一条 if 比较就治好。
⚠️ 常见坑:两端同时 createDataChannel 同名通道,会得到两条「物理上是同一条、逻辑上各执一词」的通道吗?不会撞车,但要理解协商语义:显式声明 negotiated: true 时两端各自创建、无需监听 ondatachannel;不声明时一端 create、另一端 must 监听。两种风格混用会导致「谁都没监听」的静默失联。
背景:某协作白板需要把书写笔迹实时同步给所有参与者,网络抖动时不能出现「笔迹错位」「越写越卡」两类事故。
操作:笔迹按笔划分帧(pointermove 采样点),走不可靠无序通道,每帧带单调递增的笔号与点号;接收端按序号丢弃旧帧。笔划结束的「抬笔」消息与「撤销」「清屏」等命令走可靠有序通道——命令必须一条不丢。两端都设置背压:采样率在 bufferedAmount 超过阈值时自动降档(从每秒一百二十点降到三十点),网络恢复再升回来。
结果:弱网下表现为「笔迹偶尔稀疏」而不是「卡顿积压」;命令永不丢失;延迟稳定在通路物理延迟附近。
解读:这个案例的方法论是「按数据的重要性分流」:同一块白板,笔迹帧与控制命令走了两条性格不同的通道(同一连接上可开多条),分别匹配各自的容错模型。多数「数据通道不好用」的评价,其实是「所有数据都用一种模式」造成的——模式选对,数据通道比想象中好用得多。
变式:多人白板(三方以上)在纯 Mesh 下每人要维护多条通道,消息洪泛明显;升级到 SFU 后,数据通道变成「端到 SFU」的星型,SFU 负责广播——这是第 7 章拓扑话题在数据面的投影。到时你会看到:拓扑决策对数据通道的影响,与对媒体的影响同构。