WebRTC - 发送消息
WebRTC - 使用RTCDataChannel发送消息
Section titled “WebRTC - 使用RTCDataChannel发送消息”本示例演示如何使用WebRTC的RTCDataChannel在两个对等方之间发送文本消息。我们假设您已经在ws://localhost:9090运行了一个信令服务器(如“WebRTC - 信令”章节所述)。
页面将包含用于输入登录名、要连接的目标用户名以及要发送的消息的输入字段。创建一个index.html文件:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>WebRTC DataChannel Demo</title></head><body> <div> <label for="loginInput">Your Username: </label> <input id="loginInput" type="text"/> <button id="loginBtn">Login</button> </div> <hr> <div> <label for="otherUsernameInput">Connect to Username: </label> <input id="otherUsernameInput" type="text"/> <button id="connectToOtherUsernameBtn">Establish Connection</button> </div> <hr> <div> <label for="msgInput">Message: </label> <input id="msgInput" type="text"/> <button id="sendMsgBtn">Send Message</button> </div> <div> <h3>Received Messages:</h3> <div id="messages"></div> </div>
<script src="client.js"></script></body></html>现在,创建一个client.js文件,其中包含以下JavaScript代码。此代码使用现代JavaScript特性,如async/await来处理WebRTC API返回的Promise。
const loginInput = document.querySelector('#loginInput');const loginBtn = document.querySelector('#loginBtn');const otherUsernameInput = document.querySelector('#otherUsernameInput');const connectToOtherUsernameBtn = document.querySelector('#connectToOtherUsernameBtn');const msgInput = document.querySelector('#msgInput');const sendMsgBtn = document.querySelector('#sendMsgBtn');const messagesDiv = document.querySelector('#messages');
let connection;let name = '';let connectedUser = '';let myPeerConnection;let dataChannel;
const iceServers = [ { urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun1.l.google.com:19302' } // Add TURN server URIs here if needed for robust NAT traversal // { // urls: 'turn:your.turn.server.com:3478', // username: 'user', // credential: 'password' // }];
function displayMessage(message, sender = 'Me') { const p = document.createElement('p'); p.textContent = `${sender}: ${message}`; messagesDiv.appendChild(p); msgInput.value = ''; // Clear input after sending}
// Connect to the signaling serverfunction connectSignaling() { connection = new WebSocket('ws://localhost:9090');
connection.onopen = () => { console.log('Connected to signaling server'); };
connection.onmessage = async (message) => { console.log('Got message from signaling server:', message.data); const data = JSON.parse(message.data);
switch (data.type) { case 'login': onLogin(data.success); break; case 'offer': await onOffer(data.offer, data.name); break; case 'answer': await onAnswer(data.answer); break; case 'candidate': await onCandidate(data.candidate); break; case 'leave': onLeave(); break; default: break; } };
connection.onerror = (err) => { console.error('Signaling server error:', err); };}
function sendSignalingMessage(message) { if (connectedUser) { message.name = connectedUser; } connection.send(JSON.stringify(message));}
loginBtn.addEventListener('click', () => { name = loginInput.value.trim(); if (name.length > 0) { if (!connection || connection.readyState !== WebSocket.OPEN) { connectSignaling(); // Wait a bit for connection to establish before sending login setTimeout(() => { sendSignalingMessage({ type: 'login', name: name }); }, 500); } else { sendSignalingMessage({ type: 'login', name: name }); } } else { alert('Please enter a username.'); }});
async function onLogin(success) { if (success === false) { alert('Login failed. Username might be taken.'); } else { console.log('Logged in successfully as', name); // Initialize RTCPeerConnection myPeerConnection = new RTCPeerConnection({ iceServers }); console.log('RTCPeerConnection object created');
myPeerConnection.onicecandidate = (event) => { if (event.candidate) { sendSignalingMessage({ type: 'candidate', candidate: event.candidate }); } };
myPeerConnection.oniceconnectionstatechange = () => { console.log('ICE connection state change:', myPeerConnection.iceConnectionState); if (myPeerConnection.iceConnectionState === 'disconnected' || myPeerConnection.iceConnectionState === 'failed' || myPeerConnection.iceConnectionState === 'closed') { onLeave(); // Handle disconnection } };
// For the receiving peer, this event is fired when the data channel is added. myPeerConnection.ondatachannel = (event) => { console.log('Data channel received'); dataChannel = event.channel; setupDataChannelEvents(); }; }}
connectToOtherUsernameBtn.addEventListener('click', async () => { const otherUsername = otherUsernameInput.value.trim(); if (otherUsername.length > 0 && myPeerConnection) { connectedUser = otherUsername; console.log('Attempting to connect to', connectedUser);
// Create data channel for the initiating peer dataChannel = myPeerConnection.createDataChannel('myDataChannel'); console.log('Data channel created'); setupDataChannelEvents();
try { const offer = await myPeerConnection.createOffer(); await myPeerConnection.setLocalDescription(offer); sendSignalingMessage({ type: 'offer', offer: offer }); console.log('Offer sent'); } catch (error) { console.error('Error creating offer:', error); alert('Error creating offer.'); } } else if (!myPeerConnection) { alert('Please login first.'); } else { alert('Please enter a username to connect to.'); }});
async function onOffer(offer, offeringUserName) { if (myPeerConnection) { connectedUser = offeringUserName; console.log('Received offer from', connectedUser); try { await myPeerConnection.setRemoteDescription(new RTCSessionDescription(offer)); const answer = await myPeerConnection.createAnswer(); await myPeerConnection.setLocalDescription(answer); sendSignalingMessage({ type: 'answer', answer: answer }); console.log('Answer sent'); } catch (error) { console.error('Error handling offer:', error); alert('Error handling offer.'); } } else { console.warn('Received offer but not logged in or peer connection not initialized.'); }}
async function onAnswer(answer) { console.log('Received answer'); try { await myPeerConnection.setRemoteDescription(new RTCSessionDescription(answer)); console.log('Remote description set for answer'); } catch (error) { console.error('Error setting remote description for answer:', error); }}
async function onCandidate(candidate) { console.log('Received ICE candidate'); try { await myPeerConnection.addIceCandidate(new RTCIceCandidate(candidate)); console.log('ICE candidate added'); } catch (error) { console.error('Error adding ICE candidate:', error); }}
function setupDataChannelEvents() { dataChannel.onopen = () => { console.log('Data channel OPEN'); alert('Data channel connection established!'); };
dataChannel.onerror = (error) => { console.error('Data channel error:', error); };
dataChannel.onmessage = (event) => { console.log('Got message:', event.data); displayMessage(event.data, connectedUser); };
dataChannel.onclose = () => { console.log('Data channel CLOSED'); };}
sendMsgBtn.addEventListener('click', () => { const val = msgInput.value; if (val.length > 0 && dataChannel && dataChannel.readyState === 'open') { dataChannel.send(val); displayMessage(val, 'Me'); } else if (!dataChannel || dataChannel.readyState !== 'open'){ alert('Data channel is not open. Please establish a connection first.'); } else { alert('Please type a message.'); }});
function onLeave() { console.log('Disconnecting or peer left.'); connectedUser = null; if (myPeerConnection) { myPeerConnection.close(); myPeerConnection = null; } if (dataChannel) { dataChannel.close(); dataChannel = null; } // Optionally, inform the UI alert('Connection closed or peer left.'); otherUsernameInput.value = ''; // Clear the connected user input}
// Automatically connect to signaling when page loads, or prompt user// For simplicity, we'll rely on the login button to initiate signaling connection.console.log('Client script loaded. Please login to start.');在此更新的client.js中:
- 我们使用
let和const进行变量声明。 RTCPeerConnection实例化时无需使用供应商前缀。iceServers配置包含STUN服务器。为了在各种网络(特别是具有对称NAT的网络)中实现稳定的连接,您还需要TURN服务器。createOffer()、createAnswer()、setLocalDescription()、setRemoteDescription()和addIceCandidate()都基于Promise,并使用async/await进行处理,以获得更清晰的异步代码。RTCPeerConnection构造函数不再需要特殊标志,如RtpDataChannels: true;数据通道是API的标准部分。- 包含了WebRTC操作和数据通道事件的错误处理。
- 添加了一个简单的区域来显示接收到的消息。
- 使用
oniceconnectionstatechange来监控连接状态并处理断开连接。 onLeave函数有助于在对等方断开连接时清理资源。
测试方法:1. 运行您的信令服务器(node server.js)。2. 在两个浏览器标签页(或同一网络上的两个不同浏览器)中打开index.html。3. 在第一个标签页中,输入一个用户名(例如,“Alice”)并单击“Login”。4. 在第二个标签页中,输入另一个用户名(例如,“Bob”)并单击“Login”。5. 在Alice的标签页中,在“Connect to Username”字段输入“Bob”并单击“Establish Connection”。6. 弹出框和控制台日志应指示offer/answer交换和数据通道建立的进度。7. 一旦连接建立,尝试发送消息。它们应该会出现在另一个标签页的“Received Messages”部分。
您将在控制台和页面上观察到消息的交换。这表明RTCDataChannel是WebRTC用于任意数据传输的强大功能,适用于聊天、游戏、文件共享等。