nginx集群聊天室(一) 初步讲解集群聊天室所需库的搭建

整体架构流程

复制代码
客户端 → Nginx(负载均衡)→ ChatServer集群 → Redis(消息队列)→ 其他ChatServer → MySQL(数据持久化)

数据流向

  • 用户注册/登录 → MySQL

  • 在线消息 → Redis发布/订阅 → 目标服务器

  • 离线消息 → MySQL → 用户登录时推送

  • 好友/群组关系 → MySQL

技术栈

  • Json序列化和反序列化

  • muduo网络库开发

  • nginx源码编译安装和环境部署

  • nginx的tcp负载均衡器配置

  • redis缓存服务器编程实践

  • 基于发布-订阅的服务器中间件redis消息队列编程实践

  • MySQL数据库编程

  • CMake构建编译环境

  • Github托管项目

项目需求

  1. 客户端新用户注册

  2. 客户端用户登录

  3. 添加好友和添加群组

  4. 好友聊天

  5. 群组聊天

  6. 离线消息

  7. nginx配置tcp负载均衡

  8. 集群聊天系统支持客户端跨服务器通信

项目目标

  1. 掌握服务器的网络I/O模块,业务模块,数据模块分层的设计思想

  2. 掌握c++ muduo网络库的编程以及实现原理

  3. 掌握Json的编程应用

  4. 掌握nginx配置部署tcp负载均衡器的应用以及原理

  5. 掌握服务器中间件的应用场景和基于发布-订阅的redis编程实践以及应用原理

  6. 掌握CMake构建自动化编译环境

  7. 掌握Github管理项目

开发环境

  1. ubuntu linux环境

  2. 安装Json开发库

  3. 安装boost + muduo网络库开发环境

  4. 安装redis环境

  5. 安装mysql数据库环境

  6. 安装nginx

  7. 安装CMake环境

Muduo 网络库完整搭建教程(Linux 环境)

Muduo 是基于 Reactor 模式的 Linux 专属 C++ 网络库,搭建核心是安装依赖→编译源码→验证安装,以下是 Ubuntu/CentOS 通用步骤,包含环境配置、编译细节、验证测试和常见问题解决。

一、搭建前提

操作系统:Linux(推荐 Ubuntu 18.04+/CentOS 7+,muduo 依赖 epoll,不支持 Windows/macOS);

核心依赖:C++ 编译器(gcc 4.8+)、CMake(3.0+)、Boost 库(1.58+)、Git(拉取源码)。

二、分步搭建流程

步骤 1:安装基础编译工具与依赖

更新软件源

sudo apt update

安装基础编译工具(gcc/g++/make/cmake)

sudo apt install -y build-essential cmake

安装 Boost 库(muduo 依赖 boost-thread/boost-system)

sudo apt install -y libboost-dev libboost-thread-dev

安装 Git(拉取源码)

sudo apt install -y git

步骤 2:拉取 Muduo 源码

克隆官方仓库(若网络慢,可换国内镜像,如 gitee 镜像)

git clone https://github.com/chenshuo/muduo.git

进入源码目录

cd muduo

步骤 3:编译 Muduo 源码

Muduo 采用 CMake 构建,编译前需创建构建目录,避免污染源码:

创建 build 目录(规范编译目录)

mkdir build && cd build

配置 CMake(指定安装路径为 /usr/local,方便系统查找)

cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local

编译(-j 后接线程数,建议等于 CPU 核心数,加速编译)

例如 4 线程:make -j4

make -j$(nproc)

步骤 4:安装 Muduo 库(关键)

编译完成后,将库文件和头文件安装到系统目录(/usr/local),方便后续项目链接:

sudo make install

步骤 5:验证安装是否成功

安装后会生成两类文件,检查是否存在即可确认:

检查头文件(muduo 核心头文件)

ls /usr/local/include/muduo/

检查库文件(静态库,muduo_base 基础库 + muduo_net 网络库)

ls /usr/local/lib/libmuduo_*.a

