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

接收端

点击可下载文件
使用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 网友回复
有没有兼容Puppeteer和 Playwright使用的docker独立chrome浏览器?
geo与seo区别?
chrome插件能否实现网页远程控制鼠标选择网页文字滚动网页?
nativescript开发的安卓与ios app应用是原生的吗?
go如何写一个类似redis的nosql数据库让python客户端调用?
php7中为啥无法使用$_SERVER['HTTP_RAW_POST_DATA'] ?
chrome插件能否实现2台电脑的远程协助桌面控制?
python如何通过webrtc网页远程协助控制python运行的电脑?
systemd-nspawn与unshare区别?
Firejail、Bubblewrap沙箱与docker、podman容器区别?


