515 lines
13 KiB
Markdown
515 lines
13 KiB
Markdown
# 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<WebSocket>() });
|
||
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<WebSocket>; // 参与者集合(后续连接)
|
||
}
|
||
|
||
const connectionGroup: Map<string, ConnectionGroup> = new Map<string, ConnectionGroup>();
|
||
```
|
||
|
||
### 应用场景
|
||
|
||
- ✅ **视频会议** (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<WebSocket>() });
|
||
}
|
||
|
||
// 向所有其他客户端广播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
|