返回首页

WebRTC DataChannel:无缓冲区错误的文件

本文分析了通过 WebRTC DataChannel 进行文件传输的生产问题:管理 SCTP 缓冲区、将块适应中继、接收端背压。提供了重叠 I/O、动态超时和 ACK 机制的代码示例,以确保可靠性。

WebRTC 中的文件:如何避免中继上的截断
Advertisement 728x90

WebRTC DataChannel 文件传输:生产环境中的缓冲区管理和可靠性

在 WebRTC DataChannel 中,dc.send() 方法会将数据排队到发送端的 SCTP 缓冲区,但并不保证数据能送达接收端。在慢速中继连接上,缓冲区会堆积数 MB 数据,进度条达到 100%,文件却被截断。一个典型的 P2P 文件共享架构包括信令服务器用于协调、直接点对点连接作为主要路径,以及 TURN 中继作为备用路径。服务器不存储内容,只管理传输状态:从分享创建到接收者队列。

关键挑战:缓冲区反压、File 对象在页面刷新时丢失、同行准备状态不匹配,以及重连问题。本文分析基于生产环境的实现,包含中继限额和自定义 TURN 支持。

无服务器端文件存储的架构

服务器通过 REST API 和 WebSocket 进行协调:创建分享、使用 ECDSA 签名连接,并跟踪状态。分享生命周期——active → matched → transferring → active——允许发送者复用于多个接收者。队列自动分发给新参与者。

Google AdInline article slot

终端状态:expiredcancelledFailed 状态通过 reactivate 恢复。内容采用 DTLS 端到端加密,中继仅代理流量而不访问数据。

问题 1:TURN 备用路径与最优分片大小

由于对称 NAT、防火墙或移动网络,P2P 并非总能实现。连接类型从 ICE 候选中确定:p2pserver_relay(coturn 带配额)、custom_relay(自定义 TURN 无限制)。

分片大小需自适应以最小化重传延迟:

Google AdInline article slot
function getOptimalChunkSize(classification: string): number {
  switch (classification) {
    case 'p2p':          return 1024 * 1024;  // 1 MB
    case 'custom_relay': return 512 * 1024;   // 512 KB
    case 'server_relay': return 64 * 1024;    // 64 KB
    default:             return 512 * 1024;
  }
}

server_relay 上使用更小分片可加速恢复:丢失数据包需完整重传,延迟随大小线性增长。预检查 TURN 可通过专用 RTCPeerConnectioniceTransportPolicy: 'relay')完成,大约需 5 秒。

问题 2:发送循环中的反压与重叠 I/O

朴素循环顺序发送分片,会因 file.slice().arrayBuffer() 阻塞。生产实现采用双缓冲和高低水位线:

const BUFFER_THRESHOLD = 8 * 1024 * 1024; // 8 MB
const BUFFER_LOW       = 6 * 1024 * 1024; // 6 MB

dc.bufferedAmountLowThreshold = BUFFER_LOW;
let nextBuffer = await file.slice(0, chunkSize).arrayBuffer();

while (offset < file.size) {
  const buffer = nextBuffer;
  const readPromise = offset + buffer.byteLength < file.size
    ? file.slice(offset + buffer.byteLength, nextEnd).arrayBuffer()
    : null;

  if (dc.bufferedAmount >= BUFFER_THRESHOLD) {
    await waitForBufferDrain(dc, computeDrainTimeout(dc.bufferedAmount, speed));
  }

  dc.send(buffer);
  offset += buffer.byteLength;

  const effectiveSent = Math.max(0, offset - dc.bufferedAmount);
  const pct = Math.min((effectiveSent / file.size) * 100, isLast ? 100 : 99);

  nextBuffer = readPromise ? await readPromise : null;
}
  • 重叠 I/O:并行读取下一分片与发送当前分片。
  • 动态超时estimatedDrainMs = (bufferedAmount / speed) * 1000,上限 5–60 秒。
  • 真实进度offset - dc.bufferedAmount,传输完成前不超过 99%。

完成需缓冲区清空 + 接收端 transfer_ack,然后才发 transfer_done

Google AdInline article slot

问题 3:接收端与磁盘反压

接收端复制 ArrayBuffer 以避免 DataChannel 缓冲区复用:

const chunk = event.data.slice(0);  // 复制
const writeChain = writeChain.then(() => writer.write(chunk));

if (bytesReceived === fileInfo.size) {
  dc.send(JSON.stringify({ type: 'transfer_ack' }));
}

writeChain 是顺序 Promise 链。慢速磁盘(如 U 盘)会向上游产生反压,节流 SCTP 数据摄入。

其他生产挑战

  • 文件丢失File 对象不可序列化;存为 ArrayBuffer 或 Blob 于 IndexedDB。
  • 重连:信令可能先于 DataChannel 断开;视 peer_left 为正常。
  • Safari 特性:分别处理有序/无序模式及缓冲区。

核心要点:

  • dc.send() 写入 SCTP 缓冲区而非网络;始终监控 bufferedAmount
  • 自适应分片大小:P2P 用 1MB,server_relay 用 64KB 优化重传。
  • 清空缓冲后等 transfer_ack 再发 transfer_done
  • 使用重叠 I/O 和动态超时获最佳性能。
  • 双向验证中继配额;传输前运行 TURN 健康检查。

— Editorial Team

Advertisement 728x90

继续阅读