数据通道发送二进制数据:Blob、ArrayBuffer、TypedArray 的发送与接收、大文件分片思路

聊到 WebRTC 数据通道,很多人第一反应是「传文本消息」。但说实话,真正用到生产环境里,传文本的场景其实不多。我做过好几个项目,最后发现核心需求都是传文件、传图片、传二进制流。今天我们就来聊聊数据通道怎么搞定二进制数据。

为什么需要二进制传输?

你想想看,文本消息能传的东西太有限了。一张图片、一段音频、一个 PDF 文件,这些本质上都是二进制数据。WebRTC 数据通道底层用的是 SCTP 协议,它本身是支持二进制传输的。但浏览器层面,我们需要用合适的 API 来包装这些数据。

我个人习惯把二进制数据分成三类来理解:

  • Blob:文件级别的二进制数据,比如你从 <input type="file"> 拿到的文件对象
  • ArrayBuffer:内存中的原始二进制缓冲区,可以理解为「字节数组」
  • TypedArray:ArrayBuffer 的视图,让你能按特定类型(如 Int8、Uint16、Float32)读写数据

核心要点:数据通道的 send() 方法可以直接接收 ArrayBuffer、Blob、String 三种类型。TypedArray 需要先转成 ArrayBuffer 再发送。

发送 ArrayBuffer

ArrayBuffer 是最底层的二进制容器。我在项目中经常用它来传输自定义协议的数据包。比如一个简单的例子:

// 发送端
const buffer = new ArrayBuffer(8);
const view = new DataView(buffer);
view.setInt32(0, 12345);      // 前4字节存一个整数
view.setFloat32(4, 3.14159);  // 后4字节存一个浮点数

dataChannel.send(buffer);

接收端怎么处理?嗯,这里要注意:

// 接收端
dataChannel.onmessage = (event) => {
  if (event.data instanceof ArrayBuffer) {
    const view = new DataView(event.data);
    const intVal = view.getInt32(0);
    const floatVal = view.getFloat32(4);
    console.log('收到数据:', intVal, floatVal);
  }
};

我曾经踩过一个坑:发送端用 send(buffer) 后,接收端拿到的 event.data 类型取决于你创建数据通道时的配置。如果你创建通道时设置了 binaryType: 'blob',那收到的就是 Blob 而不是 ArrayBuffer。所以一定要确认两边的 binaryType 一致。

避坑指南:创建数据通道时,建议显式设置 binaryType: 'arraybuffer',这样接收端拿到的数据就是 ArrayBuffer,处理起来更灵活。

发送 TypedArray

TypedArray 是 ArrayBuffer 的「视图」。常见的类型有 Uint8Array、Int16Array、Float32Array 等。它们不能直接传给 send(),需要先取出底层的 buffer:

// 发送端
const uint8 = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" 的 ASCII
dataChannel.send(uint8.buffer);  // 注意是 .buffer 不是直接传 uint8

接收端收到后,可以重新包装成 TypedArray:

dataChannel.onmessage = (event) => {
  if (event.data instanceof ArrayBuffer) {
    const uint8 = new Uint8Array(event.data);
    const str = String.fromCharCode(...uint8);
    console.log('收到字符串:', str); // "Hello"
  }
};

为什么不能直接传 TypedArray?说白了,send() 方法内部会检查参数类型,它只认 ArrayBuffer、Blob、String 这三种。TypedArray 不在白名单里,传进去会报错。我刚开始做的时候也犯过这个错,调试了半天才发现。

发送 Blob

Blob 通常用于文件传输。比如用户上传了一张图片:

// 发送端 - 从文件输入框获取 Blob
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', (e) => {
  const file = e.target.files[0];
  dataChannel.send(file); // 直接传 File 对象,File 继承自 Blob
});

接收端收到 Blob 后,可以转成 URL 或 ArrayBuffer 来使用:

dataChannel.onmessage = async (event) => {
  if (event.data instanceof Blob) {
    // 方式1:转成 URL 直接显示
    const url = URL.createObjectURL(event.data);
    document.getElementById('preview').src = url;

    // 方式2:转成 ArrayBuffer 进一步处理
    const buffer = await event.data.arrayBuffer();
    // ... 处理 buffer
  }
};

