施磊老师基于muduo网络库的集群聊天服务器(七)
文章目录
- 数据表字符集问题
- 支持中文和英文
- **为什么使用 `utf8mb4`?**
- 推荐 查看整个表, 再单独修改
- 客户端群组功能
- 创建群组
- 添加群组
- 群组聊天
- 接收在线群组消息
- 接收离线群组消息
- 补充服务器事件处理器
- 补充服务器查询群组列表
- 问题解决
- 测试
- 目前报错总结
- 目前为止最恶心的错误
- 客户端用户注销功能
- 1. 用户注销功能的设计与实现
- 2. 客户端状态管理与循环控制
- 3. 数据重复加载问题与解决方案
- 4. 接收线程的单例控制-**重点**
- 巧用静态局部变量
- 老师这节课进行了一个小总结
- 5. 架构设计思想
- 6. 集群化扩展的铺垫
- 错误
- 代码
数据表字符集问题
支持中文和英文
为什么使用 utf8mb4
?
- 支持完整的 Unicode 字符:
utf8mb4
可以存储几乎所有语言的字符,支持 中文、日文、韩文,以及 Emoji 和符号。
- 比
utf8
更加可靠:utf8
只支持最多三个字节的字符(不支持一些 4 字节字符,如 Emoji),而utf8mb4
支持最多四个字节的字符。
查看数据库 所有表的 字符集
SELECT TABLE_NAME,TABLE_COLLATION
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = '你的数据库名';
单独修改某个表
ALTER TABLE 表名 CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
批量修改字符集–修改某个字符集的所有表
SELECT CONCAT('ALTER TABLE ', TABLE_NAME, ' CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;')
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = '你的数据库名'AND TABLE_COLLATION LIKE 'latin1%';
推荐 查看整个表, 再单独修改
客户端群组功能
创建群组
// creategroup函数
void creategroup(int clientfd, string msg)
{int idx = msg.find(":"); // 查找第一个:的位置if (idx == string::npos) // 没有找到 ==-1->不建议用{cout << "creategroup command: group name is invalid!" << endl;return;}string groupname = msg.substr(0, idx); // 截取群组名称string groupdesc = msg.substr(idx + 1, msg.size() - idx - 1); // 截取群组描述json js;js["msgid"] = CREATE_GROUP_MSG; // 创建群组消息js["id"] = g_currentUser.getId(); // 当前登录用户idjs["groupname"] = groupname; // 群组名称js["groupdesc"] = groupdesc; // 群组描述string request = js.dump(); // json转字符串 序列化int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0); // 发送数据if (len < 0){cerr << "send creategroup msg error: " << request << endl;}
}
添加群组
// addgroup函数
void addgroup(int clientfd, string msg)
{int groupid = atoi(msg.c_str()); // 转成整型json js;js["msgid"] = ADD_GROUP_MSG; // 添加群组消息js["id"] = g_currentUser.getId(); // 当前登录用户idjs["groupid"] = groupid; // 群组idstring request = js.dump(); // json转字符串 序列化int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0); // 发送数据if (len < 0){cerr << "send addgroup msg error: " << request << endl;}
}
群组聊天
// groupchat函数
void groupchat(int clientfd, string msg)
{int idx = msg.find(":"); // 查找第一个:的位置if (idx == string::npos) // 没有找到 ==-1->不建议用{cout << "groupchat command: group id is invalid!" << endl;return;}int groupid = atoi(msg.substr(0, idx).c_str()); // 截取群组idstring message = msg.substr(idx + 1, msg.size() - idx - 1); // 截取聊天信息json js;js["msgid"] = GROUP_CHAT_MSG; // 群组聊天消息js["id"] = g_currentUser.getId(); // 当前登录用户idjs["name"] = g_currentUser.getName(); // 当前登录用户姓名js["groupid"] = groupid; // 群组id -- 字段要对应服务器那边的js["msg"] = message; // 聊天信息js["time"] = getCurrentTime(); // 时间// 发送聊天请求string request = js.dump(); // json转字符串 序列化int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0); // 发送数据if (len < 0){cerr << "send groupchat msg error: " << request << endl;}
}
接收在线群组消息
区分群组消息和个人消息
readTaskHandler函数
// 解析json数据
json response = json::parse(buffer); // 反序列化 字符串转json
if (response["msgid"] == ONE_CHAT_MSG) // 一对一聊天消息
{cout << response["time"].get<string>() << "[" << response["id"] << "] " << response["name"].get<string>() << " said: " << response["msg"].get<string>() << endl;continue;
}if (response["msgid"] == GROUP_CHAT_MSG) // 群组聊天消息
{cout << "群消息-->[" << response["groupid"] << "] " << response["time"].get<string>() << "[" << response["id"] << "] " << response["name"].get<string>() << " said: " << response["msg"].get<string>() << endl;
}
接收离线群组消息
同样进行区分
main
// 处理离线消息
if (response.contains("offlinemsg")) // 判断是否包含字段, 跟好点, 而不是看 是不是空
{vector<string> offlinemsg = response["offlinemsg"]; // 类型是vector<string>, 不是vector<User>, 根据服务器业务,存的是js.dump() 字符串for (auto &msg : offlinemsg){json js = json::parse(msg); // 反序列化// 时间+fromid+fromname+msg-----详看笔记 一对一聊天发送的格式// 分一下 个人离线和群组离线if (js["msgid"] == ONE_CHAT_MSG){cout << js["time"].get<string>() << "[" << js["id"].get<int>() << "] " << js["name"].get<string>() << " said: " << js["msg"].get<string>() << endl;}if (js["msgid"] == GROUP_CHAT_MSG) // 群组聊天消息{cout << "群消息-->[" << js["groupid"] << "] " << js["time"].get<string>() << "[" << js["id"] << "] " << js["name"].get<string>() << " said: " << js["msg"].get<string>() << endl;}}
}
补充服务器事件处理器
服务器业务—> 没有加 群组相关的 事件处理器
补充服务器查询群组列表
登录成功–查询群组列表
// 查询群组列表
vector<Group> groupVec = _groupModel.queryGroups(id);
if (!groupVec.empty())
{vector<string> vec;for (auto &groupl : groupVec){json js ;js["id"] = groupl.getId();js["groupname"] = groupl.getName();js["groupdesc"] = groupl.getDesc();vector<string> usersvec;for (auto &user : groupl.getUsers()){json js;js["id"]=user.getId();js["name"]=user.getName();js["state"]=user.getState();js["role"] = user.getRole();usersvec.push_back(js.dump());}js["users"] = usersvec;vec.push_back(js.dump());}response["groups"] = vec; // 群组列表
}
问题解决
服务器部分 groupmodel.cpp 有一些逻辑问题, 导致读不到用户, 提前返回了, 已进行修改
测试
自行测试— 至此,功能都正常
目前报错总结
目前了解的:
数据库不支持 中文-----英文正常
数据库本身问题----把语句 先在mysql命令行试一下, 看情况处理
json在gdb不能直接看----在代码上添加 临时量 存储 js.dump(), 进行查看
由于客户端 和 服务器 和 mysql分离, 所以 有时候 可以分步测试, 看mysql 有没有, 再去排除问题
目前为止最恶心的错误
terminate called after throwing an instance of 'nlohmann::json_abi_v3_12_0::detail::type_error'
what(): [json.exception.type_error.302] type must be string, but is null
Aborted (core dumped)
这个错误, 基本就是 json 变量名写错了
type must be string, but is null: 你试图像字符串一样使用一个字段(如 j["name"].get<std::string>()),但 j["name"] 实际是 null,无法转换成字符串。
下面这段, cout里面是 js. 不是response.
// 处理离线消息// 分一下 个人离线和群组离线
if (js["msgid"] == ONE_CHAT_MSG)
{cout << js["time"].get<string>() << "[" << js["id"].get<int>() << "] " << js["name"].get<string>() << " said: " << js["msg"].get<string>() << endl;
}
if (js["msgid"] == GROUP_CHAT_MSG) // 群组聊天消息
{cout << "群消息-->[" << js["groupid"] << "] " << js["time"].get<string>() << "[" << js["id"] << "] " << js["name"].get<string>() << " said: " << js["msg"].get<string>() << endl;
}
客户端用户注销功能
用户登出:
1. 结束mainMenu
2. 结束对应的 接受线程
1. 用户注销功能的设计与实现
区分了 quit–>正常注销 和 客户端异常–>ctrl+c
- 正常注销 vs. 异常退出:
- 正常注销:客户端主动发送
LOGOUT
消息,服务端收到后:- 从
connectionMap
移除该用户的连接(需加锁保证线程安全)。 - 更新数据库,将该用户状态设为
offline
。
- 从
- 异常退出(如客户端强制关闭):
- 服务端检测到连接断开,触发
closeException
处理。 - 由于无法直接获取用户ID,需遍历
connectionMap
比对连接来删除。
- 服务端检测到连接断开,触发
- 正常注销:客户端主动发送
- 关键点:
- 正常注销比异常退出更高效,因为能直接通过
userID
定位用户连接。 - 服务端需要保证对共享资源(如
connectionMap
)的线程安全操作。
- 正常注销比异常退出更高效,因为能直接通过
2. 客户端状态管理与循环控制
我们希望, 在正常退出后, 会回到主菜单页面!
之前的代码, 在登录后会进入 菜单, 进入死循环, 可以一直 发送消息
- 全局变量控制页面跳转:
- 引入
isMainMenuRunning
(布尔值)控制主菜单循环:- 登录成功 →
true
(进入主菜单循环)。 - 注销 →
false
(退出循环,返回登录页面)。
- 登录成功 →
- 避免因循环阻塞导致无法返回登录界面。
- 引入
- 关键点:
- 通过状态变量而非死循环控制流程,使代码更清晰、可维护。
- 注销后需重置用户数据(如好友列表、群组列表),防止重复加载。
3. 数据重复加载问题与解决方案
- 问题:
- 用户注销后重新登录时,未清空旧数据,导致好友/群组列表重复累积。
- 解决方案:
- 每次登录时先清空容器(如
friendList.clear()
),再加载新数据。 - 确保数据唯一性,避免重复存储。
- 每次登录时先清空容器(如
4. 接收线程的单例控制-重点
- 问题:
- 多次登录会重复启动接收线程,导致多个线程同时监听消息,引发混乱。
- 解决方案:
- 使用静态变量确保线程只启动一次:
- 首次登录时启动线程,后续登录直接复用。
- 避免资源浪费和线程竞争。
- 使用静态变量确保线程只启动一次:
巧用静态局部变量
静态局部变量(static
局部变量)是 C/C++ 中的一种局部变量,其特点是:
- 作用域:与普通的局部变量一样,静态局部变量的作用域仅限于函数内部。它只能在声明它的函数内使用,外部无法访问。
- 生命周期:与普通的局部变量不同,静态局部变量的生命周期是整个程序运行期间。它在程序开始时就被分配内存,并在程序结束时销毁,而普通局部变量则是在函数调用时创建,在函数调用结束时销毁。
- 初始化:静态局部变量只会在第一次进入该函数时进行初始化,之后不会再次初始化。如果没有显式初始化,静态局部变量会被默认初始化为零(对于基本数据类型)。
#include <iostream>void counterFunction() {static int counter = 0; // 静态局部变量counter++;std::cout << "Counter: " << counter << std::endl;
}int main() {counterFunction(); // 输出 Counter: 1counterFunction(); // 输出 Counter: 2counterFunction(); // 输出 Counter: 3return 0;
}
老师这节课进行了一个小总结
5. 架构设计思想
- 分层解耦:
- 网络层:仅负责数据传输(如收发JSON消息)。
- 业务层:处理具体逻辑(如注销、更新状态),不直接操作数据库。
- 数据层:通过ORM(如
User
、Group
类)封装数据库操作,业务层无需关心SQL细节。
- 关键优势:
- 代码可维护性高,各层职责清晰。
- 扩展性强(如未来支持集群化,只需修改服务端,客户端无感知)。
6. 集群化扩展的铺垫
- 客户端无感知:
- 无论服务端是单机还是集群,客户端只需发送请求,不关心后端如何负载均衡。
- 服务端集群化:
- 后续可通过
Nginx
实现负载均衡,水平扩展服务器性能。在很短的时间内 提升服务器并发能力 - 需解决共享状态问题(如用户连接信息需集中管理,如用Redis)。
- 后续可通过
错误
-
main 的 for循环 最后 不要return, 不然 注销后, 主线程就结束了
-
老师的 课中, 出现正常注销后, 再次登录, 会打印两边信息, 是因为 那几个变量是 全局的, 每次重新登录后变量没有清空, 之前的信息还在
g_currentUserGroupsList.clear(); // vector.clear()清空一下
代码
include/public.hpp
LOGINOUT_MSG, // 登录成功
include/server/chatservice.hpp
// 处理注销业务void loginout(const TcpConnectionPtr &conn, json &js, Timestamp time);
src/server/chatservice.cpp
_msghandlermap.insert({LOGINOUT_MSG, std::bind(&ChatService::loginout, this, _1, _2, _3)});// 处理注销业务
void ChatService::loginout(const TcpConnectionPtr &conn, json &js, Timestamp time)
{int userid = js["id"].get<int>();{lock_guard<mutex> lock(_connMutex);auto it = _userConnMap.find(userid);if(it != _userConnMap.end()){_userConnMap.erase(it);}}// 更新用户状态User user(userid, "","", "offline");// user.setId(userid);// user.setState("offline");_usermodel.updateState(user); // 仅需要id和状态, 剩下的具体 由函数在数据库完成
}
src/client/main.cpp
// 控制聊天页面--注销需要退出聊天页面
bool isMainMenuRunning = false;
// 登录成功, 启动接收线程----只要客户端 不完全退出, 就只启动一次!
static int threadnum = 0;
if (threadnum == 0)
{std::thread readTask(readTaskHandler, clientfd); // thread 支持跨平台readTask.detach();
}// 分离线程, 让其独立运行, 不阻塞主线程// 主线程继续执行, 进入聊天菜单页面
isMainMenuRunning = true;
mainMenu(clientfd);
// 删除 return 0;
// 主页面聊天程序
void mainMenu(int clientfd)
{help();// for (;;)while (isMainMenuRunning){...}
}
// quit函数
void quit(int clientfd, string msg)
{json js;js["msgid"] = LOGINOUT_MSG; // 注销消息js["id"] = g_currentUser.getId(); // 当前登录用户idstring request = js.dump(); // json转字符串 序列化int len = send(clientfd, request.c_str(), strlen(request.c_str()) + 1, 0); // 发送数据if (len < 0){cerr << "send quit msg error: " << request << endl;}isMainMenuRunning = false; // 退出聊天页面// close(clientfd); 放在服务端处理// exit(0);
}
相关文章:
施磊老师基于muduo网络库的集群聊天服务器(七)
文章目录 数据表字符集问题支持中文和英文**为什么使用 utf8mb4?** 推荐 查看整个表, 再单独修改 客户端群组功能创建群组添加群组群组聊天接收在线群组消息接收离线群组消息补充服务器事件处理器补充服务器查询群组列表问题解决测试 目前报错总结目前为止最恶心的错…...

