打造高效 P2P 文件传输与桌面共享工具:基于 WebRTC、Go 和 React

🎉 经过数月的开发,我的个人项目 File-Transfer-Go 终于实现了一个小目标:支持文件传输桌面共享文本同步!无需复杂配置,打开网页即开即用使用,数据传输全程 P2P,安全又高效!

项目地址: GitHub - MatrixSeven/file-transfer-go
体验地址: File Transfer - 文件传输

1. 项目背景与初心

作为一个经常需要向 Windows 服务器传输文件的开发者,我对传统网盘的繁琐流程(登录、下载客户端)感到厌倦。同时,我对 WebRTC 的 P2P 技术充满兴趣,想借此机会深入学习并打造一个即开即用的工具,集文件传输、桌面共享和文本同步于一体。目标是:直观、简洁、高效,符合高频使用场景。

2. 技术架构

项目采用前后端分离架构,所有数据传输基于 WebRTC 实现 P2P 连接,服务器仅用于信令交换,保障隐私和安全:

  • 后端:基于 Go 开发的轻量信令服务器,负责 WebRTC 的 ICE 候选交换和会话协商。
  • 前端:使用 ReactNext.js,提供流畅的交互界面和状态管理。
  • 核心技术:WebRTC 实现 P2P 数据通道,涵盖文件传输、桌面共享和文本同步。
  • 隐私设计:服务器不存储任何设备信息或传输数据,连接通过取件码手动匹配。

3. 核心功能与实现

3.1 p2p打洞

基于webrtc进行网络打洞和穿透
image

3.2 文件传输

通过 WebRTC 的 useSharedWebRTCManageruseFileTransferBusiness 实现点对点文件传输,支持大文件分片和传输。以下是核心代码示例:

  // 安全发送单个文件块
  const sendChunkWithAck = useCallback(async (
    fileId: string,
    chunkIndex: number,
    chunkData: ArrayBuffer,
    checksum: string,
    retryCount = 0
  ): Promise<boolean> => {
    return new Promise((resolve) => {
      const chunkKey = `${fileId}-${chunkIndex}`;
      
      // 设置确认回调
      const ackCallback = (ack: ChunkAck) => {
        if (ack.success) {
          resolve(true);
        } else {
          console.warn(`文件块 ${chunkIndex} 确认失败,准备重试`);
          resolve(false);
        }
      };

      // 注册确认回调
      if (!chunkAckCallbacks.current.has(chunkKey)) {
        chunkAckCallbacks.current.set(chunkKey, new Set());
      }
      chunkAckCallbacks.current.get(chunkKey)!.add(ackCallback);

      // 设置超时定时器
      const timeout = setTimeout(() => {
        console.warn(`文件块 ${chunkIndex} 确认超时`);
        chunkAckCallbacks.current.get(chunkKey)?.delete(ackCallback);
        resolve(false);
      }, ACK_TIMEOUT);

      pendingChunks.current.set(chunkKey, timeout);

      // 发送块信息
      connection.sendMessage({
        type: 'file-chunk-info',
        payload: {
          fileId,
          chunkIndex,
          totalChunks: 0, // 这里不需要,因为已经在元数据中发送
          checksum
        }
      }, CHANNEL_NAME);

      // 发送块数据
      connection.sendData(chunkData);
    });
  }, [connection]);

然后通过webrtc的数据通道进行发送


  // 发送二进制数据
  const sendData = useCallback((data: ArrayBuffer) => {
    const dataChannel = dcRef.current;
    if (!dataChannel || dataChannel.readyState !== 'open') {
      console.error('[SharedWebRTC] 数据通道未准备就绪');
      return false;
    }

    try {
      dataChannel.send(data);
      console.log('[SharedWebRTC] 发送数据:', data.byteLength, 'bytes');
      return true;
    } catch (error) {
      console.error('[SharedWebRTC] 发送数据失败:', error);
      return false;
    }
  }, []);

3.3 桌面共享

通过webrtcAPI拿到 MediaStream 然后进行传输

  // 添加媒体轨道
  const addTrack = useCallback((track: MediaStreamTrack, stream: MediaStream) => {
    const pc = pcRef.current;
    if (!pc) {
      console.error('[SharedWebRTC] PeerConnection 不可用');
      return null;
    }
    
    try {
      return pc.addTrack(track, stream);
    } catch (error) {
      console.error('[SharedWebRTC] 添加轨道失败:', error);
      return null;
    }
  }, []);

接收方


  // 设置视频流
  useEffect(() => {
    if (videoRef.current && stream) {
      console.log('[DesktopViewer] 🎬 设置视频流,轨道数量:', stream.getTracks().length);
      stream.getTracks().forEach(track => {
        console.log('[DesktopViewer] 轨道详情:', track.kind, track.id, track.enabled, track.readyState);
      });
      
      videoRef.current.srcObject = stream;
      console.log('[DesktopViewer] ✅ 视频元素已设置流');
      
      // 重置状态
      hasAttemptedAutoplayRef.current = false;
      setNeedsUserInteraction(false);
      setIsPlaying(false);
      
      // 添加事件监听器来调试视频加载
      const video = videoRef.current;
      const handleLoadStart = () => console.log('[DesktopViewer] 📹 视频开始加载');
      const handleLoadedMetadata = () => {
        console.log('[DesktopViewer] 📹 视频元数据已加载');
        console.log('[DesktopViewer] 📹 视频尺寸:', video.videoWidth, 'x', video.videoHeight);
      };
      const handleCanPlay = () => {
        console.log('[DesktopViewer] 📹 视频可以开始播放');
        // 只在还未尝试过自动播放时才尝试
        if (!hasAttemptedAutoplayRef.current) {
          hasAttemptedAutoplayRef.current = true;
          video.play()
            .then(() => {
              console.log('[DesktopViewer] ✅ 视频自动播放成功');
              setIsPlaying(true);
              setNeedsUserInteraction(false);
            })
            .catch(e => {
              console.log('[DesktopViewer] 📹 自动播放被阻止,需要用户交互:', e.message);
              setIsPlaying(false);
              setNeedsUserInteraction(true);
            });
        }
      };
      const handlePlay = () => {
        console.log('[DesktopViewer] 📹 视频开始播放');
        setIsPlaying(true);
        setNeedsUserInteraction(false);
      };
      const handlePause = () => {
        console.log('[DesktopViewer] 📹 视频暂停');
        setIsPlaying(false);
      };
      const handleError = (e: Event) => console.error('[DesktopViewer] 📹 视频播放错误:', e);
      
      video.addEventListener('loadstart', handleLoadStart);
      video.addEventListener('loadedmetadata', handleLoadedMetadata);
      video.addEventListener('canplay', handleCanPlay);
      video.addEventListener('play', handlePlay);
      video.addEventListener('pause', handlePause);
      video.addEventListener('error', handleError);
      
      return () => {
        video.removeEventListener('loadstart', handleLoadStart);
        video.removeEventListener('loadedmetadata', handleLoadedMetadata);
        video.removeEventListener('canplay', handleCanPlay);
        video.removeEventListener('play', handlePlay);
        video.removeEventListener('pause', handlePause);
        video.removeEventListener('error', handleError);
      };
    } else if (videoRef.current && !stream) {
      console.log('[DesktopViewer] ❌ 清除视频流');
      videoRef.current.srcObject = null;
      setIsPlaying(false);
      setNeedsUserInteraction(false);
      hasAttemptedAutoplayRef.current = false;
    }
  }, [stream]);

image

posted @ 2025-08-27 15:18  菜狗_无知  阅读(1175)  评论(0)    收藏  举报