综合实战:构建一个WebRTC文件共享应用

说实话,做了这么多年WebRTC开发,我一直觉得文件共享才是最能体现这门技术魅力的场景。视频通话大家见得多了,但点对点传文件——尤其是大文件——那种「嗖一下传完,不经过任何服务器」的感觉,真的很爽。

这一章,咱们就把之前学过的所有知识点串起来。从需求分析到架构设计,从前端实现到后端部署,完整走一遍。我保证,做完这个项目,你对WebRTC的理解会上一个台阶。

1. 需求分析:到底要做什么?

先别急着写代码。我见过太多人上来就撸代码,结果做到一半发现需求没想清楚。咱们先花10分钟把需求理清楚。

这个文件共享应用,核心功能就三个:

  • 建立连接:两个浏览器之间建立WebRTC连接
  • 传输文件:支持任意类型、任意大小的文件
  • 进度反馈:实时显示传输进度、速度、剩余时间

嗯,听起来简单。但实际做起来,坑不少。

核心需求清单

  • 支持点对点文件传输,不经过服务器存储
  • 支持断点续传(这个我后面会讲为什么重要)
  • 实时显示传输进度条和速度
  • 支持多个文件同时传输
  • 传输完成后自动校验文件完整性

你可能会问:「为什么需要断点续传?」

我跟你讲个真实案例。有一次我在咖啡馆给同事传一个2GB的设计稿文件,传了80%的时候网络断了。没有断点续传,只能从头再来。那种绝望,你懂的。

2. 架构设计:画张图就清楚了

做架构设计,我习惯先画一张整体流程图。这样前后端怎么分工、数据怎么流动,一目了然。

WebRTC文件共享应用架构图 发送端 (Peer A) 文件读取 → 分片 DataChannel 发送 进度上报 信令服务器 WebSocket 通信 SDP 交换 ICE Candidate 转发 接收端 (Peer B) DataChannel 接收 分片重组 → 文件 完整性校验 SDP/ICE SDP/ICE DataChannel 点对点传输 传输流程 ① 发送端读取文件 ② 按 16KB 分片 ③ 通过 DataChannel 发送 ④ 接收端接收分片 ⑤ 写入 ArrayBuffer ⑥ 完成后校验并保存

这张图其实已经把核心逻辑说清楚了。发送端和接收端之间,通过信令服务器交换连接信息,然后直接建立DataChannel进行文件传输。信令服务器只负责「牵线搭桥」,不碰文件数据本身。

3. 前端实现:核心代码拆解

前端部分,我重点讲三个模块:信令通信、文件分片、DataChannel传输。

3.1 信令通信模块

信令服务器我用的是WebSocket。为什么不用HTTP轮询?说白了,WebSocket的双工通信更适合这种实时性要求高的场景。

// signaling.js - 信令通信模块
class SignalingChannel {
  constructor(url) {
    this.ws = new WebSocket(url);
    this.callbacks = {};
    
    this.ws.onmessage = (event) => {
      const msg = JSON.parse(event.data);
      const handler = this.callbacks[msg.type];
      if (handler) handler(msg.payload);
    };
  }
  
  send(type, payload) {
    this.ws.send(JSON.stringify({ type, payload }));
  }
  
  on(type, callback) {
    this.callbacks[type] = callback;
  }
}

这段代码看着简单,但有个细节我吃过亏:一定要在WebSocket连接建立后再发送消息。我曾经在onopen回调里没做状态判断,结果消息发了个寂寞。

3.2 文件分片逻辑

WebRTC的DataChannel虽然支持大消息,但实际传输时,超过16KB的消息会被底层自动分片。我个人习惯手动控制分片大小,这样能更好地做进度追踪和断点续传。

// file-chunker.js - 文件分片
const CHUNK_SIZE = 16 * 1024; // 16KB

class FileChunker {
  constructor(file) {
    this.file = file;
    this.chunks = [];
    this.totalChunks = Math.ceil(file.size / CHUNK_SIZE);
  }
  
  getChunk(index) {
    const start = index * CHUNK_SIZE;
    const end = Math.min(start + CHUNK_SIZE, this.file.size);
    return this.file.slice(start, end);
  }
  
  *[Symbol.iterator]() {
    for (let i = 0; i < this.totalChunks; i++) {
      yield { index: i, data: this.getChunk(i) };
    }
  }
}

这里用了生成器(Generator),好处是懒加载——不会一次性把所有分片都加载到内存里。传大文件时,内存占用很稳定。

3.3 DataChannel 传输

传输部分,我选择用有序、可靠的DataChannel模式。文件传输不能丢包,也不能乱序。

// file-transfer.js - 文件传输
class FileSender {
  constructor(dc, file) {
    this.dc = dc;
    this.chunker = new FileChunker(file);
    this.currentChunk = 0;
    this.startTime = Date.now();
  }
  
  start() {
    this.dc.onbufferedamountlow = () => this.sendNext();
    this.sendNext();
  }
  
