WebRTC项目架构详解 - 指南

news/2025/11/27 13:46:37/文章来源:https://www.cnblogs.com/gccbuaa/p/19277180

目录

️ WebRTC项目架构详解

项目结构总览

服务器启动命令

启动开发服务器

直接启动Node.js

检查node_modules

网络测试命令

测试HTTP服务

测试通话页面

进程管理命令

查看Node.js进程

等待命令

️ 权限管理命令

修复npm权限

系统监控命令

检查网络连接

检查进程状态

完整工作流程命令序列

项目初始化流程:

问题排查流程:

 常用命令组合

快速启动:

完整重启:

状态检查:

⚠️ 常见问题解决

1. npm安装卡住

2. 权限问题

3. 端口占用

关闭服务器的方法总结

✅ 方法1:Ctrl+C(最简单)

✅ 方法2:查找并杀死进程

✅ 方法3:使用pkill命令

✅ 方法4: 使用lsof查找端口

1. 服务器端模块

`server.js` - HTTP服务器

核心功能:

`signaling.js` - WebSocket信令服务器

核心功能:

`user-manager.js` - 用户管理

核心功能:

2. 客户端模块

`auth.js` - 认证逻辑

核心功能:

`webrtc-client.js` - WebRTC核心

核心功能:

完整流程解析

1. 服务器启动流程

2. 客户端连接流程

3. WebRTC连接建立流程

关键技术点

WebRTC项目协议设计详解

️ 协议架构层次

1. 传输层协议

2. 应用层协议

3. 媒体传输协议

协议消息规范

认证协议

客户端 → 服务器

服务器 → 客户端

用户管理协议

用户列表广播

通话控制协议

发起通话

通话响应

WebRTC信令协议

Offer消息

Answer消息

ICE候选

挂断消息

错误消息

完整通信流程

1. 连接建立阶段

2. 通话建立阶段

️ 协议安全设计

1. 连接安全

2. 消息验证

3. 状态管理

协议性能优化

1. 消息压缩

2. 连接管理

3. 状态同步

协议特点总结

✅ 优势

 技术特点

协议消息类型总结


本文摘要:本文详细解析了一个基于WebRTC的视频通话系统架构与协议设计。系统采用分层结构,服务器端包含HTTP服务器、WebSocket信令服务和用户管理模块;客户端实现认证和WebRTC核心功能。关键协议采用JSON格式消息,涵盖登录认证、用户管理、通话控制等全流程,支持WebRTC信令交换(Offer/Answer/ICE候选)。通过WebSocket实现实时通信后建立P2P音视频连接,具有状态管理完善、错误处理健全等特点,并提供了完整的命令行操作指南和问题解决方案。该系统实现了信令中转与P2P传输的高效结合,确保实时通信质量与系统稳定性。

相关文章,对本文章项目一些概念和细节的补充:WebRTC学习中各项概念笔记

️ WebRTC项目架构详解

项目结构总览

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交互

服务器启动命令

启动开发服务器
npm start

作用: 启动Node.js服务器

说明: 执行package.json中定义的start脚本

输出示例:

> server@1.0.0 start

> node server.js

服务器运行在 http://localhost:3000

直接启动Node.js
node server.js

作用: 直接运行服务器文件

说明: 等同于npm start,但更直接

检查node_modules
ls -la node_modules

作用: 检查依赖包是否安装成功

说明: 如果node_modules不存在,说明依赖未安装

网络测试命令

测试HTTP服务

curl -s http://localhost:3000 | head -10

作用: 测试服务器是否响应

参数说明:

  • -s: 静默模式
  • head -10: 只显示前10行

输出示例:


    
    WebRTC 视频通话

测试通话页面

curl -s http://localhost:3000/call | head -10

作用: 测试通话页面路由

说明: 验证服务器路由配置

进程管理命令

查看Node.js进程

ps aux | grep node

作用: 查看正在运行的Node.js进程

输出示例:

pupu       32063   0.0  0.3 411112512  54640   ??  S    12:52PM   0:00.11 node server.js

说明: 确认服务器进程正在运行

等待命令

sleep 3

作用: 等待3秒

说明: 给服务器启动时间,然后测试连接

️ 权限管理命令

修复npm权限

sudo chown -R 501:20 "/Users/pupu/.npm"

作用: 修复npm缓存权限问题

说明: 解决EACCES权限错误

注意: 需要管理员权限

系统监控命令

检查网络连接

netstat -an | grep :443

作用: 检查HTTPS端口使用情况

说明: 验证网络连接状态

检查进程状态

ps ajx | grep -v grep|grep npm

作用: 查看npm相关进程

说明: 检查npm安装是否卡住

完整工作流程命令序列