多态以及多态底层的实现原理
本章目标 1.多态的概念 2.多态的定义实现 3.虚函数 4.多态的原理 1.多态的概念 多态作为面对三大特性之一,它所指代的和它的名字一样,多种形态.但是这个多种形态更多的指代是函数的多种形态. 多态分为静态多态和动态多态. 静态多态在前面已经学习过了,就是函数重载以及模板,…...

使用Go语言实现轻量级消息队列
文章目录 一、引言1.1 消息队列的重要性1.2 为什么选择Go语言1.3 本文实现的轻量级消息队列特点 二、核心设计2.1 消息队列的基本概念2.1.1 消息类型定义2.1.2 消息结构设计 2.2 架构设计2.2.1 基于Go channel的实现方案2.2.2 单例模式的应用2.2.3 并发安全设计 2.3 消息发布与…...
Vue3后代组件多祖先通讯设计方案
在 Vue3 中,当需要设计一个被多个祖先组件使用的后代组件的通讯方式时,可以采用以下方案(根据场景优先级排序): 方案一:依赖注入(Provide/Inject) 响应式上下文 推荐场景ÿ…...

路由与OSPF学习
【路由是跨网段通讯的必要条件】 路由指的是在网络中,数据包从源主机传输到目的主机的路径选择过程。 路由通常涉及以下几个关键元素: 1.路由器:是一种网络设备,负责将数据包从一个网络传输到另一个网络。路由器根据路由表来决定…...