正常输出应包含:

头文件:muduo/base/、muduo/net/ 等目录;

库文件:libmuduo_base.a、libmuduo_net.a。

json

一、JSON 是什么?

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。它采用纯文本格式,结构清晰、易于阅读和编写,也易于机器解析和生成。在 C++ 项目中,常用于:

  • 数据序列化:将对象或数据结构转换为字符串,便于网络传输或存储。

  • 数据反序列化:将接收到的 JSON 字符串还原为程序中的对象或数据结构。


二、为什么在项目中使用 JSON?

在这个集群聊天服务器项目中,JSON 用于:

  1. 客户端与服务器之间的通信协议(如登录、注册、发送消息等)

  2. 存储和传输离线消息

  3. 配置信息存储

  4. 日志结构化输出

它比 XML 更轻量,比纯二进制协议更易调试。


三、JSON 的基本结构

JSON 支持以下几种数据类型:

json

复制代码
{
  "name": "张三",
  "age": 25,
  "isStudent": false,
  "hobbies": ["编程", "读书", "音乐"],
  "address": {
    "city": "北京",
    "zip": "100000"
  }
}
  • 对象:{ key: value }

  • 数组:[ value1, value2 ]

  • 字符串:"string"

  • 数字:12312.34

  • 布尔值:true / false

  • 空值:null


四、项目中使用的 JSON 库

项目使用的是 JSON for Modern C++(作者:nlohmann),特点是:

  • 只有一个头文件 json.hpp

  • 支持 C++11 及以上

  • 语法直观,类似操作 STL 容器

  • 支持序列化/反序列化 STL 容器


五、项目中 JSON 的使用示例

1. 序列化(对象 → JSON 字符串)

cpp 复制代码
#include "json.hpp"
using json = nlohmann::json;

json js;
js["id"] = 123;
js["name"] = "张三";
js["msg"]["zhang san"] = "hello world";

std::string jsonStr = js.dump(); // 序列化为字符串

2. 反序列化(JSON 字符串 → 对象)

cpp 复制代码
std::string receivedStr = R"({"id":123,"name":"张三"})";
json js = json::parse(receivedStr);

int id = js["id"];
std::string name = js["name"];

3. 直接序列化 STL 容器

cpp 复制代码
std::vector<int> vec = {1, 2, 3, 4};
json js;
js["list"] = vec;

std::map<int, std::string> m = {{1, "黄山"}, {2, "华山"}};
js["path"] = m;

六、JSON 在聊天服务器中的典型应用场景

假设你要发送一条聊天消息:

cpp 复制代码
// 构造消息对象
json msg;
msg["msgid"] = 1; // 消息类型:聊天
msg["id"] = 1001;
msg["name"] = "张三";
msg["to"] = 1002;
msg["msg"] = "你好,李四!";
msg["time"] = "2023-10-01 12:00:00";

// 序列化并发送
std::string sendStr = msg.dump();
send(conn, sendStr.c_str(), sendStr.size(), 0);

服务器收到后解析:

cpp 复制代码
std::string recvStr = receive(conn);
json recvMsg = json::parse(recvStr);

int msgid = recvMsg["msgid"];
std::string content = recvMsg["msg"];
// 处理消息...

基于muduo的客户端服务器编程

代码

cpp 复制代码
//muduo网络库给用户提供了两个主要的类
//tcpserver 用于编写服务器程序的类
//tcpclient 用于编写客户端程序的类

//epoll+线程池 
//好处:能够把网络I/O代码和业务代码区分开
                            //用户的连接和断开      用户的可读写事件
#include<muduo/net/TcpServer.h>
#include<muduo/net/EventLoop.h>
#include<functional>
#include<string>
#include<iostream>
using namespace std;
using namespace muduo;
using namespace muduo::net;