项目初始化流程:

# 1. 进入服务器目录

cd /Users/zhouxinrui/Desktop/code/webrtcDemo/server

# 2. 清理npm缓存

npm cache clean --force

# 3. 安装依赖

npm install --no-optional

# 4. 启动服务器

npm start

问题排查流程:

# 1. 检查目录结构

ls -la

# 2. 检查依赖安装

ls -la node_modules

# 3. 测试服务器

curl -s http://localhost:3000

# 4. 检查进程

ps aux | grep node

 常用命令组合

快速启动:

cd server && npm start

完整重启:

cd server && npm cache clean --force && npm install && npm start

状态检查:

ps aux | grep node && curl -s http://localhost:3000 | head -5

⚠️ 常见问题解决

1. npm安装卡住

# 解决方案1: 清理缓存

npm cache clean --force

# 解决方案2: 使用国内镜像

npm config set registry https://registry.npmmirror.com

# 解决方案3: 跳过可选依赖

npm install --no-optional

2. 权限问题

# 修复npm权限

sudo chown -R 501:20 "/Users/zhouxinrui/.npm"

3. 端口占用

# 检查端口使用
lsof -i :3000
# 杀死进程
kill -9 

这些命令涵盖了WebRTC项目从安装到运行的完整生命周期,每个命令都有其特定的作用和适用场景!

关闭服务器的方法总结

✅ 方法1:Ctrl+C(最简单)

如果服务器在终端前台运行:

Ctrl + C

✅ 方法2:查找并杀死进程

# 查找服务器进程
ps aux | grep "node server.js"
# 杀死进程(替换PID为实际进程ID)
kill 
# 或者强制杀死
kill -9 

✅ 方法3:使用pkill命令

# 杀死所有node server.js进程
pkill -f "node server.js"
# 或者更精确的匹配
pkill -f "server.js"

✅ 方法4: 使用lsof查找端口

# 查找占用3000端口的进程
lsof -i :3000
# 杀死占用端口的进程
kill -9 

各模块详细解析

1. 服务器端模块

`server.js` - HTTP服务器

/*----------------------------模块引入部分------------------------------*/
// 引入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信令服务器

