mirror of
https://github.com/MatrixSeven/file-transfer-go.git
synced 2026-03-12 02:57:38 +08:00
feat:传输文件优化
This commit is contained in:
@@ -141,6 +141,7 @@ export function useWebRTCTransfer() {
|
||||
// 回调注册
|
||||
onFileRequested: fileTransfer.onFileRequested,
|
||||
onFileReceived: fileTransfer.onFileReceived,
|
||||
onFileProgress: fileTransfer.onFileProgress,
|
||||
onFileListReceived,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,12 @@ interface FileTransferState {
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface FileProgressInfo {
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
progress: number;
|
||||
}
|
||||
|
||||
interface FileChunk {
|
||||
fileId: string;
|
||||
chunkIndex: number;
|
||||
@@ -21,7 +27,7 @@ interface FileMetadata {
|
||||
type: string;
|
||||
}
|
||||
|
||||
const CHUNK_SIZE = 16 * 1024; // 16KB chunks
|
||||
const CHUNK_SIZE = 256 * 1024; // 256KB chunks - 平衡传输效率和内存使用
|
||||
|
||||
export function useFileTransfer() {
|
||||
const [state, setState] = useState<FileTransferState>({
|
||||
@@ -39,9 +45,17 @@ export function useFileTransfer() {
|
||||
totalChunks: number;
|
||||
}>>(new Map());
|
||||
|
||||
// 存储当前正在接收的块信息
|
||||
const expectedChunk = useRef<{
|
||||
fileId: string;
|
||||
chunkIndex: number;
|
||||
totalChunks: number;
|
||||
} | null>(null);
|
||||
|
||||
// 文件请求回调
|
||||
const fileRequestCallbacks = useRef<Array<(fileId: string, fileName: string) => void>>([]);
|
||||
const fileReceivedCallbacks = useRef<Array<(fileData: { id: string; file: File }) => void>>([]);
|
||||
const fileProgressCallbacks = useRef<Array<(progressInfo: FileProgressInfo) => void>>([]);
|
||||
|
||||
const updateState = useCallback((updates: Partial<FileTransferState>) => {
|
||||
setState(prev => ({ ...prev, ...updates }));
|
||||
@@ -107,27 +121,54 @@ export function useFileTransfer() {
|
||||
if (event.target?.result && dataChannel.readyState === 'open') {
|
||||
const arrayBuffer = event.target.result as ArrayBuffer;
|
||||
|
||||
// 发送分块数据
|
||||
const chunkMessage = JSON.stringify({
|
||||
type: 'file-chunk',
|
||||
payload: {
|
||||
fileId,
|
||||
chunkIndex: sentChunks,
|
||||
totalChunks
|
||||
}
|
||||
});
|
||||
|
||||
dataChannel.send(chunkMessage);
|
||||
dataChannel.send(arrayBuffer);
|
||||
// 检查缓冲区状态,避免过度缓冲
|
||||
if (dataChannel.bufferedAmount > 1024 * 1024) { // 1MB 缓冲区限制
|
||||
console.log('数据通道缓冲区满,等待清空...');
|
||||
// 等待缓冲区清空后再发送
|
||||
const waitForBuffer = () => {
|
||||
if (dataChannel.bufferedAmount < 256 * 1024) { // 等到缓冲区低于 256KB
|
||||
sendChunkData();
|
||||
} else {
|
||||
setTimeout(waitForBuffer, 10);
|
||||
}
|
||||
};
|
||||
waitForBuffer();
|
||||
} else {
|
||||
sendChunkData();
|
||||
}
|
||||
|
||||
sentChunks++;
|
||||
const progress = (sentChunks / totalChunks) * 100;
|
||||
updateState({ transferProgress: progress });
|
||||
function sendChunkData() {
|
||||
// 发送分块数据
|
||||
const chunkMessage = JSON.stringify({
|
||||
type: 'file-chunk',
|
||||
payload: {
|
||||
fileId,
|
||||
chunkIndex: sentChunks,
|
||||
totalChunks
|
||||
}
|
||||
});
|
||||
|
||||
dataChannel.send(chunkMessage);
|
||||
dataChannel.send(arrayBuffer);
|
||||
|
||||
console.log(`发送进度: ${progress.toFixed(1)}%, 块: ${sentChunks}/${totalChunks}`);
|
||||
sentChunks++;
|
||||
const progress = (sentChunks / totalChunks) * 100;
|
||||
updateState({ transferProgress: progress });
|
||||
|
||||
// 短暂延迟,避免阻塞
|
||||
setTimeout(sendNextChunk, 10);
|
||||
// 通知文件级别的进度
|
||||
fileProgressCallbacks.current.forEach(callback => {
|
||||
callback({
|
||||
fileId,
|
||||
fileName: file.name,
|
||||
progress
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`发送进度: ${progress.toFixed(1)}%, 块: ${sentChunks}/${totalChunks}, 文件: ${file.name}, 缓冲区: ${dataChannel.bufferedAmount} bytes`);
|
||||
|
||||
// 立即发送下一个块,不等待
|
||||
sendNextChunk();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -179,7 +220,15 @@ export function useFileTransfer() {
|
||||
|
||||
case 'file-chunk':
|
||||
const chunkInfo = message.payload;
|
||||
console.log(`接收文件块: ${chunkInfo.chunkIndex + 1}/${chunkInfo.totalChunks}`);
|
||||
const { fileId: chunkFileId, chunkIndex, totalChunks } = chunkInfo;
|
||||
console.log(`接收文件块信息: ${chunkIndex + 1}/${totalChunks}, 文件ID: ${chunkFileId}`);
|
||||
|
||||
// 设置期望的下一个块
|
||||
expectedChunk.current = {
|
||||
fileId: chunkFileId,
|
||||
chunkIndex,
|
||||
totalChunks
|
||||
};
|
||||
break;
|
||||
|
||||
case 'file-end':
|
||||
@@ -229,18 +278,37 @@ export function useFileTransfer() {
|
||||
const arrayBuffer = event.data;
|
||||
console.log('收到文件块数据:', arrayBuffer.byteLength, 'bytes');
|
||||
|
||||
// 找到最近开始接收的文件(简化逻辑)
|
||||
for (const [fileId, fileInfo] of receivingFiles.current.entries()) {
|
||||
if (fileInfo.receivedChunks < fileInfo.totalChunks) {
|
||||
fileInfo.chunks.push(arrayBuffer);
|
||||
fileInfo.receivedChunks++;
|
||||
// 检查是否有期望的块信息
|
||||
if (expectedChunk.current) {
|
||||
const { fileId, chunkIndex } = expectedChunk.current;
|
||||
const fileInfo = receivingFiles.current.get(fileId);
|
||||
|
||||
if (fileInfo) {
|
||||
// 确保chunks数组足够大
|
||||
if (!fileInfo.chunks[chunkIndex]) {
|
||||
fileInfo.chunks[chunkIndex] = arrayBuffer;
|
||||
fileInfo.receivedChunks++;
|
||||
|
||||
const progress = (fileInfo.receivedChunks / fileInfo.totalChunks) * 100;
|
||||
updateState({ transferProgress: progress });
|
||||
const progress = (fileInfo.receivedChunks / fileInfo.totalChunks) * 100;
|
||||
updateState({ transferProgress: progress });
|
||||
|
||||
console.log(`文件接收进度: ${progress.toFixed(1)}%, 块: ${fileInfo.receivedChunks}/${fileInfo.totalChunks}`);
|
||||
break;
|
||||
// 通知文件级别的进度
|
||||
fileProgressCallbacks.current.forEach(callback => {
|
||||
callback({
|
||||
fileId,
|
||||
fileName: fileInfo.metadata.name,
|
||||
progress
|
||||
});
|
||||
});
|
||||
|
||||
console.log(`文件接收进度: ${progress.toFixed(1)}%, 块: ${fileInfo.receivedChunks}/${fileInfo.totalChunks}, 文件: ${fileInfo.metadata.name}`);
|
||||
}
|
||||
|
||||
// 清除期望的块信息
|
||||
expectedChunk.current = null;
|
||||
}
|
||||
} else {
|
||||
console.warn('收到块数据但没有对应的块信息');
|
||||
}
|
||||
}
|
||||
}, [updateState]);
|
||||
@@ -288,6 +356,19 @@ export function useFileTransfer() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 注册文件进度回调
|
||||
const onFileProgress = useCallback((callback: (progressInfo: FileProgressInfo) => void) => {
|
||||
fileProgressCallbacks.current.push(callback);
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
const index = fileProgressCallbacks.current.indexOf(callback);
|
||||
if (index > -1) {
|
||||
fileProgressCallbacks.current.splice(index, 1);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
sendFile,
|
||||
@@ -295,5 +376,6 @@ export function useFileTransfer() {
|
||||
handleMessage,
|
||||
onFileRequested,
|
||||
onFileReceived,
|
||||
onFileProgress,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { url } from 'inspector';
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
|
||||
interface WebRTCConnectionState {
|
||||
@@ -22,6 +23,7 @@ export function useWebRTCConnection() {
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const pcRef = useRef<RTCPeerConnection | null>(null);
|
||||
const dcRef = useRef<RTCDataChannel | null>(null);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const STUN_SERVERS = [
|
||||
{ urls: 'stun:stun.l.google.com:19302' },
|
||||
@@ -29,29 +31,78 @@ export function useWebRTCConnection() {
|
||||
{ urls: 'stun:stun2.l.google.com:19302' },
|
||||
];
|
||||
|
||||
// 连接超时时间(30秒)
|
||||
const CONNECTION_TIMEOUT = 30000;
|
||||
|
||||
const updateState = useCallback((updates: Partial<WebRTCConnectionState>) => {
|
||||
setState(prev => ({ ...prev, ...updates }));
|
||||
}, []);
|
||||
|
||||
// 清理超时定时器
|
||||
const clearConnectionTimeout = useCallback(() => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 处理连接超时
|
||||
const handleConnectionTimeout = useCallback(() => {
|
||||
console.warn('WebRTC连接超时');
|
||||
updateState({
|
||||
error: '连接超时,请检查取件码是否正确或稍后重试',
|
||||
isConnecting: false
|
||||
});
|
||||
|
||||
// 清理连接
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
}
|
||||
if (pcRef.current) {
|
||||
pcRef.current.close();
|
||||
}
|
||||
}, [updateState]);
|
||||
|
||||
const connect = useCallback(async (roomCode: string, role: 'sender' | 'receiver') => {
|
||||
console.log('=== 开始WebRTC连接 ===');
|
||||
console.log('房间代码:', roomCode, '角色:', role);
|
||||
|
||||
// 清理之前的超时定时器
|
||||
clearConnectionTimeout();
|
||||
|
||||
updateState({ isConnecting: true, error: null });
|
||||
|
||||
// 设置连接超时
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
handleConnectionTimeout();
|
||||
}, CONNECTION_TIMEOUT);
|
||||
|
||||
try {
|
||||
// 创建PeerConnection
|
||||
const pc = new RTCPeerConnection({ iceServers: STUN_SERVERS });
|
||||
pcRef.current = pc;
|
||||
|
||||
// 连接WebSocket信令服务器
|
||||
const ws = new WebSocket(`ws://localhost:8080/ws/webrtc?room=${roomCode}&role=${role}`);
|
||||
const ws = new WebSocket(`ws://localhost:8080/ws/webrtc?code=${roomCode}&role=${role}`);
|
||||
wsRef.current = ws;
|
||||
|
||||
// WebSocket事件处理
|
||||
ws.onopen = () => {
|
||||
ws.onopen = async () => {
|
||||
console.log('WebSocket连接已建立');
|
||||
updateState({ isWebSocketConnected: true });
|
||||
|
||||
// 如果是发送方,在WebSocket连接建立后创建offer
|
||||
if (role === 'sender') {
|
||||
try {
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
ws.send(JSON.stringify({ type: 'offer', payload: offer }));
|
||||
console.log('已发送offer');
|
||||
} catch (error) {
|
||||
console.error('创建offer失败:', error);
|
||||
updateState({ error: '创建连接失败', isConnecting: false });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ws.onmessage = async (event) => {
|
||||
@@ -61,19 +112,26 @@ export function useWebRTCConnection() {
|
||||
|
||||
switch (message.type) {
|
||||
case 'offer':
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(message.offer));
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
ws.send(JSON.stringify({ type: 'answer', answer }));
|
||||
if (message.payload) {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(message.payload));
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'answer', payload: answer }));
|
||||
console.log('已发送answer');
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'answer':
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(message.answer));
|
||||
if (message.payload) {
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(message.payload));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ice-candidate':
|
||||
if (message.candidate) {
|
||||
await pc.addIceCandidate(new RTCIceCandidate(message.candidate));
|
||||
if (message.payload) {
|
||||
await pc.addIceCandidate(new RTCIceCandidate(message.payload));
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -104,7 +162,7 @@ export function useWebRTCConnection() {
|
||||
console.log('发送ICE候选:', event.candidate);
|
||||
ws.send(JSON.stringify({
|
||||
type: 'ice-candidate',
|
||||
candidate: event.candidate
|
||||
payload: event.candidate
|
||||
}));
|
||||
}
|
||||
};
|
||||
@@ -113,12 +171,19 @@ export function useWebRTCConnection() {
|
||||
pc.onconnectionstatechange = () => {
|
||||
console.log('连接状态:', pc.connectionState);
|
||||
const isConnected = pc.connectionState === 'connected';
|
||||
|
||||
if (isConnected) {
|
||||
// 连接成功,清理超时定时器
|
||||
clearConnectionTimeout();
|
||||
}
|
||||
|
||||
updateState({
|
||||
isConnected,
|
||||
isConnecting: !isConnected && pc.connectionState !== 'failed'
|
||||
});
|
||||
|
||||
if (pc.connectionState === 'failed') {
|
||||
clearConnectionTimeout();
|
||||
updateState({ error: '连接失败', isConnecting: false });
|
||||
}
|
||||
};
|
||||
@@ -126,19 +191,27 @@ export function useWebRTCConnection() {
|
||||
// 如果是发送方,创建数据通道
|
||||
if (role === 'sender') {
|
||||
const dataChannel = pc.createDataChannel('fileTransfer', {
|
||||
ordered: true
|
||||
ordered: true,
|
||||
maxPacketLifeTime: undefined,
|
||||
maxRetransmits: undefined
|
||||
});
|
||||
dcRef.current = dataChannel;
|
||||
|
||||
// 设置缓冲区管理
|
||||
dataChannel.bufferedAmountLowThreshold = 256 * 1024; // 256KB
|
||||
|
||||
dataChannel.onopen = () => {
|
||||
console.log('数据通道已打开 (发送方)');
|
||||
console.log('数据通道配置:', {
|
||||
id: dataChannel.id,
|
||||
label: dataChannel.label,
|
||||
maxPacketLifeTime: dataChannel.maxPacketLifeTime,
|
||||
maxRetransmits: dataChannel.maxRetransmits,
|
||||
ordered: dataChannel.ordered,
|
||||
bufferedAmountLowThreshold: dataChannel.bufferedAmountLowThreshold
|
||||
});
|
||||
updateState({ localDataChannel: dataChannel });
|
||||
};
|
||||
|
||||
// 创建offer
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
ws.send(JSON.stringify({ type: 'offer', offer }));
|
||||
} else {
|
||||
// 接收方等待数据通道
|
||||
pc.ondatachannel = (event) => {
|
||||
@@ -155,16 +228,20 @@ export function useWebRTCConnection() {
|
||||
|
||||
} catch (error) {
|
||||
console.error('连接失败:', error);
|
||||
clearConnectionTimeout();
|
||||
updateState({
|
||||
error: error instanceof Error ? error.message : '连接失败',
|
||||
isConnecting: false
|
||||
});
|
||||
}
|
||||
}, [updateState]);
|
||||
}, [updateState, clearConnectionTimeout, handleConnectionTimeout]);
|
||||
|
||||
const disconnect = useCallback(() => {
|
||||
console.log('断开WebRTC连接');
|
||||
|
||||
// 清理超时定时器
|
||||
clearConnectionTimeout();
|
||||
|
||||
if (dcRef.current) {
|
||||
dcRef.current.close();
|
||||
dcRef.current = null;
|
||||
@@ -188,7 +265,7 @@ export function useWebRTCConnection() {
|
||||
localDataChannel: null,
|
||||
remoteDataChannel: null,
|
||||
});
|
||||
}, []);
|
||||
}, [clearConnectionTimeout]);
|
||||
|
||||
const getDataChannel = useCallback(() => {
|
||||
return dcRef.current;
|
||||
|
||||
Reference in New Issue
Block a user