当前位置: 首页 > news >正文

WebRTC(十三):信令服务器

作用

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 链接。

部署流程

  1. 在云主机或公网服务器部署信令服务器:
./webrtc-signal-server --port 9001
  1. 配置 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";}
}
  1. 客户端连接信令服务器:
const socket = new WebSocket("wss://signal.example.com");

WebRTC整体部署流程图

                           ┌────────────────────┐│   信令服务器       ││  (wss://signal)    │└───────┬────────────┘│┌──────────────────┼──────────────────┐│                                      │┌────────▼─────────┐                ┌──────────▼──────────┐│   Client A        │                │     Client B        ││  (WebRTC App)     │                │    (WebRTC App)     │└────────┬──────────┘                └──────────┬──────────┘│                                       │┌───────▼────────┐                    ┌────────▼───────┐│   STUN/TURN    │◀──────────────────▶│   STUN/TURN    │└────────────────┘                    └────────────────┘

示例

// WebRTC 信令服务器(支持房间机制 + 用户状态广播 + 心跳 + 可扩展协议 + WSS + 查询 + 限制房间人数)
// 编译依赖: uWebSockets (v20+) + OpenSSL + pthread + nlohmann::json#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;                // userId -> ws
std::unordered_map<std::string, std::unordered_set<std::string>> roomMap; // roomId -> userId setvoid 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();
}

相关文章:

  • #Redis分布式缓存# ——1.Redis持久化
  • 【Docker基础】Docker容器管理:docker events及其参数详解
  • 06_注意力机制
  • 通过交互式可视化探索波动方程-AI云计算数值分析和代码验证
  • LRU缓存设计与实现详解
  • 什么是MPC(多方安全计算,Multi-Party Computation)
  • word换行居中以后 前面的下划线不显示
  • Python商务数据分析——CHAPTER4-Pandas 数据分析全攻略
  • Qt事件系统
  • 浅谈AI大模型-MCP
  • 机器学习(一)Kaggle泰坦尼克乘客生存预测之线性模型
  • Kafka的下载安装
  • Matlab自学笔记六十一:快速上手解方程
  • 用户行为序列建模(篇九)-【阿里】BERT4Rec
  • 在 Spring Boot 中使用 MyBatis-Plus 的详细教程
  • 实战篇----利用 LangChain 和 BERT 用于命名实体识别-----完整代码
  • Java爬虫实战指南:按关键字搜索京东商品
  • rabbitmq springboot 有哪些配置参数
  • Leetcode 3482. 分析组织层级
  • 状态模式 - Flutter中的状态变身术,让对象随“状态“自由切换行为!