使用 PeerJS 构建点对点文件传输 Web 应用

我希望有一种传送文件且不需要占用服务器磁盘的方法,于是灵犀传出现了,一款基于 PeerJSQRCode.js 和现代 Web 技术构建的简洁且用户友好的点对点(P2P)文件传输 Web 应用。该应用允许用户在设备间直接传输文件,无需依赖中央服务器,利用 WebRTC 实现快速且安全的传输。

点击后方链接试用:https://me.bbb-lsy07.sbs/dianduidian-app/

项目概述

灵犀传 旨在以简约的界面和强大的功能简化文件分享。它支持两种模式:

  • 分享模式:用户选择文件,生成可分享的链接或二维码,然后等待接收者连接。

  • 接收模式:用户粘贴分享链接,连接到分享者并下载文件。

该应用具有以下主要功能:

  • 通过拖放或点击选择文件进行分享。

  • 生成分享链接或二维码,方便跨设备连接。

  • 显示实时传输进度、速度和连接状态。

  • 支持多接收者同时连接(分享者端)。

  • 提供错误处理和网络重试机制,确保可靠的连接。

技术栈

灵犀传 使用了以下技术:

  • HTML5CSS3:构建响应式和现代化的用户界面。

  • JavaScript:处理核心逻辑和动态交互。

  • PeerJS:简化 WebRTC 的实现,用于建立 P2P 连接。

  • QRCode.js:生成二维码,便于移动设备扫描链接。

  • WebRTC:通过 ICE 服务器(STUN)实现点对点数据传输。

  • CSS 变量渐变背景:打造美观的暗色主题界面。

  • FileReader API:分片读取文件以优化大文件传输。

设计

1. 用户体验优先

灵犀传 的界面设计以简洁和直观为核心:

  • 拖放支持:用户可以拖放文件或点击选择,简化操作。

  • 实时反馈:通过进度条、速度显示和状态通知,让用户随时了解传输情况。

  • 通知系统:使用动画通知提示成功或错误信息。

  • 二维码支持:方便移动设备用户通过扫描快速加入。

2. WebRTC 和 PeerJS

WebRTC 提供了浏览器原生的 P2P 数据传输能力,但其配置较为复杂。PeerJS 通过封装 WebRTC 提供了更简单的 API,用于:

  • 建立信令服务器连接以交换 ICE 候选者。

  • 管理点对点数据通道,传输文件分片。

  • 处理连接错误和断开事件。

为了提高连接成功率,我配置了多个 PeerJS 信令服务器(包括 NTT、PeerJS 官方服务器等)并实现了自动重试机制。如果一个服务器连接失败,应用会切换到下一个服务器,最多重试 3 次。

3. 文件传输优化

为了支持大文件传输,我将文件分片处理:

  • 分片大小:每个分片 256KB,平衡了传输效率和内存使用。

  • 进度更新:每 500 毫秒更新一次进度和速度,避免过于频繁的 DOM 操作。

  • 缓冲区管理:监控 PeerJS 连接的缓冲区大小,避免数据堆积导致性能下降。

4. 错误处理

网络环境复杂,WebRTC 连接可能因防火墙、NAT 穿越失败等原因中断。我实现了以下错误处理机制:

  • 超时检测:接收者在连接分享者时设置 15 秒超时。

  • 重试逻辑:连接失败时自动重试,最多尝试 3 次。

  • 用户提示:通过通知系统显示具体的错误信息(如“对方设备已离线”或“网络错误”)。

实现细节

以下是灵犀传 核心功能的代码解析和实现思路。

1. 初始化 PeerJS

应用启动时初始化 PeerJS 实例,连接到信令服务器:

function initPeer() {
  if (peer) peer.destroy();
  const server = PEERJS_SERVERS[currentServerIndex];
  updateStatus(`正在连接信令服务器... (${currentServerIndex + 1}/${PEERJS_SERVERS.length})`);
  try {
    peer = new Peer({ ...server, debug: 2, config: ICE_SERVERS });
    peer.on('open', id => {
      peerIdReady = true;
      retryCount = 0;
      updateStatus('网络已就绪', false);
    });
    peer.on('connection', handleNewConnection);
    peer.on('error', handlePeerError);
  } catch (error) {
    handlePeerError(error);
  }
}
  • 使用多个信令服务器(PEERJS_SERVERS)提高可靠性。

  • 配置 STUN 服务器(Google 提供的免费 STUN)以辅助 NAT 穿越。

  • 监听 openconnectionerror 事件,分别处理连接成功、新连接建立和错误情况。

2. 文件分享流程

分享者选择文件后,生成包含 Peer ID 的分享链接:

function generateShareLink() {
  if (!peerIdReady) return showNotification('error', '网络未就绪,请稍等');
  const shareLink = `${window.location.origin}${window.location.pathname}?peerId=${peer.id}`;
  getEl('shareLink').innerHTML = `<a href="${shareLink}" target="_blank">${shareLink}</a>`;
  getEl('shareStep2').classList.remove('hidden');
  getEl('shareStep1').style.opacity = "0.5";
  updateStatus('已生成链接,等待接收者连接...', true);
}