//基于muduo网络库开发服务器程序
//1.组合TcpServer对象
//2.创建EventLoop事件循环对象的指针
//3.明确服务器的构造函数需要什么参数,输出chatServer的构造函数
//4.在当前服务器类的构造函数当中,注册用户连接和断开回调函数,注册用户读写事件回调函数
//5.设置合适的服务端线程数量,muduo库会自动分配I/O线程和worker线程
class chatServer{
private:
    //专门处理用户连接创建和断开  epoll listenfd accept  
    void onConnection(const TcpConnectionPtr& conn){// TcpConnectionPtr是muduo封装的智能指针,指向一个TCP连接对象
        if(conn->connected()){
            // 输出客户端上线信息:客户端IP端口 --> 服务器IP端口
            // peerAddress():获取对等方(客户端)的地址信息
            // localAddress():获取本地(服务器)的地址信息
            // toIpPort():将地址信息转换为 "IP:Port" 字符串格式
           cout<<conn->peerAddress().toIpPort()<<"-->"<<conn->localAddress().toIpPort()<<" online " <<endl;
        }
        else{
            cout<<conn->peerAddress().toIpPort()<<"-->"<<conn->localAddress().toIpPort()<<" offline " <<endl;
            conn->shutdown();//等价于 close(fd)
            //loop_->quit(); // 可选:如果需要在客户端断开时退出事件循环,可启用
        }
    }
    // conn:当前通信的TCP连接对象
    // buf:muduo封装的缓冲区,存储客户端发送的数据
    // time:接收到数据的时间戳
    void onMessage(const TcpConnectionPtr& conn,Buffer* buf,Timestamp time){
        string buffer=buf->retrieveAllAsString();
        cout<<"recv data:"<<buffer<<" time:"<<time.toString()<<endl;
        conn->send(buffer);
    }
    TcpServer server_; //1
    EventLoop* loop_;  //2 epoll


public:
    chatServer(EventLoop* loop,          //事件循环
               const InetAddress& listenAddr,//IP+port
               const string& nameArg)    //服务器的名字
            :server_(loop,listenAddr,nameArg),loop_(loop) {
        //给服务器注册用户连接的创建和断开回调函数 当有新连接/断开连接时,触发onConnection
        // bind作用:将类成员函数onConnection绑定到当前对象(this),_1是占位符(接收TcpConnectionPtr参数)
        server_.setConnectionCallback(bind(&chatServer::onConnection,this,_1));
        //给服务器注册用户读写事件回调函数
        server_.setMessageCallback(bind(&chatServer::onMessage,this,_1,_2,_3));// _1/_2/_3分别对应onMessage的三个参数:conn, buf, time
        //设置服务器的线程数量  1个I/O线程  3个worker线程
        server_.setThreadNum(4);
    }
    //开启事件循环
    void start(){
        server_.start();
    }
};
int main(){
    EventLoop loop; //epoll
    InetAddress addr("127.0.0.1",6000);
    chatServer server(&loop,addr,"chatServer");
    server.start();//listenfd epoll_ctl => epoll
    loop.loop();//epoll_wait以阻塞方式等待新用户连接,已连接用户的读写事件等
    return 0;
}

1. 头文件与命名空间(基础依赖)

cpp 复制代码
#include<muduo/net/TcpServer.h>   // Muduo 服务器核心类,封装监听/连接管理/线程池
#include<muduo/net/EventLoop.h>   // Reactor 核心,负责 epoll 事件循环
#include<functional>              // 用于 std::bind 绑定类成员函数为回调
#include<string>
#include<iostream>
using namespace muduo;            // 简化 muduo 库命名
using namespace muduo::net;       // 简化 muduo 网络模块命名
using namespace std;              // 简化标准库命名
  • TcpServer:Muduo 封装的 TCP 服务器,无需手动处理 epoll/accept,只需注册回调即可;
  • EventLoop:Reactor 模式的核心,每个线程最多一个,负责事件监听、分发;
  • functional:因为类的成员函数有隐含的 this 指针,需用 std::bind 绑定后才能适配 Muduo 的回调类型。

2. ChatServer 类定义(核心封装)

