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 实战知识结构。你看一眼,心里就有谱了。

DataChannel 实战 P2P 聊天室 信令交换 → 创建通道 send() / onmessage 文件传输 分片发送 (16KB/片) 元信息 + 序号 + 拼接 流控与背压 bufferedAmount 监控 onbufferedamountlow 核心:可靠传输 + 分片策略 + 缓冲区控制 = 稳定的 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。里面能看到 bytesSentmessagesSentbufferedAmount 的实时曲线。背压有没有问题,一眼就看得出来。

10.6 总结一下

DataChannel 这东西,说白了就是「你想怎么传就怎么传」。但自由也意味着责任——你得自己管好分片、流控、状态。我做了这么多项目,发现一个规律:凡是 DataChannel 出问题的,90% 都是没做背压处理

所以,记住三句话:分片要小、缓冲区要盯、状态要查。做到这三点,你的 P2P 聊天室和文件传输基本就稳了。


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