• 施磊老师基于muduo网络库的集群聊天服务器(八)

施磊老师基于muduo网络库的集群聊天服务器(八)

2025-05-01 09:57:08 0 阅读

文章目录

  • 负载均衡
    • 常见网络模型
    • 为什么要引入集群
    • 用户怎么知道连接哪台服务器?
    • 负载均衡分层理解
    • 负载均衡器的核心作用(3 个关键功能)
    • 选择的负载均衡器:Nginx TCP 模块
    • 更高级的扩展方案(**LVS** + Nginx)
    • 负载均衡算法(简要了解)
    • 结合聊天业务的特点总结(gpt)
    • 面试或项目总结时可以怎么讲?(gpt)
  • 聊天系统集群中的跨服务器通信
    • 问题背景:
    • 高内聚低耦合:
    • 设计目标:
    • 解决方案:引入 Redis 发布订阅机制
    • 常用中间件
    • Redis
  • linux负载均衡配置与验证
    • ngnix安装
    • 报错问题
    • nginx配置文件
    • 负载均衡配置
      • 用法说明
      • 解释
      • 工作流程图示(逻辑):
      • 参数意义
    • 平滑启动
    • 修改服务器main.cpp代码
    • 测试
  • Redis环境安装
    • Redis 在项目中的角色
    • 安装
    • 键值对操作
    • 持久化存储(简单了解)
    • 发布 / 订阅机制的原理
    • 客户端订阅频道
    • 发布消息到频道
    • 项目中如何应用发布/订阅
  • 补充:A给B发消息的全过程
    • 场景
    • 步骤 1:A发送消息
    • 步骤 2:服务器1处理
    • 步骤 3:服务器1向Redis发布
    • 步骤 4:Redis分发
    • 步骤 5:服务器2回调触发
    • 步骤 6:服务器2推给客户端B
    • 这个例子里面有哪些关键点?
    • 最后总结
  • Redis编程
    • 支持多语言
    • 安装hiredis
    • 为什么要用 Redis 客户端?
    • 代码目录调整
    • 修改CMakeLists.txt
    • 核心设计
    • 功能类
    • 老师有两个bug解决
    • redisAppendCommand和redisCommand
      • 为什么用 `redisAppendCommand`?
      • `redisCommand` 的内部流程(实际上是分三步走的)
    • redis小结-问题
  • 同步redis代码-**重点代码**
    • redis.hpp
    • redis.cpp
  • 添加redis到服务器
    • ChatService.hpp
    • ChatService.cpp
      • 构造函数
      • 登陆成功
      • 退出登录时
      • 客户端异常断开
      • 服务器异常
      • 一对一聊天(oneChat)逻辑
      • 群聊(groupChat)逻辑
      • Redis回调处理
    • 测试

负载均衡

常见网络模型

为什么要引入集群

1. 单台服务器并发能力有限

  • 在 32 位 Linux 下,一个进程默认能使用的文件描述符是 1024 个。
  • 即使通过 ulimit 增加上限,也只能撑到 2 万左右。
  • 所以单台服务器最大只能支持约 2 万个用户同时在线聊天。

2. 想支持更多用户怎么办?

  • 横向扩展:服务器集群部署,即多台服务器协同处理用户请求。
  • 垂直扩展: 意思是 升级这台单机, 让这台单机更强
  • 每台服务器运行同一个 ChatServer 程序,互不干扰。
  • 本质上是“复制多台服务”,每台承接一部分用户。

用户怎么知道连接哪台服务器?

问题

  • 像 QQ、微信那样的客户端不会让你选择连接哪台服务器。
  • 因为用户不知道哪台服务器空闲、哪台繁忙。

解决方案:引入“负载均衡器”

  • 用户统一连接 负载均衡器(Load Balancer)。
  • 负载均衡器再根据一定策略,把请求分发到后端的某台 ChatServer

负载均衡分层理解

层级代表技术 / 工具负载方式特点
数据链路层(第2层)Switch(交换机)基于 MAC 地址很低层,常用于局域网广播控制,不用于业务层面负载均衡
网络层(第3层)LVS(DR 模式、TUN 模式)基于 IP 分发性能极高,只做 IP 层转发,不理解上层协议
传输层(第4层)LVS、Nginx(stream 模块)、HAProxy基于 TCP/UDP 端口分发适合聊天这种 TCP 长连接业务
应用层(第7层)Nginx(http 模块)、HAProxy、Traefik基于 URL、Cookie、Header 等灵活但性能略低,适合 Web 请求

负载均衡器的核心作用(3 个关键功能)

1. 客户端请求的分发者

  • 接收所有客户端连接。
  • 根据负载算法(轮询、权重、IP 哈希等)将连接分发给某台后端服务器
  • 对于聊天这种长连接业务,客户端连接会一直保持,不关闭。

2. 服务器状态的监测者(心跳机制)

  • 要实时知道后端哪些 ChatServer 还能用,哪些已经故障。
  • 做法:
    • 与后端服务器之间建立长连接。
    • 定时发送心跳包,如果连续几次无响应,就认为该服务器失效。
  • 如果某台服务器宕机或网络异常,立即停止将新请求分发过去

3. 支持动态扩容,平滑接入新服务器

  • 用户量增加后,可以动态添加新服务器
  • 负载均衡器可以在不中断服务的情况下,热加载新配置(如 nginx 的 reload 命令)。
  • 不影响原有用户在线聊天,真正做到“平滑扩容”


选择的负载均衡器:Nginx TCP 模块

牛逼的人— 可以去看看 nginx 源码

为什么选 Nginx?

  • 支持高并发:一台 Nginx 轻松支持 5~6 万连接
  • 拓展能力强:可以配置多种负载算法。
  • 稳定性好:Nginx 本身的网络模型是高性能的。

Nginx 如何处理聊天这种长连接业务?

  • 保持连接通道一直存在(不是每次请求都重连)。
  • 所有消息进出(客户端→服务端、服务端→客户端)都经过负载均衡器
  • 而 如果 客户端发送经过负载均衡, 服务器回应不经过负载均衡 也是可行的, 只要知道 客户端ip信息即可

更高级的扩展方案(LVS + Nginx)

如果要支持十几万连接?

  • 单个 Nginx 可能瓶颈了,可以再前置一个更底层的负载均衡器:

    • LVS(Linux Virtual Server)
    • 工作在 IP 层或传输层(性能更高)
  • 架构变为:

    客户端 → LVS → 多台 Nginx → 多台 ChatServer
    

负载均衡算法(简要了解)

  • 轮询(Round Robin):每个请求轮着来,最简单。
  • 加权轮询:给性能高的机器多分配一些请求。
  • IP 哈希:同一 IP 的用户总是分配到同一台服务器上。
  • 最少连接数:选择当前连接数最少的服务器。

结合聊天业务的特点总结(gpt)

方面说明
连接类型长连接(用户连接一旦建立,就持续存在)
分发要求请求分发需保持一致性(一个用户连接在哪台服务器,就一直在那里)
响应路径服务端响应必须走负载均衡器(除非配置直连隧道)
心跳监测防止把请求分发给失效的服务节点
热扩展添加新服务器时不需要重启负载均衡器

面试或项目总结时可以怎么讲?(gpt)

“在我们的项目中,为了解决单机并发瓶颈,我们采用了集群部署 + Nginx TCP 负载均衡。客户端只连接到 Nginx,由它根据配置的负载算法把请求转发给后端服务器。为了保证可用性,我们还实现了心跳监测机制,能动态剔除失效节点,同时支持服务的平滑扩展。通过这种方式,我们系统的并发能力从 2 万提升到了 6 万以上,且具备良好的可扩展性。”

聊天系统集群中的跨服务器通信

问题背景:

  • 在集群架构中,用户 A 和用户 B 可能登录在不同服务器上,如何实现两人之间的一对一聊天?

造成该问题的根本原因在于:
当用户登录在不同服务器上时,原本用于存储在线用户连接的 _userConnMap(只在本地服务器维护)无法获取到其他服务器上已登录用户的连接信息,从而导致无法直接向其转发消息。

最简单的想法是 服务期间建立连接, 但是这样, 服务器压力就大了

高内聚低耦合:

一、什么是高内聚?