  sendNext() {
    while (this.dc.bufferedAmount < this.dc.bufferedAmountLowThreshold) {
      if (this.currentChunk >= this.chunker.totalChunks) {
        this.sendComplete();
        return;
      }
      
      const chunk = this.chunker.getChunk(this.currentChunk);
      this.dc.send(chunk);
      this.currentChunk++;
      this.reportProgress();
    }
  }
  
  reportProgress() {
    const progress = (this.currentChunk / this.chunker.totalChunks) * 100;
    const elapsed = (Date.now() - this.startTime) / 1000;
    const speed = this.currentChunk * CHUNK_SIZE / elapsed;
    
    // 通过另一个DataChannel发送进度信息
    this.dc.send(JSON.stringify({
      type: 'progress',
      progress,
      speed,
      remaining: (this.chunker.totalChunks - this.currentChunk) * CHUNK_SIZE / speed
    }));
  }
}

小技巧:用 bufferedamountlow 事件来控制发送节奏,比用定时器轮询高效得多。我早期版本用 setInterval 做流控,CPU占用高得离谱。

4. 后端实现:信令服务器

后端我用Node.js + ws库,轻量又好用。信令服务器不需要存储任何文件数据,只负责转发SDP和ICE Candidate。

// server.js - 信令服务器
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

const rooms = new Map();

wss.on('connection', (ws) => {
  let currentRoom = null;
  
  ws.on('message', (data) => {
    const msg = JSON.parse(data);
    
    switch(msg.type) {
      case 'join':
        currentRoom = msg.roomId;
        if (!rooms.has(currentRoom)) {
          rooms.set(currentRoom, []);
        }
        rooms.get(currentRoom).push(ws);
        
        // 通知房间内其他用户
        broadcast(currentRoom, {
          type: 'peer-joined',
          payload: { peerId: ws._peerId }
        }, ws);
        break;
        
      case 'offer':
      case 'answer':
      case 'ice-candidate':
        // 转发给目标peer
        const target = findPeer(currentRoom, msg.targetId);
        if (target) target.send(JSON.stringify(msg));
        break;
    }
  });
  
  ws.on('close', () => {
    if (currentRoom) {
      const room = rooms.get(currentRoom);
      if (room) {
        const idx = room.indexOf(ws);
        if (idx > -1) room.splice(idx, 1);
      }
    }
  });
});

这段代码我简化了房间管理和peer标识的逻辑。实际生产环境,你还需要考虑:

  • 房间最大人数限制(一般2人)
  • 心跳检测,防止僵尸连接
  • 连接超时自动清理

注意:信令服务器虽然不存文件,但它是整个系统的「单点」。如果服务器挂了,所有正在进行的传输都会中断。建议部署时至少做2个实例,用负载均衡。

5. 部署上线:从开发到生产

部署这块,我推荐用Docker + Nginx的组合。前端静态文件用Nginx托管,信令服务器用Node.js跑在Docker容器里。

这是我的Dockerfile:

# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 8080
CMD ["node", "server.js"]

Nginx配置也很简单,主要是做反向代理和静态文件服务:

server {
    listen 80;
    server_name file-share.example.com;
    
    # 前端静态文件
    location / {
        root /var/www/html;
        index index.html;
    }
    
    # WebSocket信令服务
    location /ws {
        proxy_pass http://localhost:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

部署完成后,记得做两件事:

  1. 配置HTTPS:浏览器要求WebRTC必须在安全上下文中运行
  2. 开启TURN服务器:如果用户在内网,需要TURN做中继

6. 避坑指南:我踩过的那些坑

做这个项目,我前前后后踩了不少坑。挑几个典型的说说:

坑1:大文件内存溢出

我第一次实现时,把整个文件读到内存里再分片。传个500MB的文件,浏览器直接崩了。后来改成流式读取,用File.slice()按需读取,问题解决。

坑2:DataChannel消息大小限制

SCTP协议对单个消息有大小限制(通常是256KB)。我一开始没注意,传大分片时消息被静默丢弃。后来统一用16KB分片,再也没出过问题。

坑3:ICE连接超时

有些网络环境下,ICE协商需要较长时间。我默认的超时时间是5秒,结果经常连接失败。后来改成30秒,配合重试机制,成功率从70%提升到了95%以上。

7. 总结

这个文件共享应用,说难不难,说简单也不简单。核心就三点:

  • 信令服务器:负责连接协商,用WebSocket实现
  • 文件分片:16KB一片,流式读取,避免内存爆炸
  • DataChannel:有序可靠模式,配合bufferedamountlow做流控

做完这个项目,你会发现WebRTC不只是用来做视频通话的。文件共享、屏幕共享、甚至实时游戏,底层都是这套东西。

嗯,代码都在上面了。你可以直接拿去用,也可以根据自己的需求改。如果有问题,欢迎来公众号找我聊。

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