Skip to content

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 地址、端口),帮助对等端找到最佳连接路径。

典型的信令流程可能如下所示:

  1. 用户 A 和用户 B 连接到信令服务器。
  2. 用户 A 决定呼叫用户 B。用户 A 创建一个 SDP offer。
  3. 用户 A 通过信令服务器将 offer 发送给用户 B。
  4. 用户 B 收到 offer 并创建一个 SDP answer。
  5. 用户 B 通过信令服务器将 answer 发送回用户 A。
  6. 在此过程中,用户 A 和用户 B 也都会发现 ICE candidate 并通过信令服务器交换它们。
  7. 一旦 offer/answer 和 ICE candidate 交换完成,就会尝试建立直接的对等连接。

这只是一个例子;信令逻辑的具体实现可以有很大差异。WebSockets 因其双向、低延迟的通信能力而成为信令的常用选择。

我们的服务器将允许用户使用用户名“登录”,然后促进他们之间 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 9090
const 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”},服务器将记录转发操作。这展示了基本的 संदेश 中继能力。

  • **连接处理:**当新的客户端连接时(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 客户端互相发现和连接的关键中介。