作用
WebRTC 本身只处理媒体流的 P2P 传输、编解码与传输优化,但不包含信令协议。WebRTC 的 PeerConnection 建立流程,需要两端完成连接协商和网络打洞信息的交换。这些内容包括:
功能模块 |
说明 |
SDP 协商 |
中转 offer/answer 信息(媒体能力) |
ICE 候选交换 |
中转 NAT 穿透相关的候选地址 |
用户身份验证 |
确保用户合法(如 token 登录) |
房间管理 |
支持多人房间、用户列表维护 |
心跳检测 |
检测用户连接状态 |
广播通知 |
通知用户上线、下线、离开房间 |
拓展支持 |
可扩展为 SFU 适配、统计分析等 |
工作流程
┌──────────────┐
│ Peer A │
└──────┬───────┘
│ Login
▼
┌──────────────┐
│ Signaling │
│ Server │
└──────┬───────┘
│ Notify online
▼
┌──────────────┐
│ Peer B │
└──────────────┘
当 A 和 B 都上线后,建立连接时:
Peer A Signaling Server Peer B
| | |
|────── Login (userA) ────────► | |
| | |
|◄──── Login ack ─────────────── | |
| | |
|───── Signal: Offer ───────────► | ───────► Offer ───────────► |
| | |
|◄──── Signal: Answer ◄────────── | ◄────── Answer ◄─────────── |
| | |
|───── ICE Candidate ───────────► | ───────► Candidate ───────► |
|◄──── ICE Candidate ◄────────── | ◄────── Candidate ◄──────── |
信令消息
类型 |
说明 |
login |
用户登录 |
join |
加入房间 |
leave |
离开房间 |
signal |
转发 SDP / ICE 消息 |
ping |
心跳保活 |
room-users |
查询当前房间用户列表 |
user-joined |
广播:新用户加入房间 |
user-left |
广播:用户离开房间或掉线 |
核心职责
步骤 |
描述 |
用户登录 |
记录客户端 ID,与连接对象关联(如 WebSocket) |
信令转发 |
将一个客户端发来的信令(SDP / ICE)转发给目标客户端 |
用户管理 |
管理在线用户、断线清理、广播状态等 |
会话控制(可选) |
支持 room、会议、group call、用户状态通知等 |
信令服务器部署要求
要求
要点 |
说明 |
公网可访问 |
信令服务器必须有一个公网 IP 或域名 |
使用 TLS/WSS |
推荐使用 wss:// (加密 WebSocket),提升浏览器兼容性和安全性 |
防火墙设置 |
打开 WebSocket 监听端口(默认如 443 , 8443 , 9001 ) |
使用 CDN/反代(可选) |
Nginx、Caddy 等反向代理支持 WSS 路由 |
跨网测试 |
客户端部署在不同网络(如:4G/家宽/云主机)进行真实互通测试 |
部署结构图
+--------------------------+
| 信令服务器 (WSS) |
| wss://signal.example.com |
+--------------------------+
▲ ▲
│ │
WebRTC A WebRTC B
(家宽/4G) (云主机/4G)
A 和 B 都通过 WebSocket 连接到信令服务器。服务器转发 offer/answer/ICE 信息后,A 和 B 就可以尝试建立直连 P2P 链接。
部署流程
- 在云主机或公网服务器部署信令服务器:
./webrtc-signal-server --port 9001
- 配置 Nginx 反向代理 + TLS(WSS):
server {
listen 443 ssl;
server_name signal.example.com;
ssl_certificate /etc/ssl/cert.pem;
ssl_certificate_key /etc/ssl/key.pem;
location / {
proxy_pass http://localhost:9001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
- 客户端连接信令服务器:
const socket = new WebSocket("wss://signal.example.com");
WebRTC整体部署流程图
┌────────────────────┐
│ 信令服务器 │
│ (wss://signal) │
└───────┬────────────┘
│
┌──────────────────┼──────────────────┐
│ │
┌────────▼─────────┐ ┌──────────▼──────────┐
│ Client A │ │ Client B │
│ (WebRTC App) │ │ (WebRTC App) │
└────────┬──────────┘ └──────────┬──────────┘
│ │
┌───────▼────────┐ ┌────────▼───────┐
│ STUN/TURN │◀──────────────────▶│ STUN/TURN │
└────────────────┘ └────────────────┘
示例
#include <uwebsockets/App.h>
#include <unordered_map>
#include <unordered_set>
#include <nlohmann/json.hpp>
#include <iostream>
#include <chrono>
#include <thread>
#include <optional>
using json = nlohmann::json;
using namespace std::chrono;
constexpr int MAX_USERS_PER_ROOM = 5;
struct UserData {
std::string userId;
std::string roomId;
std::string protocol;
time_point<steady_clock> lastPing;
};
using WS = uWS::WebSocket<false, true, UserData>;
std::unordered_map<std::string, WS*> userMap;
std::unordered_map<std::string, std::unordered_set<std::string>> roomMap;
void broadcastToRoom(const std::string& roomId, const std::string& senderId, const std::string& message) {
if (!roomMap.count(roomId)) return;
for (const auto& userId : roomMap[roomId]) {
if (userMap.count(userId)) {
userMap[userId]->send(message, uWS::OpCode::TEXT);
}
}
}
void removeUser(WS* ws) {
auto userId = ws->getUserData()->userId;
auto roomId = ws->getUserData()->roomId;
if (!userId.empty()) {
userMap.erase(userId);
if (!roomId.empty()) {
roomMap[roomId].erase(userId);
json offline = {
{"type", "user-left"},
{"userId", userId},
{"roomId", roomId}
};
broadcastToRoom(roomId, userId, offline.dump());
}
std::cout << "[Disconnected] " << userId << "\n";
}
}
int main() {
std::thread([] {
while (true) {
std::this_thread::sleep_for(seconds(30));
auto now = steady_clock::now();
for (auto it = userMap.begin(); it != userMap.end();) {
auto ws = it->second;
if (duration_cast<seconds>(now - ws->getUserData()->lastPing).count() > 60) {
std::cout << "[Timeout] " << it->first << "\n";
removeUser(ws);
it = userMap.erase(it);
} else {
++it;
}
}
}
}).detach();
uWS::SSLApp({
.key_file_name = "./certs/key.pem",
.cert_file_name = "./certs/cert.pem"
}).ws<UserData>("/*", {
.open = [](WS* ws) {
ws->getUserData()->lastPing = steady_clock::now();
},
.message = [](WS* ws, std::string_view msg, uWS::OpCode) {
try {
json j = json::parse(msg);
std::string type = j["type"];
auto& userData = *ws->getUserData();
if (type == "ping") {
userData.lastPing = steady_clock::now();
} else if (type == "login") {
std::string userId = j["userId"];
userData.userId = userId;
userMap[userId] = ws;
if (j.contains("protocol")) {
userData.protocol = j["protocol"];
}
json ack = {
{"type", "login"},
{"success", true},
{"protocol", userData.protocol}
};
ws->send(ack.dump(), uWS::OpCode::TEXT);
} else if (type == "join") {
std::string roomId = j["roomId"];
if (roomMap[roomId].size() >= MAX_USERS_PER_ROOM) {
json err = {
{"type", "join"},
{"success", false},
{"error", "room-full"}
};
ws->send(err.dump(), uWS::OpCode::TEXT);
return;
}
userData.roomId = roomId;
roomMap[roomId].insert(userData.userId);
json joined = {
{"type", "user-joined"},
{"userId", userData.userId},
{"roomId", roomId},
{"protocol", userData.protocol}
};
broadcastToRoom(roomId, "", joined.dump());
} else if (type == "leave") {
std::string roomId = userData.roomId;
roomMap[roomId].erase(userData.userId);
userData.roomId.clear();
json left = {
{"type", "user-left"},
{"userId", userData.userId},
{"roomId", roomId}
};
broadcastToRoom(roomId, userData.userId, left.dump());
} else if (type == "signal") {
std::string roomId = userData.roomId;
broadcastToRoom(roomId, userData.userId, msg);
} else if (type == "room-users") {
std::string roomId = j["roomId"];
json resp = {
{"type", "room-users"},
{"roomId", roomId},
{"users", json::array()}
};
if (roomMap.count(roomId)) {
for (const auto& uid : roomMap[roomId]) {
resp["users"].push_back(uid);
}
}
ws->send(resp.dump(), uWS::OpCode::TEXT);
}
} catch (...) {
ws->send("{\"type\":\"error\",\"msg\":\"invalid json\"}", uWS::OpCode::TEXT);
}
},
.close = [](WS* ws, int, std::string_view) {
removeUser(ws);
}
})
.listen(9003, [](auto* token) {
if (token) std::cout << "[✔] WSS signaling server running at wss://localhost:9003\n";
else std::cerr << "[✘] Failed to start WSS server\n";
}).run();
}