打造高效 P2P 文件傳輸與桌面共享工具:基于 WebRTC、Go 和 React
?? 經(jīng)過(guò)數(shù)月的開(kāi)發(fā),我的個(gè)人項(xiàng)目 File-Transfer-Go 終于實(shí)現(xiàn)了一個(gè)小目標(biāo):支持文件傳輸、桌面共享和文本同步!無(wú)需復(fù)雜配置,打開(kāi)網(wǎng)頁(yè)即開(kāi)即用使用,數(shù)據(jù)傳輸全程 P2P,安全又高效!
項(xiàng)目地址: GitHub - MatrixSeven/file-transfer-go
體驗(yàn)地址: File Transfer - 文件傳輸
1. 項(xiàng)目背景與初心
作為一個(gè)經(jīng)常需要向 Windows 服務(wù)器傳輸文件的開(kāi)發(fā)者,我對(duì)傳統(tǒng)網(wǎng)盤(pán)的繁瑣流程(登錄、下載客戶(hù)端)感到厭倦。同時(shí),我對(duì) WebRTC 的 P2P 技術(shù)充滿(mǎn)興趣,想借此機(jī)會(huì)深入學(xué)習(xí)并打造一個(gè)即開(kāi)即用的工具,集文件傳輸、桌面共享和文本同步于一體。目標(biāo)是:直觀、簡(jiǎn)潔、高效,符合高頻使用場(chǎng)景。
2. 技術(shù)架構(gòu)
項(xiàng)目采用前后端分離架構(gòu),所有數(shù)據(jù)傳輸基于 WebRTC 實(shí)現(xiàn) P2P 連接,服務(wù)器僅用于信令交換,保障隱私和安全:
- 后端:基于 Go 開(kāi)發(fā)的輕量信令服務(wù)器,負(fù)責(zé) WebRTC 的 ICE 候選交換和會(huì)話(huà)協(xié)商。
- 前端:使用 React 和 Next.js,提供流暢的交互界面和狀態(tài)管理。
- 核心技術(shù):WebRTC 實(shí)現(xiàn) P2P 數(shù)據(jù)通道,涵蓋文件傳輸、桌面共享和文本同步。
- 隱私設(shè)計(jì):服務(wù)器不存儲(chǔ)任何設(shè)備信息或傳輸數(shù)據(jù),連接通過(guò)取件碼手動(dòng)匹配。
3. 核心功能與實(shí)現(xiàn)
3.1 p2p打洞
基于webrtc進(jìn)行網(wǎng)絡(luò)打洞和穿透

3.2 文件傳輸
通過(guò) WebRTC 的 useSharedWebRTCManager和 useFileTransferBusiness 實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn)文件傳輸,支持大文件分片和傳輸。以下是核心代碼示例:
// 安全發(fā)送單個(gè)文件塊
const sendChunkWithAck = useCallback(async (
fileId: string,
chunkIndex: number,
chunkData: ArrayBuffer,
checksum: string,
retryCount = 0
): Promise<boolean> => {
return new Promise((resolve) => {
const chunkKey = `${fileId}-${chunkIndex}`;
// 設(shè)置確認(rèn)回調(diào)
const ackCallback = (ack: ChunkAck) => {
if (ack.success) {
resolve(true);
} else {
console.warn(`文件塊 ${chunkIndex} 確認(rèn)失敗,準(zhǔn)備重試`);
resolve(false);
}
};
// 注冊(cè)確認(rèn)回調(diào)
if (!chunkAckCallbacks.current.has(chunkKey)) {
chunkAckCallbacks.current.set(chunkKey, new Set());
}
chunkAckCallbacks.current.get(chunkKey)!.add(ackCallback);
// 設(shè)置超時(shí)定時(shí)器
const timeout = setTimeout(() => {
console.warn(`文件塊 ${chunkIndex} 確認(rèn)超時(shí)`);
chunkAckCallbacks.current.get(chunkKey)?.delete(ackCallback);
resolve(false);
}, ACK_TIMEOUT);
pendingChunks.current.set(chunkKey, timeout);
// 發(fā)送塊信息
connection.sendMessage({
type: 'file-chunk-info',
payload: {
fileId,
chunkIndex,
totalChunks: 0, // 這里不需要,因?yàn)橐呀?jīng)在元數(shù)據(jù)中發(fā)送
checksum
}
}, CHANNEL_NAME);
// 發(fā)送塊數(shù)據(jù)
connection.sendData(chunkData);
});
}, [connection]);
然后通過(guò)webrtc的數(shù)據(jù)通道進(jìn)行發(fā)送
// 發(fā)送二進(jìn)制數(shù)據(jù)
const sendData = useCallback((data: ArrayBuffer) => {
const dataChannel = dcRef.current;
if (!dataChannel || dataChannel.readyState !== 'open') {
console.error('[SharedWebRTC] 數(shù)據(jù)通道未準(zhǔn)備就緒');
return false;
}
try {
dataChannel.send(data);
console.log('[SharedWebRTC] 發(fā)送數(shù)據(jù):', data.byteLength, 'bytes');
return true;
} catch (error) {
console.error('[SharedWebRTC] 發(fā)送數(shù)據(jù)失敗:', error);
return false;
}
}, []);
3.3 桌面共享
通過(guò)webrtcAPI拿到 MediaStream 然后進(jìn)行傳輸
// 添加媒體軌道
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;
}
}, []);
接收方
// 設(shè)置視頻流
useEffect(() => {
if (videoRef.current && stream) {
console.log('[DesktopViewer] ?? 設(shè)置視頻流,軌道數(shù)量:', 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] ? 視頻元素已設(shè)置流');
// 重置狀態(tài)
hasAttemptedAutoplayRef.current = false;
setNeedsUserInteraction(false);
setIsPlaying(false);
// 添加事件監(jiān)聽(tīng)器來(lái)調(diào)試視頻加載
const video = videoRef.current;
const handleLoadStart = () => console.log('[DesktopViewer] ?? 視頻開(kāi)始加載');
const handleLoadedMetadata = () => {
console.log('[DesktopViewer] ?? 視頻元數(shù)據(jù)已加載');
console.log('[DesktopViewer] ?? 視頻尺寸:', video.videoWidth, 'x', video.videoHeight);
};
const handleCanPlay = () => {
console.log('[DesktopViewer] ?? 視頻可以開(kāi)始播放');
// 只在還未嘗試過(guò)自動(dòng)播放時(shí)才嘗試
if (!hasAttemptedAutoplayRef.current) {
hasAttemptedAutoplayRef.current = true;
video.play()
.then(() => {
console.log('[DesktopViewer] ? 視頻自動(dòng)播放成功');
setIsPlaying(true);
setNeedsUserInteraction(false);
})
.catch(e => {
console.log('[DesktopViewer] ?? 自動(dòng)播放被阻止,需要用戶(hù)交互:', e.message);
setIsPlaying(false);
setNeedsUserInteraction(true);
});
}
};
const handlePlay = () => {
console.log('[DesktopViewer] ?? 視頻開(kāi)始播放');
setIsPlaying(true);
setNeedsUserInteraction(false);
};
const handlePause = () => {
console.log('[DesktopViewer] ?? 視頻暫停');
setIsPlaying(false);
};
const handleError = (e: Event) => console.error('[DesktopViewer] ?? 視頻播放錯(cuò)誤:', 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]);


浙公網(wǎng)安備 33010602011771號(hào)