定义:
一个模块内部的功能尽量相关,集中完成某一类任务。

通俗理解:
一个模块只做一件事,而且把这件事做好。

好处:

  • 易于维护和修改
  • 易于理解和测试
  • 逻辑清晰、职责单一

示例:
在聊天系统中,把“消息发送”相关的逻辑(如构造消息、转发消息、存储离线消息)放在一个 MessageService 模块里,而不是散落在多个地方。


二、什么是低耦合?

定义:
模块与模块之间的依赖尽量少,依赖的内容尽量简单。

通俗理解:
模块之间互不干扰,改变一个模块对其他模块影响最小。

好处:

  • 提升模块的独立性
  • 更容易替换、扩展模块
  • 降低系统出错的可能性

示例:
在聊天系统中,客户端与服务器通过 JSON 协议交互,而不是直接调用彼此的函数。这种“协议通信”就是一种低耦合的体现。


三、一句话总结:

高内聚是“自己事自己干”,低耦合是“别人的事少管”。

设计目标:

  • 客户端无感知集群结构
  • 服务器之间不直接连接,避免强耦合
  • 支持任意用户间的通信,无论在哪台服务器登录

解决方案:引入 Redis 发布订阅机制

集群部署的服务器之间进行通信,最好的方式就是引入中间件消息队列,解耦各个服务器,使整个系统松耦合,提高服务器的响应能力,节省服务器的带宽资源

常用中间件

在集群分布式环境中,经常使用的中间件消息队列有ActiveMQ、RabbitMQ、Kafka等,都是应用场景广泛并且性能很好的消息队列,供集群服务器之间,分布式服务之间进行消息通信。

kafka 企业用的多–大型, 十几万, 几十万

限于我们的项目业务类型并不是非常复杂,对并发请求量也没有太高的要求,因此我们的中间件消息队列选型的是-基于发布-订阅模式的redis

Redis

观察者设计模式的应用

1. Redis 作为中间件的作用:

  • 消息转发中心:用于转发跨服务器的聊天消息
  • 状态共享工具:可存储用户在线状态、服务器分配信息等(可选)

2. 服务端订阅用户频道:

  • 每台服务器在用户登录时订阅 Redis 频道 channel_userId,表示当前用户在此服务器上活跃

3. 跨服务器消息流程:

  • 用户 A 发消息 → 所在服务器判断 B 是否在本地
    • 是:直接发送
    • 否:将消息发布到 Redis 的 channel_userIdB
  • 用户 B 所在服务器收到 Redis 推送后 → 将消息转发给 B

4. 用户退出处理:

  • 用户退出时,取消对应频道的订阅,释放资源

linux负载均衡配置与验证

ngnix安装

nginx默认并没有编译tcp负载均衡模块,编写它时,需要加入–with-stream参数来激活这个模块。

把nginx 安装在 package文件夹

./configure --with-stream

make && make install

报错问题

src/os/unix/ngx_user.c: In function ‘ngx_libc_crypt’:
src/os/unix/ngx_user.c:36:7: error: ‘struct crypt_data’ has no member named ‘current_salt’
   36 |     cd.current_salt[0] = ~salt[0];
      |       ^

nginx 版本太久, linux版本太新

nginx配置文件

/usr/local/nginx 

这个文件夹很重要!!!

里面有

./conf/nginx.conf-----配置文件
./sbin/nginx----服务的启动

负载均衡配置

vim ./conf/nginx.conf
event{....}
//---
stream {
    # 1️⃣ 定义一个后端服务器集群(upstream)
    upstream MyServer {
        server 127.0.0.1:6000 weight=1 max_fails=3 fail_timeout=30s;
        server 127.0.0.1:6002 weight=1 max_fails=3 fail_timeout=30s;
    }

    # 2️⃣ 设置监听端口,接收客户端请求
    server {
        listen 8000;                       # Nginx 对外开放的 TCP 端口
        proxy_connect_timeout 1s;         # 连接后端服务器超时时间
        proxy_timeout 3s;                 # 与后端建立连接后的传输超时时间
        proxy_pass MyServer;              # 把请求转发到名为 MyServer 的后端集群
        tcp_nodelay on;                   # 优化 TCP,禁用 Nagle 算法,降低延迟
    }
}

//----
http{...}

用法说明

  • listen 8000: Nginx 监听 8000 端口,客户端连接这个端口。
  • proxy_pass MyServer: 请求被转发给后端的 MyServer 集群(你定义的两个端口)。
  • proxy_connect_timeout: 连接后端超时设置。最多等待多久, 超过这个时间,连接就会被判定失败。
  • proxy_timeout: 转发请求后的超时时间。已连接后、数据多久没动就超时----本项目不需要!!长连接不断开
  • tcp_nodelay: 减少延迟(禁用 Nagle 算法)

解释

这段配置使用了 Nginx 的 stream 模块,用于四层 TCP 代理和负载均衡,也就是:

把客户端发往 Nginx(如端口 8000)的 TCP 请求,按负载策略转发到后端多个服务器(如 6000、6002 端口)。

工作流程图示(逻辑):

[客户端] ---> [Nginx:8000] ---> (负载均衡) ---> [127.0.0.1:6000 或 6002]
  • Nginx 接收客户端 TCP 请求
  • 根据轮询策略将请求转发给后端服务器
  • 如果某个后端连接失败超过 3 次30 秒内不会再尝试连接max_fails + fail_timeout 控制)

参数意义

参数含义
listen 8000-重点Nginx 监听的 TCP 端口
proxy_pass MyServer-重点使用上面定义的后端集群
proxy_connect_timeout 1s后端连接超时,Nginx 给后端发 TCP 连接请求,多久没连上就放弃
proxy_timeout 3s数据传输超时(连接建立后),建立连接后,多久没收到数据就断开连接
tcp_nodelay on禁用 Nagle 算法,提升小包实时性
weight=1每个后端权重
max_fails=3最多失败 3 次判定该节点不可用
fail_timeout=30s在 30 秒内不再访问故障节点

负载均衡算法 可以配置, 但是需要插件

平滑启动

netstat -tanp
   
/usr/local/nginx/sbin/nginx 

# 修改配置, 平滑启动
/usr/local/nginx/sbin/nginx -s reload

# 停止服务,杀进程不可取
/usr/local/nginx/sbin/nginx -s stop

修改服务器main.cpp代码

从命令行获取 ip 和 port