cpp 复制代码
class ChatServer
{
    // 成员变量:
    TcpServer server_;    // Muduo 服务器对象(核心)
    EventLoop* loop_;     // Reactor 事件循环指针(指向主线程的 EventLoop)
 
    // 私有成员函数(回调函数):
    void onConnection(const TcpConnectionPtr&);  // 连接状态变化回调
    void onMessage(const TcpConnectionPtr&,Buffer*,Timestamp);  // 数据读写回调
 
public:
    // 构造函数:初始化服务器对象、绑定回调、设置线程池
    ChatServer(EventLoop*loop, const InetAddress& listenAddr,const string& nameArg);
    void start();  // 启动服务器
};
  • 成员变量设计:

server_:TcpServer 是服务器的核心载体,封装了监听、连接管理、线程调度;

loop_:指向主线程的 EventLoop,传递给 TcpServer 后,服务器的 Acceptor(监听模块)会运行在该 EventLoop 中。

  • 回调函数声明:

onConnection:处理 "连接建立 / 断开" 事件;

onMessage:处理 "数据接收" 事件,参数包含连接对象、数据缓冲区、接收时间戳(Muduo 预定义的回调参数格式)。

3. onConnection 回调函数(连接事件处理)

cpp 复制代码
void ChatServer::onConnection(const TcpConnectionPtr &conn)
{
    if (conn->connected())  // 判断连接是否建立(三次握手完成)
    {
        printf("ChatServer - %s connected\n", conn->peerAddress().toIpPort().c_str());
    }
    else  // 连接断开(四次挥手/网络异常)
    {
        printf("ChatServer - %s disconnected\n", conn->peerAddress().toIpPort().c_str());
        conn->shutdown();  // 主动关闭连接(半关闭写端,释放资源)
    }
}
  • 核心参数 TcpConnectionPtr:是 std::shared_ptr<TcpConnection> 的别名,Muduo 用智能指针管理连接生命周期,避免野指针;
  • 关键接口:

conn->connected():判断当前连接是否处于 "已建立" 状态;

conn->peerAddress():获取客户端的 IP:Port(InetAddress 类型),toIpPort() 转为字符串;

conn->shutdown():主动关闭连接(关闭写端,触发 TCP 四次挥手,避免资源泄漏)。

4. onMessage 回调函数(数据读写处理)

cpp 复制代码
void ChatServer::onMessage(const TcpConnectionPtr &conn, Buffer *buffer, Timestamp time)
{
    string buff=buffer->retrieveAllAsString();  // 读取缓冲区所有数据并清空
    printf("ChatServer received message:%s\n", buff.c_str());
    conn->send(buff);  // 回显数据给客户端(异步发送)
}
  • 参数解析:

Buffer* buffer:Muduo 封装的缓冲区,解决 TCP 粘包 / 拆包问题(流式协议无消息边界);

Timestamp time:数据接收完成的时间戳(示例中未使用,可用于日志 / 性能统计);

  • 核心操作:

buffer->retrieveAllAsString():读取缓冲区中所有可读数据,转为字符串,并移动读指针(清空缓冲区);

conn->send(buff):异步发送数据给客户端 ------ 若数据能一次性写入 socket,则直接发送;若写满,则存入写缓冲区,等待 EPOLLOUT 事件触发后继续发送(Muduo 自动处理,无需手动关注非阻塞写)。

5. ChatServer 构造函数(初始化核心逻辑)