CUDA编程之Grid、Block、Thread线程模型
一、线程模型:Grid、Block、Thread概念 1. 层级定义 Thread(线程) CUDA中最基本的执行单元,对应GPU的单个CUDA核心(SP)。每个线程独立执行核函数指令,拥有独立的寄存器和局部内存空间。 Block(线程块) 由多个线程组成(通常为32的倍数),是逻辑上的并…...
postgres 导出导入(基于数据库,模式,表)
在 PostgreSQL 中,导出和导入数据库、模式(schema)或表的数据可以使用多种工具和方法。以下是常用的命令和步骤,分别介绍如何导出和导入整个数据库、特定的模式以及单个表的数据。 一、导出数据 1. 使用 pg_dump 导出整个数据库…...

小学数学出题器:自动化作业生成
小学数学出题器是专为教师、家长设计的自动化作业生成工具,通过预设参数快速生成符合教学要求的练习题,大幅降低备课与辅导压力。跨平台兼容:支持 Windows 系统免安装运行(解压即用)。免费无广告:永…...
systemctl 命令详解与常见问题解决
在 Linux 系统中,service 命令和 chkconfig 命令一直用于管理服务,但随着 systemd 的引入,systemctl 命令逐渐成为主流。systemctl 命令不仅功能强大,而且使用简单。本文将详细介绍 systemctl 命令的作用以及常见问题的解决方法。…...
12.桥接模式:思考与解读
原文地址:桥接模式:思考与解读 更多内容请关注:7.深入思考与解读设计模式 引言 在软件设计中,尤其是在处理复杂系统时,你是否遇到过这样的情况:你的系统中有多个功能模块,而这些功能模块需要与不同的平台…...

