# WebRTC 信令模式说明:私有模式 vs 公有模式 ## 📊 核心对比 | 特性 | **私有模式 (private)** | **公有模式 (public)** | |------|----------------------|---------------------| | **连接关系** | 1对多 (Host-Participants) | 多对多 (广播) | | **角色分配** | 有明确的 host 和 participant 角色 | 所有客户端平等,无角色区分 | | **信令路由** | 定向转发 (host ↔ participants) | 全局广播 (除发送者外所有人) | | **连接组管理** | 使用 `connectionGroup` 管理 | 使用 `clients` 全局列表 | | **适用场景** | 视频会议、主控-从控 | 直播、公开房间 | --- ## 🔒 私有模式 (Private Mode) ### 工作原理 ``` 客户端A (Host) ←→ 服务器 ←→ 客户端B (Participant 1) ←→ 客户端C (Participant 2) ←→ 客户端D (Participant 3) ``` ### 关键特性 #### 1. 角色分配机制 - **第一个**连接到服务器的客户端成为 **Host** - **后续**连接的客户端成为 **Participants** - 通过 `polite` 参数区分角色: - `host`: `polite = false` - `participant`: `polite = true` **代码实现** (`src/class/websockethandler.ts` 第 150-176 行): ```typescript function onConnect(ws: WebSocket, connectionId: string): void { let polite = true; // 处理私有模式 if (isPrivate) { if (connectionGroup.has(connectionId)) { const group = connectionGroup.get(connectionId); // 已有host,新连接作为participant加入 group.participants.add(ws); console.log(`Participant joined connectionId: ${connectionId}, total participants: ${group.participants.size}`); } else { // 第一个连接成为host connectionGroup.set(connectionId, { host: ws, participants: new Set() }); polite = false; console.log(`Host created connectionId: ${connectionId}`); } } // 发送连接成功消息(包含角色信息) const role = polite ? 'participant' : 'host'; ws.send(JSON.stringify({ type: "connect", connectionId: connectionId, polite: polite, role: role })); } ``` #### 2. 信令转发规则 - **Host 发送** → 转发给**所有 participants** - **Participant 发送** → 只转发给 **host** - **Participants 之间不能直接通信** **代码实现** (`src/class/websockethandler.ts` 第 96-108 行): ```typescript function broadcastToGroup(connectionId: string, senderWs: WebSocket, message: any): void { const group = connectionGroup.get(connectionId); if (!group) return; // 如果发送者是host,转发给所有participants if (senderWs === group.host) { group.participants.forEach(participantWs => { participantWs.send(JSON.stringify(message)); }); } else { // 如果发送者是participant,转发给host group.host.send(JSON.stringify(message)); } } ``` #### 3. Offer 信令处理 **代码实现** (`src/class/websockethandler.ts` 第 220-243 行): ```typescript function onOffer(ws: WebSocket, message: any): void { const connectionId = message.connectionId as string; const newOffer = new Offer(message.sdp, Date.now(), false); // 处理私有模式 if (isPrivate) { if (connectionGroup.has(connectionId)) { const group = connectionGroup.get(connectionId); if (group.host === ws) { // host发送offer,转发给所有participants newOffer.polite = true; group.participants.forEach(participantWs => { participantWs.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer })); }); } else { // participant发送offer,转发给host newOffer.polite = true; group.host.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer })); } } return; } // ... 公有模式处理 } ``` #### 4. 断开连接处理 - **Host 断开** → 通知所有 participants,删除整个连接组 - **Participant 断开** → 只通知 host,从 participants 中移除 **代码实现** (`src/class/websockethandler.ts` 第 114-142 行): ```typescript function remove(ws: WebSocket): void { const connectionIds = clients.get(ws); if (!connectionIds) return; connectionIds.forEach(connectionId => { const group = connectionGroup.get(connectionId); if (group) { if (group.host === ws) { // host断开连接,通知所有participants group.participants.forEach(participantWs => { participantWs.send(JSON.stringify({ type: "disconnect", connectionId: connectionId })); }); // 删除整个连接组 connectionGroup.delete(connectionId); } else { // participant断开连接,从participants中移除并通知host group.participants.delete(ws); group.host.send(JSON.stringify({ type: "disconnect", connectionId: connectionId })); } } console.log(`Remove connectionId: ${connectionId}`); }); clients.delete(ws); } ``` ### 数据结构 ```typescript interface ConnectionGroup { host: WebSocket; // 主机(第一个连接) participants: Set; // 参与者集合(后续连接) } const connectionGroup: Map = new Map(); ``` ### 应用场景 - ✅ **视频会议** (1个主播 + 多个观众) - ✅ **远程桌面控制** (1个主控 + 多个观察者) - ✅ **直播互动** (主播与观众连麦) - ✅ **教学系统** (教师 + 多个学生) - ✅ **主从控制** (Unity 应用为主机,多个 Web 客户端为参与者) --- ## 🌐 公有模式 (Public Mode) ### 工作原理 ``` 客户端A ←→ 服务器 ←→ 客户端B ↕ (广播) ↕ 客户端C ←────────────→ 客户端D ``` ### 关键特性 #### 1. 无角色区分 - 所有客户端地位平等 - 没有 host/participant 的概念 - `polite` 始终为 `true` #### 2. 全局广播机制 - 任何客户端发送的消息 → **广播给所有其他客户端** - 支持 **多对多** 通信 - 每个客户端既是发送者也是接收者 **代码实现** (`src/class/websockethandler.ts` 第 245-256 行): ```typescript function onOffer(ws: WebSocket, message: any): void { const connectionId = message.connectionId as string; const newOffer = new Offer(message.sdp, Date.now(), false); // ... 私有模式处理 // 公共模式:创建新的连接组(如果不存在) if (!connectionGroup.has(connectionId)) { connectionGroup.set(connectionId, { host: ws, participants: new Set() }); } // 向所有其他客户端广播offer clients.forEach((_v, k) => { if (k == ws) { return; // 跳过发送者 } k.send(JSON.stringify({ from: connectionId, to: "", type: "offer", data: newOffer })); }); } ``` #### 3. 对等通信 **消息流转示例**: ``` A 发送消息 → B、C、D 都收到 B 发送消息 → A、C、D 都收到 C 发送消息 → A、B、D 都收到 D 发送消息 → A、B、C 都收到 ``` ### 应用场景 - ✅ **多人聊天室** (所有用户平等) - ✅ **公开游戏房间** (多玩家对战) - ✅ **协作编辑** (多人实时编辑) - ✅ **广播直播** (单向推流,多人观看) - ✅ **P2P 网状网络** (Full Mesh 拓扑) --- ## 💻 启动方式 ### 私有模式 ```bash # 使用 npm 脚本 npm start -- -m private # 或直接运行 node ./build/index.js -s -p 8080 -m private -k ./server.key -c ./server.cert # 开发模式 npm run dev -- -m private ``` ### 公有模式 ```bash # 使用 npm 脚本(默认模式) npm start # 或明确指定 npm start -- -m public # 或直接运行 node ./build/index.js -s -p 8080 -m public -k ./server.key -c ./server.cert ``` ### 参数说明 | 参数 | 说明 | 示例 | |------|------|------| | `-m` | 通信模式 | `-m private` 或 `-m public` | | `-p` | 端口号 | `-p 8080` | | `-s` | 启用 HTTPS | `-s` | | `-k` | 密钥文件 | `-k ./server.key` | | `-c` | 证书文件 | `-c ./server.cert` | | `-t` | 信令类型 | `-t websocket` 或 `-t http` | --- ## 📝 实际例子 ### 私有模式示例 假设有 3 个客户端连接到同一个 `connectionId: "room1"`: **连接顺序**: 1. **客户端A** 先连接 → 成为 **Host** (`polite: false`) 2. **客户端B** 连接 → 成为 **Participant 1** (`polite: true`) 3. **客户端C** 连接 → 成为 **Participant 2** (`polite: true`) **消息流转**: ``` A (Host) 发送消息 → B 收到 → C 收到 B (Participant) 发送消息 → A 收到 → C 收不到 ❌ C (Participant) 发送消息 → A 收到 → B 收不到 ❌ ``` **信令流程图**: ``` B --[offer]--> 服务器 --[offer]--> A (Host) A --[answer]--> 服务器 --[answer]--> B A --[offer]--> 服务器 --[offer]--> C C --[answer]--> 服务器 --[answer]--> A ``` ### 公有模式示例 假设有 3 个客户端连接: **连接顺序**: 1. **客户端A** 连接 (`polite: true`) 2. **客户端B** 连接 (`polite: true`) 3. **客户端C** 连接 (`polite: true`) **消息流转**: ``` A 发送消息 → B 收到 ✅ → C 收到 ✅ B 发送消息 → A 收到 ✅ → C 收到 ✅ C 发送消息 → A 收到 ✅ → B 收到 ✅ ``` **信令流程图**: ``` A --[offer]--> 服务器 --[offer]--> B A --[offer]--> 服务器 --[offer]--> C B --[offer]--> 服务器 --[offer]--> A B --[offer]--> 服务器 --[offer]--> C C --[offer]--> 服务器 --[offer]--> A C --[offer]--> 服务器 --[offer]--> B ``` --- ## 🔍 代码中的模式判断 ### 初始化模式 **`src/class/websockethandler.ts` 第 62-65 行**: ```typescript function reset(mode: string): void { // 设置是否为私有模式 isPrivate = mode == "private"; } ``` ### 模式判断逻辑 在各个信令处理函数中,通过 `isPrivate` 变量判断: ```typescript // Offer 处理 if (isPrivate) { // 私有模式逻辑:定向转发 } else { // 公有模式逻辑:全局广播 } // Candidate 处理 if (isPrivate) { // 私有模式逻辑 return; } // Message 处理 if (connectionGroup.has(connectionId)) { const group = connectionGroup.get(connectionId); if (group.host === ws) { // host 发送 → 转发给 participants } else { // participant 发送 → 转发给 host } } ``` --- ## 🎯 模式选择建议 ### 选择私有模式的场景 - ✅ 需要明确的主从关系 - ✅ 中心节点需要控制所有通信 - ✅ 参与者之间不需要直接通信 - ✅ 需要管理连接层级结构 - ✅ 资源优化(减少不必要的 P2P 连接) ### 选择公有模式的场景 - ✅ 所有参与者地位平等 - ✅ 需要多对多通信 - ✅ Full Mesh P2P 拓扑 - ✅ 每个客户端都需要相互连接 - ✅ 去中心化的应用场景 --- ## 📊 性能对比 | 指标 | 私有模式 | 公有模式 | |------|---------|---------| | **连接数** | O(n) - 线性增长 | O(n²) - 平方增长 | | **服务器负载** | 中等(需要路由转发) | 较低(主要是广播) | | **客户端负载** | 较低(只与 host 建立连接) | 较高(需要与所有客户端建立连接) | | **网络带宽** | 节省(定向传输) | 较高(广播传输) | | **扩展性** | 好(适合大量 participants) | 受限(连接数随用户数平方增长) | --- ## 🔧 测试方式 ### 单元测试 项目中已包含完整的单元测试: **后端测试** (`test/websockethandler.test.ts`): ```typescript // 公有模式测试 describe('websocket signaling test in public mode', () => { beforeAll(async () => { wsHandler.reset("public"); // ... }); }); // 私有模式测试 describe('websocket signaling test in private mode', () => { beforeAll(async () => { wsHandler.reset("private"); // ... }); }); ``` **前端测试** (`client/test/signaling.test.js`): ```javascript // 公有模式测试 describe.each([ { mode: "mock" }, { mode: "http" }, { mode: "websocket" }, ])('signaling test in public mode', ({ mode }) => { // ... }); // 私有模式测试 describe.each([ { mode: "mock" }, { mode: "http" }, { mode: "websocket" }, ])('signaling test in private mode', ({ mode }) => { // ... }); ``` ### 运行测试 ```bash # 运行后端测试 npm test # 运行前端测试 cd client npm test ``` --- ## 📚 相关文件 - **WebSocket 处理器**: `src/class/websockethandler.ts` - **HTTP 处理器**: `src/class/httphandler.ts` - **WebSocket 服务**: `src/websocket.ts` - **前端信令**: `client/src/signaling.js` - **主入口**: `src/index.ts` - **后端测试**: `test/websockethandler.test.ts` - **前端测试**: `client/test/signaling.test.js` --- ## 💡 总结 | 模式 | 核心特点 | 最佳用途 | |------|---------|---------| | **私有模式** | 1对多、主从架构、定向转发 | 视频会议、远程控制、教学系统 | | **公有模式** | 多对多、对等网络、全局广播 | 聊天室、多人游戏、协作编辑 | 选择合适的模式取决于你的应用架构需求和通信拓扑结构。 --- **文档生成时间**: 2026-04-22 **项目版本**: 3.1.0