cpp 复制代码
ChatServer::ChatServer(EventLoop *loop, const InetAddress &listenAddr, const string &nameArg) 
    : loop_(loop), server_(loop, listenAddr, nameArg)  // 初始化列表
{
    // 绑定连接回调:将类成员函数 onConnection 转为 Muduo 可识别的回调类型
    server_.setConnectionCallback(
        std::bind(&ChatServer::onConnection,this,_1)
    );
    // 绑定消息回调:将类成员函数 onMessage 转为 Muduo 可识别的回调类型
    server_.setMessageCallback(
        std::bind(&ChatServer::onMessage,this,_1,_2,_3)
    );
 
    server_.setThreadNum(4);  // 设置工作线程池大小为 4
}
  • 初始化列表注意点:类中 server_ 声明在 loop_ 之前,但初始化列表先初始化 loop_、后初始化 server_------ 编译器会触发 "成员初始化顺序不匹配" 警告(不影响功能,但建议调整类成员声明顺序:先 loop_,后 server_)。
  • 回调绑定核心(std::bind):Muduo 的回调类型是 std::function,而类的成员函数有隐含的 this 指针,需用 std::bind 绑定:

this:指向当前 ChatServer 对象,保证回调能调用到当前对象的成员函数;

_1/_2/_3:占位符,对应回调函数的参数(onConnection 有 1 个参数,onMessage 有 3 个参数)。

  • setThreadNum(4):设置 Muduo 的工作线程池大小为 4------Muduo 采用 "单 Reactor 多线程" 模型:

主线程(Reactor 线程):处理 Acceptor 监听事件(新连接建立);

4 个工作线程:处理已建立连接的读写事件(onMessage 运行在工作线程)。

6. start 成员函数(启动服务器)

cpp 复制代码
void ChatServer::start()
{
    server_.start();  // 调用 TcpServer::start(),底层启动 Acceptor 监听端口 + 启动工作线程池
}
  • TcpServer::start() 做两件核心事:
    1. 启动 Acceptor:调用 listen() 监听指定端口,开始接收新连接;
    2. 启动线程池:创建 4 个工作线程,每个线程绑定一个 EventLoop,用于处理连接的读写事件。

7. main 函数(程序入口)

cpp 复制代码
int main(){
    EventLoop loop;  // 创建主线程的 EventLoop(Reactor 核心,阻塞运行)
    InetAddress addr("127.0.0.1", 6000);  // 监听地址:127.0.0.1:6000
    ChatServer server(&loop, addr, "ChatServer");  // 创建服务器对象
    server.start();  // 启动服务器
    loop.loop();     // 启动 Reactor 事件循环(阻塞,直到退出)
    return 0;
}
  • 核心流程:
  1. EventLoop loop:主线程创建 EventLoop,是整个服务器的 "心脏",负责 epoll 事件监听 / 分发;
  2. InetAddress addr:指定服务器监听的 IP 和端口(127.0.0.1 表示仅本地可访问,若需外网访问改为 0.0.0.0);
  3. ChatServer server:初始化服务器,绑定回调、设置线程池;
  4. server.start():启动监听和线程池;
  5. loop.loop():进入无限循环,调用 epoll_wait 等待事件(新连接、数据读写、信号等),并分发事件到对应回调 ------这行代码会阻塞主线程,直到服务器退出。

连接测试

相关推荐
mjhcsp2 小时前
C++ 后缀树(Suffix Tree):原理、实现与应用全解析
java·开发语言·c++·suffix-tree·后缀树
mjhcsp2 小时前
C++ 有限状态自动机(FSM):原理、实现与应用全解析
开发语言·c++·有限状态自动机
mjhcsp2 小时前
C++ KMP 算法:原理、实现与应用全解析
java·c++·算法·kmp
好大哥呀2 小时前
C++ IDE
开发语言·c++·ide
WW_千谷山4_sch3 小时前
MYOJ_10599:CSP初赛题单10:计算机网络
c++·计算机网络·算法
ICT董老师3 小时前
通过kubernetes部署nginx + php网站环境
运维·nginx·云原生·容器·kubernetes·php
梵尔纳多3 小时前
绘制一个矩形
c++·图形渲染·opengl
橘颂TA3 小时前
【剑斩OFFER】算法的暴力美学——leetCode 946 题:验证栈序列
c++·算法·leetcode·职场和发展·结构与算法
闻缺陷则喜何志丹3 小时前
【状态机动态规划】3686. 稳定子序列的数量|1969
c++·算法·动态规划·力扣·状态机动态规划