卷积神经网络迁移学习:原理与实践指南
引言 在深度学习领域,卷积神经网络(CNN)已经在计算机视觉任务中取得了巨大成功。然而,从头开始训练一个高性能的CNN模型需要大量标注数据和计算资源。迁移学习(Transfer Learning)技术为我们提供了一种高效解决方案,它能够将预训练模型的知识…...
Centos虚拟机远程连接缓慢
文章目录 Centos虚拟机远程连接缓慢1. 问题:SSH远程连接卡顿现象2. 原因:SSH服务端DNS检测机制3. 解决方案:禁用DNS检测与性能调优3.1 核心修复步骤3.2 辅助优化措施 4. 扩展认识:SSH协议的核心机制4.1 SSH工作原理4.2 关键配置文…...

Spark与Hadoop之间的联系和对比
(一)Spark概述 Apache Spark 是一个快速、通用、可扩展的大数据处理分析引擎。它最初由加州大学伯克利分校 AMPLab 开发,后成为 Apache 软件基金会的顶级项目。Spark 以其内存计算的特性而闻名,能够在内存中对数据进行快速处理&am…...
C++学习笔记(三十九)——STL之删除算法
STL 算法分类: 类别常见算法作用排序sort、stable_sort、partial_sort、nth_element等排序搜索find、find_if、count、count_if、binary_search等查找元素修改copy、replace、replace_if、swap、fill等修改容器内容删除remove、remove_if、unique等删除元素归约for…...
C++——Lambda表达式
在C中,Lambda表达式是一种匿名函数对象,它允许你在代码中直接定义一个函数,而不需要提前声明一个单独的函数。Lambda表达式是从C11标准开始引入的,它极大地增强了C语言的灵活性和表达能力,尤其在处理函数对象、回调函数…...