小技巧:用 URL.createObjectURL() 生成临时 URL 时,记得在不用的时候调用 URL.revokeObjectURL() 释放内存。不然随着文件传输增多,内存会一直涨。

大文件分片思路

数据通道虽然底层支持大包,但实际传输时有大小限制。我记得 Chrome 的 SCTP 实现默认最大消息大小是 256KB。超过这个值,send() 会直接抛异常。

所以传大文件必须分片。我的做法是这样的:

  1. 把文件切成固定大小的块(比如 16KB 一块)
  2. 每块加上序号和总块数信息
  3. 接收端按序号重组

来看一个简单的分片发送示例:

const CHUNK_SIZE = 16 * 1024; // 16KB

function sendFile(file) {
  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
  let offset = 0;

  for (let i = 0; i < totalChunks; i++) {
    const chunk = file.slice(offset, offset + CHUNK_SIZE);
    const header = {
      index: i,
      total: totalChunks,
      name: file.name
    };

    // 把头部信息和数据打包在一起
    const packet = {
      type: 'file-chunk',
      header: header,
      data: chunk
    };

    // 这里需要序列化,因为不能直接传对象
    // 我习惯用 JSON 序列化头部,然后和二进制数据拼接
    const headerStr = JSON.stringify(packet.header);
    const headerBytes = new TextEncoder().encode(headerStr);
    const headerLen = new Uint32Array([headerBytes.length]);

    // 组装:4字节头部长度 + 头部JSON + 分片数据
    const combined = new Blob([headerLen.buffer, headerBytes, chunk]);
    dataChannel.send(combined);

    offset += CHUNK_SIZE;
  }
}

接收端需要解析这个自定义协议:

let receivedChunks = [];
let fileInfo = null;

dataChannel.onmessage = async (event) => {
  if (event.data instanceof Blob) {
    const buffer = await event.data.arrayBuffer();
    const view = new DataView(buffer);

    // 读取头部长度(前4字节)
    const headerLen = view.getUint32(0, true);
    // 读取头部 JSON
    const headerBytes = new Uint8Array(buffer, 4, headerLen);
    const headerStr = new TextDecoder().decode(headerBytes);
    const header = JSON.parse(headerStr);

    // 读取分片数据
    const chunkData = buffer.slice(4 + headerLen);

    if (!fileInfo) {
      fileInfo = {
        name: header.name,
        total: header.total,
        chunks: []
      };
    }

    fileInfo.chunks[header.index] = chunkData;

    // 检查是否收完
    if (fileInfo.chunks.length === fileInfo.total) {
      // 合并所有分片
      const fullFile = new Blob(fileInfo.chunks);
      // 保存或展示文件
      const url = URL.createObjectURL(fullFile);
      const a = document.createElement('a');
      a.href = url;
      a.download = fileInfo.name;
      a.click();
    }
  }
};

注意:分片传输时,数据不保证按序到达!虽然 SCTP 支持有序传输,但如果你设置了 ordered: false,接收端必须自己处理乱序问题。上面的代码用 chunks[header.index] 来按索引存放,就是为了应对乱序。

三种二进制类型的对比

类型 适用场景 发送方式 接收方式
ArrayBuffer 自定义协议、结构化数据 直接 send(buffer) event.data instanceof ArrayBuffer
TypedArray 数值数组、音视频帧 send(typedArray.buffer) new Uint8Array(event.data)
Blob 文件传输、图片 直接 send(blob) event.data instanceof Blob

知识体系总览

下面这张图总结了二进制数据传输的核心流程:

二进制数据传输核心流程 发送端 Blob ArrayBuffer TypedArray 数据通道 大文件分片(16KB/块) 接收端 解析头部 + 重组分片 完整文件/数据 发送端:Blob/ArrayBuffer 直接 send,TypedArray 需取 .buffer 接收端:检查 event.data 类型,按需转换 大文件:分片 + 序号 + 重组,注意乱序问题

这张图把整个流程串起来了。从左到右看:发送端准备数据,经过数据通道传输,接收端解析重组。大文件分片是中间的关键环节,也是实际项目中最容易出问题的地方。

好了,二进制数据传输的核心内容就这些。记住三点:类型匹配、分片策略、乱序处理。把这三点搞明白,大部分二进制传输场景你都能应付。

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