支持多人加入/下载

This commit is contained in:
seven
2025-07-28 18:12:05 +08:00
parent 8031a29037
commit 22cbaae0ab
12 changed files with 2485 additions and 370 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,256 +0,0 @@
// P2P文件传输系统
// 全局变量
let websocket = null;
let peerConnection = null;
let dataChannel = null;
let selectedFiles = [];
let currentPickupCode = '';
let currentRole = ''; // 'sender' or 'receiver'
let fileTransfers = new Map(); // 存储文件传输状态
let isP2PConnected = false; // P2P连接状态
let isConnecting = false; // 是否正在连接中
let connectionTimeout = null; // 连接超时定时器
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
initializeEventListeners();
});
// 初始化事件监听器
function initializeEventListeners() {
// 文件选择事件
document.getElementById('fileInput').addEventListener('change', handleFileSelect);
// 取件码输入事件
document.getElementById('pickupCodeInput').addEventListener('input', (e) => {
e.target.value = e.target.value.toUpperCase();
if (e.target.value.length === 6) {
// 自动连接
setTimeout(() => joinRoom(), 100);
}
});
// 拖拽上传
setupDragAndDrop();
}
// 设置拖拽上传
function setupDragAndDrop() {
const dropArea = document.querySelector('.border-dashed');
dropArea.addEventListener('dragover', (e) => {
e.preventDefault();
dropArea.classList.add('border-blue-400');
});
dropArea.addEventListener('dragleave', () => {
dropArea.classList.remove('border-blue-400');
});
dropArea.addEventListener('drop', (e) => {
e.preventDefault();
dropArea.classList.remove('border-blue-400');
const files = Array.from(e.dataTransfer.files);
if (files.length > 0) {
selectedFiles = files;
displaySelectedFiles();
}
});
}
// 处理文件选择
function handleFileSelect(event) {
const files = Array.from(event.target.files);
if (files.length > 0) {
selectedFiles = files;
displaySelectedFiles();
}
}
// 显示选中的文件
function displaySelectedFiles() {
const container = document.getElementById('selectedFiles');
const filesList = document.getElementById('filesList');
if (selectedFiles.length === 0) {
container.classList.add('hidden');
return;
}
container.classList.remove('hidden');
filesList.innerHTML = '';
selectedFiles.forEach((file, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'flex items-center justify-between bg-gray-50 p-3 rounded-lg';
fileItem.innerHTML = `
<div class="flex items-center">
<span class="text-2xl mr-3">${getFileIcon(file.type)}</span>
<div>
<div class="font-medium">${file.name}</div>
<div class="text-sm text-gray-500">${formatFileSize(file.size)}</div>
</div>
</div>
<button onclick="removeFile(${index})" class="text-red-500 hover:text-red-700 p-1">
</button>
`;
filesList.appendChild(fileItem);
});
}
// 移除文件
function removeFile(index) {
selectedFiles.splice(index, 1);
displaySelectedFiles();
}
// 生成取件码
async function generatePickupCode() {
if (selectedFiles.length === 0) return;
// 准备文件信息
const fileInfos = selectedFiles.map((file, index) => ({
id: 'file_' + index,
name: file.name,
size: file.size,
type: file.type,
lastModified: file.lastModified
}));
try {
const response = await fetch('/api/create-room', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ files: fileInfos })
});
const data = await response.json();
if (data.success) {
currentPickupCode = data.code;
currentRole = 'sender';
showPickupCode(data.code);
connectWebSocket();
} else {
alert('生成取件码失败: ' + data.message);
}
} catch (error) {
console.error('生成取件码失败:', error);
alert('生成取件码失败,请重试');
}
}
// 显示取件码
function showPickupCode(code) {
document.getElementById('pickupCodeDisplay').textContent = code;
document.getElementById('pickupCodeSection').classList.remove('hidden');
document.getElementById('generateCodeBtn').classList.add('hidden');
}
// 复制取件码
function copyPickupCode() {
navigator.clipboard.writeText(currentPickupCode).then(() => {
alert('取件码已复制到剪贴板');
});
}
// 重置发送方
function resetSender() {
selectedFiles = [];
currentPickupCode = '';
currentRole = '';
if (websocket) {
websocket.close();
}
document.getElementById('selectedFiles').classList.add('hidden');
document.getElementById('pickupCodeSection').classList.add('hidden');
document.getElementById('generateCodeBtn').classList.remove('hidden');
document.getElementById('fileInput').value = '';
}
// 加入房间
async function joinRoom() {
const code = document.getElementById('pickupCodeInput').value.trim();
if (code.length !== 6) {
alert('请输入6位取件码');
return;
}
try {
const response = await fetch(`/api/room-info?code=${code}`);
const data = await response.json();
if (data.success) {
currentPickupCode = code;
currentRole = 'receiver';
displayReceiverFiles(data.files);
connectWebSocket();
} else {
alert(data.message);
}
} catch (error) {
console.error('连接失败:', error);
alert('连接失败,请检查取件码是否正确');
}
}
// 显示接收方文件列表
function displayReceiverFiles(files) {
document.getElementById('codeInputSection').classList.add('hidden');
document.getElementById('receiverFilesSection').classList.remove('hidden');
const filesList = document.getElementById('receiverFilesList');
filesList.innerHTML = '';
files.forEach((file, index) => {
const fileItem = document.createElement('div');
fileItem.className = 'flex items-center justify-between bg-gray-50 p-3 rounded-lg';
fileItem.innerHTML = `
<div class="flex items-center">
<span class="text-2xl mr-3">${getFileIcon(file.type)}</span>
<div>
<div class="font-medium">${file.name}</div>
<div class="text-sm text-gray-500">${formatFileSize(file.size)}</div>
</div>
</div>
<button onclick="downloadFile('${file.id}')" disabled
class="bg-blue-500 text-white px-4 py-2 rounded font-semibold opacity-50 cursor-not-allowed">
📥 下载
</button>
`;
filesList.appendChild(fileItem);
});
// 初始化时显示正在建立连接状态
updateP2PStatus(false);
}
// 工具函数
function getFileIcon(mimeType) {
if (mimeType.startsWith('image/')) return '🖼️';
if (mimeType.startsWith('video/')) return '🎥';
if (mimeType.startsWith('audio/')) return '🎵';
if (mimeType.includes('pdf')) return '📄';
if (mimeType.includes('zip') || mimeType.includes('rar')) return '📦';
return '📄';
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
// 页面卸载时清理
window.addEventListener('beforeunload', () => {
if (websocket) {
websocket.close();
}
if (peerConnection) {
peerConnection.close();
}
});

View File

@@ -1,9 +1,18 @@
// WebSocket和WebRTC连接管理
// 全局变量
let clientConnections = new Map(); // 存储与其他客户端的P2P连接
let currentClientId = ''; // 当前客户端ID
// WebSocket连接
function connectWebSocket() {
console.log('尝试连接WebSocket, 角色:', currentRole, '取件码:', currentPickupCode);
if (!currentPickupCode || !currentRole) {
console.error('缺少必要参数:取件码或角色');
return;
}
if (isConnecting) {
console.log('已在连接中,跳过');
return;
@@ -22,54 +31,228 @@ function connectWebSocket() {
const wsUrl = `${wsProtocol}//${window.location.host}/ws/p2p?code=${currentPickupCode}&role=${currentRole}`;
console.log('WebSocket URL:', wsUrl);
websocket = new WebSocket(wsUrl);
websocket.onopen = () => {
console.log('WebSocket连接已建立');
isConnecting = false;
updateConnectionStatus(true);
try {
websocket = new WebSocket(wsUrl);
// 发送方在WebSocket连接建立后立即初始化P2P但不创建offer
if (currentRole === 'sender') {
console.log('发送方初始化P2P连接等待接收方就绪');
initPeerConnectionForSender();
}
};
websocket.onmessage = async (event) => {
try {
const message = JSON.parse(event.data);
await handleWebSocketMessage(message);
} catch (error) {
console.error('解析WebSocket消息失败:', error);
}
};
websocket.onerror = (error) => {
console.error('WebSocket错误:', error);
isConnecting = false;
updateConnectionStatus(false);
updateP2PStatus(false);
};
websocket.onclose = (event) => {
console.log('WebSocket连接已关闭, 代码:', event.code, '原因:', event.reason);
isConnecting = false;
updateConnectionStatus(false);
updateP2PStatus(false);
websocket = null;
websocket.onopen = () => {
console.log('WebSocket连接已建立');
isConnecting = false;
updateConnectionStatus(true);
// 连接建立后启用P2P功能
if (currentRole === 'receiver') {
updateP2PStatus(true); // 接收方连接成功后立即启用下载
}
// 发送方在WebSocket连接建立后初始化等待接收方连接
if (currentRole === 'sender') {
console.log('发送方初始化完成,等待接收方连接');
showRoomStatus();
}
};
// 如果不是正常关闭且还需要连接,尝试重连
if (event.code !== 1000 && currentPickupCode && !isConnecting) {
console.log('WebSocket异常关闭5秒后尝试重连');
setTimeout(() => {
if (currentPickupCode && !websocket && !isConnecting) {
console.log('尝试重新连接WebSocket');
connectWebSocket();
websocket.onmessage = async (event) => {
try {
const message = JSON.parse(event.data);
console.log('收到WebSocket消息:', message);
await handleWebSocketMessage(message);
} catch (error) {
console.error('解析WebSocket消息失败:', error, event.data);
}
};
websocket.onerror = (error) => {
console.error('WebSocket错误:', error);
isConnecting = false;
updateConnectionStatus(false);
updateP2PStatus(false);
showNotification('WebSocket连接失败请检查网络连接', 'error');
};
websocket.onclose = (event) => {
console.log('WebSocket连接已关闭, 代码:', event.code, '原因:', event.reason);
isConnecting = false;
updateConnectionStatus(false);
updateP2PStatus(false);
websocket = null;
// 清理所有P2P连接
clientConnections.forEach((conn, clientId) => {
if (conn.peerConnection) {
conn.peerConnection.close();
}
}, 5000);
});
clientConnections.clear();
// 如果不是正常关闭且还需要连接,尝试重连
if (event.code !== 1000 && currentPickupCode && !isConnecting) {
console.log('WebSocket异常关闭5秒后尝试重连');
showNotification('连接断开5秒后自动重连...', 'info');
setTimeout(() => {
if (currentPickupCode && !websocket && !isConnecting) {
console.log('尝试重新连接WebSocket');
connectWebSocket();
}
}, 5000);
}
};
// 设置连接超时
setTimeout(() => {
if (websocket && websocket.readyState === WebSocket.CONNECTING) {
console.log('WebSocket连接超时');
websocket.close();
showNotification('连接超时,请重试', 'error');
}
}, 10000);
} catch (error) {
console.error('创建WebSocket连接失败:', error);
isConnecting = false;
showNotification('无法创建WebSocket连接', 'error');
}
}
// 处理WebSocket消息
async function handleWebSocketMessage(message) {
console.log('处理WebSocket消息:', message.type, message);
switch (message.type) {
case 'file-list':
// 接收到文件列表
if (currentRole === 'receiver') {
displayReceiverFiles(message.payload.files);
}
break;
case 'room-status':
// 房间状态更新
updateRoomStatus(message.payload);
break;
case 'new-receiver':
// 新接收方加入
if (currentRole === 'sender') {
console.log('新接收方加入:', message.payload.client_id);
// 发送方可以准备为新接收方创建P2P连接
}
break;
case 'new-sender':
// 新发送方加入
if (currentRole === 'receiver') {
console.log('新发送方加入:', message.payload.client_id);
}
break;
case 'client-left':
// 客户端离开
console.log('客户端离开:', message.payload.client_id, message.payload.role);
// 清理对应的P2P连接
if (clientConnections.has(message.payload.client_id)) {
const conn = clientConnections.get(message.payload.client_id);
if (conn.peerConnection) {
conn.peerConnection.close();
}
clientConnections.delete(message.payload.client_id);
}
break;
case 'file-request':
// 文件请求
if (currentRole === 'sender') {
await handleFileRequest(message.payload);
}
break;
// WebRTC信令消息
case 'offer':
await handleOffer(message.payload);
break;
case 'answer':
await handleAnswer(message.payload);
break;
case 'ice-candidate':
await handleIceCandidate(message.payload);
break;
default:
console.log('未知消息类型:', message.type);
}
}
// 更新房间状态显示
function updateRoomStatus(status) {
console.log('更新房间状态:', status);
const totalClients = status.sender_count + status.receiver_count;
// 更新发送方界面的房间状态
if (currentRole === 'sender') {
const onlineCountEl = document.getElementById('onlineCount');
const senderCountEl = document.getElementById('senderCount');
const receiverCountEl = document.getElementById('receiverCount');
if (onlineCountEl) onlineCountEl.textContent = totalClients;
if (senderCountEl) senderCountEl.textContent = status.sender_count;
if (receiverCountEl) receiverCountEl.textContent = status.receiver_count;
const clientsList = document.getElementById('clientsList');
if (clientsList) {
clientsList.innerHTML = '';
status.clients.forEach(client => {
if (client.id !== currentClientId) { // 不显示自己
const clientDiv = document.createElement('div');
clientDiv.className = 'text-xs text-blue-600';
const role = client.role === 'sender' ? '📤 发送' : '📥 接收';
const joinTime = new Date(client.joined_at).toLocaleTimeString();
clientDiv.textContent = `${role} - ${joinTime}`;
clientsList.appendChild(clientDiv);
}
});
}
};
// 显示房间状态区域
const roomStatusSection = document.getElementById('roomStatusSection');
if (roomStatusSection) {
roomStatusSection.classList.remove('hidden');
}
}
// 更新接收方界面的房间状态
if (currentRole === 'receiver') {
const receiverOnlineCountEl = document.getElementById('receiverOnlineCount');
const receiverSenderCountEl = document.getElementById('receiverSenderCount');
const receiverReceiverCountEl = document.getElementById('receiverReceiverCount');
if (receiverOnlineCountEl) receiverOnlineCountEl.textContent = totalClients;
if (receiverSenderCountEl) receiverSenderCountEl.textContent = status.sender_count;
if (receiverReceiverCountEl) receiverReceiverCountEl.textContent = status.receiver_count;
const clientsList = document.getElementById('receiverClientsList');
if (clientsList) {
clientsList.innerHTML = '';
status.clients.forEach(client => {
if (client.id !== currentClientId) { // 不显示自己
const clientDiv = document.createElement('div');
clientDiv.className = 'text-xs text-blue-600';
const role = client.role === 'sender' ? '📤 发送' : '📥 接收';
const joinTime = new Date(client.joined_at).toLocaleTimeString();
clientDiv.textContent = `${role} - ${joinTime}`;
clientsList.appendChild(clientDiv);
}
});
}
}
}
// 显示房间状态区域
function showRoomStatus() {
if (currentRole === 'sender') {
document.getElementById('roomStatusSection').classList.remove('hidden');
}
}
// 更新连接状态
@@ -81,13 +264,358 @@ function updateConnectionStatus(connected) {
senderStatus.innerHTML = connected ?
`<div class="inline-flex items-center px-3 py-1 rounded-full bg-green-100 text-green-800">
<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
接收方已连接
WebSocket已连接
</div>` :
`<div class="inline-flex items-center px-3 py-1 rounded-full bg-yellow-100 text-yellow-800">
<span class="w-2 h-2 bg-yellow-500 rounded-full mr-2"></span>
等待接收方连接...
`<div class="inline-flex items-center px-3 py-1 rounded-full bg-red-100 text-red-800">
<span class="w-2 h-2 bg-red-500 rounded-full mr-2"></span>
连接断开
</div>`;
}
if (currentRole === 'receiver' && receiverStatus) {
receiverStatus.innerHTML = connected ?
`<div class="inline-flex items-center px-3 py-1 rounded-full bg-green-100 text-green-800">
<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
已连接,可以下载文件
</div>` :
`<div class="inline-flex items-center px-3 py-1 rounded-full bg-red-100 text-red-800">
<span class="w-2 h-2 bg-red-500 rounded-full mr-2"></span>
连接断开
</div>`;
}
}
// 处理文件请求
async function handleFileRequest(payload) {
console.log('处理文件请求:', payload);
const fileId = payload.file_id;
const requesterId = payload.requester;
const requestId = payload.request_id;
// 找到对应的文件
const file = selectedFiles.find(f => f.id === fileId || selectedFiles.indexOf(f).toString() === fileId);
if (!file) {
console.error('未找到请求的文件:', fileId);
return;
}
// 创建或获取与请求者的P2P连接
let connection = clientConnections.get(requesterId);
if (!connection) {
connection = await createPeerConnection(requesterId);
clientConnections.set(requesterId, connection);
}
// 发送文件
if (connection.dataChannel && connection.dataChannel.readyState === 'open') {
await sendFileToClient(file, connection.dataChannel, requestId);
} else {
console.log('等待数据通道建立...');
connection.pendingFiles = connection.pendingFiles || [];
connection.pendingFiles.push({ file, requestId });
}
}
// 创建P2P连接
async function createPeerConnection(targetClientId) {
console.log('创建P2P连接到:', targetClientId);
const connection = {
peerConnection: null,
dataChannel: null,
pendingFiles: []
};
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
});
connection.peerConnection = pc;
// 创建数据通道(发送方)
if (currentRole === 'sender') {
const dataChannel = pc.createDataChannel('fileTransfer', {
ordered: true
});
connection.dataChannel = dataChannel;
dataChannel.onopen = () => {
console.log('数据通道已打开,可以传输文件');
// 发送待发送的文件
if (connection.pendingFiles && connection.pendingFiles.length > 0) {
connection.pendingFiles.forEach(({ file, requestId }) => {
sendFileToClient(file, dataChannel, requestId);
});
connection.pendingFiles = [];
}
};
dataChannel.onmessage = (event) => {
console.log('数据通道收到消息:', event.data);
};
}
// 处理数据通道(接收方)
pc.ondatachannel = (event) => {
const channel = event.channel;
connection.dataChannel = channel;
channel.onopen = () => {
console.log('接收方数据通道已打开');
};
channel.onmessage = (event) => {
handleFileData(event.data, targetClientId);
};
};
// ICE候选者
pc.onicecandidate = (event) => {
if (event.candidate) {
websocket.send(JSON.stringify({
type: 'ice-candidate',
payload: {
candidate: event.candidate,
target_client: targetClientId
}
}));
}
};
return connection;
}
// 处理WebRTC信令消息
async function handleOffer(payload) {
console.log('处理offer:', payload);
// 实现WebRTC offer处理逻辑
}
async function handleAnswer(payload) {
console.log('处理answer:', payload);
// 实现WebRTC answer处理逻辑
}
// 发送文件给客户端
async function sendFileToClient(file, dataChannel, requestId) {
console.log('开始发送文件:', file.name, '到客户端');
// 发送文件信息
const fileInfo = {
type: 'file-info',
file_id: requestId,
name: file.name,
size: file.size,
mime_type: file.type,
last_modified: file.lastModified
};
dataChannel.send(JSON.stringify(fileInfo));
// 分块发送文件
const chunkSize = 65536; // 64KB chunks
let offset = 0;
const sendChunk = () => {
if (offset >= file.size) {
// 发送完成消息
const completeMsg = {
type: 'file-complete',
file_id: requestId
};
dataChannel.send(JSON.stringify(completeMsg));
console.log('文件发送完成:', file.name);
return;
}
const slice = file.slice(offset, offset + chunkSize);
const reader = new FileReader();
reader.onload = (e) => {
const chunk = e.target.result;
// 发送块元数据
const metadata = {
type: 'file-chunk-meta',
file_id: requestId,
offset: offset,
size: chunk.byteLength,
is_last: offset + chunk.byteLength >= file.size
};
dataChannel.send(JSON.stringify(metadata));
// 发送二进制数据
dataChannel.send(chunk);
offset += chunk.byteLength;
// 继续发送下一块
setTimeout(sendChunk, 10); // 小延时以避免阻塞
};
reader.readAsArrayBuffer(slice);
};
sendChunk();
}
// 处理接收到的文件数据
function handleFileData(data, senderId) {
console.log('从发送方接收文件数据:', senderId);
// 检查是否是二进制数据
if (data instanceof ArrayBuffer) {
// 处理二进制数据块
if (pendingChunkMeta) {
receiveFileChunk(pendingChunkMeta, data, senderId);
pendingChunkMeta = null;
}
} else {
// 处理JSON消息
try {
const message = JSON.parse(data);
console.log('接收到文件传输消息:', message.type);
switch (message.type) {
case 'file-chunk-meta':
// 存储chunk元数据等待二进制数据
pendingChunkMeta = message;
break;
case 'file-info':
// 初始化文件传输
initFileTransfer(message, senderId);
break;
case 'file-complete':
// 文件传输完成
completeFileDownload(message.file_id, senderId);
break;
default:
console.log('未知文件传输消息类型:', message.type);
}
} catch (error) {
console.error('解析文件传输消息失败:', error);
}
}
}
// 初始化文件传输
function initFileTransfer(fileInfo, senderId) {
console.log('初始化文件传输:', fileInfo);
const transferKey = `${fileInfo.file_id}_${senderId}`;
if (!fileTransfers.has(transferKey)) {
fileTransfers.set(transferKey, {
fileId: fileInfo.file_id,
senderId: senderId,
chunks: [],
totalSize: fileInfo.size,
receivedSize: 0,
fileName: fileInfo.name,
mimeType: fileInfo.mime_type || fileInfo.type,
startTime: Date.now()
});
console.log('文件传输已初始化:', transferKey);
}
}
// 接收文件数据块
function receiveFileChunk(metadata, chunk, senderId) {
const transferKey = `${metadata.file_id}_${senderId}`;
const transfer = fileTransfers.get(transferKey);
if (!transfer) {
console.error('未找到对应的文件传输:', transferKey);
return;
}
// 存储数据块
transfer.chunks.push({
offset: metadata.offset,
data: chunk
});
transfer.receivedSize += chunk.byteLength;
// 更新进度
const progress = (transfer.receivedSize / transfer.totalSize) * 100;
updateTransferProgress(metadata.file_id, progress, transfer.receivedSize, transfer.totalSize);
console.log(`文件块接收进度: ${progress.toFixed(1)}% (${transfer.receivedSize}/${transfer.totalSize})`);
// 检查是否是最后一块
if (metadata.is_last || transfer.receivedSize >= transfer.totalSize) {
console.log('文件接收完成,开始合并数据块');
assembleAndDownloadFile(transferKey);
}
}
// 组装文件并触发下载
function assembleAndDownloadFile(transferKey) {
const transfer = fileTransfers.get(transferKey);
if (!transfer) {
console.error('未找到文件传输信息:', transferKey);
return;
}
// 按偏移量排序数据块
transfer.chunks.sort((a, b) => a.offset - b.offset);
// 合并所有数据块
const totalSize = transfer.chunks.reduce((sum, chunk) => sum + chunk.data.byteLength, 0);
const mergedData = new Uint8Array(totalSize);
let currentOffset = 0;
transfer.chunks.forEach(chunk => {
const chunkView = new Uint8Array(chunk.data);
mergedData.set(chunkView, currentOffset);
currentOffset += chunkView.length;
});
// 创建Blob并触发下载
const blob = new Blob([mergedData], { type: transfer.mimeType });
// 创建下载链接
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = transfer.fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
// 清理传输信息
fileTransfers.delete(transferKey);
// 显示完成状态
hideTransferProgress(transfer.fileId);
// 恢复下载按钮
const button = document.querySelector(`button[onclick="downloadFile('${transfer.fileId}')"]`);
if (button) {
button.disabled = false;
button.textContent = '📥 下载';
}
const transferTime = (Date.now() - transfer.startTime) / 1000;
const speed = (transfer.totalSize / transferTime / 1024 / 1024).toFixed(2);
console.log(`文件下载完成: ${transfer.fileName}`);
console.log(`传输时间: ${transferTime.toFixed(1)}秒,平均速度: ${speed} MB/s`);
// 显示成功消息
showNotification(`文件 "${transfer.fileName}" 下载完成!传输速度: ${speed} MB/s`, 'success');
}
// 为发送方初始化P2P连接不立即创建offer

View File

@@ -0,0 +1,50 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>WebSocket连接测试</title>
</head>
<body>
<h1>WebSocket连接测试</h1>
<div id="status">未连接</div>
<button onclick="testConnection()">测试连接</button>
<div id="log"></div>
<script>
function log(message) {
const logDiv = document.getElementById('log');
logDiv.innerHTML += '<div>' + new Date().toLocaleTimeString() + ': ' + message + '</div>';
console.log(message);
}
function testConnection() {
const code = '354888'; // 使用已存在的房间码
const role = 'receiver';
const wsUrl = `ws://localhost:8080/ws/p2p?code=${code}&role=${role}`;
log('尝试连接: ' + wsUrl);
const ws = new WebSocket(wsUrl);
ws.onopen = function() {
log('WebSocket连接成功!');
document.getElementById('status').textContent = '已连接';
};
ws.onerror = function(error) {
log('WebSocket错误: ' + JSON.stringify(error));
};
ws.onclose = function(event) {
log('WebSocket关闭: 代码=' + event.code + ', 原因=' + event.reason);
document.getElementById('status').textContent = '已断开';
};
ws.onmessage = function(event) {
log('收到消息: ' + event.data);
};
}
</script>
</body>
</html>

View File

@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html>
<head>
<title>WebSocket调试测试</title>
</head>
<body>
<h1>WebSocket连接测试</h1>
<div>
<input type="text" id="testCode" placeholder="输入取件码" maxlength="6" style="padding: 10px; margin: 10px;">
<select id="testRole" style="padding: 10px; margin: 10px;">
<option value="sender">发送方</option>
<option value="receiver">接收方</option>
</select>
<button onclick="testWebSocket()" style="padding: 10px; margin: 10px;">测试连接</button>
<button onclick="closeWebSocket()" style="padding: 10px; margin: 10px;">关闭连接</button>
<button onclick="clearLog()" style="padding: 10px; margin: 10px;">清空日志</button>
</div>
<div>
<h2>连接状态:<span id="status">未连接</span></h2>
<h3>日志:</h3>
<div id="log" style="border: 1px solid #ccc; padding: 10px; height: 400px; overflow-y: auto; font-family: monospace; background: #f5f5f5;"></div>
</div>
<script>
let testSocket = null;
let logElement = document.getElementById('log');
let statusElement = document.getElementById('status');
function log(message) {
const time = new Date().toLocaleTimeString();
logElement.innerHTML += `[${time}] ${message}<br>`;
logElement.scrollTop = logElement.scrollHeight;
console.log(`[${time}] ${message}`);
}
function updateStatus(status) {
statusElement.textContent = status;
log(`状态更新: ${status}`);
}
function testWebSocket() {
const code = document.getElementById('testCode').value.trim();
const role = document.getElementById('testRole').value;
if (!code) {
log('错误: 请输入取件码');
return;
}
if (testSocket) {
log('关闭现有连接...');
testSocket.close();
testSocket = null;
}
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${window.location.host}/ws/p2p?code=${code}&role=${role}`;
log(`尝试连接: ${wsUrl}`);
updateStatus('连接中...');
try {
testSocket = new WebSocket(wsUrl);
testSocket.onopen = function(event) {
log('✅ WebSocket连接成功建立');
updateStatus('已连接');
};
testSocket.onmessage = function(event) {
let message = event.data;
try {
message = JSON.parse(event.data);
log(`📥 收到消息 (${message.type}): ${JSON.stringify(message, null, 2)}`);
} catch (e) {
log(`📥 收到原始消息: ${message}`);
}
};
testSocket.onerror = function(error) {
log(`❌ WebSocket错误: ${error}`);
updateStatus('连接错误');
};
testSocket.onclose = function(event) {
log(`🔌 连接关闭 - 代码: ${event.code}, 原因: ${event.reason}, 是否干净关闭: ${event.wasClean}`);
updateStatus('连接已关闭');
testSocket = null;
};
// 连接超时检测
setTimeout(() => {
if (testSocket && testSocket.readyState === WebSocket.CONNECTING) {
log('⏰ 连接超时 (10秒)');
testSocket.close();
}
}, 10000);
} catch (error) {
log(`❌ 创建WebSocket失败: ${error.message}`);
updateStatus('创建失败');
}
}
function closeWebSocket() {
if (testSocket) {
log('手动关闭连接...');
testSocket.close();
testSocket = null;
} else {
log('没有活动连接');
}
}
function clearLog() {
logElement.innerHTML = '';
}
// 页面加载时显示信息
window.onload = function() {
log('WebSocket测试页面已加载');
log('请先创建一个房间(使用取件码),然后测试连接');
};
</script>
</body>
</html>

View File

@@ -58,6 +58,17 @@
等待接收方连接...
</div>
</div>
<!-- 房间状态显示 -->
<div id="roomStatusSection" class="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg hidden">
<h5 class="font-semibold text-blue-800 mb-2">房间状态</h5>
<div id="roomConnections" class="text-sm text-blue-700">
<div>在线用户: <span id="onlineCount">0</span></div>
<div>发送方: <span id="senderCount">0</span></div>
<div>接收方: <span id="receiverCount">0</span></div>
</div>
<div id="clientsList" class="mt-2 space-y-1"></div>
</div>
</div>
</div>
@@ -93,6 +104,17 @@
已连接,可以下载文件
</div>
</div>
<!-- 房间状态显示 (接收方) -->
<div id="receiverRoomStatusSection" class="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h5 class="font-semibold text-blue-800 mb-2">房间状态</h5>
<div id="receiverRoomConnections" class="text-sm text-blue-700">
<div>在线用户: <span id="receiverOnlineCount">0</span></div>
<div>发送方: <span id="receiverSenderCount">0</span></div>
<div>接收方: <span id="receiverReceiverCount">0</span></div>
</div>
<div id="receiverClientsList" class="mt-2 space-y-1"></div>
</div>
</div>
</div>
</div>
@@ -109,7 +131,5 @@
{{define "scripts"}}
<!-- P2P文件传输相关脚本 -->
<script src="/static/js/p2p-transfer.js"></script>
<script src="/static/js/webrtc-connection.js"></script>
<script src="/static/js/file-transfer.js"></script>
<script src="/static/js/p2p-transfer-new.js"></script>
{{end}}