基于线性LDA算法对鸢尾花数据集进行分类
基于线性LDA算法对鸢尾花数据集进行分类 1、效果 2、流程 1、加载数据集 2、划分训练集、测试集 3、创建模型 4、训练模型 5、使用LDA算法 6、画图3、示例代码 # 基于线性LDA算法对鸢尾花数据集进行分类# 基于线性LDA算法对鸢尾花数据集进行分类 import numpy as np import …...

【Deepseek基础篇】--v3基本架构
目录 MOE参数 1.基本架构 1.1. Multi-Head Latent Attention多头潜在注意力 1.2.无辅助损失负载均衡的 DeepSeekMoE 2.多标记预测 2.1. MTP 模块 论文地址:https://arxiv.org/pdf/2412.19437 DeepSeek-V3 是一款采用 Mixture-of-Experts(MoE&…...
从爬楼梯到算法世界:动态规划与斐波那契的奇妙邂逅
从爬楼梯到算法世界:动态规划与斐波那契的奇妙邂逅 在算法学习的旅程中,总有一些经典问题让人印象深刻。“爬楼梯问题”就是其中之一,看似简单的题目,却蕴藏了动态规划与斐波那契数列的深刻联系。今天,我就以这个问题…...

centos7使用yum快速安装最新版本Jenkins-2.462.3
Jenkins支持多种安装方式:yum安装、war包安装、Docker安装等。 官方下载地址:https://www.jenkins.io/zh/download 本次实验使用yum方式安装Jenkins LTS长期支持版,版本为 2.462.3。 一、Jenkins基础环境的安装与配置 1.1:基本…...

