+
104
-

回答

实时显示当前可发送的终端,点击选择发送对象和本地文件,即可点对点的传输文件。

发送端

800_auto

接收端

800_auto

点击可下载文件

使用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

网友回复

我知道答案,我要回答