目录
[🏗️ WebRTC项目架构详解](#🏗️ WebRTC项目架构详解)
[📁 项目结构总览](#📁 项目结构总览)
[🌐 网络测试命令](#🌐 网络测试命令)
[🔧 进程管理命令](#🔧 进程管理命令)
[🛠️ 权限管理命令](#🛠️ 权限管理命令)
[📊 系统监控命令](#📊 系统监控命令)
[📋 常用命令组合](#📋 常用命令组合)
[⚠️ 常见问题解决](#⚠️ 常见问题解决)
[1. npm安装卡住](#1. npm安装卡住)
[2. 权限问题](#2. 权限问题)
[3. 端口占用](#3. 端口占用)
[✅ 方法1:Ctrl+C(最简单)](#✅ 方法1:Ctrl+C(最简单))
[✅ 方法2:查找并杀死进程](#✅ 方法2:查找并杀死进程)
[✅ 方法3:使用pkill命令](#✅ 方法3:使用pkill命令)
[✅ 方法4: 使用lsof查找端口](#✅ 方法4: 使用lsof查找端口)
[1. 服务器端模块](#1. 服务器端模块)
[📄 `server.js` - HTTP服务器](#📄 server.js - HTTP服务器)
[📄 `signaling.js` - WebSocket信令服务器](#📄 signaling.js - WebSocket信令服务器)
[📄 `user-manager.js` - 用户管理](#📄 user-manager.js - 用户管理)
[2. 客户端模块](#2. 客户端模块)
[📄 `auth.js` - 认证逻辑](#📄 auth.js - 认证逻辑)
[📄 `webrtc-client.js` - WebRTC核心](#📄 webrtc-client.js - WebRTC核心)
[🚀 完整流程解析](#🚀 完整流程解析)
[1. 服务器启动流程](#1. 服务器启动流程)
[2. 客户端连接流程](#2. 客户端连接流程)
[3. WebRTC连接建立流程](#3. WebRTC连接建立流程)
[🎯 关键技术点](#🎯 关键技术点)
[📡 WebRTC项目协议设计详解](#📡 WebRTC项目协议设计详解)
[🏗️ 协议架构层次](#🏗️ 协议架构层次)
[1. 传输层协议](#1. 传输层协议)
[2. 应用层协议](#2. 应用层协议)
[3. 媒体传输协议](#3. 媒体传输协议)
[📋 协议消息规范](#📋 协议消息规范)
[🔐 认证协议](#🔐 认证协议)
[客户端 → 服务器](#客户端 → 服务器)
[服务器 → 客户端](#服务器 → 客户端)
[👥 用户管理协议](#👥 用户管理协议)
[📞 通话控制协议](#📞 通话控制协议)
[🔄 WebRTC信令协议](#🔄 WebRTC信令协议)
[🔄 完整通信流程](#🔄 完整通信流程)
[1. 连接建立阶段](#1. 连接建立阶段)
[2. 通话建立阶段](#2. 通话建立阶段)
[🛡️ 协议安全设计](#🛡️ 协议安全设计)
[1. 连接安全](#1. 连接安全)
[2. 消息验证](#2. 消息验证)
[3. 状态管理](#3. 状态管理)
[📊 协议性能优化](#📊 协议性能优化)
[1. 消息压缩](#1. 消息压缩)
[2. 连接管理](#2. 连接管理)
[3. 状态同步](#3. 状态同步)
[🎯 协议特点总结](#🎯 协议特点总结)
[✅ 优势](#✅ 优势)
[🔧 技术特点](#🔧 技术特点)
[🔄 协议消息类型总结](#🔄 协议消息类型总结)
**本文摘要:**本文详细解析了一个基于WebRTC的视频通话系统架构与协议设计。系统采用分层结构,服务器端包含HTTP服务器、WebSocket信令服务和用户管理模块;客户端实现认证和WebRTC核心功能。关键协议采用JSON格式消息,涵盖登录认证、用户管理、通话控制等全流程,支持WebRTC信令交换(Offer/Answer/ICE候选)。通过WebSocket实现实时通信后建立P2P音视频连接,具有状态管理完善、错误处理健全等特点,并提供了完整的命令行操作指南和问题解决方案。该系统实现了信令中转与P2P传输的高效结合,确保实时通信质量与系统稳定性。
相关文章,对本文章项目一些概念和细节的补充:WebRTC学习中各项概念笔记
🏗️ WebRTC项目架构详解
📁 项目结构总览
bash
webrtcDemo/
├── server/ # 服务器端
│ ├── server.js # HTTP服务器 + 静态文件服务
│ ├── signaling.js # WebSocket信令服务器
│ ├── user-manager.js # 用户管理模块
│ └── package.json # 依赖配置
└── client/ # 客户端
├── index.html # 主页
├── login.html # 登录页
├── call.html # 通话页
├── css/style.css # 样式文件
└── js/
├── auth.js # 认证逻辑
├── webrtc-client.js # WebRTC核心
└── ui.js # UI交互
服务器启动命令
启动开发服务器
bashnpm start作用: 启动Node.js服务器
说明: 执行package.json中定义的start脚本
输出示例:
> server@1.0.0 start
> node server.js
服务器运行在 http://localhost:3000
直接启动Node.js
bashnode server.js作用: 直接运行服务器文件
说明: 等同于npm start,但更直接
检查node_modules
bashls -la node_modules作用: 检查依赖包是否安装成功
说明: 如果node_modules不存在,说明依赖未安装
🌐 网络测试命令
测试HTTP服务
bashcurl -s http://localhost:3000 | head -10作用: 测试服务器是否响应
参数说明:
- -s: 静默模式
- head -10: 只显示前10行
输出示例:
html<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>WebRTC 视频通话</title>
测试通话页面
bashcurl -s http://localhost:3000/call | head -10作用: 测试通话页面路由
说明: 验证服务器路由配置
🔧 进程管理命令
查看Node.js进程
bashps aux | grep node作用: 查看正在运行的Node.js进程
输出示例:
bashpupu 32063 0.0 0.3 411112512 54640 ?? S 12:52PM 0:00.11 node server.js说明: 确认服务器进程正在运行
等待命令
bashsleep 3作用: 等待3秒
说明: 给服务器启动时间,然后测试连接
🛠️ 权限管理命令
修复npm权限
bashsudo chown -R 501:20 "/Users/pupu/.npm"作用: 修复npm缓存权限问题
说明: 解决EACCES权限错误
注意: 需要管理员权限
📊 系统监控命令
检查网络连接
bashnetstat -an | grep :443作用: 检查HTTPS端口使用情况
说明: 验证网络连接状态
检查进程状态
bashps ajx | grep -v grep|grep npm作用: 查看npm相关进程
说明: 检查npm安装是否卡住
完整工作流程命令序列
项目初始化流程:
1. 进入服务器目录
bashcd /Users/zhouxinrui/Desktop/code/webrtcDemo/server2. 清理npm缓存
bashnpm cache clean --force3. 安装依赖
bashnpm install --no-optional4. 启动服务器
bashnpm start
问题排查流程:
1. 检查目录结构
bashls -la2. 检查依赖安装
bashls -la node_modules3. 测试服务器
bashcurl -s http://localhost:30004. 检查进程
bashps aux | grep node
📋 常用命令组合
快速启动:
bashcd server && npm start完整重启:
bashcd server && npm cache clean --force && npm install && npm start状态检查:
bashps aux | grep node && curl -s http://localhost:3000 | head -5
⚠️ 常见问题解决
1. npm安装卡住
解决方案1: 清理缓存
bashnpm cache clean --force解决方案2: 使用国内镜像
bashnpm config set registry https://registry.npmmirror.com解决方案3: 跳过可选依赖
bashnpm install --no-optional
2. 权限问题
修复npm权限
bash
sudo chown -R 501:20 "/Users/zhouxinrui/.npm"
3. 端口占用
bash
# 检查端口使用
lsof -i :3000
# 杀死进程
kill -9 <PID>
这些命令涵盖了WebRTC项目从安装到运行的完整生命周期,每个命令都有其特定的作用和适用场景!
关闭服务器的方法总结
✅ 方法1:Ctrl+C(最简单)
如果服务器在终端前台运行:
bashCtrl + C✅ 方法2:查找并杀死进程
bash# 查找服务器进程 ps aux | grep "node server.js" # 杀死进程(替换PID为实际进程ID) kill <PID> # 或者强制杀死 kill -9 <PID>✅ 方法3:使用pkill命令
bash# 杀死所有node server.js进程 pkill -f "node server.js" # 或者更精确的匹配 pkill -f "server.js"
✅ 方法4: 使用lsof查找端口
bash# 查找占用3000端口的进程 lsof -i :3000 # 杀死占用端口的进程 kill -9 <PID>
🔧 各模块详细解析
1. 服务器端模块
📄 `server.js` - HTTP服务器
javascript
/*----------------------------模块引入部分------------------------------*/
// 引入Express.js框架(Express是Node.js最流行的应用框架、用于快速搭建HTTP服务器和处理路由)。
const express = require('express');
// 引入 Node.js 内置的 HTTP 模块、用于创建HTTP服务器。
const http = require('http');
// 引入Node.js内置的路径模块、提供处理文件和目录路径的工具函数,如path.join();
const path = require('path');
//引入自定义信令服务器模块、从当前signaling.js文件导入、这个模块包含WebRTC信令服务器的相关逻辑。
const SignalingServer = require('./signaling');
/*----------------------------模块引入部分------------------------------*/
/*-----------------------初始化服务器------------------------------*/
// 创建Express应用实例、这是整个Web应用的核心对象,用于配置中间件、路由等。
const app = express();
// 使用Express应用创建HTTP服务器
const server = http.createServer(app);
/*-----------------------初始化服务器------------------------------*/
/*-----------------------中间件配置部分---------------------------*/
// 设置静态文件目录(客户端文件)
// express.static():Express 的内置中间件,用于提供静态文件(HTML、CSS、JS、图片等)
// path.join(__dirname, '../client'):构建静态文件目录的绝对路径
// __dirname:当前文件所在目录
// '../client':上一级目录中的 client 文件夹
// 这意味着 ../client 目录下的所有文件都可以通过 URL 直接访问
app.use(express.static(path.join(__dirname, '../client')));
/*-----------------------中间件配置部分---------------------------*/
/*-----------------------路由配置部分---------------------------*/
// 提供客户端页面
// 作用:定义根路径 '/' 的路由处理
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, '../client/login.html'));
});
// 说明:
// 当用户访问网站根路径时(如 http://localhost:3000/)
// 服务器会发送 ../client/login.html 文件给客户端
// res.sendFile():发送整个文件内容,而不是渲染模板
// 定义 '/call' 路径的路由处理
app.get('/call', (req, res) => {
res.sendFile(path.join(__dirname, '../client/call.html'));
});
// 说明:
// 当用户访问 /call 路径时(如 http://localhost:3000/call)
// 服务器发送 ../client/call.html 文件
// 这很可能是视频通话的主页面
/*-----------------------路由配置部分---------------------------*/
/*-----------------------启动信令服务器---------------------------*/
// 启动信令服务器
new SignalingServer(server);
/*
实例化 SignalingServer 类,并将 HTTP 服务器实例传递给它
这样信令服务器就可以在同一个端口上处理 WebSocket 连接
信令服务器负责处理 WebRTC 的 Offer/Answer 交换和 ICE 候选信息传递
*/
/*-----------------------启动信令服务器---------------------------*/
// 启动 HTTP 服务器
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`服务器运行在 http://localhost:${PORT}`);
});
/*-----------------------启动信令服务器---------------------------*/
核心功能:
- 🌐 提供HTTP服务(端口3000)
- 📁 静态文件服务(客户端文件)
- 🔗 路由管理(首页、登录页、通话页)
📄 `signaling.js` - WebSocket信令服务器
javascript
// 导入必要的模块:
const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid');
const UserManager = require('./user-manager');
class SignalingServer {
constructor(server) {
// 基于传入的HTTP服务器创建WebSocket服务器
this.wss = new WebSocket.Server({ server });
// 创建用户管理器实例,用于管理在线用户和通话状态
this.userManager = new UserManager();
// 设置WebSocket事件监听:
this.setupWebSocket();
}
setupWebSocket() {
this.wss.on('connection', (ws) => {
console.log('新的WebSocket连接');
ws.on('message', (data) => { // 处理接收到的WebSocket消息
try {
const message = JSON.parse(data); // 解析JSON格式的消息
this.handleMessage(ws, message); // 调用消息处理器
} catch (error) {
console.error('消息解析错误:', error);
this.sendError(ws, '消息格式错误'); // 发送格式错误响应
}
});
ws.on('close', () => { // 处理WebSocket连接关闭
console.log('WebSocket连接关闭');
const user = this.userManager.removeUser(ws); // 从用户管理器中移除用户
if (user) {
this.broadcastUserList(); // 广播更新后的用户列表
}
});
ws.on('error', (error) => { // 处理WebSocket错误
console.error('WebSocket错误:', error);
});
});
}
/**
* 处理WebSocket接收到的消息
*
* @param {WebSocket} ws - 客户端WebSocket连接对象
* @param {Object} message - 接收到的消息对象
* @param {string} message.type - 消息类型
* @param {Object} message.data - 消息数据
*
* @description
* 根据不同的消息类型调用对应的处理函数:
* - login: 处理登录请求
* - call_request: 处理呼叫请求
* - call_response: 处理呼叫响应
* - offer: 处理WebRTC offer
* - answer: 处理WebRTC answer
* - ice_candidate: 处理ICE候选
* - hangup: 处理挂断请求
* - 其他: 返回未知类型错误
*/
handleMessage(ws, message) {
const { type, data } = message;
switch (type) {
case 'login':
this.handleLogin(ws, data);
break;
case 'call_request':
this.handleCallRequest(ws, data);
break;
case 'call_response':
this.handleCallResponse(ws, data);
break;
case 'offer':
this.handleOffer(ws, data);
break;
case 'answer':
this.handleAnswer(ws, data);
break;
case 'ice_candidate':
this.handleIceCandidate(ws, data);
break;
case 'hangup':
this.handleHangup(ws, data);
break;
default:
console.log('未知消息类型:', type);
this.sendError(ws, '未知消息类型');
}
}
// 处理用户登录
handleLogin(ws, data) {
const { userName } = data;
// 验证用户名不为空
if (!userName) {
this.sendError(ws, '用户名不能为空');
return;
}
// 生成唯一用户ID并添加用户
const userId = uuidv4();
const user = this.userManager.addUser(userId, userName, ws);
// 发送登录成功响应
this.sendTo(ws, {
type: 'login_success',
data: {
userId,
userName,
onlineUsers: this.userManager.getOnlineUsers()
}
});
// 广播更新用户列表
this.broadcastUserList();
}
/**
* 处理呼叫请求
*
* @param {WebSocket} ws - 发起呼叫的用户WebSocket连接
* @param {Object} data - 呼叫请求数据
* @param {string} data.toUserId - 被呼叫用户的ID
*
* @description
* 验证呼叫请求的有效性,包括:
* - 检查主叫用户是否已登录
* - 检查被叫用户是否在线
* - 检查被叫用户是否正在通话中
*
* 验证通过后:
* - 设置双方用户的通话状态为通话中
* - 向被叫用户发送呼叫请求通知
* - 广播更新用户列表
*/
handleCallRequest(ws, data) {
const fromUser = this.userManager.getUserBySocket(ws);
const { toUserId } = data;
if (!fromUser) {
this.sendError(ws, '请先登录');
return;
}
const toUser = this.userManager.getUser(toUserId);
if (!toUser || !toUser.online) {
this.sendError(ws, '用户不在线');
return;
}
if (toUser.inCall) {
this.sendError(ws, '用户正在通话中');
return;
}
// 设置用户通话状态
this.userManager.setUserInCall(fromUser.id, true);
this.userManager.setUserInCall(toUserId, true);
// 发送呼叫请求给被叫方
this.sendTo(toUser.ws, {
type: 'call_request',
data: {
fromUserId: fromUser.id,
fromUserName: fromUser.name
}
});
// 广播更新用户列表
this.broadcastUserList();
}
// 处理呼叫响应
handleCallResponse(ws, data) {
const user = this.userManager.getUserBySocket(ws);
const { toUserId, accepted } = data;
if (!user) {
this.sendError(ws, '请先登录');
return;
}
const toUser = this.userManager.getUser(toUserId);
if (!toUser || !toUser.online) {
this.sendError(ws, '用户不在线');
return;
}
// 发送呼叫响应给主叫方
this.sendTo(toUser.ws, {
type: 'call_response',
data: {
fromUserId: user.id,
fromUserName: user.name,
accepted
}
});
if (!accepted) {
// 如果拒绝通话,重置通话状态
this.userManager.setUserInCall(user.id, false);
this.userManager.setUserInCall(toUserId, false);
this.broadcastUserList();
}
}
// 处理 WebRTC Offer
handleOffer(ws, data) {
const fromUser = this.userManager.getUserBySocket(ws);
const { toUserId, offer } = data;
if (!fromUser) {
this.sendError(ws, '请先登录');
return;
}
const toUser = this.userManager.getUser(toUserId);
if (!toUser || !toUser.online) {
this.sendError(ws, '用户不在线');
return;
}
// 转发 Offer 给被叫方
this.sendTo(toUser.ws, {
type: 'offer',
data: {
fromUserId: fromUser.id,
offer
}
});
}
// 处理 WebRTC Answer
handleAnswer(ws, data) {
const fromUser = this.userManager.getUserBySocket(ws);
const { toUserId, answer } = data;
if (!fromUser) {
this.sendError(ws, '请先登录');
return;
}
const toUser = this.userManager.getUser(toUserId);
if (!toUser || !toUser.online) {
this.sendError(ws, '用户不在线');
return;
}
// 转发 Answer 给主叫方
this.sendTo(toUser.ws, {
type: 'answer',
data: {
fromUserId: fromUser.id,
answer
}
});
}
// 处理 ICE 候选
handleIceCandidate(ws, data) {
const fromUser = this.userManager.getUserBySocket(ws);
const { toUserId, candidate } = data;
if (!fromUser) {
this.sendError(ws, '请先登录');
return;
}
const toUser = this.userManager.getUser(toUserId);
if (!toUser || !toUser.online) {
this.sendError(ws, '用户不在线');
return;
}
// 转发 ICE 候选
this.sendTo(toUser.ws, {
type: 'ice_candidate',
data: {
fromUserId: fromUser.id,
candidate
}
});
}
// 处理挂断
handleHangup(ws, data) {
const user = this.userManager.getUserBySocket(ws);
const { toUserId } = data;
if (!user) {
this.sendError(ws, '请先登录');
return;
}
// 重置通话状态
this.userManager.setUserInCall(user.id, false);
// 如果指定了对方,也重置对方状态并通知
if (toUserId) {
this.userManager.setUserInCall(toUserId, false);
const toUser = this.userManager.getUser(toUserId);
if (toUser && toUser.online) {
this.sendTo(toUser.ws, {
type: 'hangup',
data: {
fromUserId: user.id
}
});
}
}
// 广播更新用户列表
this.broadcastUserList();
}
// 发送消息给指定 WebSocket
sendTo(ws, message) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
}
}
// 发送错误消息
sendError(ws, errorMessage) {
this.sendTo(ws, {
type: 'error',
data: { message: errorMessage }
});
}
// 广播用户列表给所有客户端
broadcastUserList() {
const userList = this.userManager.getOnlineUsers();
const message = {
type: 'user_list',
data: { users: userList }
};
this.wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(message));
}
});
}
}
module.exports = SignalingServer;
核心功能:
-
🔌 WebSocket连接管理
-
📨 消息路由和转发
-
👥 用户状态管理
-
🔄 WebRTC信令转发
📄 `user-manager.js` - 用户管理
cpp
class UserManager {
constructor() {
this.users = new Map(); // userId -> {id, name, ws, online, inCall}
this.userSockets = new Map(); // socket -> user
}
// 添加用户
addUser(userId, userName, ws) {
const user = {
id: userId,
name: userName,
ws: ws,
online: true,
inCall: false
};
this.users.set(userId, user);
this.userSockets.set(ws, user);
console.log(`用户 ${userName} (${userId}) 上线`);
return user;
}
// 移除用户
removeUser(ws) {
const user = this.userSockets.get(ws);
if (user) {
user.online = false;
this.users.delete(user.id);
this.userSockets.delete(ws);
console.log(`用户 ${user.name} 下线`);
return user;
}
return null;
}
// 获取用户
getUser(userId) {
return this.users.get(userId);
}
// 获取所有在线用户(包括通话中的用户)
getOnlineUsers() {
const onlineUsers = [];
for (const [id, user] of this.users) {
if (user.online) {
onlineUsers.push({
id: user.id,
name: user.name,
inCall: user.inCall,
status: user.inCall ? '通话中' : '在线'
});
}
}
return onlineUsers;
}
// 设置用户通话状态
setUserInCall(userId, inCall) {
const user = this.users.get(userId);
if (user) {
user.inCall = inCall;
}
}
// 通过WebSocket获取用户
getUserBySocket(ws) {
return this.userSockets.get(ws);
}
}
module.exports = UserManager;
核心功能:
- 👤 用户注册和认证
- 📊 在线用户管理(包括通话中用户)
- 🔄 用户状态跟踪
- 📡 用户列表广播
2. 客户端模块
📄 `auth.js` - 认证逻辑
javascript
// 登录逻辑
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault(); // 阻止表单默认提交
const userName = document.getElementById('userName').value.trim();
const errorMessage = document.getElementById('errorMessage');
if (!userName) {
showError('请输入用户名');
return;
}
// 存储用户名并跳转到通话页面
localStorage.setItem('userName', userName);
window.location.href = '/call';
});
function showError(message) {
const errorElement = document.getElementById('errorMessage');
errorElement.textContent = message;
errorElement.style.display = 'block';
}
// 检查是否已登录
window.addEventListener('DOMContentLoaded', () => {
const savedUserName = localStorage.getItem('userName');
if (savedUserName && window.location.pathname === '/') {
window.location.href = '/call';
}
});
核心功能:
- 🔐 用户登录验证
- 💾 本地存储管理
- 🔄 自动登录检查
📄 `webrtc-client.js` - WebRTC核心
javascript
class WebRTCClient {
constructor() {
this.ws = null;
this.userId = null;
this.userName = null;
this.peerConnection = null;
this.localStream = null;
this.remoteStream = null;
this.currentCall = null;
// WebRTC 配置
this.rtcConfig = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' }, //提供服务器地址,帮助NAT获取公网IP
{ urls: 'stun:stun1.l.google.com:19302' }
]
};
this.init();
}
init() {
this.setupEventListeners();
this.connectWebSocket();
this.setupMedia();
}
/**
* 设置所有UI事件监听器
*
* 该方法负责为WebRTC客户端的所有用户界面元素绑定事件处理函数,
* 包括登出按钮、通话控制按钮(视频切换、音频切换、挂断)以及来电处理按钮。
*
* @memberof WebRTCClient
* @returns {void}
*/
setupEventListeners() {
// 登出按钮
document.getElementById('logoutBtn').addEventListener('click', () => {
this.logout();
});
// 通话控制按钮
document.getElementById('toggleVideo').addEventListener('click', () => {
this.toggleVideo();
});
document.getElementById('toggleAudio').addEventListener('click', () => {
this.toggleAudio();
});
document.getElementById('hangupBtn').addEventListener('click', () => {
this.hangup();
});
// 来电处理
document.getElementById('acceptCall').addEventListener('click', () => {
this.acceptCall();
});
document.getElementById('rejectCall').addEventListener('click', () => {
this.rejectCall();
});
}
// 连接 WebSocket
connectWebSocket() {
// 根据当前协议确定 WebSocket 协议(HTTP 用 ws://,HTTPS 用 wss://)
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}`;
// 创建 WebSocket 连接
this.ws = new WebSocket(wsUrl);
// 连接建立成功回调
this.ws.onopen = () => {
console.log('WebSocket 连接已建立');
this.login(); // 连接成功后立即登录
};
// 接收服务器消息回调
this.ws.onmessage = (event) => {
this.handleMessage(JSON.parse(event.data)); // 解析并处理消息
};
// 连接关闭回调
this.ws.onclose = () => {
console.log('WebSocket 连接已关闭');
// 尝试重新连接(3秒后重连)
setTimeout(() => this.connectWebSocket(), 3000);
};
// 连接错误回调
this.ws.onerror = (error) => {
console.error('WebSocket 错误:', error);
};
}
// 处理服务器消息
handleMessage(message) {
const { type, data } = message;
switch (type) {
case 'login_success':
this.handleLoginSuccess(data);
break;
case 'user_list':
this.updateUserList(data.users);
break;
case 'call_request':
this.handleIncomingCall(data);
break;
case 'call_response':
this.handleCallResponse(data);
break;
case 'offer':
this.handleOffer(data);
break;
case 'answer':
this.handleAnswer(data);
break;
case 'ice_candidate':
this.handleIceCandidate(data);
break;
case 'hangup':
this.handleRemoteHangup(data);
break;
case 'error':
this.showError(data.message);
break;
}
}
/**
* 发送消息到服务器
*
* @param {Object} message - 要发送的消息对象
* @param {string} message.type - 消息类型
* @param {Object} message.data - 消息数据
*/
sendMessage(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
}
}
// 用户登录
login() {
const userName = localStorage.getItem('userName');
if (!userName) {
window.location.href = '/';
return;
}
this.userName = userName;
document.getElementById('userNameDisplay').textContent = userName;
this.sendMessage({
type: 'login',
data: { userName }
});
}
// 处理登录成功
handleLoginSuccess(data) {
this.userId = data.userId;
this.updateUserList(data.onlineUsers);
}
// 更新通话状态显示
updateCallStatus() {
if (this.currentCall) {
const statusElement = document.getElementById('callStatus');
const remoteUserInfo = document.getElementById('remoteUserInfo');
const remoteVideoTitle = document.getElementById('remoteVideoTitle');
if (this.currentCall.initiated) {
// 发起方显示"正在呼叫..."
if (statusElement) {
statusElement.textContent = '正在呼叫...';
}
if (remoteVideoTitle) {
remoteVideoTitle.textContent = '等待对方接听...';
}
} else {
// 接听方显示"与 [对方姓名] 通话中"
if (statusElement) {
statusElement.textContent = `与 ${this.currentCall.fromUserName} 通话中`;
}
if (remoteVideoTitle) {
remoteVideoTitle.textContent = `${this.currentCall.fromUserName} 的视频`;
}
// 显示对方信息
this.showRemoteUserInfo(this.currentCall.fromUserName);
}
}
}
// 显示对方用户信息
showRemoteUserInfo(userName) {
const remoteUserInfo = document.getElementById('remoteUserInfo');
const userNameDisplay = document.querySelector('.user-name-display');
if (remoteUserInfo && userNameDisplay) {
userNameDisplay.textContent = userName;
remoteUserInfo.style.display = 'flex';
}
}
// 隐藏对方用户信息
hideRemoteUserInfo() {
const remoteUserInfo = document.getElementById('remoteUserInfo');
if (remoteUserInfo) {
remoteUserInfo.style.display = 'none';
}
}
// 更新发起方的通话状态
updateCallStatusForInitiator(remoteUserName) {
const statusElement = document.getElementById('callStatus');
const remoteVideoTitle = document.getElementById('remoteVideoTitle');
if (statusElement) {
statusElement.textContent = `与 ${remoteUserName} 通话中`;
}
if (remoteVideoTitle) {
remoteVideoTitle.textContent = `${remoteUserName} 的视频`;
}
}
// 更新用户列表
updateUserList(users) {
const userListElement = document.getElementById('userList');
userListElement.innerHTML = '';
users.forEach(user => {
if (user.id !== this.userId) {
const userElement = document.createElement('div');
userElement.className = 'user-item';
// 根据用户状态显示不同的按钮和样式
let buttonHtml = '';
let statusClass = '';
if (user.inCall) {
buttonHtml = '<span class="status-badge in-call">通话中</span>';
statusClass = 'in-call';
} else {
buttonHtml = '<button class="call-btn" data-user-id="' + user.id + '">呼叫</button>';
statusClass = 'available';
}
userElement.className = `user-item ${statusClass}`;
userElement.innerHTML = `
<span class="user-name">${user.name}</span>
<span class="user-status">${user.status}</span>
${buttonHtml}
`;
userListElement.appendChild(userElement);
// 只有非通话中的用户才能被呼叫
if (!user.inCall) {
userElement.querySelector('.call-btn').addEventListener('click', () => {
this.startCall(user.id);
});
}
}
});
}
// 开始呼叫
async startCall(toUserId) {
if (this.currentCall) {
this.showError('您正在通话中');
return;
}
this.currentCall = {
toUserId,
initiated: true
};
// 只发送呼叫请求,不立即创建WebRTC连接
this.sendMessage({
type: 'call_request',
data: { toUserId }
});
this.updateUIForCall(true);
}
// 处理来电
handleIncomingCall(data) {
this.currentCall = {
fromUserId: data.fromUserId,
fromUserName: data.fromUserName,
initiated: false
};
// 显示来电对话框
document.getElementById('callerName').textContent = data.fromUserName;
document.getElementById('incomingCallModal').style.display = 'block';
}
// 接听来电
async acceptCall() {
document.getElementById('incomingCallModal').style.display = 'none';
// 发送呼叫接受响应
this.sendMessage({
type: 'call_response',
data: {
toUserId: this.currentCall.fromUserId,
accepted: true
}
});
this.updateUIForCall(true);
// 更新通话状态显示
this.updateCallStatus();
}
// 拒绝来电
rejectCall() {
document.getElementById('incomingCallModal').style.display = 'none';
this.sendMessage({
type: 'call_response',
data: {
toUserId: this.currentCall.fromUserId,
accepted: false
}
});
this.currentCall = null;
}
// 处理呼叫响应
async handleCallResponse(data) {
if (!data.accepted) {
this.showError('对方拒绝了您的呼叫');
this.hangup();
return;
}
// 对方接受呼叫后,开始建立WebRTC连接
try {
// 创建 PeerConnection
this.createPeerConnection();
// 添加本地流
if (this.localStream) {
this.localStream.getTracks().forEach(track => {
this.peerConnection.addTrack(track, this.localStream);
});
}
// 创建并发送 Offer
const offer = await this.peerConnection.createOffer();
await this.peerConnection.setLocalDescription(offer);
this.sendMessage({
type: 'offer',
data: {
toUserId: this.currentCall.toUserId,
offer
}
});
// 更新发起方的通话状态 - 显示与对方通话中
this.updateCallStatusForInitiator(data.fromUserName || '对方');
// 显示对方信息(发起方)
this.showRemoteUserInfo(data.fromUserName || '对方');
} catch (error) {
console.error('建立WebRTC连接失败:', error);
this.showError('连接失败');
this.hangup();
}
}
// 创建 PeerConnection
createPeerConnection() {
// 创建新的RTCPeerConnection实例
this.peerConnection = new RTCPeerConnection(this.rtcConfig);
// 处理远程流
this.peerConnection.ontrack = (event) => {
const remoteVideo = document.getElementById('remoteVideo');
if (event.streams && event.streams[0]) {
remoteVideo.srcObject = event.streams[0]; // 设置远程视频源
this.remoteStream = event.streams[0]; // 保存远程流引用
}
};
// 处理 ICE 候选
this.peerConnection.onicecandidate = (event) => {
if (event.candidate && this.currentCall) {
// 确定消息接收方ID
const toUserId = this.currentCall.initiated ?
this.currentCall.toUserId : this.currentCall.fromUserId;
// 发送ICE候选信息给对方
this.sendMessage({
type: 'ice_candidate',
data: {
toUserId,
candidate: event.candidate
}
});
}
};
// 处理连接状态变化
this.peerConnection.onconnectionstatechange = () => {
console.log('连接状态:', this.peerConnection.connectionState);
if (this.peerConnection.connectionState === 'connected') {
console.log('WebRTC 连接已建立');
} else if (this.peerConnection.connectionState === 'disconnected' ||
this.peerConnection.connectionState === 'failed') {
console.log('WebRTC 连接断开');
this.hangup(); // 连接断开时挂断通话
}
};
}
// 处理 Offer
async handleOffer(data) {
// 只有在接听状态下才处理Offer
if (!this.currentCall || this.currentCall.initiated) {
console.log('忽略Offer:未在接听状态');
return;
}
if (!this.peerConnection) {
this.createPeerConnection();
// 添加本地流
if (this.localStream) {
this.localStream.getTracks().forEach(track => {
this.peerConnection.addTrack(track, this.localStream);
});
}
}
try {
await this.peerConnection.setRemoteDescription(data.offer);
const answer = await this.peerConnection.createAnswer();
await this.peerConnection.setLocalDescription(answer);
this.sendMessage({
type: 'answer',
data: {
toUserId: data.fromUserId,
answer
}
});
} catch (error) {
console.error('处理 Offer 失败:', error);
this.hangup();
}
}
// 处理 Answer
async handleAnswer(data) {
try {
await this.peerConnection.setRemoteDescription(data.answer);
} catch (error) {
console.error('处理 Answer 失败:', error);
this.hangup();
}
}
// 处理 ICE 候选
async handleIceCandidate(data) {
try {
await this.peerConnection.addIceCandidate(data.candidate);
} catch (error) {
console.error('添加 ICE 候选失败:', error);
}
}
// 处理远程挂断
handleRemoteHangup() {
this.showError('对方已挂断');
this.hangup();
}
// 挂断通话
hangup() {
if (this.currentCall) {
const toUserId = this.currentCall.initiated ?
this.currentCall.toUserId : this.currentCall.fromUserId;
this.sendMessage({
type: 'hangup',
data: { toUserId }
});
}
this.cleanupCall();
}
// 清理通话资源
cleanupCall() {
if (this.peerConnection) {
this.peerConnection.close();
this.peerConnection = null;
}
// 清除远程视频
const remoteVideo = document.getElementById('remoteVideo');
remoteVideo.srcObject = null;
this.remoteStream = null;
// 隐藏对方信息
this.hideRemoteUserInfo();
// 重置远程视频标题
const remoteVideoTitle = document.getElementById('remoteVideoTitle');
if (remoteVideoTitle) {
remoteVideoTitle.textContent = '远程视频';
}
// 重置通话状态
const callStatus = document.getElementById('callStatus');
if (callStatus) {
callStatus.textContent = '等待通话...';
}
this.currentCall = null;
this.updateUIForCall(false);
}
// 设置媒体流
async setupMedia() {
try {
// 获取用户摄像头和麦克风权限
// 获取媒体流:使用 navigator.mediaDevices.getUserMedia() API 请求访问用户的摄像头和视频设备
this.localStream = await navigator.mediaDevices.getUserMedia({
video: true, //启用摄像头
audio: true //启用麦克风
});
// 将本地视频流显示在页面上
const localVideo = document.getElementById('localVideo');
localVideo.srcObject = this.localStream;
} catch (error) {
console.error('获取媒体设备失败:', error);
this.showError('无法访问摄像头或麦克风');
}
}
// 切换视频
toggleVideo() {
if (this.localStream) {
const videoTracks = this.localStream.getVideoTracks();
if (videoTracks.length > 0) {
const enabled = !videoTracks[0].enabled; // 切换视频轨道启用状态
videoTracks[0].enabled = enabled;
const button = document.getElementById('toggleVideo');
button.textContent = enabled ? '关闭视频' : '开启视频'; // 更新按钮文本
button.classList.toggle('muted', !enabled); // 切换静音样式
}
}
}
// 切换音频
toggleAudio() {
if (this.localStream) {
const audioTracks = this.localStream.getAudioTracks();
if (audioTracks.length > 0) {
const enabled = !audioTracks[0].enabled;
audioTracks[0].enabled = enabled;
const button = document.getElementById('toggleAudio');
button.textContent = enabled ? '关闭音频' : '开启音频';
button.classList.toggle('muted', !enabled);
}
}
}
// 更新通话界面
updateUIForCall(inCall) {
const hangupBtn = document.getElementById('hangupBtn');
const userList = document.getElementById('userList');
hangupBtn.disabled = !inCall;
userList.style.opacity = inCall ? '0.5' : '1';
// 禁用用户列表中的呼叫按钮
const callButtons = userList.querySelectorAll('.call-btn');
callButtons.forEach(btn => {
btn.disabled = inCall;
});
}
// 显示错误信息
showError(message) {
// 在实际应用中,可以使用更友好的方式显示错误
alert(message);
}
// 用户登出
logout() {
if (this.currentCall) {
this.hangup();
}
localStorage.removeItem('userName');
if (this.ws) {
this.ws.close();
}
window.location.href = '/';
}
}
// 初始化 WebRTC 客户端
let webrtcClient;
document.addEventListener('DOMContentLoaded', () => {
if (window.location.pathname === '/call') {
webrtcClient = new WebRTCClient();
}
});
核心功能:
- 🔌 WebSocket通信
- 🎥 媒体流管理
- 🔄 WebRTC连接建立(修复后)
- 📡 信令处理
- 👤 对方用户信息显示
- 📊 通话状态管理
🚀 完整流程解析
1. 服务器启动流程

2. 客户端连接流程

3. WebRTC连接建立流程

🔄 详细实现步骤
步骤1:服务器启动
javascript
graph TD
A[启动 server.js] --> B[创建 Express 应用]
B --> C[创建 HTTP 服务器]
C --> D[设置静态文件服务]
D --> E[配置路由]
E --> F[启动信令服务器]
F --> G[监听端口 3000]
G --> H[服务器运行中]
步骤2:客户端连接
javascript
graph TD
A[用户访问 localhost:3000] --> B[加载 login.html]
B --> C[用户输入用户名]
C --> D[存储到 localStorage]
D --> E[跳转到 /call]
E --> F[加载 call.html]
F --> G[初始化 WebRTCClient]
G --> H[建立 WebSocket 连接]
H --> I[发送登录消息]
I --> J[服务器验证并返回用户列表]
步骤3:用户认证
javascript
graph TD
A[用户A点击呼叫用户B] --> B[发送呼叫请求]
B --> C[用户B收到呼叫请求]
C --> D[用户B选择接听/拒绝]
D --> E{用户B是否接听?}
E -->|拒绝| F[显示拒绝消息]
E -->|接听| G[发送接听响应]
G --> H[用户A收到接听响应]
H --> I[用户A创建PeerConnection]
I --> J[用户A创建Offer]
J --> K[用户B处理Offer]
K --> L[用户B创建Answer]
L --> M[交换ICE候选]
M --> N[建立P2P连接]
N --> O[开始音视频传输]
O --> P[显示对方用户信息]
P --> Q[更新通话状态显示]
🎯 关键技术点
- 信令服务器的作用
- 🔄 转发WebRTC信令消息
- 👥 管理用户状态(包括通话中用户)
- 📡 广播用户列表
- 🔐 处理用户认证
javascriptpeerConnection.onicecandidate = (event) => { if(event.candidate) { // 发送候选至对方 signalingServer.send({type: 'candidate', candidate: event.candidate}); } };
- 实时通信机制(最终版)
- 🔌 WebSocket:实时双向通信
- 📡 信令转发:服务器中转信令
- 🎥 媒体传输:客户端直连
- 🔄 状态同步:用户列表实时更新
- 🛡️ 状态保护:通过状态控制连接建立时机
- 📊 状态显示:通话状态和对方信息实时显示
📡 WebRTC项目协议设计详解
基于代码分析,这个项目采用了分层协议架构,让我为您详细解析:
🏗️ 协议架构层次
1. 传输层协议
HTTP/HTTPS (端口3000) + WebSocket (实时通信)
2. 应用层协议
JSON消息格式 + 自定义信令协议
3. 媒体传输协议
WebRTC (P2P音视频传输)
📋 协议消息规范
🔐 认证协议
客户端 → 服务器
javascript
{
"type": "login",
"data": {
"userName": "pupu"
}
}
服务器 → 客户端
javascript
{
"type": "login_success",
"data": {
"userId": "e7a47af4-6919-4e42-8446-2a3a4b02cecc",
"userName": "pupu",
"onlineUsers": [
{
"id": "user1",
"name": "user1",
"inCall": false,
"status": "在线"
},
{
"id": "user2",
"name": "user2",
"inCall": true,
"status": "通话中"
}
]
}
}
👥 用户管理协议
用户列表广播
javascript
{
"type": "user_list",
"data": {
"users": [
{
"id": "user1",
"name": "user1",
"inCall": false,
"status": "在线"
},
{
"id": "user2",
"name": "user2",
"inCall": true,
"status": "通话中"
}
]
}
}
📞 通话控制协议
发起通话
javascript
{
"type": "call_request",
"data": {
"toUserId": "target-user-id"
}
}
通话响应
javascript
{
"type": "call_response",
"data": {
"toUserId": "caller-user-id",
"fromUserId": "responder-user-id",
"fromUserName": "responder-name",
"accepted": true
}
}
🔄 WebRTC信令协议
Offer消息
javascript
{
"type": "offer",
"data": {
"toUserId": "target-user-id",
"offer": {
"type": "offer",
"sdp": "v=0\r\no=- 1234567890 1234567890 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=msid-semantic: WMS\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:abc123\r\na=ice-pwd:def456\r\na=ice-options:trickle\r\na=fingerprint:sha-256 AA:BB:CC:DD:EE:FF\r\na=setup:actpass\r\na=mid:0\r\na=sendrecv\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 VP8/90000\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=ssrc:1234567890 cname:user@host\r\na=ssrc:1234567890 msid:stream0 video0\r\na=ssrc:1234567890 mslabel:stream0\r\na=ssrc:1234567890 label:video0"
}
}
}
Answer消息
javascript
{
"type": "answer",
"data": {
"toUserId": "caller-user-id",
"answer": {
"type": "answer",
"sdp": "v=0\r\no=- 1234567890 1234567890 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=msid-semantic: WMS\r\nm=video 9 UDP/TLS/RTP/SAVPF 96\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:xyz789\r\na=ice-pwd:uvw012\r\na=ice-options:trickle\r\na=fingerprint:sha-256 FF:EE:DD:CC:BB:AA\r\na=setup:active\r\na=mid:0\r\na=sendrecv\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 VP8/90000\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=ssrc:0987654321 cname:user@host\r\na=ssrc:0987654321 msid:stream0 video0\r\na=ssrc:0987654321 mslabel:stream0\r\na=ssrc:0987654321 label:video0"
}
}
}
ICE候选
javascript
{
"type": "ice_candidate",
"data": {
"toUserId": "target-user-id",
"candidate": {
"candidate": "candidate:1 1 UDP 2113667326 192.168.1.100 54400 typ host",
"sdpMLineIndex": 0,
"sdpMid": "0"
}
}
}
挂断消息
javascript
{
"type": "hangup",
"data": {
"toUserId": "target-user-id"
}
}
错误消息
javascript
{
"type": "error",
"data": {
"message": "用户不在线"
}
}
🔄 完整通信流程
1. 连接建立阶段

2. 通话建立阶段


🛡️ 协议安全设计
1. 连接安全
- WebSocket over HTTP/HTTPS
- 支持WSS加密连接
- 自动重连机制
2. 消息验证
javascriptsequenceDiagram participant C as 客户端 participant S as 服务器 C->>S: WebSocket连接 S-->>C: 连接确认 C->>S: {"type":"login","data":{"userName":"pupu"}} S->>S: 生成userId, 注册用户 S-->>C: {"type":"login_success","data":{...}} S->>S: 广播用户列表更新3. 状态管理·
javascriptsequenceDiagram participant A as 用户A participant S as 服务器 participant B as 用户B A->>S: {"type":"call_request","data":{"toUserId":"B"}} S->>B: {"type":"call_request","data":{"fromUserId":"A","fromUserName":"A"}} B->>S: {"type":"call_response","data":{"toUserId":"A","accepted":true}} S->>A: {"type":"call_response","data":{"fromUserName":"B","accepted":true}} A->>A: 创建PeerConnection A->>A: 创建Offer A->>S: {"type":"offer","data":{"toUserId":"B","offer":...}} S->>B: {"type":"offer","data":{"fromUserId":"A","offer":...}} B->>B: 创建Answer B->>S: {"type":"answer","data":{"toUserId":"A","answer":...}} S->>A: {"type":"answer","data":{"fromUserId":"B","answer":...}} A->>S: {"type":"ice_candidate","data":{"toUserId":"B","candidate":...}} S->>B: {"type":"ice_candidate","data":{"fromUserId":"A","candidate":...}} B->>S: {"type":"ice_candidate","data":{"toUserId":"A","candidate":...}} S->>A: {"type":"ice_candidate","data":{"fromUserId":"B","candidate":...}}
📊 协议性能优化
1. 消息压缩
- JSON格式轻量级
- 只传输必要信息
- 批量用户列表更新
2. 连接管理
javascript// 服务器端消息验证 handleMessage(ws, message) { const { type, data } = message; // 验证消息格式 if (!type || !data) { this.sendError(ws, '消息格式错误'); return; } // 验证用户状态 const user = this.userManager.getUserBySocket(ws); if (!user && type !== 'login') { this.sendError(ws, '请先登录'); return; } }3. 状态同步
javascript// 用户状态跟踪 const user = { id: userId, name: userName, ws: ws, online: true, inCall: false // 通话状态控制 };
🎯 协议特点总结
✅ 优势
- 简单高效 - JSON消息格式易解析
- 实时性强 - WebSocket双向通信
- 扩展性好 - 模块化消息类型
- 容错性强 - 自动重连和错误处理
- 状态完整 - 完整的用户状态管理
- 信息丰富 - 包含用户姓名和状态
🔧 技术特点
- 分层设计 - 传输层、应用层、媒体层分离
- 状态管理 - 服务器维护用户状态
- 信令转发 - 服务器作为信令中转站
- P2P传输 - 最终建立点对点连接
- 时机控制 - 只有在双方同意后才建立连接
- 用户信息 - 完整的用户信息传递
🔄 协议消息类型总结
消息类型 方向 用途 关键字段 login C→S 用户登录 userName login_success S→C 登录成功 userId, onlineUsers user_list S→C 用户列表更新 users[] call_request C→S 发起通话 toUserId call_response C→S 通话响应 toUserId, accepted, fromUserName offer C→S WebRTC Offer toUserId, offer answer C→S WebRTC Answer toUserId, answer ice_candidate C→S ICE候选 toUserId, candidate hangup C→S 挂断通话 toUserId error S→C 错误消息 message 这个协议设计实现了信令服务器 + WebRTC的经典架构,既保证了实时性,又确保了系统的可扩展性和稳定性!