【vue】【element-plus】 el-date-picker使用cell-class-name进行标记,type=year不生效解决方法
typedete,自定义cell-class-name打标记效果如下: 相关代码: <el-date-pickerv-model"date":clearable"false":editable"false":cell-class-name"cellClassName"type"date"format&quo…...

c++11新特性随笔
1.统一初始化特性 c98中不支持花括号进行初始化,编译时会报错,在11当中初始化可以通过{}括号进行统一初始化。 c98编译报错 c11: #include <iostream> #include <set> #include <string> #include <vector>int main() {std:…...
Linux字符设备驱动开发的详细步骤
1. 确定主设备号 手动指定:明确设备号时,使用register_chrdev_region()静态申请(需确保未被占用)。动态分配:通过alloc_chrdev_region()由内核自动分配主设备号(更灵活,推…...
Nginx 二进制部署与 Docker 部署深度对比
一、核心概念解析 1. 二进制部署 通过包管理器(如 apt/yum)或源码编译安装 Nginx,直接运行在宿主机上。其特点包括: 直接性:与操作系统深度绑定,直接使用系统库和内核功能 。定制化:支持通过编译参数(如 --with-http_ssl_module)启用或禁用模块,满足特定性能需求 。…...

C++23 中 constexpr 的重要改动
文章目录 1. constexpr 函数中使用非字面量变量、标号和 goto (P2242R3)示例代码 2. 允许 constexpr 函数中的常量表达式中使用 static 和 thread_local 变量 (P2647R1)示例代码 3. constexpr 函数的返回类型和形参类型不必为字面类型 (P2448R2)示例代码 4. 不存在满足核心常量…...
CMake ctest
CMake学习–ctest全面介绍 1. 环境准备 确保已安装 cmake 和编译工具: sudo apt update sudo apt install cmake build-essential2. 创建示例项目 假设我们要测试一个简单的数学函数 add(),项目结构如下: math_project/ ├── CMakeList…...

全面解析React内存泄漏:原因、解决方案与最佳实践
在开发React应用时,内存泄漏是一个常见但容易被忽视的问题。如果处理不当,它会导致应用性能下降、卡顿甚至崩溃。由于React的组件化特性,许多开发者可能没有意识到某些操作(如事件监听、异步请求、定时器等)在组件卸载…...
JavaScript学习教程,从入门到精通,Ajax数据交换格式与跨域处理(26)
Ajax数据交换格式与跨域处理 一、Ajax数据交换格式 1. XML (eXtensible Markup Language) XML是一种标记语言,类似于HTML但更加灵活,允许用户自定义标签。 特点: 可扩展性强结构清晰数据与表现分离文件体积相对较大 示例代码࿱…...

【FreeRTOS】事件标志组
文章目录 1 简介1.1事件标志1.2事件组 2事件标志组API2.1创建动态创建静态创建 2.2 删除事件标志组2.3 等待事件标志位2.4 设置事件标志位在任务中在中断中 2.5 清除事件标志位在任务中在中断中 2.6 获取事件组中的事件标志位在任务中在中断中 2.7 函数xEventGroupSync 3 事件标…...

超级扩音器手机版:随时随地,大声说话
在日常生活中,我们常常会遇到手机音量太小的问题,尤其是在嘈杂的环境中,如KTV、派对或户外活动时,手机自带的音量往往难以满足需求。今天,我们要介绍的 超级扩音器手机版,就是这样一款由上海聚告德业文化发…...

【数据可视化-27】全球网络安全威胁数据可视化分析(2015-2024)
🧑 博主简介:曾任某智慧城市类企业算法总监,目前在美国市场的物流公司从事高级算法工程师一职,深耕人工智能领域,精通python数据挖掘、可视化、机器学习等,发表过AI相关的专利并多次在AI类比赛中获奖。CSDN…...