9、数据通道:RTCDataChannel创建、可靠与不可靠模式、文件传输实战
聊完音视频,咱们来聊聊数据通道。
很多人觉得WebRTC就是用来传视频和音频的。其实不然。WebRTC的核心能力是建立点对点连接,而数据通道(RTCDataChannel)就是这条连接上的一条“高速公路”。你可以用它传任何数据——文本、二进制、文件,甚至游戏指令。
我个人习惯把数据通道比作“WebSocket的P2P版本”。区别在哪?WebSocket走服务器中转,数据通道直接点对点。延迟更低,隐私更好。我在项目中遇到过不少场景,比如远程桌面、文件共享、实时白板,都是靠数据通道搞定的。
核心要点:RTCDataChannel基于SCTP协议,运行在DTLS加密通道之上。它天生安全,延迟可控。
9.1 数据通道的创建
创建数据通道很简单。你只需要一个RTCPeerConnection实例,然后调用它的createDataChannel方法。
const peerConnection = new RTCPeerConnection(config);
const dataChannel = peerConnection.createDataChannel('myLabel', {
ordered: true,
maxRetransmits: 3
});
这里有个细节:createDataChannel必须在发起连接之前调用。为什么?因为SCTP的配置是在连接建立时协商的。如果你在连接建立后才创建通道,虽然也能用,但某些参数可能无法生效。
嗯,这里要注意:createDataChannel是本地创建,对方需要通过ondatachannel事件来接收。
peerConnection.ondatachannel = (event) => {
const receiveChannel = event.channel;
console.log('收到数据通道:', receiveChannel.label);
};
说白了,创建数据通道就像两个人约定好一个暗号。你这边喊一声“我要建一个叫myLabel的通道”,对方那边就会收到通知。两边都准备好了,数据就能跑了。
9.2 可靠模式 vs 不可靠模式
数据通道有两种模式:可靠和不可靠。这个选择直接影响你的应用行为。
| 特性 | 可靠模式 | 不可靠模式 |
|---|---|---|
| 数据保证 | 100%送达 | 可能丢失 |
| 顺序保证 | 有序(默认)或无序 | 无序 |
| 重传机制 | 自动重传 | 不重传或有限重传 |
| 适用场景 | 文件传输、聊天消息 | 实时游戏、视频帧数据 |
可靠模式,说白了就是TCP那套——保证数据不丢、不乱序。你传一个文件,对方必须完整收到,顺序也不能错。否则文件就坏了。
不可靠模式,类似UDP。数据可能丢,顺序可能乱。但换来的是低延迟。我在项目中做过一个实时游戏,玩家位置更新用不可靠模式。丢几帧没关系,但延迟必须低。你想想看,如果玩家移动要等重传,那游戏就没法玩了。
我的建议:如果你不确定用哪种,先问自己一个问题——“数据丢了会怎样?”如果会出大问题,用可靠模式。如果只是体验变差,用不可靠模式。
设置模式其实是通过参数控制的:
// 可靠模式(默认)
const reliable = pc.createDataChannel('reliable');
// 不可靠模式:最多重传3次
const unreliable = pc.createDataChannel('unreliable', {
ordered: false,
maxRetransmits: 3
});
// 不可靠模式:最多存活30ms
const timed = pc.createDataChannel('timed', {
ordered: false,
maxPacketLifeTime: 30
});
注意:maxRetransmits和maxPacketLifeTime不能同时设置。这是SCTP协议的限制。我曾经踩过这个坑,两个都设了,结果通道创建失败。调试了半天才发现。
9.3 文件传输实战
文件传输是数据通道的经典应用。咱们来写一个完整的例子。
核心思路:把文件切成小块,通过数据通道发送。接收方收到后重组。
// 发送端
function sendFile(file, dataChannel) {
const chunkSize = 16384; // 16KB
let offset = 0;
// 先发送文件元信息
dataChannel.send(JSON.stringify({
type: 'meta',
name: file.name,
size: file.size
}));
// 然后发送文件数据
const reader = new FileReader();
reader.onload = (e) => {
dataChannel.send(e.target.result);
offset += e.target.result.byteLength;
if (offset < file.size) {
readNextChunk();
} else {
console.log('文件发送完成');
}
};
function readNextChunk() {
const slice = file.slice(offset, offset + chunkSize);
reader.readAsArrayBuffer(slice);
}
readNextChunk();
}
接收端这边:
// 接收端
let receivedBuffers = [];
let fileMeta = null;
dataChannel.onmessage = (event) => {
const data = event.data;
if (typeof data === 'string') {
// 解析元信息
fileMeta = JSON.parse(data);
console.log('收到文件:', fileMeta.name, fileMeta.size);
return;
}
// 收到二进制数据
receivedBuffers.push(data);
// 检查是否接收完成
const totalReceived = receivedBuffers.reduce((acc, buf) => acc + buf.byteLength, 0);
if (totalReceived >= fileMeta.size) {
// 重组文件
const blob = new Blob(receivedBuffers);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileMeta.name;
a.click();
URL.revokeObjectURL(url);
}
};
这里有个关键点:分片大小。我习惯用16KB。为什么?因为SCTP的MTU一般是1200字节左右,但数据通道内部会做分片和重组。16KB是一个比较平衡的值——不会因为太小导致过多消息,也不会因为太大导致内存压力。
避坑指南:我曾经在传输大文件时遇到内存溢出。原因是把所有分片都保存在内存里,等全部收完才重组。后来我改成边收边写,用Blob的slice方法逐步构建。或者用IndexedDB做临时存储。大文件一定要考虑内存。
9.4 数据通道的注意事项
最后聊几个实战中容易踩的坑。
- 消息大小限制:单个消息不要超过64KB。虽然SCTP支持更大的消息,但浏览器实现有限制。我建议控制在16KB以内。
- 通道数量:理论上可以创建多个通道,但实际不要超过10个。每个通道都有开销。
- 连接状态:发送数据前检查
readyState。如果通道没打开,数据会丢失。 - 二进制数据:使用
ArrayBuffer而不是Blob。Blob在传输时会被转为ArrayBuffer,多一次拷贝。
数据通道是个好东西。它让WebRTC不仅仅是音视频的专利。文件传输、游戏同步、实时协作,都能靠它搞定。你想想看,一个浏览器就能直接给另一个浏览器传文件,不需要服务器中转,多酷。
总结:数据通道的核心是SCTP。可靠模式保证数据完整,不可靠模式追求低延迟。文件传输要分片、要控制内存。记住这些,你就能用好数据通道。