int main(int argc, char **argv)
{
    if(argc < 3)
    {
        cerr << "command invalid example ./bin/chatserver"<

测试

// 服务器
./bin/Chatserver 127.1 6000
./bin/Chatserver 127.1 6002
// 客户端
./bin/Chatclient 127.0.0.1 8000
./bin/Chatclient 127.0.0.1 8000
20250426 06:49:00.026995Z 28142 ERROR sockets::fromIpPort - SocketsOps.cc:241

原因是 127.1 没有写正规的 127.0.0.1

此时没有 使用中间件, 可以测试得到, 不同服务器没法通信

Redis环境安装

Redis 在项目中的角色

  • Redis 作为 服务器中间件消息队列
  • 解决多个 Chat Server 之间的“强耦合连接问题
  • 通过 发布/订阅机制(Pub/Sub) 实现消息的跨服务器分发
  • 实际上 Redis 本质是一个“基于内存的键值对缓存数据库”,但在本项目中用它来解耦通信逻辑
  • 运行在内存中的 键值对存储数据库,速度非常快。

安装

sudo apt-get install redis-server

默认端口: 6379

数据库: 3306

键值对操作

redis-cli
set "键" "值"
get "键"

还可以存存 链表, 数组,复杂数据结构等

存储在 内存上, 效率很高

有些时候, 会舍弃mysql, 直接使用 redis

持久化存储(简单了解)

想深入, 有时间自己研究研究

Redis 的数据默认是存在内存中的,为了防止服务重启后数据丢失,它支持两种数据持久化存储机制

  • Redis 是内存数据库,内存断电即失。
  • 为了在 Redis 重启后能“恢复数据”,必须有**“写入磁盘”**的手段。
  • Redis 提供两种方式:RDBAOF
  1. RDB(快照)
  • 做法:定时把内存数据一次性保存成 .rdb 文件。
  • 优点:文件小,恢复快。
  • 缺点:非实时,可能丢失几分钟数据。
  • 适合场景:数据量大、变化不频繁、用于灾备。

  1. AOF(操作日志)
  • 做法:把每次写命令都追加写入 .aof 文件。
  • 优点:数据更完整,最多丢 1 秒数据。
  • 缺点:文件大,恢复慢。
  • 适合场景:对数据完整性要求高,比如聊天记录、交易数据。

  1. 组合使用
  • 可同时开启 RDB + AOF。
  • Redis 优先用 AOF 恢复,确保数据最全。

发布 / 订阅机制的原理

发布 / 订阅的核心思想:

  • Redis 可以建立很多“频道”(Channel)
  • 你可以订阅某个频道,监听它是否有新消息
  • 当别人向这个频道“发布”一条消息时,Redis 会把这条消息**“推送”给所有订阅**了这个频道的用户

客户端订阅频道

subscribe 13
  • 阻塞命令,监听频道 13 的消息
  • 通常以 用户ID 作为频道ID,比如订阅 “用户13 的消息”

发布消息到频道

publish 13 "hello world"
  • Redis 会立即将消息推送给所有订阅了 13 频道的客户端(如服务器)

项目中如何应用发布/订阅

用户登录时

  • Chat Server 会向 Redis 订阅以用户ID为频道名的通道

    subscribe 用户ID
    

发送消息时(跨服务器)

  • 若目标用户在其他服务器,当前服务器通过 Redis 向该用户ID对应的频道发布消息:

    publish 用户ID 消息内容
    

Redis 发现订阅了该频道的服务器

  • 将消息推送给对应的 Chat Server,由它通知在线用户

补充:A给B发消息的全过程

场景

  • 用户A(id: 1001),连接到了服务器1
  • 用户B(id: 1002),连接到了服务器2
  • A想给B发一条"hello"。

步骤 1:A发送消息

  • A在客户端输入:“hello”,发给1002。
  • 客户端把消息打包成JSON,通过TCP发给服务器1。

步骤 2:服务器1处理

  • 服务器1收到消息:
    • 解析出目标用户是1002
  • 服务器1查询自己这台机器的在线用户列表:
    • 发现没有1002(因为B在服务器2)。

步骤 3:服务器1向Redis发布

  • 服务器1从数据库知道,B可能在其他服务器上。
  • 所以,它在Redis上发布到通道1002,内容是"hello"。
Redis.publish("1002", "来自1001的消息:hello")

步骤 4:Redis分发

  • Redis收到这个publish。
  • 检查有哪些服务器订阅1002这个通道。
  • 发现服务器2订阅了!
  • Redis立刻把这条消息推送给服务器2。

步骤 5:服务器2回调触发

  • 服务器2收到Redis推送。
  • 触发了之前注册的回调函数
  • 回调函数处理消息:
    • 查找本地连接,找到用户1002在线。

步骤 6:服务器2推给客户端B

  • 服务器2直接通过TCP连接,把"来自1001的消息:hello"推送给用户B的客户端。

✅ 至此,消息送达!

这个例子里面有哪些关键点?

步骤关键点说明
发布(服务器1 ➔ Redis)发布消息不关心谁来接,发到Redis
订阅(服务器2订阅1002通道)订阅机制服务器提前订阅用户ID对应通道
推送(Redis ➔ 服务器2)推送到订阅者Redis负责找对应服务器
分发(服务器2 ➔ 用户B)最终推送本地推给客户端

最后总结

Redis在这里干了两件事:

  • 统一收消息(Publish
  • 按订阅推消息(Subscribe

服务器之间自己啥都不用干,只要:

  • 登录时订阅
  • 下线时取消订阅
  • 发消息时 publish

全靠Redis帮忙转发,服务器自己专心处理连接和逻辑!

Redis编程

不需要了解太多怎么写, redis编程本身不重要, 重要的是 要了解逻辑
代码很多都是 复制过来 进行修改的

支持多语言

redis支持多种不同的客户端编程语言,例如Java对应jedis、php对应phpredis、C++对应的则是hiredis

安装hiredis

git clone https://github.com/redis/hiredis

// cd
make && make install

为什么要用 Redis 客户端?

  • 我们需要在代码里操作 Redis Server(连上它,收发消息)。
  • 而 收发消息 在代码里, 对应的就是 设置回调函数

代码目录调整

include/server/redis ➔ 放 redis.hpp
src/server/redis ➔ 放 redis.cpp

修改CMakeLists.txt

包含头文件目录

include_directories(${PROJECT_SOURCE_DIR}/include/server/redis) #redis服务头文件

加入源文件列表

链接 -lhiredis 动态库

# redis服务源文件
aux_source_directory(./redis REDIS_LIST)
# 生成可执行
add_executable(Chatserver ${SRC_LIST} ${DB_LIST} ${MODEL_LIST} ${REDIS_LIST})

# 链接库
target_link_libraries(Chatserver muduo_net muduo_base pthread mysqlclient hiredis)

核心设计

维护两个 Redis连接(Context):

  • 一个发布消息
  • 一个订阅通道(因为订阅是阻塞的,不能混用)

提供一个回调函数,让业务层注册,当有消息发布到订阅通道时通知上层(观察者模式)。

回顾一下----观察者模式的 设计模式

private:
// hiredis 同步上下文对象, 负责publish
redisContext *_publish_context;

// hiredis 同步上下文对象, 负责subscribe
redisContext *_subscribe_context;

// 回调操作, 收到订阅消息, 给service层上报
function _notify_message_handler;  // 在业务层定义具体函数
/*
    int, string
    对应 redis 回应的 (2)(3)
    1) "message"
    2) "13"
    3) "hello"
    */

功能类

  • connect()
    ➔ 连接 Redis Server,分别建两个连接(发布+订阅)。
    ➔ 订阅连接要开独立线程,因为subscribe阻塞式的,不能影响主线程。
  • publish(channel, message)
    ➔ 往指定通道发布一条消息(简单调用redisCommand就行)。
  • subscribe(channel)
    ➔ 订阅某个通道,但注意:
    • 不能直接用redisCommand,因为它会阻塞等服务器返回(不行)----发布不阻塞
    • 只能用redisAppendCommand+redisBufferWrite
      只发出去,不等返回,这样线程不卡死。
  • unsubscribe(channel)
    ➔ 取消订阅,逻辑跟订阅一样(非阻塞发送指令)。
  • observerMessage() ➔ 独立线程中阻塞式监听订阅连接上有没有消息到来,一旦有,调用注册的回调通知上层。
// 链接redis服务器
bool connect();

// 向redis指定的频道channel发布消息
bool publish(const string &channel, const string &message);

// 向redis指定的频道channel订阅消息
bool subscribe(const string &channel);

// 向redis指定的频道channel取消订阅
bool unsubscribe(const string &channel);

// 在独立线程中接受订阅频道的消息
bool oberver_channel_message();

// 初始化向业务层上报消息的回调函数
void init_notify_message_handler(function fn);

老师有两个bug解决

在老师博客里

redisAppendCommand和redisCommand

为什么用 redisAppendCommand?

不用 redisCommand

redisCommand

  • 发送命令 ➔ 然后等待服务器响应 ➔ 返回响应结果
  • 同步的(卡住线程直到有回复)
  • 其调用的第一个命令就是**redisAppendCommand, 先把命令缓存**到本地

redisAppendCommand

  • 只是把命令写到发送缓冲区
  • 不等服务器回响应,不阻塞
  • 后面自己调用 redisBufferWrite ➔ 把缓冲区数据真正发出去
  • 如果要读取响应,再手动redisBufferRead+redisGetReply

一句话总结:

  • redisCommand 是一条龙(发+收+返回结果)。
  • redisAppendCommand 只发,不收。

redisCommand 的内部流程(实际上是分三步走的)

  1. redisAppendCommand —— 构造命令并缓存到本地发送缓冲区
    • 它把你要发的 Redis 命令(比如 SET key value)按照 Redis 的 RESP 协议格式化好,先放到本地的输出缓冲区
    • 这个阶段只是准备,并没有真正发到服务器上。
  2. redisBufferWrite —— 把本地缓冲区的数据真正发送给 Redis 服务器
    • 这一步负责把上一步缓存好的命令,通过 TCP 连接真正发出去
  3. redisGetReply —— 阻塞等待 Redis 服务器返回响应
    • 发送完命令后,就阻塞住等待服务器的响应数据。
    • 收到数据后,会解析成一个 redisReply 结构体返回给应用程序。
redisCommand
 ├── redisAppendCommand  (命令格式化 + 缓存到本地)
 ├── redisBufferWrite    (把命令发出去)
 └── redisGetReply       (阻塞等待服务器返回结果)

redis小结-问题

为什么订阅和发布要分开连接?
因为一旦用订阅,那个连接就卡死了(阻塞),不能再用它发消息。

为什么订阅要用append/write而不是直接command?
因为redisCommand默认是同步的,它会等返回(而我们订阅只发命令,不等待)。

为什么单独线程?
因为订阅是阻塞式监听,如果不单开线程,主逻辑就卡住了。

同步redis代码-重点代码

这部分代码, 可以保存下来, 只要做 同步redis 差不多的, 大体上就长这样!!

redis.hpp

#ifndef REDIS_H
#define REDIS_H

#include 
#include 
#include 
using namespace std;

class Redis
{
public:
    Redis();
    ~Redis();

    // 链接redis服务器
    bool connect();

    // 向redis指定的频道channel发布消息
    bool publish(const string &channel, const string &message);

    // 向redis指定的频道channel订阅消息
    bool subscribe(const string &channel);

    // 向redis指定的频道channel取消订阅
    bool unsubscribe(const string &channel);

    // 在独立线程中接受订阅频道的消息
    void oberver_channel_message();

    // 初始化向业务层上报消息的回调函数
    void init_notify_message_handler(function fn);

private:
    // hiredis 同步上下文对象, 负责publish
    redisContext *_publish_context;

    // hiredis 同步上下文对象, 负责subscribe
    redisContext *_subscribe_context;

    // 回调操作, 收到订阅消息, 给service层上报
    function _notify_message_handler;
    /*
    int, string
    对应 redis 回应的 (2)(3)
    1) "message"
    2) "13"
    3) "hello"
    */
};

#endif

redis.cpp

连接 Redis 后你要检查两个东西:

  • _publish_context == nullptr —— 完全没连上,指针为空。
  • _publish_context->err != 0 —— 指针存在,但连接内部有错误(比如超时、拒绝连接等)

添加redis到服务器

ChatService.hpp

  • 包含 redis.hpp
  • 定义一个 Redis 成员变量 _redis
#include "redis.hpp"
#include 
using namespace std;

Redis::Redis()
    : _publish_context(nullptr), _subscribe_context(nullptr)
{
}

Redis::~Redis()
{
    if (_publish_context != nullptr)
    {
        redisFree(_publish_context);
    }
    if (_subscribe_context != nullptr)
    {
        redisFree(_subscribe_context);
    }
}

bool Redis::connect()
{
    // publish连接redis服务器
    _publish_context = redisConnect("127.0.0.1", 6379);
    if (_publish_context == nullptr || _publish_context->err)
    {
        cout << "connect redis server failed" << endl;
        return false;
    }

    // subscibe连接redis服务器
    _subscribe_context = redisConnect("127.0.0.1", 6379);
    if (_subscribe_context == nullptr || _subscribe_context->err)
    {
        cout << "connect redis server failed" << endl;
        return false;
    }

    // 在独立线程(是线程)中, 监听通道上的事件, 有消息给业务层进行上报
    thread t([&]()
             { oberver_channel_message(); });
    t.detach();

    cout << "connect redis server success" << endl;
    return true;
}

// 向redis指定的频道channel发布消息
bool Redis::publish(const int channel, const string message)
{
    redisReply *reply = (redisReply *)redisCommand(_publish_context, "PUBLISH %d %s", channel, message.c_str());
    if (reply == nullptr)
    {
        cout << "publish message failed" << endl;
        return false;
    }
    freeReplyObject(reply);
    return true;
}

// 向redis指定的频道channel订阅消息
bool Redis::subscribe(const int channel)
{
    // SUBSCRIBE命令本身会造成线程阻塞等待通道里面发生消息,这里只做订阅通道,不接收通道消息
    //  通道消息的接收专门在observer_channel_message函数中的独立线程中进行----这就是接收函数存在的意义
    // 只负责发送命令,不阻塞接收redis server响应消息,否则和notifyMsg线程抢占响应资源
    if (REDIS_ERR == redisAppendCommand(this->_subscribe_context, "SUBSCRIBE %d", channel))
    {
        cerr << "subscribe command failed!" << endl;
        return false;
    }

    //  redisBufferWrite 可以循环发送缓冲区, 直到缓冲区数据发送完毕
    int done = 0;
    while (!done)
    {
        if (REDIS_ERR == redisBufferWrite(this->_subscribe_context, &done))
        {
            cerr << "subscribe command failed!" << endl;
            return false;
        }
    }
    // redisGetReply
    /*
    redisCommand 包含的 3个 函数:
    redisAppendCommand  (命令格式化 + 缓存到本地)
    redisBufferWrite    (把命令发出去)
    redisGetReply       (阻塞等待服务器返回结果)-- 在单独的一个接收线程上!!!
    */
    return true;
}

// 向redis指定的频道channel取消订阅
bool Redis::unsubscribe(const int channel)
{
    if (REDIS_ERR == redisAppendCommand(this->_subscribe_context, "UNSUBSCRIBE %d", channel))
    {
        cerr << "unsubscribe command failed!" << endl;
        return false;
    }
    //  redisBufferWrite 可以循环发送缓冲区, 直到缓冲区数据发送完毕
    int done = 0;
    while (!done)
    {
        if (REDIS_ERR == redisBufferWrite(this->_subscribe_context, &done))
        {
            cerr << "subscribe command failed!" << endl;
            return false;
        }
    }
    return true;
}

// 在独立线程中接受订阅频道的消息--存在的意义 看订阅那里
void Redis::oberver_channel_message()
{
    redisReply *reply = nullptr;
    while (REDIS_OK == redisGetReply(this->_subscribe_context, (void **)&reply))
    {
        // 订阅收到的消息 是一个带三个元素的数组
        if (reply != nullptr && reply->element[2] != nullptr && reply->element[2]->str != nullptr)
        {
            // 给业务层上报通道上发生的消息
            _notify_message_handler(atoi(reply->element[1]->str), reply->element[2]->str);
            /*
            数组的下标1, 2
            对应 redis 回应的 (2)(3)
            1) "message"
            2) "13"
            3) "hello"
            */
        }
        freeReplyObject(reply);
    }

    cerr << ">>>>>>>>>>>>>>>>>observer_channel_message quit<<<<<<<<<<<<<<<<<<<<" << endl;
}

// 初始化向业务层上报消息的回调函数
void Redis::init_notify_message_handler(function fn)
{
    this->_notify_message_handler = fn;
}

ChatService.cpp

构造函数

  • 连接 Redis
  • 注册回调函数(Redis订阅的通道有新消息时,回调告诉我们哪个通道、什么消息)。
// redis连接
if(_redis.connect())
{
    // 设置上报消息的回调
    _redis.init_notify_message_handler(std::bind(&ChatService::handleRedisSubscribeMessage, this, _1, _2));
}

登陆成功

订阅通道:以用户ID命名的通道。

这样如果别的服务器发来消息,当前服务器就能收到。

// id用户登陆成功, 向redis订阅id channel
_redis.subscribe(id);

退出登录时

  • 取消订阅用户ID对应的通道。
_redis.unsubscribe(userid);

客户端异常断开

  • 也要 取消订阅(因为用户掉线了)。
_redis.unsubscribe(user.getId());

服务器异常

正常退出的时候,服务器会主动:

  • 取消订阅(unsubscribe)
  • 断开和Redis的连接

这样Redis知道:这个订阅者没了,不给它推消息了。

异常崩溃,比如:

  • 程序崩了
  • 服务器宕机
  • 网络断了

那么来不及 unsubscribe,怎么办?

Redis自己会清理。

原因:

  • Redis的订阅是基于连接的(TCP连接)。
  • 如果服务器异常退出,Redis检测到TCP连接断了。
  • Redis就会自动把这个服务器的所有订阅取消掉

不会出现僵尸订阅,不会浪费资源。

一对一聊天(oneChat)逻辑

  • 如果目标用户在本机:
    • 直接推送消息。
  • 如果目标用户不在本机,但状态是 online
    • 发布消息 到对应用户ID的Redis通道。
  • 如果目标用户是 offline
    • 存离线消息。
// 增加不同服务器判断
User user=_usermodel.query(toid);
if(user.getState()=="online") //在另一个服务器上
{
    _redis.publish(toid, js.dump());
    return;
}

群聊(groupChat)逻辑

  • 给群内每个成员判断:
    • 在本机就直接发。
    • 在其他机器且是online,就往对应用户ID的通道发布消息。
    • offline则存离线消息。
for (int id : userVec)
{
    // 用户在线, 就直接转发
    auto it = _userConnMap.find(id);
    if (it != _userConnMap.end())
    {
        // 在线, 转发消息
        it->second->send(js.dump());
    }
    else
    {
        // 查询是否在另一台主机上
        User user = _usermodel.query(id);
        if(user.getState()=="online")
        {
            _redis.publish(id, js.dump());
        }
        else{
            // 不在线, 存储离线消息
            _offlineMsg.insert(id, js.dump());
        }
    }
}

Redis回调处理

  • Redis发现某个通道有新消息,会调用我们的回调函数。
  • 回调函数中:
    • 查找用户connection,能找到就推送到客户端。
    • 找不到说明用户下线了,就存离线消息。
// redis 接收消息并上报 的回调
void ChatService::handleRedisSubscribeMessage(int userid, string msg)
{
    // 不需要反序列化, 客户端都做完了
    lock_guard lock(_connMutex);
    auto it = _userConnMap.find(userid);
    if(it!=_userConnMap.end())
    {
        // 在线, 转发消息
        it->second->send(msg);
        return;
    }

    // 也可能在发送时 离线了
    _offlineMsg.insert(userid, msg);
}

测试

自行测试

本文地址:https://www.vps345.com/6689.html

搜索文章

Tags

PV计算 带宽计算 流量带宽 服务器带宽 上行带宽 上行速率 什么是上行带宽? CC攻击 攻击怎么办 流量攻击 DDOS攻击 服务器被攻击怎么办 源IP 服务器 linux 运维 游戏 云计算 javascript 前端 chrome edge llama 算法 opencv 自然语言处理 神经网络 语言模型 RTSP xop RTP RTSPServer 推流 视频 科技 ai java 人工智能 个人开发 ubuntu python MCP ssh 阿里云 网络 网络安全 网络协议 进程 操作系统 进程控制 Ubuntu deepseek Ollama 模型联网 API CherryStudio 物联网 tcp/ip mcu iot 信息与通信 harmonyos 华为 开发语言 typescript 计算机网络 宝塔面板访问不了 宝塔面板网站访问不了 宝塔面板怎么配置网站能访问 宝塔面板配置ip访问 宝塔面板配置域名访问教程 宝塔面板配置教程 数据库 centos oracle 关系型 安全 分布式 vue.js audio vue音乐播放器 vue播放音频文件 Audio音频播放器自定义样式 播放暂停进度条音量调节快进快退 自定义audio覆盖默认样式 rust http ssl fastapi mcp mcp-proxy mcp-inspector fastapi-mcp agent sse filezilla 无法连接服务器 连接被服务器拒绝 vsftpd 331/530 YOLO efficientVIT YOLOv8替换主干网络 TOLOv8 android 鸿蒙 numpy 面试 性能优化 jdk intellij-idea 架构 运维开发 云原生 Flask FastAPI Waitress Gunicorn uWSGI Uvicorn 大数据 政务 分布式系统 监控运维 Prometheus Grafana conda ipython flutter word图片自动上传 word一键转存 复制word图片 复制word图文 复制word公式 粘贴word图文 粘贴word公式 ai小智 语音助手 ai小智配网 ai小智教程 智能硬件 esp32语音助手 diy语音助手 后端 编辑器 react.js 前端面试题 node.js 持续部署 YOLOv8 NPU Atlas800 A300I pro asi_bench windows 统信UOS 麒麟 bonding 链路聚合 爬虫 数据挖掘 网络用户购物行为分析可视化平台 大数据毕业设计 1024程序员节 ESP32 camera Arduino 电子信息 ESXi adb c语言 qt stm32项目 单片机 stm32 EtherCAT转Modbus ECT转Modbus协议 EtherCAT转485网关 ECT转Modbus串口网关 EtherCAT转485协议 ECT转Modbus网关 macos 鸿蒙系统 前端框架 ddos Dell R750XS ollama 大模型 mac 计算机外设 电脑 软件需求 debian PVE zotero WebDAV 同步失败 代理模式 Dify flask spring boot AI编程 AIGC 华为云 udp unity 智能路由器 dell服务器 github go pycharm ide golang pip websocket AI 数据集 学习 sql KingBase 微信 微信分享 Image wxopensdk uni-app 笔记 Agent oceanbase rc.local 开机自启 systemd chatgpt llama3 Chatglm 开源大模型 php tomcat 智能手机 NAS Termux Samba Linux postman mock mock server 模拟服务器 mock服务器 Postman内置变量 Postman随机数据 ffmpeg 音视频 c++ cuda cudnn anaconda 嵌入式硬件 温湿度数据上传到服务器 Arduino HTTP HarmonyOS Next pytorch 命名管道 客户端与服务端通信 客户端 java-ee mysql .net docker 豆瓣 追剧助手 迅雷 nas 银河麒麟 kylin v10 麒麟 v10 深度学习 目标检测 计算机视觉 GaN HEMT 氮化镓 单粒子烧毁 辐射损伤 辐照效应 自动化 vSphere vCenter 软件定义数据中心 sddc C 环境变量 进程地址空间 LDAP aws googlecloud 微服务 springcloud 嵌入式 linux驱动开发 arm开发 HCIE 数通 protobuf 序列化和反序列化 安装 maven intellij idea rpc kubernetes 容器 nginx dubbo .netcore gateway Clion Nova ResharperC++引擎 Centos7 远程开发 外网访问 内网穿透 端口映射 pillow json html5 firefox 僵尸进程 WSL win11 无法解析服务器的名称或地址 sqlserver vim rust腐蚀 VMware安装mocOS VMware macOS系统安装 学习方法 经验分享 程序人生 gpu算力 YOLOv12 jmeter 软件测试 DeepSeek-R1 API接口 live555 rtsp rtp Hyper-V WinRM TrustedHosts vscode 安装教程 GPU环境配置 Ubuntu22 CUDA PyTorch Anaconda安装 mount挂载磁盘 wrong fs type LVM挂载磁盘 Centos7.9 vue3 HTML audio 控件组件 vue3 audio音乐播放器 Audio标签自定义样式默认 vue3播放音频文件音效音乐 自定义audio播放器样式 播放暂停调整声音大小下载文件 MacOS录屏软件 asm 机器学习 c# 向日葵 WSL2 创意 社区 源码剖析 rtsp实现步骤 流媒体开发 cpu 内存 实时 使用 开源 Windsurf C语言 safari Mac 系统 系统架构 ros2 moveit 机器人运动 django 无人机 机器人 svn 串口服务器 三级等保 服务器审计日志备份 状态管理的 UDP 服务器 Arduino RTOS yum 驱动开发 jenkins devops ci/cd git gitea 媒体 微信公众平台 bootstrap html redis C# MQTTS 双向认证 emqx 交换机 telnet 远程登录 nvidia jupyter css gpt-3 文心一言 Invalid Host allowedHosts vue kali 共享文件夹 虚拟机 jar gradle matlab 低代码 压力测试 prometheus k8s资源监控 annotations自动化 自动化监控 监控service 监控jvm express 实战案例 KylinV10 麒麟操作系统 Vmware mybatis 蓝耘科技 元生代平台工作流 ComfyUI webrtc 远程工作 华为认证 网络工程师 拓扑图 课程设计 transformer DevEco Studio Java fpga开发 AI大模型 LLM 缓存 灵办AI 测试工具 web安全 kvm iBMC UltraISO wsl llm springboot远程调试 java项目远程debug docker远程debug java项目远程调试 springboot远程 宝塔面板 部署 Qwen2.5-coder 离线部署 bash microsoft kafka threejs 3D agi eureka 远程桌面 SenseVoice docker搭建nacos详解 docker部署nacos docker安装nacos 腾讯云搭建nacos centos7搭建nacos thingsboard postgresql 测试用例 功能测试 shell 磁盘监控 wsl2 vmware 卡死 服务器配置 RAID RAID技术 磁盘 存储 rabbitmq 硬件架构 ssh漏洞 ssh9.9p2 CVE-2025-23419 IIS .net core Hosting Bundle .NET Framework vs2022 ansible playbook 剧本 https apache excel 多进程 Trae AI代码编辑器 生物信息学 openEuler 多层架构 解耦 王者荣耀 Wi-Fi visual studio code DNS ecmascript KVM mariadb tcpdump DeepSeek 分析解读 kylin 远程连接 rdp 实验 linux安装配置 微信小程序 AP配网 AK配网 小程序AP配网和AK配网教程 WIFI设备配网小程序UDP开 金融 腾讯云大模型知识引擎 Deepseek 硬件 设备 GPU PCI-Express 小程序 Linux无人智慧超市 LInux多线程服务器 QT项目 LInux项目 单片机项目 UOS 统信操作系统 etcd 数据安全 RBAC wireshark 企业微信 Linux24.04 deepin 云电竞 云电脑 todesk Portainer搭建 Portainer使用 Portainer使用详解 Portainer详解 Portainer portainer ue4 着色器 ue5 虚幻 报错 ping++ 小艺 Pura X 深度优先 图论 并集查找 换根法 树上倍增 rag ragflow ragflow 源码启动 SSH DeepSeek行业应用 Heroku 网站部署 Reactor 设计模式 C++ 系统安全 JAVA spring cloud 游戏机 SWAT 配置文件 服务管理 网络共享 mamba Vmamba gaussdb ruoyi 数据结构 dify openvpn server openvpn配置教程 centos安装openvpn IIS服务器 IIS性能 日志监控 react next.js 部署next.js ocr asp.net大文件上传 asp.net大文件上传源码 ASP.NET断点续传 asp.net上传文件夹 asp.net上传大文件 .net core断点续传 .net mvc断点续传 micropython esp32 mqtt nuxt3 实时音视频 算力 矩阵 进程信号 服务器管理 配置教程 服务器安装 网站管理 其他 实时互动 银河麒麟服务器操作系统 系统激活 r语言 数据可视化 数据分析 程序员 博客 中间件 可信计算技术 安全架构 网络攻击模型 高效远程协作 TrustViewer体验 跨设备操作便利 智能远程控制 hibernate n8n 工作流 workflow 并查集 leetcode 远程 命令 执行 sshpass 操作 工业4.0 firewalld windwos防火墙 defender防火墙 win防火墙白名单 防火墙白名单效果 防火墙只允许指定应用上网 防火墙允许指定上网其它禁止 漏洞 kind 微信开放平台 微信公众号配置 负载均衡 linux 命令 sed 命令 Cookie 同步 备份 建站 安全威胁分析 unix ArcTS 登录 ArcUI GridItem gitee grafana arkUI IPMI 弹性计算 云服务器 裸金属服务器 弹性裸金属服务器 虚拟化 p2p virtualenv unity3d mongodb 权限 网络穿透 Xterminal 开机自启动 bug 致远OA OA服务器 服务器磁盘扩容 CPU 主板 电源 网卡 TRAE gitlab okhttp CORS 跨域 雨云 NPS dns yum源切换 更换国内yum源 大模型入门 QQ bot Docker spring vr arm 腾讯云 能力提升 面试宝典 技术 IT信息化 大模型微调 pygame 小游戏 五子棋 半虚拟化 硬件虚拟化 Hypervisor 强制清理 强制删除 mac废纸篓 软件工程 图像处理 华为od sqlite MS Materials openssl 密码学 监控 自动化运维 自动驾驶 code-server MQTT mosquitto 消息队列 echarts 信息可视化 网页设计 Ark-TS语言 大语言模型 ollama下载加速 VMware安装Ubuntu Ubuntu安装k8s k8s mysql离线安装 ubuntu22.04 mysql8.0 源码 毕业设计 AISphereButler 服务器繁忙 outlook kamailio sip VoIP 混合开发 环境安装 JDK 大数据平台 统信 国产操作系统 虚拟机安装 京东云 框架搭建 Docker Hub docker pull 镜像源 daemon.json 命令行 基础入门 编程 W5500 OLED u8g2 TCP服务器 hive Hive环境搭建 hive3环境 Hive远程模式 Java Applet URL操作 服务器建立 Socket编程 网络文件读取 大模型教程 remote-ssh springboot cmos ukui 麒麟kylinos openeuler centos-root /dev/mapper yum clean all df -h / du -sh RustDesk自建服务器 rustdesk服务器 docker rustdesk web3.py chrome 浏览器下载 chrome 下载安装 谷歌浏览器下载 VPS npm pyqt pyautogui 微信小程序域名配置 微信小程序服务器域名 微信小程序合法域名 小程序配置业务域名 微信小程序需要域名吗 微信小程序添加域名 职场和发展 AI写作 prompt RTMP 应用层 log4j 飞书 孤岛惊魂4 WebRTC gpt OD机试真题 华为OD机试真题 服务器能耗统计 恒源云 adobe 传统数据库升级 银行 LLMs Ubuntu Server Ubuntu 22.04.5 Python 网络编程 聊天服务器 套接字 TCP Socket IPMITOOL BMC 硬件管理 opcua opcda KEPServer安装 oneapi 飞牛NAS 飞牛OS MacBook Pro 产品经理 游戏服务器 TrinityCore 魔兽世界 dba pdf 群晖 文件分享 iis 多线程服务器 Linux网络编程 云服务 springsecurity6 oauth2 授权服务器 token sas FTP 服务器 Cline 自动化编程 rsyslog Open WebUI list 服务器数据恢复 数据恢复 存储数据恢复 raid5数据恢复 磁盘阵列数据恢复 k8s集群资源管理 云原生开发 visualstudio 银河麒麟操作系统 国产化 elasticsearch 硬件工程 嵌入式实习 iftop 网络流量监控 RoboVLM 通用机器人策略 VLA设计哲学 vlm fot robot 视觉语言动作模型 具身智能 IDE AI 原生集成开发环境 Trae AI EasyConnect Kali Linux 黑客 渗透测试 信息收集 zabbix nextjs reactjs 黑客技术 流式接口 本地部署 api selenium 蓝桥杯 安卓 DigitalOcean GPU服务器购买 GPU服务器哪里有 GPU服务器 大模型面经 大模型学习 Kylin-Server 显示过滤器 ICMP Wireshark安装 Google pay Apple pay 服务器主板 AI芯片 android studio 交互 hadoop 压测 ECS ssrf 失效的访问控制 openwrt 代码调试 ipdb xrdp string模拟实现 深拷贝 浅拷贝 经典的string类问题 三个swap Redis Desktop 开发环境 SSL证书 NFS redhat 视觉检测 VMware创建虚拟机 雨云服务器 环境迁移 直播推流 matplotlib 毕设 远程控制 远程看看 远程协助 医疗APP开发 app开发 wordpress 无法访问wordpess后台 打开网站页面错乱 linux宝塔面板 wordpress更换服务器 cursor swoole FTP服务器 虚拟局域网 selete 高级IO 无桌面 risc-v 显卡驱动 AnythingLLM AnythingLLM安装 web Kali 我的世界 我的世界联机 数码 软考 计算机 EMUI 回退 降级 升级 联想开天P90Z装win10 Claude dity make eNSP 网络规划 VLAN 企业网络 执法记录仪 智能安全帽 smarteye apt ip命令 新增网卡 新增IP 启动网卡 序列化反序列化 ecm bpm Docker引擎已经停止 Docker无法使用 WSL进度一直是0 镜像加速地址 人工智能生成内容 宕机切换 服务器宕机 ruby 游戏引擎 搜索引擎 线程 多线程 文件系统 用户缓冲区 模拟实现 单元测试 集成测试 MCP server C/S GCC aarch64 编译安装 HPC windows日志 数据库架构 数据管理 数据治理 数据编织 数据虚拟化 idm Minecraft DOIT 四博智联 openstack Xen USB网络共享 图形化界面 Playwright 自动化测试 换源 国内源 Debian H3C iDRAC R720xd minio 树莓派 VNC P2P HDLC 思科 nac 802.1 portal freebsd linux上传下载 edge浏览器 QT 5.12.12 QT开发环境 Ubuntu18.04 gcc XFS xfs文件系统损坏 I_O error 程序 性能分析 tensorflow trae 前后端分离 3d uv crosstool-ng 技能大赛 服务器无法访问 ip地址无法访问 无法访问宝塔面板 宝塔面板打不开 版本 FunASR ASR file server http server web server 云桌面 微软 AD域控 证书服务器 个人博客 docker compose X11 Xming 集成学习 uni-file-picker 拍摄从相册选择 uni.uploadFile H5上传图片 微信小程序上传图片 wps rnn 深度求索 私域 知识库 技术共享 rocketmq 游戏程序 SysBench 基准测试 seatunnel DBeaver 数据仓库 kerberos 阻塞队列 生产者消费者模型 服务器崩坏原因 jetty undertow elk ui grub 版本升级 扩容 jina ISO镜像作为本地源 frp cnn GoogLeNet 磁盘镜像 服务器镜像 服务器实时复制 实时文件备份 Erlang OTP gen_server 热代码交换 事务语义 MNN Qwen seleium chromedriver ip MacMini 迷你主机 mini Apple 备份SQL Server数据库 数据库备份 傲梅企业备份网络版 目标跟踪 OpenVINO 推理应用 宠物 免费学习 宠物领养 宠物平台 音乐服务器 Navidrome 音流 产测工具框架 IMX6ULL 管理框架 银河麒麟桌面操作系统 Kylin OS MQTT协议 消息服务器 代码 在线预览 xlsx xls文件 在浏览器直接打开解析xls表格 前端实现vue3打开excel 文件地址url或接口文档流二进 迁移指南 pppoe radius hugo Netty 即时通信 NIO Dell HPE 联想 浪潮 dns是什么 如何设置电脑dns dns应该如何设置 Linux awk awk函数 awk结构 awk内置变量 awk参数 awk脚本 awk详解 vasp安装 宝塔 AI作画 聊天室 程序员创富 lio-sam SLAM ios glibc VR手套 数据手套 动捕手套 动捕数据手套 AI agent 思科模拟器 Cisco OpenManus Radius 输入法 qt项目 qt项目实战 qt教程 muduo av1 电视盒子 机顶盒ROM 魔百盒刷机 软负载 HiCar CarLife+ CarPlay QT RK3588 yolov8 国标28181 视频监控 监控接入 语音广播 流程 SIP SDP CLion 数学建模 Node-Red 编程工具 流编程 网络结构图 远程服务 社交电子 数据库系统 curl wget EMQX 通信协议 大模型应用 keepalived sonoma 自动更新 计算虚拟化 弹性裸金属 linux环境变量 xshell termius iterm2 neo4j 数据库开发 database SSH 密钥生成 SSH 公钥 私钥 生成 小番茄C盘清理 便捷易用C盘清理工具 小番茄C盘清理的优势尽显何处? 教你深度体验小番茄C盘清理 C盘变红?!不知所措? C盘瘦身后电脑会发生什么变化? 显示管理器 lightdm gdm chrome devtools vscode 1.86 langchain arcgis 小智AI服务端 xiaozhi TTS 直流充电桩 充电桩 AD 域管理 SEO 网站搭建 serv00 chfs ubuntu 16.04 Cursor ShenTong 火绒安全 Nuxt.js Docker Compose docker-compose 内网服务器 内网代理 内网通信 自动化任务管理 交叉编译 备选 网站 调用 示例 AD域 代理 边缘计算 jvm 上传视频至服务器代码 vue3批量上传多个视频并预览 如何实现将本地视频上传到网页 element plu视频上传 ant design vue vue3本地上传视频及预览移除 HTTP 服务器控制 ESP32 DeepSeek AutoDL 图形渲染 黑苹果 sdkman webstorm 端口测试 田俊楠 指令 alias unalias 别名 mq x64 SIGSEGV SSE xmm0 业界资讯 鲲鹏 模拟退火算法 miniapp 真机调试 调试 debug 断点 网络API请求调试方法 pgpool ceph 华为机试 Linux的权限 办公自动化 自动化生成 pdf教程 自定义客户端 SAS 银河麒麟高级服务器 外接硬盘 Kylin xml Jellyfin Ubuntu DeepSeek DeepSeek Ubuntu DeepSeek 本地部署 DeepSeek 知识库 DeepSeek 私有化知识库 本地部署 DeepSeek DeepSeek 私有化部署 匿名管道 回显服务器 UDP的API使用 做raid 装系统 armbian u-boot 虚拟显示器 设置代理 实用教程 WebUI DeepSeek V3 重启 排查 系统重启 日志 原因 运维监控 flash-attention SSH 服务 SSH Server OpenSSH Server 相机 ftp 私有化 CVE-2024-7347 rustdesk VM搭建win2012 win2012应急响应靶机搭建 攻击者获取服务器权限 上传wakaung病毒 应急响应并溯源 挖矿病毒处置 应急响应综合性靶场 ros ux vscode1.86 1.86版本 ssh远程连接 open Euler dde LLM Web APP Streamlit minicom 串口调试工具 fd 文件描述符 MySql big data DenseNet opensearch helm uniapp qemu libvirt web3 昇腾 npu tcp 英语 单一职责原则 Xinference RAGFlow xcode 系统开发 binder 车载系统 framework 源码环境 open webui IMM 智能音箱 智能家居 北亚数据恢复 oracle数据恢复 邮件APP 免费软件 docker命令大全 asp.net大文件上传下载 DocFlow VSCode 移动云 信号处理 Linux PID XCC Lenovo spark HistoryServer Spark YARN jobhistory 繁忙 解决办法 替代网站 汇总推荐 AI推理 LInux nfs 服务器部署ai模型 embedding SSL 域名 centos 7 Anolis nginx安装 linux插件下载 deepseek r1 怎么卸载MySQL MySQL怎么卸载干净 MySQL卸载重新安装教程 MySQL5.7卸载 Linux卸载MySQL8.0 如何卸载MySQL教程 MySQL卸载与安装 skynet IM即时通讯 剪切板对通 HTML FORMAT RAGFLOW zookeeper 阿里云ECS LORA NLP deep learning iphone Ubuntu 24 常用命令 Ubuntu 24 Ubuntu vi 异常处理 大大通 第三代半导体 碳化硅 Windows ai工具 监控k8s 监控kubernetes 健康医疗 v10 软件 ldap make命令 makefile文件 GIS 遥感 WebGIS h.264 URL epoll c 多个客户端访问 IO多路复用 TCP相关API 路径解析 网卡的名称修改 eth0 ens33 HarmonyOS 反向代理 大文件分片上传断点续传及进度条 如何批量上传超大文件并显示进度 axios大文件切片上传详细教 node服务器合并切片 vue3大文件上传报错提示错误 大文件秒传跨域报错cors 网工 CrewAI MI300x rime Deepseek-R1 私有化部署 推理模型 vue-i18n 国际化多语言 vue2中英文切换详细教程 如何动态加载i18n语言包 把语言json放到服务器调用 前端调用api获取语言配置文件 SRS 流媒体 直播 CH340 串口驱动 CH341 uart 485 dash 正则表达式 rclone AList webdav fnOS 性能测试 5G 3GPP 卫星通信 odoo 服务器动作 Server action RAG 检索增强生成 文档解析 大模型垂直应用 tidb GLIBC 常用命令 文本命令 目录命令 崖山数据库 YashanDB python3.11 监控k8s集群 集群内prometheus 视频编解码 链表 Ubuntu 24.04.1 轻量级服务器 es bcompare Beyond Compare 高效日志打印 串口通信日志 服务器日志 系统状态监控日志 异常记录日志 信创 信创终端 中科方德 相差8小时 UTC 时间 sqlite3 netty 远程过程调用 Windows环境 ROS docker run 数据卷挂载 交互模式 强化学习 Python基础 Python教程 Python技巧 佛山戴尔服务器维修 佛山三水服务器维修 历史版本 下载 本地知识库部署 DeepSeek R1 模型 etl 干货分享 黑客工具 密码爆破 嵌入式Linux IPC gnu powerpoint 环境配置 加解密 Yakit yaklang ArkUI 多端开发 智慧分发 应用生态 鸿蒙OS 像素流送api 像素流送UE4 像素流送卡顿 像素流送并发支持 基础环境 tailscale derp derper 中转 triton 模型分析 线性代数 电商平台 服务器时间 IDEA 互信 ubuntu20.04 开机黑屏 searxng C++软件实战问题排查经验分享 0xfeeefeee 0xcdcdcdcd 动态库加载失败 程序启动失败 程序运行权限 标准用户权限与管理员权限 WSL2 上安装 Ubuntu 中兴光猫 换光猫 网络桥接 自己换光猫 主从复制 c/c++ 串口 vpn lua cfssl 沙盒 支付 微信支付 开放平台 ArkTs PX4 can 线程池 安防软件 Mac内存不够用怎么办 虚拟现实 ssh远程登录 cocoapods 进程优先级 调度队列 进程切换 互联网医院 双系统 GRUB引导 Linux技巧 域名服务 DHCP 符号链接 配置 元服务 应用上架 音乐库 飞牛 渗透 iventoy VmWare OpenEuler dock 镜像 加速 yaml Ultralytics 可视化 deekseek Linux环境 影刀 #影刀RPA# trea idea Spring Security OpenSSH su sudo rtsp服务器 rtsp server android rtsp服务 安卓rtsp服务器 移动端rtsp服务 大牛直播SDK 我的世界服务器搭建 Ubuntu22.04 开发人员主页 perf 带外管理 信号 figma eclipse IPv4 子网掩码 公网IP 私有IP 游戏开发 k8s二次开发 集群管理 Linux的基础指令 大模型推理 键盘 TCP协议 composer Ubuntu共享文件夹 共享目录 Linux共享文件夹 代理服务器 xss Logstash 日志采集 llama.cpp 开发 嵌入式系统开发 saltstack 分布式训练 ubuntu24.04.1 网络建设与运维 7z NLP模型 自学笔记 小米 澎湃OS Android SVN Server tortoise svn 根服务器 clickhouse 安卓模拟器 UOS1070e fast 稳定性 看门狗 VS Code 端口 查看 ss Typore HAProxy 大模型部署 读写锁 物联网开发 AI Agent 字节智能运维 本地环回 bind laravel 单例模式 服务网格 istio junit docker部署翻译组件 docker部署deepl docker搭建deepl java对接deepl 翻译组件使用 上传视频文件到服务器 uniApp本地上传视频并预览 uniapp移动端h5网页 uniapp微信小程序上传视频 uniapp app端视频上传 uniapp uview组件库 easyui 需求分析 规格说明书 CentOS 语法 飞牛nas fnos OpenHarmony 毕昇JDK minecraft 捆绑 链接 谷歌浏览器 youtube google gmail 查询数据库服务IP地址 SQL Server 语音识别 DeepSeek r1 sequoiaDB wpf 策略模式 EtherNet/IP串口网关 EIP转RS485 EIP转Modbus EtherNet/IP网关协议 EIP转RS485网关 EIP串口服务器 prometheus数据采集 prometheus数据模型 prometheus特点 AI-native Docker Desktop 免费域名 域名解析 Doris搭建 docker搭建Doris Doris搭建过程 linux搭建Doris Doris搭建详细步骤 Doris部署 hosts bat 李心怡 regedit 开机启动 flink 存储维护 NetApp存储 EMC存储 TrueLicense 软件构建 g++ g++13 docker部署Python webgl 本地化部署 考研 玩机技巧 软件分享 软件图标 超融合 项目部署到linux服务器 项目部署过程 企业网络规划 华为eNSP 软链接 硬链接 autodl 推荐算法 HarmonyOS NEXT 原生鸿蒙 onlyoffice sysctl.conf vm.nr_hugepages 增强现实 沉浸式体验 应用场景 技术实现 案例分析 AR 知识图谱 移动魔百盒 USB转串口 harmonyOS面试题 cd 目录切换 虚幻引擎 virtualbox ubuntu24 vivado24 网络药理学 生信 gromacs 分子动力学模拟 MD 动力学模拟 本地部署AI大模型 IO模型 CDN Headless Linux react native perl xpath定位元素 僵尸世界大战 游戏服务器搭建 WLAN CentOS Stream rancher DIFY java-rocketmq lsb_release /etc/issue /proc/version uname -r 查看ubuntu版本 内网环境 实习 架构与原理 banner IMX317 MIPI H265 VCU WebVM 流水线 脚本式流水线 nlp less cpp-httplib PPI String Cytoscape CytoHubba 金仓数据库 2025 征文 数据库平替用金仓 抗锯齿 nftables 防火墙 Attention firewall NAT转发 NAT Server Unity Dedicated Server Host Client 无头主机 db 聚类 区块链 模拟器 教程 midjourney sublime text 浏览器开发 AI浏览器 代码托管服务 css3 sentinel Linux权限 权限命令 特殊权限 proxy模式 性能调优 安全代理 在线office MAC SecureCRT UDP 磁盘清理 容器技术 流量运营 ranger MySQL8.0 UEFI Legacy MBR GPT U盘安装操作系统 word 多路转接 MacOS docker搭建pg docker搭建pgsql pg授权 postgresql使用 postgresql搭建 欧标 OCPP mcp服务器 client close 大模型技术 本地部署大模型 IO Unity插件 项目部署 浏览器自动化 lb 协议 kernel fork wait waitpid exit Qwen2.5-VL vllm MAVROS 四旋翼无人机 风扇控制软件 国产数据库 瀚高数据库 数据迁移 下载安装 vu大文件秒传跨域报错cors iperf3 带宽测试 对比 工具 meld DiffMerge 内核 服务器安全 网络安全策略 防御服务器攻击 安全威胁和解决方案 程序员博客保护 数据保护 安全最佳实践 端口聚合 windows11 milvus 达梦 DM8 win服务器架设 windows server System V共享内存 进程通信 极限编程 安装MySQL 通信工程 毕业 机柜 1U 2U conda配置 conda镜像源 状态模式 Reactor反应堆 linux内核 docker desktop image React Next.js 开源框架 云耀服务器 deployment daemonset statefulset cronjob 查看显卡进程 fuser ArtTS hexo rpa 合成模型 扩散模型 图像生成 visual studio nvm whistle Linux find grep 钉钉 鸿蒙开发 移动开发 powerbi 抓包工具 话题通信 服务通信 热榜 mm-wiki搭建 linux搭建mm-wiki mm-wiki搭建与使用 mm-wiki使用 mm-wiki详解 智能电视 ABAP 宝塔面板无法访问 wsgiref Web 服务器网关接口 离线部署dify ardunio BLE WebServer MDK 嵌入式开发工具 论文笔记 vnc zip unzip 网络爬虫 Linux 维护模式 AI员工 计算生物学 生物信息 基因组 fstab 电视剧收视率分析与可视化平台 top Linux top top命令详解 top命令重点 top常用参数 视频平台 录像 视频转发 视频流 ros1 Noetic 20.04 apt 安装 java-rabbitmq kotlin 论文阅读 Sealos nosql 烟花代码 烟花 元旦 粘包问题 网络搭建 神州数码 神州数码云平台 云平台 copilot navicat yolov5 ip协议 Web服务器 多线程下载工具 PYTHON 隐藏文件 隐藏目录 管理器 通配符 windows 服务器安装 问题解决 ubuntu 18.04 软件卸载 系统清理 MVS 海康威视相机 搭建个人相关服务器 CPU 使用率 系统监控工具 linux 命令 samba export import save load 迁移镜像 ELF加载 CosyVoice 华为证书 HarmonyOS认证 华为证书考试 接口优化 js macOS 数字证书 签署证书 解决方案 开源软件 ebpf uprobe 硅基流动 ChatBox 服务器正确解析请求体 西门子PLC 通讯 服务器扩容没有扩容成功 搜狗输入法 中文输入法 yum换源 notepad MobaXterm tar 智慧农业 开源鸿蒙 团队开发 WINCC