跳到主要内容

RTCDataChannel

DataChannel 是 WebRTC 在加密 P2P 链路上提供的"任意数据通道",本质是 SCTP-over-DTLS。和 WebSocket 最大的区别:它是端到端 P2P,不经过服务器(除非走 TURN 中继)。

1. 适用与不适用

适用:

  • P2P 文件传输(不想中转大文件)。
  • 实时多人协作的状态广播(白板、协同编辑、游戏同步)。
  • 远程协助里的鼠标键盘事件、剪贴板、屏幕注释。
  • WebRTC 通话内的"低延迟控制信令"(举手、点赞、表情)。

不适用:

  • 需要服务端持久化、离线消息、跨房间路由 → 用 WebSocket。
  • 需要服务端中转给所有人 → 经过 SFU 反而比 WebSocket 复杂。
  • 需要保证消息顺序到达每一个对端 → SFU 转发 DataChannel 不一定保留顺序。

2. 创建与可靠性参数

// 发起方
const dc = pc.createDataChannel('chat', {
ordered: true,
maxRetransmits: undefined,
maxPacketLifeTime: undefined,
protocol: 'json',
});

// 接收方
pc.addEventListener('datachannel', ({channel}) => {
bindEvents(channel);
});
配置组合语义类似于用途
ordered:true(默认)可靠 + 有序TCP文件、文本聊天
ordered:false, maxRetransmits:0不可靠 + 无序UDP游戏状态、鼠标轨迹
ordered:true, maxPacketLifeTime:100部分可靠(100ms 内重传)RTP-like低延迟控制
ordered:true, maxRetransmits:3部分可靠(最多 3 次重传)RTP-like弱网下控制信令

maxRetransmitsmaxPacketLifeTime 互斥,只能设一个。

3. 事件与生命周期

dc.addEventListener('open', () => console.log('就绪'));
dc.addEventListener('message', (e) => handle(e.data));
dc.addEventListener('error', (e) => console.error(e));
dc.addEventListener('close', () => console.log('关闭'));

dc.readyState; // 'connecting' | 'open' | 'closing' | 'closed'

open 事件可能比 pc.connectionState === 'connected' 晚几十毫秒(要等 SCTP 握手)。

4. 数据类型

dc.send('字符串');
dc.send(new Uint8Array([1,2,3]).buffer); // ArrayBuffer
dc.send(new Blob([blobData])); // Blob(部分浏览器)

// 接收
dc.binaryType = 'arraybuffer'; // 默认 'blob',几乎都该改成 arraybuffer
dc.onmessage = (e) => {
if (typeof e.data === 'string') { /* 文本 */ }
else { /* ArrayBuffer */ }
};

5. 包大小与分片

SCTP 没有自动分片到任意大小。每次 send 一条消息会作为一个 SCTP user message:

浏览器实现单条消息上限备注
Chrome / Edge / Safari256 KB超过会失败
Firefox1 GB(理论)互通时按低的算

生产建议:单条消息 ≤ 16 KB,自己做分片:

const CHUNK = 16 * 1024;
async function sendBuffer(dc, buf) {
for (let off = 0; off < buf.byteLength; off += CHUNK) {
await waitDrain(dc);
dc.send(buf.slice(off, off + CHUNK));
}
}

6. 背压(Backpressure)

发送速率超过对端处理速度时,数据会堆在浏览器内核缓冲区。bufferedAmount 看不到的话很容易把内存打爆

const HIGH_WATERMARK = 1 * 1024 * 1024; // 1 MB
const LOW_WATERMARK = 256 * 1024; // 256 KB

dc.bufferedAmountLowThreshold = LOW_WATERMARK;

function waitDrain(dc) {
if (dc.bufferedAmount < HIGH_WATERMARK) return Promise.resolve();
return new Promise((resolve) => {
dc.addEventListener('bufferedamountlow', resolve, { once: true });
});
}

async function send(dc, data) {
await waitDrain(dc);
dc.send(data);
}

任何"循环发数据"的场景都必须有 waitDrain

7. 文件传输示例

async function sendFile(dc, file) {
// 头:文件元信息
dc.send(JSON.stringify({type: 'header', name: file.name, size: file.size}));

const reader = file.stream().getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
await waitDrain(dc);
dc.send(value);
}

dc.send(JSON.stringify({type: 'eof'}));
}

// 接收端
let receiving = null;
dc.onmessage = (e) => {
if (typeof e.data === 'string') {
const msg = JSON.parse(e.data);
if (msg.type === 'header') {
receiving = { name: msg.name, size: msg.size, chunks: [], received: 0 };
} else if (msg.type === 'eof') {
const blob = new Blob(receiving.chunks);
saveAs(blob, receiving.name);
receiving = null;
}
} else {
receiving.chunks.push(e.data);
receiving.received += e.data.byteLength;
onProgress(receiving.received / receiving.size);
}
};

8. 在 SFU 架构中

DataChannel 在 SFU 模型里有两种用法:

  1. 客户端 ↔ SFU:业务级控制信令、与媒体同优先级的低延迟事件。
  2. SFU 帮忙转发:SFU 把一端的 DataChannel 消息广播给房间内所有人(mediasoup DirectTransport、LiveKit data publish 都支持)。

注意:SFU 转发的 DataChannel 不保证全房间顺序一致。需要顺序的业务(白板编辑)必须自己加序号或走业务后端。

9. 与 WebSocket 的对比

维度DataChannelWebSocket
路径P2P / TURN 中继经过服务器
加密强制 DTLS可选 wss
可靠性可选可靠/不可靠仅可靠
顺序可选有序/无序仅有序
服务端持久化业务自己加
离线消息业务自己加
单包上限16 KB(推荐)实现相关
适合实时控制、P2P 数据IM、业务消息

10. 反模式

反模式后果修复
不监听 bufferedAmount 直接 send 大文件浏览器 OOM背压
用 DataChannel 替代 WebSocket没有离线消息、断线重连用对工具
单条消息发 1MBChrome 直接拒收分片到 16KB
不约定二进制协议头接收端无法分辨消息边界自定义 framing
不监听 close/error状态不一致完整事件处理

11. 权威资料