综合实战:构建一个WebRTC文件共享应用
说实话,做了这么多年WebRTC开发,我一直觉得文件共享才是最能体现这门技术魅力的场景。视频通话大家见得多了,但点对点传文件——尤其是大文件——那种「嗖一下传完,不经过任何服务器」的感觉,真的很爽。
这一章,咱们就把之前学过的所有知识点串起来。从需求分析到架构设计,从前端实现到后端部署,完整走一遍。我保证,做完这个项目,你对WebRTC的理解会上一个台阶。
1. 需求分析:到底要做什么?
先别急着写代码。我见过太多人上来就撸代码,结果做到一半发现需求没想清楚。咱们先花10分钟把需求理清楚。
这个文件共享应用,核心功能就三个:
- 建立连接:两个浏览器之间建立WebRTC连接
- 传输文件:支持任意类型、任意大小的文件
- 进度反馈:实时显示传输进度、速度、剩余时间
嗯,听起来简单。但实际做起来,坑不少。
核心需求清单
- 支持点对点文件传输,不经过服务器存储
- 支持断点续传(这个我后面会讲为什么重要)
- 实时显示传输进度条和速度
- 支持多个文件同时传输
- 传输完成后自动校验文件完整性
你可能会问:「为什么需要断点续传?」
我跟你讲个真实案例。有一次我在咖啡馆给同事传一个2GB的设计稿文件,传了80%的时候网络断了。没有断点续传,只能从头再来。那种绝望,你懂的。
2. 架构设计:画张图就清楚了
做架构设计,我习惯先画一张整体流程图。这样前后端怎么分工、数据怎么流动,一目了然。
这张图其实已经把核心逻辑说清楚了。发送端和接收端之间,通过信令服务器交换连接信息,然后直接建立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";
}
}
部署完成后,记得做两件事:
- 配置HTTPS:浏览器要求WebRTC必须在安全上下文中运行
- 开启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