// 导入必要的模块:
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 OfferhandleOffer(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 AnswerhandleAnswer(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();}// 发送消息给指定 WebSocketsendTo(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` - 用户管理

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` - 认证逻辑

// 登录逻辑
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核心

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();});}// 连接 WebSocketconnectWebSocket() {// 根据当前协议确定 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 = '通话中';statusClass = 'in-call';} else {buttonHtml = '';statusClass = 'available';}userElement.className = `user-item ${statusClass}`;userElement.innerHTML = `${user.name}${user.status}${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 {// 创建 PeerConnectionthis.createPeerConnection();// 添加本地流if (this.localStream) {this.localStream.getTracks().forEach(track => {this.peerConnection.addTrack(track, this.localStream);});}// 创建并发送 Offerconst 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();}}// 创建 PeerConnectioncreatePeerConnection() {// 创建新的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) {// 确定消息接收方IDconst 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();  // 连接断开时挂断通话}};}// 处理 Offerasync handleOffer(data) {// 只有在接听状态下才处理Offerif (!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();}}// 处理 Answerasync 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:服务器启动

graph TDA[启动 server.js] --> B[创建 Express 应用]B --> C[创建 HTTP 服务器]C --> D[设置静态文件服务]D --> E[配置路由]E --> F[启动信令服务器]F --> G[监听端口 3000]G --> H[服务器运行中]

步骤2:客户端连接

graph TDA[用户访问 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:用户认证

graph TDA[用户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[更新通话状态显示]

关键技术点

1. 信令服务器的作用

  • 转发WebRTC信令消息

  • 管理用户状态(包括通话中用户)

  • 广播用户列表

  • 处理用户认证

peerConnection.onicecandidate = (event) => {if(event.candidate) {// 发送候选至对方signalingServer.send({type: 'candidate', candidate: event.candidate});}
};

2. 实时通信机制(最终版)

  • WebSocket:实时双向通信

  • 信令转发:服务器中转信令

  • 媒体传输:客户端直连

  • 状态同步:用户列表实时更新

  • ️ 状态保护:通过状态控制连接建立时机

  •  状态显示:通话状态和对方信息实时显示


WebRTC项目协议设计详解

基于代码分析,这个项目采用了分层协议架构,让我为您详细解析:

️ 协议架构层次

1. 传输层协议

HTTP/HTTPS (端口3000) + WebSocket (实时通信)

2. 应用层协议

JSON消息格式 + 自定义信令协议

3. 媒体传输协议

WebRTC (P2P音视频传输)

协议消息规范

认证协议

客户端 → 服务器
{
  "type": "login",
  "data": {
    "userName": "pupu"
  }
}
服务器 → 客户端
{"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": "通话中"}]}
}

用户管理协议

用户列表广播
{"type": "user_list","data": {"users": [{"id": "user1","name": "user1","inCall": false,"status": "在线"},{"id": "user2","name": "user2","inCall": true,"status": "通话中"}]}
}

通话控制协议

发起通话
{"type": "call_request","data": {"toUserId": "target-user-id"}
}
通话响应
{"type": "call_response","data": {"toUserId": "caller-user-id","fromUserId": "responder-user-id","fromUserName": "responder-name","accepted": true}
}

WebRTC信令协议

Offer消息
{"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消息
{"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候选
{"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"}}
}
挂断消息
{"type": "hangup","data": {"toUserId": "target-user-id"}
}
错误消息
{"type": "error","data": {"message": "用户不在线"}
}

完整通信流程

1. 连接建立阶段

2. 通话建立阶段

️ 协议安全设计

1. 连接安全

  • WebSocket over HTTP/HTTPS
  • 支持WSS加密连接
  • 自动重连机制

2. 消息验证

sequenceDiagramparticipant 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. 状态管理·                

sequenceDiagramparticipant A as 用户Aparticipant S as 服务器participant B as 用户BA->>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: 创建PeerConnectionA->>A: 创建OfferA->>S: {"type":"offer","data":{"toUserId":"B","offer":...}}S->>B: {"type":"offer","data":{"fromUserId":"A","offer":...}}B->>B: 创建AnswerB->>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. 连接管理

// 服务器端消息验证
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. 状态同步

// 用户状态跟踪
const user = {id: userId,name: userName,ws: ws,online: true,inCall: false  // 通话状态控制
};

协议特点总结

✅ 优势

  1. 简单高效 - JSON消息格式易解析
  2. 实时性强 - WebSocket双向通信
  3. 扩展性好 - 模块化消息类型
  4. 容错性强 - 自动重连和错误处理
  5. 状态完整 - 完整的用户状态管理
  6. 信息丰富 - 包含用户姓名和状态

 技术特点

  1. 分层设计 - 传输层、应用层、媒体层分离
  2. 状态管理 - 服务器维护用户状态
  3. 信令转发 - 服务器作为信令中转站
  4. P2P传输 - 最终建立点对点连接
  5. 时机控制 - 只有在双方同意后才建立连接
  6. 用户信息 - 完整的用户信息传递

协议消息类型总结

消息类型方向用途关键字段
loginC→S用户登录userName
login_successS→C登录成功userId, onlineUsers
user_listS→C用户列表更新users[]
call_requestC→S发起通话toUserId
call_responseC→S通话响应toUserId, accepted, fromUserName
offerC→SWebRTC OffertoUserId, offer
answerC→SWebRTC AnswertoUserId, answer
ice_candidateC→SICE候选toUserId, candidate
hangupC→S挂断通话toUserId
errorS→C错误消息message

这个协议设计实现了信令服务器 + WebRTC的经典架构,既保证了实时性,又确保了系统的可扩展性和稳定性!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/978398.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

翻转课堂 2st 解惑

目前遇到的问题列指针的*(arr + i * 4 + n)是什么意思?是这样的,我们先假设这里有个二维数组int a[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};如你所见,这个整型二维数组已经被1~12所填充,计算机底层的存储逻…

2025年度专业AI搜索优化公司排名:国内第一AI搜索优化公

在AI大模型重构流量生态的2025年,企业能否抢占AI搜索入口,直接决定了品牌曝光与获客效率。面对市场上鱼龙混杂的AI搜索优化服务商,如何找到真正能实现精准占位、高效转化的靠谱伙伴?以下结合技术实力、服务案例、行…

2025电磁阀制造企业TOP5权威推荐:助力企业快速定制降本

工业自动化进程中,企业对高性能电磁阀的需求持续攀升。2024年数据显示,国内电磁阀市场规模突破320亿元,年增速达18%,但30%的采购投诉集中在品质不稳定、定制周期长、配套服务缺失三大痛点——如某汽车制造企业因电…

Ai元人文构想:外行人的新思路——能否推动学术界对价值对齐的集体认知革新?

Ai元人文构想:外行人的新思路——能否推动学术界对价值对齐的集体认知革新? 摘要 本文探讨非专业研究者提出的AI元人文构想理论对学术界价值对齐研究的启示。分析表明,价值对齐问题的情境依赖性要求超越单一学科局限…

EF Core 深入学习

EF Core操作实体属性的内部机制核心概念: EF Core 的直接字段访问EF Core 在操作实体属性时,会尽量绕过属性的 getter/setter,直接操作背后的私有字段。为什么要这么做?基于性能和对特殊功能支持的考虑using System…

Unit 4 Intensive Listening 2

Part 1 Before I describe those studies, lets talk about how we are defining art. describe vt. 描写, 叙述 defining adj. 最典型的, 起决定性作用的

深入解析:51单片机基础-IO扩展(并转串 74HC165)

深入解析:51单片机基础-IO扩展(并转串 74HC165)pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas"…

实用指南:如何在 React 中实现键盘快捷键管理器以提升用户体验

实用指南:如何在 React 中实现键盘快捷键管理器以提升用户体验2025-11-27 13:34 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !impo…

2025年十大GEO源头厂家口碑排行榜,南方网通GEO源头厂

为帮助企业精准筛选技术可靠、口碑优质的GEO源头厂家,避免踩中技术断层、服务脱节的坑,我们从技术原创性(如核心算法自研能力)、口碑真实性(基于真实客户案例与复购率)、全链路服务能力(覆盖训练-投喂-运营全周…

无线讲解器品牌TOP5权威推荐:哪个品牌适合导游使用、耐用性

在文旅行业复苏与政企接待标准化升级的双重驱动下,无线讲解器市场需求持续攀升。据2024年文旅装备行业报告显示,景区、政企接待场景中无线讲解器的采购量同比增长35%,但市场上超60%的产品存在音质模糊、续航不足、耐…

2025年五大靠谱纸桶包装设备制造商推荐,专业纸桶包装设备厂

在绿色包装浪潮席卷全球的当下,纸桶包装凭借环保可持续的特性,成为化工、医药、食品等行业的新选择。而优质的纸桶包装设备,是企业实现高效生产、合规出口的核心支撑。面对市场上良莠不齐的供应商,如何挑选靠谱的纸…

2025年深圳USB充电器外壳厂家推荐:安全环保充电器外壳厂

TOP1 推荐:深圳市金鸿兴塑胶模具有限公司 推荐指数:★★★★★ 口碑评分:珠三角安全环保充电器外壳标杆厂家 专业能力:深耕塑胶模具行业19年,拥有独立的模具结构设计、产品结构设计及产品开发部门,实现模具开发-…

.Net中WebApiController如何实现多版本兼容?

理解多版本兼容的需求 Web API 版本兼容的必要性:业务迭代、客户端适配、接口演进时的平滑过渡。常见场景包括新增字段、废弃旧接口、重构参数结构等。 版本控制实现方式 URL 路径版本控制 在路由中嵌入版本号(如 ap…

2025年在线客服系统深度评测:五款主流产品全方位对比

2025年在线客服系统深度评测:五款主流产品全方位对比 在数字化转型浪潮下,在线客服系统已成为企业提升服务效率、优化客户体验的核心工具。2025年,市场涌现出多款功能全面、技术领先的客服系统,其中ttkefu、360客…

写题-2025.11

2025.11.25 开始写超级水题来复健 ……难道我只能写写橙题了吗 洛谷 P5887 Ringed Genesis 最开始把 \(k\) 看成了每只兔子不同,吓了一跳,想了一会儿没想出来。感觉能做,留个坑。 由于 \(\gcd(a,b) \mid (ax+by)\),…

修正开源控件kcontrols不能在linux使用中文输入法的bug

修正开源控件kcontrols不能在linux使用中文输入法的bugkcontrols这个控件TKMemo可以使用rich功能,但有个致命问题是在linux不能使用中文输入法。今天尝试修复这个Bug。打开kcontrols\source\kmemo.pas添加红色代码:{…

8种定位元素方法

ID定位 定义:通过元素的id属性来定位 示例: 往注册账号A填写admin,鼠标定位到输入框,发现有id属性。定位代码如下: driver.find_element(By.ID,"userA").send_keys("admin")name 定义:通过元…

2025年铰链品牌全国门店售后保障哪家好?炬森五金成行业标杆

家居装修中,铰链虽小,却直接影响柜体使用寿命和日常体验。当铰链出现故障,售后保障成为消费者最关心的问题。今天,我们为您揭晓"铰链品牌全国门店售后保障哪家好"的真相,揭秘炬森五金为何能成为全国门店…

【转载】Qt 路径中常用字符“./”、“../”、“/”、“*”的含义

在Qt中进行编程时,当我们需要调用某些路径时候,特别是调用QDir类进行访问时,会定义一系列的字符串,并将这些字符串赋值给路径,这时候就出现了一个问题,这些字符串所代表的的含义是什么呢,现在就来解释一下这些字…

完整教程:掌握3D手部关键点检测:PyTorch深度学习实战

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …