一、实现思路

  • Web端就是使用html + JavaScript来实现页面,通过WebSocket长连接和服务器保持通讯,协议的payload使用JSON格式封装

  • 服务端使用C++配合第三方库WebSocket++nlonlohmann库来实现

二、Web端

2.1 界面显示

首先,使用html来设计一个简单的静态框架:

  • 有一个聊天室的标题
  • 然后一个文本框加上一个发送按钮
<body><h1>WebSocket简易聊天室</h1><div id="app"><input id="sendMsg" type="text" /><button id="sendBtn">发送</button></div>
</body>

然后我们还需要显示聊天信息,我们可以利用脚本每次有信息的时候,就把这个信息嵌入到<body>里面,这个信息本身存放在<div>里面,如下:

  • 加入房间显示蓝色
  • 离开房间显示红色
  • 聊天信息显示黑色
function showMessage(str, type) {var div = document.createElement("div");div.innerHTML = str;if (type == "enter") div.style.color = "blue";else if (type == "leave") div.style.color = "red";document.body.appendChild(div);
}

2.2 WebSocket 连接

接下来我们配合JavaScript脚本,先连接服务端的WebSocket服务器

var websocket = new WebSocket('ws://192.168.217.128:9002');

我们需要实现WebSocket的几个回调函数:

  1. onopen
  • 这个回调函数在连接服务器成功时触发,我们在连接成功的时候,绑定上发送按钮的点击事件
// 连接成功
websocket.onopen = function () {console.log("连接服务器成功");document.getElementById("sendBtn").onclick = function () {var msg = document.getElementById("sendMsg").value;if (msg) {websocket.send(msg);}};
};
  1. onmessage
  • 这个回调函数在接收到服务端的消息后触发,这里是JSON格式,我们服务端定义了datakey,对应的消息就是data后面的value
websocket.onmessage = function (e) {var mes = JSON.parse(e.data);showMessage(mes.data, mes.type);
};
  1. onclose
  • 这个回调函数在连接断开的时候触发,在这里我们简单的打印一下即可:
// 连接关闭
websocket.onclose = function (event) {console.log("连接已关闭", "代码:", event.code, "原因:", event.reason);
};

2.3 完整代码

完整的Web代码如下:

<!DOCTYPE html>
<html><body><h1>WebSocket简易聊天室</h1><div id="app"><input id="sendMsg" type="text" /><button id="sendBtn">发送</button></div>
</body>
<script>function showMessage(str, type) {var div = document.createElement("div");div.innerHTML = str;if (type == "enter") div.style.color = "blue";else if (type == "leave") div.style.color = "red";document.body.appendChild(div);}var websocket = new WebSocket('ws://192.168.217.128:9002');// 连接成功websocket.onopen = function () {console.log("连接服务器成功");document.getElementById("sendBtn").onclick = function () {var msg = document.getElementById("sendMsg").value;if (msg) {websocket.send(msg);}};};// 接收消息websocket.onmessage = function (e) {var mes = JSON.parse(e.data);showMessage(mes.data, mes.type);};// 连接关闭websocket.onclose = function (event) {console.log("连接已关闭", "代码:", event.code, "原因:", event.reason);};// 错误处理websocket.onerror = function (error) {console.error("WebSocket错误:", error);};</script></html>

三、服务端代码

确保你已经配置好了第三方库,下面我们开始讲解服务端代码:

3.1 echo_server改造

首先这里我们是使用WebSocket++这个第三方库的配套示例echo_server.cpp改造的,因此我们只讲解改造的部分,未修改源代码echo_server.cpp如下:

// examples目录是官方的一些例子 本次使用的是echo_server\echo_server.cpp
// 该原程序只支持一对一发送后回复
// 改造后可以通知所有连接上来的客户端。
// 编译 g++ main.cpp -o main -lboost_system -lboost_chrono#include <websocketpp/config/asio_no_tls.hpp>#include <websocketpp/server.hpp>#include <iostream>
#include <list>#include <functional> typedef websocketpp::server<websocketpp::config::asio> server;using websocketpp::lib::bind;
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;// pull out the type of messages sent by our config
typedef server::message_ptr message_ptr;std::list<websocketpp::connection_hdl> vgdl;// Define a callback to handle incoming messages
void on_message(server *s, websocketpp::connection_hdl hdl, message_ptr msg)
{// std::cout << "on_message called with hdl: " << hdl.lock().get()//           << " and message: " << msg->get_payload()//           << std::endl;// check for a special command to instruct the server to stop listening so// it can be cleanly exited.if (msg->get_payload() == "stop-listening"){s->stop_listening();return;}for (auto it = vgdl.begin(); it != vgdl.end(); it++){if (it->expired())//移除连接断开的{it = vgdl.erase(it);continue;}if (it != vgdl.end())s->send(*it, msg->get_payload(), msg->get_opcode());// t.wait();}// try {//     s->send(hdl, msg->get_payload()+std::string("aaaaa"), msg->get_opcode());// } catch (websocketpp::exception const & e) {//     std::cout << "Echo failed because: "//               << "(" << e.what() << ")" << std::endl;// }
}
//将每个连接存入容器
void on_open(websocketpp::connection_hdl hdl)
{std::string msg = "link OK";printf("%s\n", msg.c_str());// printf("fd %d\n",(int)hdl._M_ptr());vgdl.push_back(hdl);
}void on_close(websocketpp::connection_hdl hdl)
{std::string msg = "close OK";printf("%s\n", msg.c_str());
}
int main()
{// Create a server endpointserver echo_server;try{// Set logging settings 设置logecho_server.set_access_channels(websocketpp::log::alevel::all);echo_server.clear_access_channels(websocketpp::log::alevel::frame_payload);// Initialize Asio 初始化asioecho_server.init_asio();// Register our message handler// 绑定收到消息后的回调echo_server.set_message_handler(bind(&on_message, &echo_server, ::_1, ::_2));//当有客户端连接时触发的回调std::function<void(websocketpp::connection_hdl)> f_open;f_open = on_open;echo_server.set_open_handler(websocketpp::open_handler(f_open));//关闭是触发std::function<void(websocketpp::connection_hdl)> f_close(on_close);echo_server.set_close_handler(f_close);// Listen on port 9002echo_server.listen(9002);//监听端口// Start the server accept loopecho_server.start_accept();// Start the ASIO io_service run loopecho_server.run();}catch (websocketpp::exception const &e){std::cout << e.what() << std::endl;}catch (...){std::cout << "other exception" << std::endl;}
}

3.2 管理用户名

  • 对于每一个连接上来的用户,对应唯一的connection_hdl类型的值,是一个用于标识和跟踪 WebSocket 连接的句柄类型,其本质是对连接对象的弱引用(封装了 std::weak_ptr

  • 因此我们可以把它映射到每一个用户名,这样我们就知道发送过来的连接句柄对应是哪个用户了,实际这里我们每次连接都分配了一个用户名:

std::string username = "user:" + std::to_string(++totalUser);

这里我们使用std::map进行映射,由于websocketpp::connection_hdl类型不具备运算符<,因此我们自己写一个仿函数,里面用它内部的weak_ptr进行比较

struct ConnectionHdlCompare {bool operator()(const websocketpp::connection_hdl& a, const websocketpp::connection_hdl& b) const {// 通过获取底层的弱指针的原始指针进行比较return a.lock() < b.lock();}
};

然后这样定义std::map

std::map<websocketpp::connection_hdl, std::string, ConnectionHdlCompare> user_map; 

3.3 连接事件

  • 每个用户连接的时候,需要分配一个唯一的用户名,然后广播给全部用户,该用户加入房间了,我们使用JSON进行序列化,类型是enter,同时,把这个连接句柄加入全局的链表中
void on_open(websocketpp::connection_hdl hdl)
{std::string msg = "link OK";printf("%s\n", msg.c_str());vgdl.push_back(hdl);// 生成唯一用户名std::string username = "user:" + std::to_string(++totalUser);user_map[hdl] = username;  // 记录连接对应的用户名// 广播用户加入消息json j;j["type"] = "enter";j["data"] = username + "加入房间";std::string json_str = j.dump();std::cout << "json_str = " << json_str << std::endl;send_msg(&echo_server, json_str);
}
  • 广播函数send_msg我们重载了两个版本,其中一个版本是字符串std::string发送,另一个则是使用message_ptr类型,其内部封装了消息帧的数据,比如payload负载、opcode操作码,它本质也是一个weak_ptr

  • 创建文本帧如下,我们发送JSON,使用的是text

    • 0x0:延续帧(用于分片传输大消息,当前帧是消息的中间部分);
    • 0x1:文本帧(payload 是 UTF-8 编码的文本数据);
    • 0x2:二进制帧(payload 是任意二进制数据,如图片、protobuf 等);
    • 0x8:关闭帧(通知对方关闭连接,payload 可包含关闭原因);
    • 0x9:Ping 帧(心跳检测,用于确认连接活性);
    • 0xA:Pong 帧(响应 Ping 帧的心跳回复)。
  • 遍历的时候,需要判断是否指针失效了,因为weak_ptr本身并不能管理对象,需要转换为shared_ptr来查看,可以调用expire()函数来看看是否为nullptr,我们只对有效的连接发送消息,无效的连接直接从链表删除这个句柄

void send_msg(server *s, message_ptr msg){for (auto it = vgdl.begin(); it != vgdl.end(); ){if (it->expired()){it = vgdl.erase(it);  // 正确处理迭代器失效:连接断开}else{try {s->send(*it, msg->get_payload(), msg->get_opcode());} catch (websocketpp::exception const & e) {std::cout << "Broadcast failed because: " << e.what() << std::endl;}++it;  // 只有在未删除元素时才递增迭代器}}
}void send_msg(server *s,std::string msg){for (auto it = vgdl.begin(); it != vgdl.end(); ){if (it->expired()){it = vgdl.erase(it);  // 正确处理迭代器失效:连接断开}else{try {s->send(*it, msg, websocketpp::frame::opcode::text);} catch (websocketpp::exception const & e) {std::cout << "Broadcast failed because: " << e.what() << std::endl;}++it;  // 只有在未删除元素时才递增迭代器}}
}

3.4 关闭事件

在触发关闭连接的回调函数中,我们要删除对应map里面的用户名,并且我们将这个用户离开房间的消息转发给所有人,JSON序列化的typeleave

void on_close(websocketpp::connection_hdl hdl)
{for (auto it = vgdl.begin(); it != vgdl.end(); ){if (it->expired()){it = vgdl.erase(it);  // 正确处理迭代器失效:连接断开}}std::string msg = "close OK";printf("%s\n", msg.c_str());// 清理用户映射表并广播离开消息if (user_map.find(hdl) != user_map.end()) {std::string username = user_map[hdl];user_map.erase(hdl);  // 删除映射记录json j;j["type"] = "leave";j["data"] = username + "离开房间";std::string json_str = j.dump();send_msg(&echo_server, json_str);}
}

3.5 发送消息事件

  • 在收到客户端的消息之后,我们需要将这个消息转发给所有人,JSON序列化的typemessage,发送的时候需要加上用户名,这样前端显示才有用户名:

  • 这里是通过修改message_ptrpayload的形式来发送消息的,因此我们调用send_msg是第一个重载版本

void on_message(server *s, websocketpp::connection_hdl hdl, message_ptr msg)
{std::cout << "on_message called with hdl: " << hdl.lock().get()<< " and message: " << msg->get_payload()<< std::endl;// check for a special command to instruct the server to stop listening so// it can be cleanly exited.if (msg->get_payload() == "stop-listening"){s->stop_listening();return;}//转发消息json j;j["type"] = "message";j["data"] =  user_map[hdl] + "说:" + msg->get_payload();std::string json_str = j.dump();msg->set_payload(json_str);std::cout << "msg = " << msg->get_payload() << std::endl;send_msg(s, msg);
}

3.6 完整代码

完整的服务端代码如下:

// examples目录是官方的一些例子 本次使用的是echo_server\echo_server.cpp
// 该原程序只支持一对一发送后回复
// 改造后可以通知所有连接上来的客户端。
// 编译 g++ main.cpp -o main -lboost_system -lboost_chrono#include <websocketpp/config/asio_no_tls.hpp>#include <websocketpp/server.hpp>#include <iostream>
#include <list>
#include <functional> 
#include <mutex>  // 添加互斥锁头文件//json解析
#include<nlohmann/json.hpp>
using json = nlohmann::json;typedef websocketpp::server<websocketpp::config::asio> server;using websocketpp::lib::bind;
using websocketpp::lib::placeholders::_1;
using websocketpp::lib::placeholders::_2;// pull out the type of messages sent by our config
typedef server::message_ptr message_ptr;std::list<websocketpp::connection_hdl> vgdl;
std::mutex vgdl_mutex;  // 添加互斥锁保护连接列表// 自定义比较函数对象
struct ConnectionHdlCompare {bool operator()(const websocketpp::connection_hdl& a, const websocketpp::connection_hdl& b) const {// 通过获取底层的弱指针的原始指针进行比较return a.lock() < b.lock();}
};// 使用自定义比较函数对象的连接-用户名映射表
std::map<websocketpp::connection_hdl, std::string, ConnectionHdlCompare> user_map;  // 新增:连接-用户名映射表// Define a callback to handle incoming messages// Create a server endpoint
server echo_server;int totalUser = 0;
void send_msg(server *s, message_ptr msg){for (auto it = vgdl.begin(); it != vgdl.end(); ){if (it->expired()){it = vgdl.erase(it);  // 正确处理迭代器失效:连接断开}else{try {s->send(*it, msg->get_payload(), msg->get_opcode());} catch (websocketpp::exception const & e) {std::cout << "Broadcast failed because: " << e.what() << std::endl;}++it;  // 只有在未删除元素时才递增迭代器}}
}void send_msg(server *s,std::string msg){for (auto it = vgdl.begin(); it != vgdl.end(); ){if (it->expired()){it = vgdl.erase(it);  // 正确处理迭代器失效:连接断开}else{try {s->send(*it, msg, websocketpp::frame::opcode::text);} catch (websocketpp::exception const & e) {std::cout << "Broadcast failed because: " << e.what() << std::endl;}++it;  // 只有在未删除元素时才递增迭代器}}
}
void on_message(server *s, websocketpp::connection_hdl hdl, message_ptr msg)
{std::cout << "on_message called with hdl: " << hdl.lock().get()<< " and message: " << msg->get_payload()<< std::endl;// check for a special command to instruct the server to stop listening so// it can be cleanly exited.if (msg->get_payload() == "stop-listening"){s->stop_listening();return;}//转发消息json j;j["type"] = "message";j["data"] =  user_map[hdl] + "说:" + msg->get_payload();std::string json_str = j.dump();msg->set_payload(json_str);std::cout << "msg = " << msg->get_payload() << std::endl;send_msg(s, msg);
}//将每个连接存入容器
void on_open(websocketpp::connection_hdl hdl)
{std::string msg = "link OK";printf("%s\n", msg.c_str());vgdl.push_back(hdl);// 生成唯一用户名std::string username = "user:" + std::to_string(++totalUser);user_map[hdl] = username;  // 记录连接对应的用户名// 广播用户加入消息json j;j["type"] = "enter";j["data"] = username + "加入房间";std::string json_str = j.dump();std::cout << "json_str = " << json_str << std::endl;send_msg(&echo_server, json_str);
}void on_close(websocketpp::connection_hdl hdl)
{for (auto it = vgdl.begin(); it != vgdl.end(); ){if (it->expired()){it = vgdl.erase(it);  // 正确处理迭代器失效:连接断开}}std::string msg = "close OK";printf("%s\n", msg.c_str());// 清理用户映射表并广播离开消息if (user_map.find(hdl) != user_map.end()) {std::string username = user_map[hdl];user_map.erase(hdl);  // 删除映射记录json j;j["type"] = "leave";j["data"] = username + "离开房间";std::string json_str = j.dump();send_msg(&echo_server, json_str);}
}int main()
{try{// Set logging settings 设置logecho_server.set_access_channels(websocketpp::log::alevel::all);echo_server.clear_access_channels(websocketpp::log::alevel::frame_payload);// Initialize Asio 初始化asioecho_server.init_asio();// Register our message handler// 绑定收到消息后的回调echo_server.set_message_handler(bind(&on_message, &echo_server, ::_1, ::_2));//当有客户端连接时触发的回调std::function<void(websocketpp::connection_hdl)> f_open;f_open = on_open;echo_server.set_open_handler(websocketpp::open_handler(f_open));//关闭是触发std::function<void(websocketpp::connection_hdl)> f_close(on_close);echo_server.set_close_handler(f_close);// Listen on port 9002echo_server.listen(9002);//监听端口// Start the server accept loopecho_server.start_accept();// Start the ASIO io_service run loopecho_server.run();}catch (websocketpp::exception const &e){std::cout << e.what() << std::endl;}catch (...){std::cout << "other exception" << std::endl;}
}

四、运行结果

编译服务端并启动:

g++ main.cpp -o main -lboost_system -lboost_chrono
./main

在两个浏览器中打开我们的Web服务端,这里是本地的,所以是

http://127.0.0.1:5500/chatClient.html

两个用户加入房间、聊天、离开的效果有不同的颜色,如下所示

在这里插入图片描述

更多资料:https://github.com/0voice

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

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

相关文章

AI 驱动、设施扩展、验证器强化、上线 EVM 测试网,Injective 近期动态全更新!

作为一个专注于金融应用、且具有高度可互操作性的高性能 Layer-1 区块链&#xff0c;Injective 自诞生以来便为开发者提供有即插即用的技术模块&#xff0c;以便开发者能够更好地搭建新一代 Web3 金融类应用。谈及项目发展的愿景和基本定位&#xff0c;创始团队曾提到希望 Inje…

Qt-----初识

1. 什么是Qt定义&#xff1a;Qt是一个跨平台的应用程序和用户界面框架&#xff0c;主要用于开发具有图形用户界面的应用程序&#xff0c;同时也支持非GUI程序的开发。 编程语言&#xff1a;主要使用C&#xff0c;但也提供了对Python&#xff08;PyQt&#xff09;、JavaScript&a…

理解微信体系中的 AppID、OpenID 和 UnionID

前言: 在开发微信相关的服务(如小程序,公众号,微信开放平台等)时,很多人都会接触到几个看起来相似但实际用途不同的额ID: AppiD, OpenID,UnionID. 搞清楚这三者的区别,是微信生态开发中的基本功,本文将从开发者视角触发,深入浅出地解释它们的关系,区别以及实际应用场景一.什么是…

FFmpeg,如何插入SEI自定义数据

FFmpeg&#xff0c;如何插入SEI自定义数据 一、什么是SEI&#xff1f; SEI&#xff08;Supplemental Enhancement Information&#xff0c;补充增强信息&#xff09;是H.264/H.265视频编码标准中的一种元数据载体&#xff0c;它允许在视频流中嵌入额外的信息&#xff0c;如时…

为什么分类任务偏爱交叉熵?MSE 为何折戟?

在机器学习的世界里&#xff0c;损失函数是模型的“指南针”——它定义了模型“好坏”的标准&#xff0c;直接决定了参数优化的方向。对于分类任务&#xff08;比如判断一张图片是猫还是狗&#xff09;&#xff0c;我们通常会选择交叉熵作为损失函数&#xff1b;而在回归任务&a…

[echarts]横向柱状图

前言 接到一个需求&#xff0c;需要展示一个横向的柱状图&#xff0c;按数量从大到小排序&#xff0c;并定时刷新 使用react配合echarts进行实现。 react引入echarts import React, { useEffect, useRef } from react; import * as echarts from echarts; import DeviceApi fro…

【开源项目】轻量加速利器 HubProxy 自建 Docker、GitHub 下载加速服务

​​引言​​ 如果你经常被 Docker 镜像拉取、GitHub 文件下载的龟速折磨&#xff0c;又不想依赖第三方加速服务&#xff08;担心稳定性或隐私&#xff09;&#xff0c;今天分享的 ​​HubProxy​​ 可能正是你需要的。这个开源工具用一行命令就能部署&#xff0c;以极低资源消…

java web jsp jstl练习

JSP 的学习。 核心功能模块 1. 源代码层 &#xff08; src &#xff09; HelloWorld &#xff1a;主程序入口领域模型 &#xff1a; domain 包含User.java和ceshi.java控制器 &#xff1a; servlet 包含登录验证和验证码相关ServletWeb表现层 &#xff08; web &#xff09; JS…

VSCode 完全指南:释放你的编码潜能

零、简介 在当今的软件开发领域&#xff0c;代码编辑器的选择至关重要&#xff0c;它就像是工匠手中的工具&#xff0c;直接影响着工作效率和成果质量。Visual Studio Code&#xff08;简称 VSCode&#xff09;自问世以来&#xff0c;迅速在全球开发者社区中崭露头角&#xff…

《n8n基础教学》第一节:如何使用编辑器UI界面

在本课中&#xff0c;你将学习如何操作编辑器界面。我们将浏览画布&#xff0c;向您展示每个图标的含义&#xff0c;以及在 n8n 中构建工作流程时在哪里可以找到您需要的东西。本课程基于 n8n 最新版本 。在其他版本中&#xff0c;某些用户界面可能有所不同&#xff0c;但这不会…

gcc g++ makefile CMakeLists.txt cmake make 的关系

gcc&#xff1a;C语言编译器g&#xff1a;C编译器makefile&#xff1a;定义编译规则、依赖关系和构建目标。可以手动编写&#xff0c;也可以由CMakeLists.txt生成cmake&#xff1a;读取CMakeLists.txt文件&#xff0c;生成Makefilemake&#xff1a;构建工具&#xff0c;执行Mak…

SFT 训练器

SFT 训练器 “训练时间到!” 我们现在终于可以创建一个监督微调训练器的实例了: trainer = SFTTrainer( model=model, processing_class=tokenizer, args=sft_config, train_dataset=dataset, )SFTTrainer 已经对数据集进行了预处理,因此我们可以深入查看,了解每个小批次…

Android Material Components 全面解析:打造现代化 Material Design 应用

引言 在当今移动应用开发领域&#xff0c;用户体验(UX)已成为决定应用成功与否的关键因素之一。Google推出的Material Design设计语言为开发者提供了一套完整的视觉、交互和动效规范&#xff0c;而Material Components for Android(MDC-Android)则是将这些设计理念转化为可重用…

Windows使用Powershell自动安装SqlServer2025服务器与SSMS管理工具

安装结果: 安装前准备: 1.下载mssql server 2025安装器 2.下载iso镜像 3.下载好SSMS安装程序,并放到iso同目录下 4.执行脚本开始自动安装

09 RK3568 Debian11 ES8388 模拟音频输出

1、设备树配置 确认自己的i2c,使用sdk带的驱动es8323 /SDK/kernel/sound/soc/codecs/es8323.c es8388_sound: es8388-sound {status = "okay";compatible = "rockchip,multicodecs-card"; rockchip,card-name = "rockchip,es8388-codec"; …

力扣-199.二叉树的右视图

题目链接 199.二叉树的右视图 class Solution {public List<Integer> rightSideView(TreeNode root) {List<Integer> res new ArrayList<>();Queue<TreeNode> queue new LinkedList<>();if (root null)return res;queue.offer(root);while …

Android Bitmap 完全指南:从基础到高级优化

在 Android 开发中&#xff0c;图像处理是一个核心且复杂的领域&#xff0c;而 Bitmap 作为 Android 中表示图像的基本单位&#xff0c;贯穿了从简单图片显示到复杂图像编辑的各个场景。然而&#xff0c;Bitmap 处理不当往往会导致应用性能下降、内存溢出&#xff08;OOM&#…

unity日志过滤器

背景&#xff1a;之前做游戏的时候和同组的同事聊过说日志过滤尽量不要限制大家怎么使用日志打印的接口&#xff0c;不要加额外的参数&#xff0c;比如多加一个标签string,或者使用特定的接口&#xff0c;枚举。最好就是日志大家还是用Debug.Log无感去用&#xff0c;然后通过勾…

OpenGL Camera

一. lookAt函数的参数含义glm::mat4 view glm::lookAt(cameraPos, // 相机在世界坐标系中的位置&#xff08;任意值&#xff09;cameraPos cameraFront, // 相机看向的目标点&#xff08;位置朝向&#xff09;cameraUp // 相机的"上方向"&#xff08;通…

Android RTMP推送|轻量级RTSP服务同屏实践:屏幕+音频+录像全链路落地方案

一、背景&#xff1a;从“移动终端”到“远程协作节点”&#xff0c;同屏音频录像为何成刚需&#xff1f; 在数字化办公、智慧医疗与远程教育等快速发展的推动下&#xff0c;手机作为随身终端&#xff0c;已不再只是“内容接收者”&#xff0c;而逐步成为远程信息发布与可视化…