10、DataChannel实战:实现一个简单的P2P聊天室、文件传输功能、DataChannel的流控与背压处理
DataChannel 这东西,说实话,刚接触 WebRTC 的时候我差点把它忽略了。那时候满脑子都是音视频流,觉得数据通道就是个附属品。直到有一次做远程协作工具,需要在视频通话的同时传一份实时批注数据,我才发现——DataChannel 才是真正的瑞士军刀。
它走的是 SCTP 协议,不是 UDP 也不是 TCP。这意味着什么?意味着你可以自己选:要可靠还是要快。我习惯把 DataChannel 比作「WebSocket 的加强版」,因为它不需要服务器中转,两端直接怼。
10.1 先搭个最简单的 P2P 聊天室
聊天室的核心逻辑其实就三步:建立连接 → 发送消息 → 展示消息。但这里有个坑——你得先搞定信令交换。我一般用 WebSocket 做信令中转,但为了演示,咱们直接用 localStorage 模拟一下。
核心思路:两个浏览器标签页,通过 BroadcastChannel 交换 SDP 和 ICE 候选。这不是真正的 P2P,但原理一模一样。
// 创建 DataChannel
const peerConnection = new RTCPeerConnection();
const dataChannel = peerConnection.createDataChannel('chat', {
ordered: true, // 保证顺序
maxRetransmits: 3 // 最多重传3次
});
// 监听消息
dataChannel.onmessage = (event) => {
displayMessage(event.data);
};
// 发送消息
function sendMessage(text) {
if (dataChannel.readyState === 'open') {
dataChannel.send(text);
} else {
console.warn('通道还没准备好');
}
}
嗯,这里要注意 readyState。我见过不少新手直接调 send(),结果通道还没打开,消息丢了。我的习惯是在 onopen 回调里设置一个标志位,发送前先检查。
10.2 文件传输:分片才是王道
传文本简单,传文件就有点意思了。DataChannel 的 send() 方法有大小限制——不同浏览器不一样,Chrome 大概是 16KB 到 64KB。你想想看,一个 10MB 的图片直接塞进去,肯定炸。
所以必须分片。我做过一个文件传输工具,当时踩了个坑:分片大小设成了 256KB,结果在 Firefox 上直接报错。后来统一改成 16KB,稳如老狗。
function sendFile(file) {
const chunkSize = 16 * 1024; // 16KB
let offset = 0;
// 先发送文件元信息
dataChannel.send(JSON.stringify({
type: 'file-meta',
name: file.name,
size: file.size
}));
// 逐片发送
const reader = new FileReader();
reader.onload = (e) => {
dataChannel.send(e.target.result);
offset += chunkSize;
if (offset < file.size) {
readNextChunk();
} else {
dataChannel.send(JSON.stringify({ type: 'file-end' }));
}
};
function readNextChunk() {
const slice = file.slice(offset, offset + chunkSize);
reader.readAsArrayBuffer(slice);
}
readNextChunk();
}
我的经验:接收端一定要先解析元信息,再拼接分片。我曾经犯过傻——直接往 Blob 里塞,结果顺序乱了。加个序号字段,稳得很。
10.3 流控与背压:别把通道撑爆了
文件传着传着,突然卡住了?或者消息发出去没反应?大概率是背压问题。
DataChannel 底层是 SCTP,它有自己的流控机制。但如果你发得太快,缓冲区会满。这时候 send() 会返回 false,或者直接抛异常。
我当年做一个实时数据同步项目,每秒要发几百条消息。一开始没做流控,结果通道直接挂了。后来学乖了——监听缓冲区的变化。
// 监听缓冲量
dataChannel.onbufferedamountlow = () => {
console.log('缓冲区空了,可以继续发');
startSending();
};
// 发送前检查
function safeSend(data) {
if (dataChannel.bufferedAmount > 1024 * 1024) { // 超过1MB就等
console.warn('缓冲区太满,稍后再发');
return false;
}
dataChannel.send(data);
return true;
}
注意:bufferedAmountLowThreshold 默认是 0,你得手动设一个值。我一般设成 256KB,这样既不会太频繁触发,也不会溢出。
10.4 知识体系总览
下面这张图是我自己梳理的 DataChannel 实战知识结构。你看一眼,心里就有谱了。
10.5 避坑指南与最佳实践
做 DataChannel 开发,有几个坑我反复踩过,今天一次性告诉你。
- 通道状态检查:发送前一定检查
readyState === 'open'。我曾经在onnegotiationneeded里直接发数据,结果通道还没建好,数据丢了。 - 分片大小:别超过 64KB。我建议用 16KB,兼容性最好。如果你传的是二进制数据,记得用
ArrayBuffer,别用Blob——Blob 在接收端不好拼。 - 背压处理:别一股脑发。设一个
bufferedAmountLowThreshold,配合onbufferedamountlow回调,实现「发一点等一会」的节奏。 - 有序 vs 无序:聊天消息用
ordered: true,文件传输也用有序。但如果你传的是实时位置数据,可以试试ordered: false+maxPacketLifetime,丢包就丢包,别卡住。
我的小技巧:调试 DataChannel 的时候,打开 Chrome 的 chrome://webrtc-internals。里面能看到 bytesSent、messagesSent、bufferedAmount 的实时曲线。背压有没有问题,一眼就看得出来。
10.6 总结一下
DataChannel 这东西,说白了就是「你想怎么传就怎么传」。但自由也意味着责任——你得自己管好分片、流控、状态。我做了这么多项目,发现一个规律:凡是 DataChannel 出问题的,90% 都是没做背压处理。
所以,记住三句话:分片要小、缓冲区要盯、状态要查。做到这三点,你的 P2P 聊天室和文件传输基本就稳了。