基于 C++ 开发集群聊天服务器
大纲
前言
本文将基于 C++ 实现集群聊天服务器,使用了 Json 库和 Muduo 网络库,并引入了 Redis、MySQL、Nginx 等中间件。
开发工具
软件 | 版本 | 说明 |
---|---|---|
C++ 标准 | 11 | |
Boost | 1.74.0.3 | Muduo 库依赖 Boost 库 |
Muduo | 2.0.3 | Muduo 库,基于 C++ 开发,用于网络编程 |
hiredis | 1.3.0 | Reids 库 ,基于 C 语言开发,用于操作 Redis |
nlohmann/json | 3.12.0 | Json 库,基于 C++ 开发,用于 Json 序列化和反序列化 |
Redis | 7.0.15 | Redis 服务器 |
MySQL | 8.4.5 | MySQL 服务器 |
Nginx | 1.28.0 | Nginx 服务器 |
G++(GCC) | 12.2.0 | 建议使用 5.5 、7.5 版本的 G++(GCC) 编译器 |
CMake | 3.25.1 | C/C++ 项目构建工具 |
Linux | Debian 12 | Muduo 库不支持 Windows 平台 |
Visual Studio Code | 1.100.2 | 使用 VSCode 远程开发特性 |
平台兼容性说明
由于使用了 Muduo 库,且 Muduo 库仅支持 Linux 平台;因此本文提供的所有 C++ 集群聊天服务器代码支持在 Linux 平台运行,不支持 Windows 平台,默认是基于 Debian 12 进行远程开发。
开发环境
使用 C++ 开发 Linux 应用时,常见的开发环境有以下几种:
- (1) Linux 环境下直接使用 VSCode、Clion 等 IDE 进行本地开发
- (2) Windows + VSCode + MinGW 搭建本地 C/C++ 开发环境
- (3) Windows + Visual Studio 搭建远程 Linux 跨平台项目
- (4) Windows + VSCode 搭建远程 Linux 开发环境
提示
上面介绍的三种开发环境,任意选择一种自己熟悉的就可以;如果日常使用的是 Windows 系统,建议选择第四种开发环境(VSCode 远程开发);如果习惯使用 Linux 系统,强烈建议选择第一种开发环境(Linux 本地开发)。
准备工作
安装工具
- 安装常用的工具
1 | # 安装开发工具 |
安装 GCC
1 | # 安装 GCC、G++、GDB |
安装 CMake
1 | # 安装 CMake |
安装 Redis
- 安装 Redis
1 | # 安装 Redis |
- 验证安装
1 | # 查看 Redis 版本 |
Redis 默认配置文件的路径
通过 APT 安装 Redis 服务器后,其主配置文件的路径为 /etc/redis/redis.conf
。
安装 Nginx
- 安装依赖包
1 | # 安装依赖软件(比如 pcre、zlib、ssl) |
- 编译安装 Nginx
1 | # 下载源码 |
- Nginx 安装说明
安装说明 | 路径 |
---|---|
Nginx 默认安装路径 | /usr/local/nginx |
Nginx 主配置文件路径 | /usr/local/nginx/conf/nginx.conf |
Nginx 二进制可执行文件路径 | /usr/local/nginx/sbin/nginx |
- Nginx 管理命令
1 | # 启动 Nginx |
- 添加 Systemd 服务(实现 Nginx 服务自启动)
1 | # 创建 Systemd 服务配置文件,添加以下配置内容 |
1 | [Unit] |
1 | # 重载系统配置文件 |
1 | # 启动 Nginx 服务 |
特别注意
在 Nginx 1.9.0
版本之前,Nginx 仅支持基于 HTTP 协议 的 Web 服务器负载均衡,无法处理 TCP 层的流量转发。自 Nginx 1.9.0
版本开始,官方引入了名为 stream
的新模块,使 Nginx 能够支持基于 TCP 和 UDP 的四层负载均衡,从而扩展了其在数据库代理、邮件服务、消息中间件等非 HTTP 场景中的应用能力。值得一提的是,尽管 stream
模块在 1.9.0
版本中开始被引入,但在官方源码中该模块默认并未启用。因此,在编译 Nginx 源码时,如果希望使用 stream
模块的功能,则必须显式添加 --with-stream
编译参数,这样才能将其集成进最终构建的二进制可执行文件中。
安装 MySQL
- 添加 MySQL 官方 APT 源
1 | # 下载 APT 配置包(访问 https://dev.mysql.com/downloads/repo/apt/ 可以获取最新版本) |
- 安装 MySQL
1 | # 安装 MySQL(安装过程中会提示输入 MySQL 的 root 用户的密码) |
MySQL 默认配置文件的路径
通过 APT 安装 MySQL 服务器后,其主配置文件的路径为 /etc/mysql/my.cnf
。
- 验证 MySQL 安装
1 | # 登录 MySQL |
1 | -- 查看 MySQL 版本 |
- 验证 MySQL 开发包的安装
1 | sudo find /usr -iname libmysqlclient* |
1 | /usr/lib/x86_64-linux-gnu/libmysqlclient.so |
MySQL C API 库
本文使用 libmysqlclient.so
库来操作 MySQL 数据库,该库称为 MySQL C API(也叫 Connector/C),它是基于 C 语言实现的。C++ 项目也可以使用这个库,只要用 extern "C"
来链接(或者直接使用 MySQL 提供的头文件中已经加好的处理)。
安装 Boost 库
1 | # 安装 Boost 的所有组件和头文件 |
提示
由于 Muduo 使用了 Boost 库(如 boost::any
),因此需要安装 Boost 库。
安装 Muduo 库
- 编译安装 Muduo 库
1 | # Git 克隆代码 |
- 验证安装
1 | # 查看 Muduo 库的头文件 |
提示
- Muduo 的编译依赖 CMake 和 Boost 库,默认编译生成的是静态库(
.a
),如果需要编译生成共享库(.so
),可以自行修改CMakeLists.txt
中的配置。 - Muduo 支持 C++ 11,仅支持 Linux 平台,不支持 Windows 平台,建议使用
7.x
及以后版本的g++
编译器。
安装 Hiredis 库
- 编译安装 Hiredis 库
1 | # Git 克隆代码 |
- 验证安装
1 | # 查看 Hiredis 库的头文件 |
项目介绍
项目需求
- 客户端新用户注册
- 客户端用户登录
- 添加好友和添加群组
- 好友聊天
- 群组聊天
- 离线消息
- Nginx 配置 TCP 负载均衡
- 集群聊天系统支持客户端跨服务器通信
项目目标
- 掌握 Json 的编程应用
- 掌握 CMake 构建自动化编译环境
- 掌握 Muduo 网络库的编程以及实现原理
- 掌握 Nginx 配置部署 TCP 负载均衡器的应用以及原理
- 掌握服务器的网络 I/O 模块、业务模块、数据模块分层的设计思想
- 掌握 Redis 发布 - 订阅的编程实践以及应用原理
项目架构
在集群聊天服务器项目中,使用 Nginx 作为 TCP 负载均衡器,同时使用 Redis 的发布 - 订阅特性来解决客户端跨服务器通信问题。整体工作流程如下图所示:
提示
Nginx 单机作为负载均衡器时,经过合理的系统参数优化和配置(如 ulimit
、内核参数调整等),通常可以稳定支撑 5 万~6 万个并发 TCP 连接。但是,由于 Nginx 本质上是属于应用层的四层代理,其性能仍受限于单机的 CPU、内存、网络带宽和文件描述符等资源限制。若需支持 超过 10 万甚至上百万的并发 TCP 连接,可采用 LVS + Keepalived + Nginx 的分层架构实现更高性能、更高可用性、更强伸缩性的负载均衡方案,该方案的部署拓扑结构 如图 所示。
项目技术栈
- 单例设计模式
- Muduo 网络库
- MySQL 数据库编程
- CMake 构建编译环境
- Json 序列化和反序列化
- Nginx 的 TCP 负载均衡器使用
- Redis 的发布 - 订阅编程实践
项目开发
Nginx 负载均衡配置
在集群聊天服务器项目中,由于使用了 Nginx 作为 TCP 负载均衡器,因此需要在 Nginx 的配置文件中(nginx.conf
)添加 stream
模块的配置内容,如下所示:
1 | events { |
Redis 发布 - 订阅使用
在集群聊天服务器项目中,使用 Redis 的发布 - 订阅特性来解决客户端跨服务器通信问题。Reids 的发布 - 订阅功能,主要使用以下几个 Redis 命令来实现:
- 订阅指定的 Channel
1 | subscribe news |
- 发布消息到指定的 Channel
1 | publish news "hello" |
- 取消订阅指定的 Channel
1 | unsubscribe news |
提示
在集群聊天服务器项目中,为了方便操作 Redis,会使用到 Hiredis 库。
MySQL 数据库初始化
数据库设计
C++ 集群聊天服务器项目的数据库表设计如下:
数据库初始化
- 创建数据库
1 | CREATE DATABASE `chat` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; |
- 切换数据库
1 | USE `chat`; |
- 创建用户表
1 | CREATE TABLE `user` ( |
- 创建好友表
1 | CREATE TABLE `friend` ( |
- 创建用户组表
1 | CREATE TABLE `allgroup` ( |
- 创建用户与用户组关联表
1 | CREATE TABLE `groupuser` ( |
- 创建离线消息表
1 | CREATE TABLE `offlinemessage` ( |
集群聊天服务器开发
项目结构
1 | ├── CMakeLists.txt |
项目代码
下载完整的项目代码
由于篇幅有限,下面只给出集群聊天服务端和客户端的部分核心代码,完整的项目代码可以在 这里 下载得到。
服务端核心代码
提示
在集群聊天服务端中,使用了 Json 库和 Muduo 网络库,并引入了 MySQL、Redis。
Redis 代码
include/server/redis/redis.hpp
头文件
1 |
|
src/server/redis/redis.cpp
源文件
1 |
|
MySQL 代码
include/server/db/db.hpp
源文件
1 |
|
src/server/db/db.cpp
源文件
1 |
|
聊天服务端代码
聊天服务器的代码
include/server/chatserver.hpp
头文件
1 |
|
src/server/chatserver.cpp
源文件
1 | /** |
聊天核心业务的代码
include/server/chatservice.hpp
头文件
1 |
|
src/server/chatservice.cpp
源文件
1 | /** |
数据库操作的代码
include/server/dao/friendmodel.hpp
头文件
1 |
|
src/server/dao/friendmodel.cpp
源文件
1 |
|
客户端核心代码
提示
在集群聊天客户端中,使用了 Linux 的 socket
和 semaphore
,并没有引入 Muduo 网络库。
src/client/main.cpp
源文件
1 |
|
项目输出
输出求职简历
项目名称
- 集群聊天服务器
- 基于 Muduo 网络库实现的集群聊天服务器
开发工具
- VSCode 远程 Linux 开发
- CMake 构建 C/C++ 项目
- Linux Shell 编写项目自动编译脚本
项目内容
- 使用 Muduo 网络库实现项目的网络核心模块,提供高并发网络 I/O 服务,解耦网络和业务模块的代码
- 使用 Json 序列化和反序列化消息作为私有通信协议
- 配置 Nginx 基于 TCP 的负载均衡,实现聊天服务器的集群功能,提高后端服务的并发能力
- 基于 Redis 的发布 - 订阅功能,实现客户端跨服务器通信
- 使用 MySQL 关系型数据库作为项目数据的落地存储
- 使用数据库连接池提高数据库的访问性能
项目收获
- 熟悉了基于 Muduo 网络库进行服务端程序开发
- 掌握了 Nginx 的 TCP 负载均衡配置
- 掌握了 MySQL 和服务端中间件 Redis 的应用
项目问题
- 问题描述
- 通过代码脚本或者专业的压测工具(比如 JMeter)测试聊天服务器的并发性能
- 问题解决
- 设置进程可使用文件描述符(
fd
)资源的上限数量,提高聊天服务器的并发性能
- 设置进程可使用文件描述符(
- 问题描述
特别注意
在面试流程中描述项目内容时,切忌详细罗列项目中的业务,重点是介绍项目用到什么技术,突出技术点 。
常见的面试题
为什么要使用 Redis
问题描述
- 为什么要使用 Redis 来实现客户端跨服务器通信?各个聊天服务器之间能不能直接进行通信呢?
问题解答
- 这里的设计,会在各个 ChatServer 服务器互相之间直接建立 TCP 连接进行通信,相当于在服务器网络之间进行广播。这样的设计使得各个服务器之间耦合度太高,不利于系统扩展,并且会占用系统大量的 Socket 资源,各服务器之间的带宽压力很大,不能够节省资源给更多的客户端提供服务,因此绝对不是一个好的设计。
- 集群部署的服务器之间进行通信,最好的方式就是引入消息队列中间件,解耦各个服务器,使整个系统松耦合,提高服务器的响应能力,节省服务器的带宽资源,整体的设计应该 如此。
- 在集群环境中,经常使用的消息队列中间件有 ActiveMQ、RabbitMQ、Kafka、RocketMQ 等,它们都是应用场景广泛并且性能很好的消息队列,供集群服务器、分布式服务之间进行消息通信。限于集群聊天服务器项目的业务并不是非常复杂,并且对并发性能也没有太高的要求,因此消息队列选型的是 - Redis 发布 - 订阅。
Redis 实现的功能不稳定
问题描述
- 当消息的生产速度大于消息的消费速度时,随着时间的推移,会造成 Redis 积压消息;如果消息积压的数量太大,会导致内存占用激增,Redis 最终可能会宕机。
问题解答
- 使用消息队列中间件替代 Redis 的发布 - 订阅功能,比如 Kafka、RabbitMQ、RocketMQ 等。
特别注意
Redis 的主要功能有:缓存数据库(支持持久化)、分布式锁、发布 - 订阅,其中发布 - 订阅功能只适用于非核心业务、流量不是很大的业务。
如何保证消息的可靠传输
问题描述
- 如何保证客户端发送出去的消息,一定能够被服务端接收到,从而保证消息不丢失呢?
问题解答
- 在业务层中,可以通过消息序号 + ACK 应答机制实现消息的可靠传输,实现步骤如下:
- (1) 客户端发送的每条消息都附加一个递增的
seq
序号(比如 1、2、3)。 - (2) 客户端将未被确认的消息保存在本地的缓存队列中,用于后续重发和确认处理。
- (3) 服务端收到消息后,返回 ACK 应答给客户端,标明确认的消息序号(比如
seq:1
)。 - (4) 客户端处理 ACK 响应:客户端接收到 ACK 响应后,从本地缓存队列中移除对应
seq
的消息,表示消息已被成功确认。 - (5) 消息重发机制:客户端启动一个定时器线程,定时扫描缓存队列中未被确认的消息(比如每 3 秒扫描一次)。如果某条消息在一定时间内(比如 5 秒)未收到 ACK 确认,则自动重发该消息,直到收到服务端的 ACK 确认或者超过最大重试次数。
如何保证数据传输的安全性
问题描述
- 由于数据(比如聊天消息)是明文传输的,存在一定的数据安全问题,如何解决?
问题解答
- 使用对称加密算法(如 AES)和非对称加密算法(如 RSA)来保证数据传输的安全性,实现步骤如下:
- (1) 客户端登录时,使用服务端的 RSA 公钥加密数据,其中的数据包含一个随机生成的 AES 密钥和登录信息,然后将加密后的数据发送给服务端。
- (2) 服务端收到数据后,使用自己的 RSA 私钥解密数据,获得客户端的 AES 密钥和登录信息,并完成身份验证。
- (3) 后续通信阶段,客户端与服务端使用同一个 AES 密钥对数据进行对称加密和解密,从而实现高效且安全的数据传输。
- (4) AES 密钥应为一次性生成的会话密钥(Session Key)。RSA 加解密只用于密钥交换和登录信息保护,后续通信使用高性能的 AES 加解密。
加密算法介绍
- 在大型的 IM 软件(比如 QQ)中,会同时使用到对称加密算法和非对称加密算法,兼顾考虑数据安全性和加解密效率。
对称加密算法
:使用同一个密钥进行加密和解密,速度快,但需要考虑安全地共享密钥。常见的对称加密算法包括 DES、AES、3DES 等。非对称加密算法
:使用一对公钥和私钥,公钥负责加密、私钥负责解密,安全性高,速度慢,适用于密钥交换与身份验证。常见的非对称加密算法包括 DSA、RSA、ECC 等。
客户端消息如何按顺序显示
问题描述
- 客户端接收到服务端发送的消息,如何按顺序显示?
问题解答
- (1) 服务端消息加序号:服务端发送的每条消息都附加一个递增的
seq
序号,每个用户或会话维护独立的seq
序号。 - (2) 客户端缓存乱序消息:客户端接收到消息后,放入本地的有序缓存中,并使用
expected_seq
表示下一条应该显示的消息的序号。 - (3) 按序显示 + 缓存清理:若客户端接收到消息序号等于
expected_seq
,则立即显示,并从缓存中继续查找后续的连续消息,依次显示。 - (4) 处理乱序和丢包:如果客户端接收到的是
seq > expected_seq
,则先缓存消息,暂时不显示。若某条消息长时间未到达,可发起消息重传请求或跳过处理。
- (1) 服务端消息加序号:服务端发送的每条消息都附加一个递增的
历史聊天消息应该如何存储
问题描述
- 历史聊天消息,有哪些存储方案?
问题解答
- 本地消息存储
- 使用 SQLite 等嵌入式数据库:轻量、便于查询,适合单设备离线存储。
- 使用本地文件系统:以用户或群组为单位创建文件夹,按日期或大小分多个文件保存,适合大批量存储,读取简单但查询不方便。
- 云端消息存储
- 使用关系型数据库(如 MySQL):结构化存储,支持高效查询和分页加载,适合结构清晰的聊天记录。
- 使用文件存储系统(如对象存储或分布式文件系统):适合存储大批量的聊天原始记录或备份数据,读取顺序性强,但查询性能较弱。
- 本地消息存储
大规模系统中的历史聊天消息存储
- (1) 消息队列 + 消息落库架构,提升写入性能。
- (2) Elasticsearch 作为全文索引系统,用于历史聊天记录搜索。
- (3) 冷热数据分离:近期消息存储在数据库,历史消息转存在文件存储系统(如对象存储或分布式文件系统)。
如何感知客户端在线还是掉线
问题描述
- 如果网络拥堵严重,ChatServer(聊天服务端)如何感知 ChatClient(聊天客户端)在线还是掉线呢?
问题解答
- 在 ChatServer(聊天服务端)和 ChatClient(聊天客户端)之间实现心跳保持机制,实现步骤如下:
- (1) 客户端定期发送心跳包(比如:每秒发一次
MSG_TYPE: heartbeat
) - (2) 服务端为每个客户端维护一个心跳计数器,每秒自动加一
- (3) 服务端每收到一次客户端发送的心跳包,就将该客户端的心跳计数器归零
- (4) 若客户端的心跳计数器超过 5(即 5 秒内未收到心跳),则判断客户端已掉线
- (5) 客户端掉线后,服务端开始清理该客户端的连接和资源
- (1) 客户端定期发送心跳包(比如:每秒发一次
- 在 ChatServer(聊天服务端)和 ChatClient(聊天客户端)之间实现心跳保持机制,实现步骤如下:
特别注意
- TCP 传输层有 Keepalive 机制,可以通过 Linux 内核参数来调整(如下表所示)。但是,ChatServer 不能依赖该机制来实现心跳保持机制,因为 ChatServer 检测到 ChatClient 掉线后,需要主动清理相应的连接和资源。
参数名 | 作用 | 默认值(一般情况) |
---|---|---|
net.ipv4.tcp_keepalive_time | TCP 连接空闲多久后开始发送 Keepalive 探测包(单位:秒) | 7200 秒(2 小时) |
net.ipv4.tcp_keepalive_intvl | 发送 Keepalive 探测包之间的时间间隔(单位:秒) | 75 秒 |
net.ipv4.tcp_keepalive_probes | Keepalive 最大探测次数,超过则认定连接失效(断开) | 9 次 |