WebRTC - 信令
WebRTC - 构建一个基本的信令服务器
Section titled “WebRTC - 构建一个基本的信令服务器”WebRTC 本身并没有定义对等端如何发现彼此或交换控制消息(例如 SDP offer/answer 和 ICE candidate)。这个过程称为“信令”(Signaling),开发者必须实现自己的信令机制。在本章中,我们将使用 Node.js 和 WebSockets 构建一个基本的信令服务器。
在建立直接的对等端(Peer-to-Peer)WebRTC 连接之前,对等端需要交换信息:
- **会话控制消息:**用于发起、管理或终止呼叫(例如,用户 A 想呼叫用户 B)。
- **会话描述协议(SDP)Offer/Answer:**用于协商媒体能力(编解码器、分辨率等)。
- **ICE Candidate:**网络信息(IP 地址、端口),帮助对等端找到最佳连接路径。
典型的信令流程可能如下所示:
- 用户 A 和用户 B 连接到信令服务器。
- 用户 A 决定呼叫用户 B。用户 A 创建一个 SDP offer。
- 用户 A 通过信令服务器将 offer 发送给用户 B。
- 用户 B 收到 offer 并创建一个 SDP answer。
- 用户 B 通过信令服务器将 answer 发送回用户 A。
- 在此过程中,用户 A 和用户 B 也都会发现 ICE candidate 并通过信令服务器交换它们。
- 一旦 offer/answer 和 ICE candidate 交换完成,就会尝试建立直接的对等连接。
这只是一个例子;信令逻辑的具体实现可以有很大差异。WebSockets 因其双向、低延迟的通信能力而成为信令的常用选择。
使用 Node.js 和 ws 构建服务器
Section titled “使用 Node.js 和 ws 构建服务器”我们的服务器将允许用户使用用户名“登录”,然后促进他们之间 WebRTC 消息的交换。下面的图概念性地说明了消息流:用户 A 和用户 B 都连接到信令服务器。用户 A 向服务器发送给用户 B 的 offer。服务器将此 offer 转发给用户 B。用户 B 生成一个 answer 并通过服务器发送给用户 A。ICE candidate 也以类似方式交换。此过程持续进行,直到建立对等连接或失败。
首先,确保你已安装 Node.js 和 ws 库(在你的项目目录中运行 npm install ws)。创建一个名为 server.js 的文件:
const WebSocket = require('ws');
// Create a WebSocket server on port 9090const wss = new WebSocket.Server({ port: 9090 });
// Store connected users: { username: WebSocketConnection }const users = {};
console.log('Signaling server started on ws://localhost:9090');
wss.on('connection', (connection) => { console.log('User connected');
connection.on('message', (message) => { let data; try { data = JSON.parse(message); } catch (e) { console.log('Invalid JSON received:', message); data = {}; }
console.log('Received message:', data);
switch (data.type) { case 'login': handleLogin(connection, data); break; case 'offer': case 'answer': case 'candidate': handleWebRTCMsg(connection, data); break; case 'leave': handleLeave(connection); break; default: sendTo(connection, { type: 'error', message: 'Command not found: ' + data.type }); break; } });
connection.on('close', () => { handleDisconnect(connection); });
connection.on('error', (error) => { console.error('Connection error for user:', connection.name, error); handleDisconnect(connection); // Treat errors as disconnections for cleanup });
// Send a welcome message (optional) // sendTo(connection, { type: 'info', message: 'Connected to signaling server' });});
function sendTo(conn, message) { try { conn.send(JSON.stringify(message)); } catch (error) { console.error('Failed to send message to', conn.name, ':', error); }}
function handleLogin(connection, data) { if (users[data.name]) { sendTo(connection, { type: 'login', success: false, message: 'Username already taken' }); } else { users[data.name] = connection; connection.name = data.name; console.log('User logged in:', data.name); sendTo(connection, { type: 'login', success: true }); }}
function handleWebRTCMsg(connection, data) { const targetUser = data.name; // The recipient of the WebRTC message const targetConnection = users[targetUser];
if (targetConnection) { // Add sender's name to the message for the recipient const outgoingMessage = { ...data, sender: connection.name }; delete outgoingMessage.name; // Remove target name, as it's now the recipient
// If it's an offer or answer, store the other party's name for potential leave/disconnect handling if (data.type === 'offer' || data.type === 'answer') { connection.otherName = targetUser; targetConnection.otherName = connection.name; }
console.log(`Forwarding ${data.type} from ${connection.name} to ${targetUser}`); sendTo(targetConnection, outgoingMessage); } else { console.log(`User ${targetUser} not found for ${data.type} from ${connection.name}`); sendTo(connection, { type: 'error', message: `User ${targetUser} not found.` }); }}
function handleLeave(connection) { console.log('User leaving:', connection.name, '; notifying other party:', connection.otherName); if (connection.otherName) { const otherUserConnection = users[connection.otherName]; if (otherUserConnection) { otherUserConnection.otherName = null; // Clear their otherName reference sendTo(otherUserConnection, { type: 'leave', sender: connection.name }); } } connection.otherName = null; // Clear this user's otherName reference // Note: We don't remove the user from `users` here, that happens on 'close'. // This 'leave' is for active disconnection from a call, not from the server itself.}
function handleDisconnect(connection) { console.log('User disconnected:', connection.name); if (connection.name) { // Notify the other party if they were in a call if (connection.otherName) { const otherUserConnection = users[connection.otherName]; if (otherUserConnection) { otherUserConnection.otherName = null; sendTo(otherUserConnection, { type: 'leave', sender: connection.name, reason: 'disconnect' }); } } delete users[connection.name]; }}要运行此服务器,将其保存为 server.js,并在终端中执行 node server.js。你应该会看到 ‘Signaling server started on ws://localhost:9090’。
使用 wscat 测试:
打开两个终端窗口。在第一个窗口中,运行你的服务器:node server.js。
在第二个终端中,以 ‘userA’ 身份连接:wscat -c ws://localhost:9090
连接成功后(你会看到 Connected (press CTRL+C to quit)),输入 userA 的登录消息:
{“type”:“login”,“name”:“userA”} 然后按回车。
服务器应记录 ‘User logged in: userA’,并且 wscat 应该收到:{“type”:“login”,“success”:true}。
打开第三个终端并以 ‘userB’ 身份连接:wscat -c ws://localhost:9090
以 userB 登录:{“type”:“login”,“name”:“userB”}。
现在,从 userA 的 wscat 终端向 userB 发送一个 offer:
{“type”:“offer”,“name”:“userB”,“offer”:“SDP_OFFER_DATA_HERE”}
userB 的 wscat 终端应该收到类似以下内容:{“type”:“offer”,“offer”:“SDP_OFFER_DATA_HERE”,“sender”:“userA”},服务器将记录转发操作。这展示了基本的 संदेश 中继能力。
服务器逻辑说明
Section titled “服务器逻辑说明”- **连接处理:**当新的客户端连接时(
wss.on(‘connection’, …)),服务器会设置监听器来接收来自该客户端的消息、关闭事件和错误。 - **用户注册(登录):**客户端发送带有用户名的
login消息。服务器检查用户名是否已被占用。如果没有,则存储与该用户名关联的连接。 - **WebRTC 消息中继(Offer, Answer, Candidate):**当服务器接收到
offer、answer或candidate消息时,它会查找消息name字段中指定的的目标用户,并将消息转发到该用户的 WebSocket 连接。消息中会添加发送者的名称,以便接收者知道消息来自谁。 - **处理断开连接(Leave/Close):**如果用户发送
leave消息或其 WebSocket 连接关闭(触发‘close’事件),服务器会清除其条目,并通知正在与其通话的另一方(如果存在),以便对方也能正常终止 WebRTC 连接。
这个信令服务器非常基础。生产级的信令服务器需要处理认证、可扩展性、错误恢复、用户在线状态以及多方通话的房间管理。然而,此示例涵盖了建立双人 WebRTC 连接所需的基本消息传递要求。
在本章中,我们使用 Node.js 和 WebSockets 构建了一个简单的信令服务器。我们探讨了信令过程,包括用户注册以及 WebRTC 特定消息(如 offer、answer 和 ICE candidate)的中继。这个服务器是 WebRTC 客户端互相发现和连接的关键中介。