Skip to content

WebRTC - 发送消息

本示例演示如何使用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 server
function 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用于任意数据传输的强大功能,适用于聊天、游戏、文件共享等。