当接收者连接时,分享者发送文件信息和分片:

function sendFile(conn) {
  if (!file) return conn.send({ type: 'error', payload: '分享者未选择文件' });
  const CHUNK_SIZE = 256 * 1024;
  const reader = new FileReader();
  let offset = 0, lastUpdate = Date.now(), lastLoaded = 0;
  conn.send({ type: 'file-info', payload: { name: file.name, size: file.size } });
  reader.onload = e => {
    conn.send({ type: 'file-chunk', payload: e.target.result });
    offset += e.target.result.byteLength;
    if (offset < file.size) {
      if (conn.bufferedAmount > CHUNK_SIZE * 2) setTimeout(sendNextChunk, 100);
      else sendNextChunk();
    } else {
      conn.send({ type: 'file-complete' });
    }
  };
  const sendNextChunk = () => reader.readAsArrayBuffer(file.slice(offset, offset + CHUNK_SIZE));
  sendNextChunk();
}
  • 使用 FileReader 按分片读取文件。

  • 监控 bufferedAmount 避免数据堆积。

  • 发送 file-infofile-chunkfile-complete 三种消息类型,分别传递文件元数据、分片数据和完成信号。

3. 文件接收流程

接收者粘贴链接后,解析 Peer ID 并建立连接:

function connectToPeer() {
  const url = getEl('shareUrlInput').value;
  const match = url.match(/peerId=([a-zA-Z0-9-]+)/);
  if (!match) return showNotification('error', '链接格式无效');
  updateStatus('正在连接分享者...', true);
  activeConn = peer.connect(match[1], { reliable: true });
  activeConn.on('open', () => {
    getEl('receiveStep2').classList.remove('hidden');
    getEl('receiveStep1').classList.add('hidden');
    activeConn.send({ type: 'request-file' });
  });
}

接收文件分片并保存:

function handleFileData(data) {
  if (data.type === 'file-info') {
    totalBytes = data.payload.size;
    getEl('fileName').textContent = data.payload.name;
    getEl('fileSize').textContent = (totalBytes / 1024 / 1024).toFixed(2) + ' MB';
    getEl('downloadButton').disabled = false;
  } else if (data.type === 'file-chunk') {
    receivedBytes += data.payload.byteLength;
    receivedChunks.push(data.payload);
    const progress = (receivedBytes / totalBytes) * 100;
    getEl('downloadProgress').style.width = `${progress}%`;
  } else if (data.type === 'file-complete') {
    const blob = new Blob(receivedChunks);
    const a = document.createElement('a');
    a.href = URL.createObjectURL(blob);
    a.download = getEl('fileName').textContent;
    a.click();
  }
}
  • 收集分片到 receivedChunks 数组。

  • 传输完成后,使用 Blob 合并分片并触发下载。

4. 用户界面

界面使用 CSS 变量和现代样式打造:

  • 进度条:使用 CSS 过渡动画平滑显示进度。

  • 通知动画:通过 opacitytransform 实现淡入淡出效果。

.progress-bar {
  height: 8px; background: var(--background-light); border-radius: 4px; overflow: hidden; margin: 20px 0;
}
.progress-fill {
  height: 100%; background: var(--primary-color); transition: width 0.3s ease;
}
.notification {
  padding: 12px 20px; border-radius: 8px; opacity: 0; transform: translateY(-20px); transition: all 0.3s;
}
.notification.show {
  opacity: 1; transform: translateY(0);
}

遇到的问题

  1. 网络限制:某些网络(如企业防火墙)可能阻止 WebRTC 连接。

    • 解决方案:提供多个信令服务器和 STUN 服务器,增加连接成功率。

  2. 大文件传输:大文件可能导致浏览器内存溢出。

    • 解决方案:分片传输并及时清理缓冲区。

  3. 移动端兼容性:部分移动浏览器对 WebRTC 支持不一致。

    • 解决方案:通过二维码简化移动端连接,并测试主流浏览器(如 Chrome、Safari)。

仍然没有实现的

  • 加密传输:为数据通道添加端到端加密,增强安全性。

  • 多文件支持:允许用户一次分享多个文件。

  • 断点续传:记录已传输的分片,支持中途断开后恢复。

  • 自定义 Peer ID:让用户设置易记的 Peer ID,简化连接。

  • 文件预览:在接收端支持图片或视频的预览功能。

总结

灵犀传 展示了如何利用 PeerJS 和 WebRTC 构建一个高效的 P2P 文件传输应用。通过简洁的界面、实时反馈和可靠的错误处理,它为用户提供了流畅的分享体验。这个项目不仅让我深入理解了 WebRTC 的工作原理,还让我在前端设计和性能优化方面积累了宝贵经验。

如果您想尝试灵犀传,可以直接在浏览器中运行代码,或访问 GitHub 仓库 获取源码。欢迎提出建议或贡献代码!