实时显示当前可发送的终端,点击选择发送对象和本地文件,即可点对点的传输文件。
发送端
接收端
点击可下载文件
使用webrtc技术实现这个功能,我们需要一个信令服务器来传递sdp,我们用nodejs的ws来搭建:
nodejs搭建的websocket服务器代码:
const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); const clients = new Map(); wss.on('connection', (ws) => { const clientId = generateClientId(); clients.set(clientId, ws); // 发送当前客户端的ID ws.send(JSON.stringify({ type: 'id', id: clientId })); ws.on('message', (message) => { const data = JSON.parse(message); if (data.type === 'offer') { const targetClient = clients.get(data.to); if (targetClient) { targetClient.send(JSON.stringify({ type: 'offer', offer: data.offer, from: clientId })); } } else if (data.type === 'answer') { const targetClient = clients.get(data.to); if (targetClient) { targetClient.send(JSON.stringify({ type: 'answer', answer: data.answer, from: clientId })); } } else if (data.type === 'ice-candidate') { const targetClient = clients.get(data.to); if (targetClient) { targetClient.send(JSON.stringify({ type: 'ice-candidate', candidate: data.candidate, from: clientId })); } } }); ws.on('close', () => { clients.delete(clientId); broadcastClients(); }); broadcastClients(); }); function broadcastClients() { const clientIds = Array.from(clients.keys()); clients.forEach((client, id) => { client.send(JSON.stringify({ type: 'clients', clients: clientIds })); }); } function generateClientId() { return Math.random().toString(36).substr(2, 9); }前端html5客户端代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>WebRTC File Transfer</title> <style> body { font-family: Arial, sans-serif; margin: 20px; } #clients { margin-bottom: 20px; } #file-input { margin-bottom: 20px; } #status { margin-top: 20px; font-weight: bold; } #download-link { display: none; margin-top: 20px; color: blue; text-decoration: underline; cursor: pointer; } </style> </head> <body> <h1>WebRTC File Transfer</h1> <div id="clients"> <h2>Available Clients:</h2> <ul id="client-list"></ul> </div> <input type="file" id="file-input"> <button id="send-button" disabled>Send File</button> <div id="status"></div> <a id="download-link">Download File</a> <script > const socket = new WebSocket('ws://nodejs搭建的websocket服务器:8080'); const CHUNK_SIZE = 16384; // 16KB 的块大小 let receivedSize = 0; let fileSize = 0; let receivedChunks = []; const clientList = document.getElementById('client-list'); const fileInput = document.getElementById('file-input'); const sendButton = document.getElementById('send-button'); const statusDiv = document.getElementById('status'); const downloadLink = document.getElementById('download-link'); // 用于显示下载链接 let peerConnection; let selectedClientId; let currentClientId; // 当前客户端的ID // Initialize peerConnection function initializePeerConnection() { peerConnection = new RTCPeerConnection(); peerConnection.onicecandidate = (event) => { if (event.candidate) { socket.send(JSON.stringify({ type: 'ice-candidate', candidate: event.candidate, to: selectedClientId })); } }; } // WebSocket connection opened socket.addEventListener('open', (event) => { statusDiv.textContent = 'Connected to signaling server'; }); // WebSocket message received socket.addEventListener('message', async (event) => { const message = JSON.parse(event.data); if (message.type === 'clients') { updateClientList(message.clients); } else if (message.type === 'id') { // 服务器发送当前客户端的ID currentClientId = message.id; } else if (message.type === 'offer') { await handleOffer(message); } else if (message.type === 'answer') { await handleAnswer(message); } else if (message.type === 'ice-candidate') { await handleIceCandidate(message); } }); // Update the list of available clients function updateClientList(clients) { clientList.innerHTML = ''; clients.forEach(client => { // 排除自己 if (client !== currentClientId) { const li = document.createElement('li'); li.textContent = client; li.addEventListener('click', () => { selectedClientId = client; sendButton.disabled = false; statusDiv.textContent = `Selected client: ${client}`; }); clientList.appendChild(li); } }); } // Handle incoming answer async function handleAnswer(message) { await peerConnection.setRemoteDescription(new RTCSessionDescription(message.answer)); } // Handle incoming ICE candidate async function handleIceCandidate(message) { await peerConnection.addIceCandidate(new RTCIceCandidate(message.candidate)); } // 修改 handleOffer 函数 async function handleOffer(message) { initializePeerConnection(); peerConnection.ondatachannel = (event) => { const dataChannel = event.channel; let fileName; let fileSize; let receivedSize = 0; let receivedChunks = []; dataChannel.onmessage = (event) => { if (typeof event.data === 'string') { // 接收文件元数据 const metadata = JSON.parse(event.data); fileName = metadata.name; fileSize = metadata.size; statusDiv.textContent = `Receiving ${fileName} (0%)`; } else { // 接收文件块 receivedChunks.push(event.data); receivedSize += event.data.byteLength; // 更新进度 const progress = Math.round((receivedSize / fileSize) * 100); statusDiv.textContent = `Receiving ${fileName} (${progress}%)`; // 检查是否接收完成 if (receivedSize === fileSize) { const blob = new Blob(receivedChunks); const url = URL.createObjectURL(blob); downloadLink.href = url; downloadLink.download = fileName; downloadLink.textContent = `Download ${fileName}`; downloadLink.style.display = 'block'; statusDiv.textContent = `File received: ${fileName}`; // 清理内存 receivedChunks = []; } } }; dataChannel.onopen = () => { statusDiv.textContent = 'Data channel opened'; }; dataChannel.onerror = (error) => { console.error('Data channel error:', error); statusDiv.textContent = 'Error in data channel'; }; }; await peerConnection.setRemoteDescription(new RTCSessionDescription(message.offer)); const answer = await peerConnection.createAnswer(); await peerConnection.setLocalDescription(answer); socket.send(JSON.stringify({ type: 'answer', answer: answer, to: message.from })); } // 修改发送文件的逻辑 sendButton.addEventListener('click', async () => { if (!selectedClientId || !fileInput.files.length) return; if (selectedClientId === currentClientId) { statusDiv.textContent = 'Error: Cannot send file to yourself!'; return; } if (!peerConnection) { initializePeerConnection(); } const file = fileInput.files[0]; const dataChannel = peerConnection.createDataChannel('fileTransfer'); dataChannel.onopen = async () => { // 发送文件元数据 const metadata = { name: file.name, size: file.size, type: file.type }; dataChannel.send(JSON.stringify(metadata)); // 分块读取并发送文件 let offset = 0; while (offset < file.size) { const chunk = file.slice(offset, offset + CHUNK_SIZE); const buffer = await chunk.arrayBuffer(); // 等待数据通道准备好发送下一块 if (dataChannel.bufferedAmount > CHUNK_SIZE * 2) { await new Promise(resolve => { const checkBuffer = () => { if (dataChannel.bufferedAmount <= CHUNK_SIZE) { resolve(); } else { setTimeout(checkBuffer, 100); } }; checkBuffer(); }); } dataChannel.send(buffer); offset += buffer.byteLength; // 更新进度 const progress = Math.round((offset / file.size) * 100); statusDiv.textContent = `Sending ${file.name} (${progress}%)`; } }; dataChannel.onerror = (error) => { console.error('Data channel error:', error); statusDiv.textContent = 'Error sending file'; }; const offer = await peerConnection.createOffer(); await peerConnection.setLocalDescription(offer); socket.send(JSON.stringify({ type: 'offer', offer: offer, to: selectedClientId })); }); // 添加错误处理 peerConnection.onerror = (error) => { console.error('PeerConnection error:', error); statusDiv.textContent = 'Connection error occurred'; }; peerConnection.oniceconnectionstatechange = () => { if (peerConnection.iceConnectionState === 'failed') { console.error('ICE connection failed'); statusDiv.textContent = 'Connection failed'; } }; socket.onerror = (error) => { console.error('WebSocket error:', error); statusDiv.textContent = 'WebSocket connection error'; }; </script> </body> </html>还有开源的ui好看的项目:https://github.com/sunzsh/internal-chat
网友回复