21、DataChannel:RTCDataChannel介绍、发送文本消息、发送文件数据

聊到 WebRTC,大家第一反应往往是音视频。但有个功能容易被忽略——DataChannel。说白了,它就是在两个浏览器之间,直接开一条数据通道。不需要经过服务器中转,数据点对点传输。

我个人习惯把 DataChannel 比作「WebSocket 的升级版」。为什么?因为 WebSocket 必须经过服务器,而 DataChannel 是端到端的。延迟更低,隐私性更强。

RTCDataChannel 是什么?

RTCDataChannel 是 WebRTC 提供的一个 API。它允许你在两个 peer 之间,传输任意数据。文本、文件、二进制数据,统统可以。

它的底层基于 SCTP 协议。SCTP 是个好东西,它结合了 TCP 的可靠性和 UDP 的低延迟。你可以自己选模式:

  • 可靠模式:数据包必须到达,顺序不能乱。适合文件传输。
  • 不可靠模式:允许丢包,适合实时游戏、高频数据流。

我在项目中遇到过一个问题:有个同事把 DataChannel 当 WebSocket 用,结果发现丢包率很高。后来一看,他设的是不可靠模式,但传输的是配置文件。嗯,这里要注意——选对模式很重要

核心特点:

  • 点对点传输,不经过服务器
  • 支持可靠/不可靠两种模式
  • 传输文本、二进制数据
  • 延迟极低,适合实时场景

DataChannel 的工作流程

先看一张图,帮你理清整体逻辑:

RTCDataChannel 工作流程 Peer A createDataChannel() 发送文本/文件 SCTP 通道 可靠 / 不可靠模式 Peer B ondatachannel 事件 接收数据 关键步骤 ① Peer A 调用 createDataChannel() 创建通道 ② 通过信令交换 SDP,建立 PeerConnection ③ Peer B 触发 ondatachannel 事件,拿到通道对象 ④ 双方通过 send() / onmessage 收发数据

流程其实不复杂。你想想看,核心就三步:创建通道、建立连接、收发数据。

发送文本消息

先来个最简单的——发文本。代码量很少,我直接贴出来:

// Peer A 端
const pc = new RTCPeerConnection();
const dc = pc.createDataChannel('chat', {
  ordered: true  // 保证顺序
});

dc.onopen = () => {
  console.log('通道已打开');
  dc.send('你好,这是第一条消息!');
};

dc.onmessage = (event) => {
  console.log('收到回复:', event.data);
};

// Peer B 端
pc.ondatachannel = (event) => {
  const dc = event.channel;
  dc.onmessage = (e) => {
    console.log('收到:', e.data);
    dc.send('收到,over!');
  };
};

你看,代码很简洁。send() 方法直接传字符串就行。onmessage 里拿到的 event.data 就是文本内容。

小技巧: 我建议在 onopen 回调里再开始发数据。别在通道还没准备好时就 send,会丢消息。曾经有个同事踩过这个坑,调试了半天才发现是时序问题。

发送文件数据

文件传输稍微复杂一点。因为文件是二进制数据,我们需要用 Blob 或 ArrayBuffer 来处理。

我的做法是:先把文件切成小块,分片发送。为什么?因为大文件一次性 send 会撑爆缓冲区。你想想看,一个 100MB 的文件直接塞进去,浏览器会崩溃的。

来看代码:

// 发送端
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (e) => {
  const file = e.target.files[0];
  const chunkSize = 16 * 1024; // 16KB 一块
  let offset = 0;

  // 先发送文件元信息
  dc.send(JSON.stringify({
    type: 'meta',
    name: file.name,
    size: file.size
  }));

  // 分片发送
  while (offset < file.size) {
    const chunk = file.slice(offset, offset + chunkSize);
    const buffer = await chunk.arrayBuffer();
    dc.send(buffer);
    offset += chunkSize;
    
    // 可以加个进度回调
    const progress = Math.round((offset / file.size) * 100);
    console.log(`发送进度:${progress}%`);
  }

  // 发送完成标记
  dc.send(JSON.stringify({ type: 'end' }));
});
// 接收端
let receivedChunks = [];
let fileMeta = null;

dc.onmessage = async (event) => {
  const data = event.data;

  // 判断是文本还是二进制
  if (typeof data === 'string') {
    const msg = JSON.parse(data);
    
    if (msg.type === 'meta') {
      fileMeta = msg;
      console.log(`开始接收文件:${msg.name},大小:${msg.size}`);
    } else if (msg.type === 'end') {
      // 组装文件
      const blob = new Blob(receivedChunks);
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = fileMeta.name;
      a.click();
      URL.revokeObjectURL(url);
      
      // 重置
      receivedChunks = [];
      fileMeta = null;
    }
  } else {
    // 二进制数据,直接收集
    receivedChunks.push(data);
  }
};

注意: 分片大小不要太大。16KB 到 64KB 是比较安全的范围。我曾经试过用 1MB 的分片,结果在弱网环境下频繁丢包,重传搞得头大。

DataChannel 的配置选项

创建 DataChannel 时,可以传一个配置对象。我整理了一张表:

选项 类型 说明
ordered boolean 是否保证顺序,默认 true
maxPacketLifeTime number 不可靠模式下,数据包存活时间(毫秒)
maxRetransmits number 不可靠模式下,最大重传次数
protocol string 自定义子协议标识,默认空

举个例子。如果你要做实时游戏,不在乎丢几帧,那就用不可靠模式:

const dc = pc.createDataChannel('game', {
  ordered: false,
  maxRetransmits: 0  // 不重传
});

如果是文件传输,必须可靠:

const dc = pc.createDataChannel('file', {
  ordered: true
});

避坑指南: 我曾经把 maxPacketLifeTime 和 maxRetransmits 同时设置了,结果通道创建失败。这两个参数是互斥的,只能选一个。文档里其实写了,但当时没仔细看,浪费了半天时间。

实际项目中的经验

最后分享几个我在项目中积累的经验:

  • 通道数量:不要创建太多 DataChannel。每个通道都会占用资源。我一般控制在 3-5 个以内。
  • 数据压缩:传输大文件前,建议先压缩。用 pako.js 做 gzip 压缩,能省不少带宽。
  • 心跳检测:DataChannel 不会自动检测连接状态。我习惯每隔 5 秒发一个心跳包,如果连续 3 次没回应,就判定断开。
  • 错误处理:send() 方法可能会抛出异常。比如通道关闭后还在发数据。记得用 try-catch 包一下。

嗯,DataChannel 的内容就这些。它虽然不像音视频那么炫酷,但在很多场景下非常实用。比如在线协作编辑、实时白板、文件共享等等。掌握了它,你的 WebRTC 技能树就又多了一个分支。