5、DataChannel:数据通道原理、文本与文件传输、P2P数据传输实战

聊完了音视频,咱们来聊聊数据通道。说实话,很多人做WebRTC都只盯着摄像头和麦克风,忽略了DataChannel这个宝藏。我刚开始接触时也觉得它就是个“顺便”的功能,直到有一次项目里需要做屏幕共享时的实时批注——嗯,那时候才发现DataChannel才是真正的幕后英雄。

5.1 DataChannel是什么?说白了就是一条“旁路”

WebRTC建立连接后,除了跑音视频的媒体流,还留了一条独立的、可靠的、低延迟的数据通道。你可以把它想象成在高速公路旁边修了一条专用的“数据小道”——不堵车,不排队,想传什么传什么。

为什么需要它?你想想看,如果我要在视频会议里发一条聊天消息,总不能把它编码成视频帧吧?那太蠢了。DataChannel就是干这个的——文本、文件、二进制数据,统统可以走这条通道。

核心特点:

  • 基于SCTP协议(流控制传输协议),不是TCP也不是UDP
  • 支持可靠模式(类似TCP)和不可靠模式(类似UDP)
  • 数据直接走P2P,不经过服务器中转
  • 与音视频流共享同一个ICE连接,不需要额外打洞

我在项目中遇到过有人问:“DataChannel和WebSocket有什么区别?”区别大了。WebSocket走服务器中转,DataChannel是端到端直连。说白了,DataChannel才是真正的P2P数据传输。

5.2 数据通道的两种模式

DataChannel支持两种模式,这个选择很重要。我习惯这样理解:

模式 特点 适用场景
可靠有序 保证数据到达,且按顺序交付 文件传输、聊天消息、状态同步
不可靠无序 可能丢包,不保证顺序,延迟更低 游戏位置更新、实时涂鸦、高频传感器数据

创建时通过参数控制:

// 可靠模式(默认)
const dataChannel = peerConnection.createDataChannel('chat', {
  ordered: true
});

// 不可靠模式
const dataChannel = peerConnection.createDataChannel('game', {
  ordered: false,
  maxRetransmits: 0  // 不重传
});

我的经验:文件传输一定要用可靠有序模式。我曾经试过用不可靠模式传图片,结果收到的是半张“抽象画”——像素都错位了。嗯,那次教训挺深刻的。

5.3 文本传输:最简单的开始

文本传输是DataChannel最基础的应用。代码其实很简单:

// 发送端
dataChannel.send('大家好,我是张三');

// 接收端
dataChannel.onmessage = (event) => {
  console.log('收到消息:', event.data);
};

就这么简单?对,就这么简单。但要注意一点:send()方法只能传字符串、Blob、ArrayBuffer或ArrayBufferView。如果你要传对象,记得先序列化:

const message = {
  type: 'chat',
  sender: '张三',
  content: '大家好',
  timestamp: Date.now()
};

dataChannel.send(JSON.stringify(message));

// 接收时反序列化
dataChannel.onmessage = (event) => {
  const data = JSON.parse(event.data);
  console.log(data.sender + '说:' + data.content);
};

5.4 文件传输:分片是关键

文件传输就有点意思了。DataChannel的单个消息大小有限制——Chrome大概是16KB,Safari更小。所以大文件必须分片发送。

我曾经做过一个文件传输功能,一开始没做分片,传个几MB的图片直接报错。后来老老实实写了分片逻辑:

// 发送文件
function sendFile(dataChannel, file) {
  const chunkSize = 16 * 1024; // 16KB一片
  let offset = 0;

  // 先发送文件元信息
  dataChannel.send(JSON.stringify({
    type: 'file-meta',
    name: file.name,
    size: file.size,
    totalChunks: Math.ceil(file.size / chunkSize)
  }));

  // 逐片发送
  const reader = new FileReader();
  reader.onload = (e) => {
    dataChannel.send(e.target.result);
    offset += chunkSize;

    if (offset < file.size) {
      readNextChunk();
    } else {
      console.log('文件发送完成');
    }
  };

  function readNextChunk() {
    const slice = file.slice(offset, offset + chunkSize);
    reader.readAsArrayBuffer(slice);
  }

  readNextChunk();
}

// 接收文件
let receivedChunks = [];
let fileMeta = null;

dataChannel.onmessage = (event) => {
  if (typeof event.data === 'string') {
    const msg = JSON.parse(event.data);
    if (msg.type === 'file-meta') {
      fileMeta = msg;
      receivedChunks = [];
      console.log('开始接收文件:', msg.name);
    }
  } else {
    // 二进制数据,就是文件分片
    receivedChunks.push(event.data);

    if (receivedChunks.length === fileMeta.totalChunks) {
      // 所有分片收齐,合并
      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);
    }
  }
};

避坑指南:我曾经遇到过接收端分片顺序错乱的问题。虽然可靠模式保证数据到达,但如果发送太快,接收端的onmessage回调可能被并发调用。建议在接收端加一个缓冲区队列,按顺序组装分片。

5.5 P2P数据传输实战:完整流程

咱们把上面的知识点串起来,做一个完整的P2P数据传输示例。核心流程是这样的:

DataChannel P2P数据传输流程 节点A(发送端) createDataChannel() 节点B(接收端) ondatachannel ICE连接建立后自动触发 步骤1: A创建DataChannel → 通过信令将标签和协议告知B → B触发ondatachannel 步骤2: A调用 dataChannel.send() → 数据经SCTP封装 → 通过DTLS加密 → 走ICE通道 步骤3: B收到数据 → dataChannel.onmessage触发 → 解析数据 → 渲染/保存 整个过程不需要服务器中转,数据直连P2P

完整代码示例:

// 发送端完整逻辑
const pc = new RTCPeerConnection(config);
const dc = pc.createDataChannel('file-transfer', {
  ordered: true
});

dc.onopen = () => {
  console.log('数据通道已打开');
  // 可以开始传输了
};

dc.onclose = () => {
  console.log('数据通道已关闭');
};

// 接收端完整逻辑
const pc = new RTCPeerConnection(config);

pc.ondatachannel = (event) => {
  const dc = event.channel;

  dc.onopen = () => {
    console.log('收到数据通道:', dc.label);
  };

  dc.onmessage = (event) => {
    // 处理收到的数据
    if (typeof event.data === 'string') {
      // 文本或JSON
    } else {
      // 二进制数据
    }
  };
};

我的建议:实际项目中,建议给DataChannel加一个简单的协议头。比如每条消息前4个字节表示消息类型,后面是内容。这样接收端可以统一解析,不用每次都判断数据类型。我在一个多人协作白板项目里就是这么做的,效果很好。

5.6 性能与注意事项

最后聊几个实战中容易踩的坑:

  • 消息大小限制:不同浏览器限制不同,建议单条消息不超过16KB。大文件一定要分片。
  • 并发发送:DataChannel内部有流控,但如果你连续快速发送大量小消息,接收端可能处理不过来。我建议加一个发送队列,控制发送速率。
  • 连接状态监测:DataChannel有readyState属性,取值connectingopenclosingclosed。发送前最好检查一下状态。
  • 重连处理:ICE连接断开后,DataChannel也会关闭。需要监听onclose事件,做好重连逻辑。

嗯,DataChannel的内容差不多就这些。说白了它就是一条P2P的“数据高速公路”,用好了能做出很多有意思的功能。我见过有人用它做实时协作编辑、做P2P文件分享、甚至做区块链节点通信——只要你敢想,它就能传。

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