当前位置: 首页 > 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();
}
http://www.lryc.cn/news/577013.html

相关文章:

  • Qt Windows下编译动态库生成的.a文件是什么?
  • 新生代潜力股刘小北:演艺路上的璀璨新星
  • Function Calling与MCP的区别
  • Ubuntu开放mysql 3306端口
  • X-Search:Spring AI实现的AI智能搜索
  • SpringMVC实战:从配置到JSON处理全解析
  • AlphaFold3安装报错
  • SpringCloud系列(40)--SpringCloud Gateway的Filter的简介及使用
  • cron 表达式 0 10 0/2 * * ? 的含义
  • Linux基本命令篇 —— head命令
  • 5 c++核心——文件操作
  • Origin绘制复合子母饼状图—复合柱饼图、复合环饼图及复合饼图
  • [Linux] PXE
  • es6特性-第一部分
  • Tomcat 安装使用教程
  • mybatis-plus从入门到入土(一):快速开始
  • 云端可视化耦合电磁场:麦克斯韦方程组的应用-AI云计算数值分析和代码验证
  • Spring Cloud:服务监控与追踪的高级实践
  • 为什么要安装create-vue@3.17.0
  • 2025年渗透测试面试题总结-2025年HW(护网面试) 20(题目+回答)
  • 通过交互式可视化探索波动方程-AI云计算数值分析和代码验证
  • redisson看门狗实现原理
  • ai数据分析黄金
  • RuoYi-Vue学习环境搭建
  • Vue 与react 生命周期对比
  • WSL连不上网解决方案(包含WSL虚拟交换机不正常以及WSL网络配置不正常两种解决方案)
  • wpf的Binding之UpdateSourceTrigger
  • 【惯性导航知识杂记7】舒拉 (休拉、舒勒)周期、傅科周期、地球周期
  • Boss:技能选取——随机概率
  • 【51单片机按下按键1,8位共阴极数码管输出2022-606。按下按键2,8位共阴极数码管输出606-1132。】2022-6-10