作用
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 ( ) ;
}