如何手撕集群聊天室项目?

看完这两篇你会对该项目有所了解 第二篇是帮你手搓muduo框架的 感觉跟lievent差不多

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

nginx集群聊天室(二)muduo的客户端服务器编程框架

谁能懂一下这个项目恶心的架构 好吧 看着很复杂 其实我们只用关心这两个部分

架构

1.头文件夹include

src源文件夹

CMakeLists.txt编写

最外层CMakeLists.txt

cpp 复制代码
cmake_minimum_required(VERSION 3.0)
project(chat)
#配置编译器选项
set(CMAKE_CXX_FLAGS ${CMAKE_CXX_FLAGS} -g)

#配置最终的可执行文件输出的路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/bin)

include_directories(
    ${PROJECT_SOURCE_DIR}/include
    ${PROJECT_SOURCE_DIR}/include/server
    ${PROJECT_SOURCE_DIR}/include/server/mysql
    ${PROJECT_SOURCE_DIR}/include/server/muduo
    ${PROJECT_SOURCE_DIR}/thirdparty
)
add_subdirectory(muduo)
#加载子目录
add_subdirectory(src)

src层的

src里面就俩文件夹 一个是client 一个是server 这个项目主要还是C/S架构 要在client那里呈现给用户

client的cmake文件

server的cmake文件

cpp 复制代码
#定义了一个SRC_LIST 变量 ,包含了该项目下的所有源文件
aux_source_directory(. SRC_LIST)
aux_source_directory(./mysql MYSQL_LIST)
aux_source_directory(./muduo MUDUO_LIST)
aux_source_directory(./redis REDIS_LIST)

#指定可执行文件
add_executable(chatserver ${SRC_LIST} ${MYSQL_LIST} ${MUDUO_LIST} ${REDIS_LIST})
#制定可执行性文件链接需要依赖的库文件
target_link_libraries(chatserver muduo_net muduo_base mysqlclient hiredis pthread)

建立框架

这个项目内部架构我没优化 因为是刚照着码完 所以我们就按照他的逻辑过一遍

先写chatserver文件 管理聊天初始化的类

头文件

cpp 复制代码
#ifndef CHATSERVER_HPP
#define CHATSERVER_HPP
#include<muduo/net/TcpServer.h>
#include<muduo/net/EventLoop.h>
#include<iostream>
using namespace muduo;
using namespace muduo::net;
using namespace std;

class ChatServer{
private:
    TcpServer _server;  //服务器对象
    EventLoop *_loop;   //指向事件循环对象
    void onConnection(const TcpConnectionPtr &);//连接回调函数
    void onMessage(const TcpConnectionPtr &,Buffer *,Timestamp);//消息回调函数 上报读写事件

public://初始化聊天服务器对象
    ChatServer(EventLoop *loop,const InetAddress &listenAddr,const string &nameArg);
    void start();//启动服务
};
#endif

源文件

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

ChatServer::ChatServer(EventLoop *loop,const InetAddress &listenAddr,const string &nameArg)
                :_server(loop,listenAddr,nameArg),_loop(loop){
                //注册回调函数
                _server.setConnectionCallback(bind(&ChatServer::onConnection,this,_1));
                //注册信息回调
                _server.setMessageCallback(bind(&ChatServer::onMessage,this,_1,_2,_3));
                //设置线程数量
                _server.setThreadNum(4);
}   
//启动服务
void ChatServer::start(){
    _server.start();
}
//上报连接信息的回调函数
void ChatServer::onConnection(const TcpConnectionPtr &conn){
    if(conn->connected()){
        cout<<conn->peerAddress().toIpPort()<<" -> "<<conn->localAddress().toIpPort()<<" state:online"<<endl;
    }else{
    ChatService::instance()->clientCloseException(conn);
       conn->shutdown();
        //TODO 用户退出,注销用户信息
    }
}
//上报读写事件
void ChatServer::onMessage(const TcpConnectionPtr &conn,Buffer *buffer,Timestamp time){
    string msg=buffer->retrieveAllAsString();
    json js=json::parse(msg);//反序列化
    //通过js["msgid"]获取消息内容 目的:完全解耦网络模块的代码和业务模块的代码
    auto msghander=ChatService::instance()->getHandler(js["msgid"].get<int>()); 
    //回调消息绑定好的事件处理器,来执行相应的业务处理
    msghander(conn,js,time);
}   

再写chatserver 业务类

头文件

cpp 复制代码
#ifndef CHATSERVICE_HPP
#define CHATSERVICE_HPP
#include<muduo/net/TcpConnection.h>
#include<unordered_map>
#include<functional>
#include<mutex>
#include"json.hpp"
using json=nlohmann::json;
using namespace std;
using namespace muduo;
using namespace muduo::net;
#include"redis.hpp"             
#include"friendmodel.hpp"
#include"usermodel.hpp"
#include"offlinemessagemodel.hpp"
#include"groupmodel.hpp"
#include"usermodel.hpp"
#include"offlinemessagemodel.hpp"
//表示处理消息的事件回调方法类型
//定义了一个名为 MsgHandler 的类型,它专门用来表示 "处理聊天室消息的函数",任何符合以下特征的函数 / 可调用对象,都可以被归为 MsgHandler 类型:
using msghandler=function<void(const TcpConnectionPtr &conn,json &js,Timestamp time)>;

//聊天服务器业务类
class ChatService{
private:
    ChatService();//单例模式
    
    //存储消息id和对应的业务处理方法
    unordered_map<int,msghandler> _msgHandlerMap;
    
    //存储在线用户的通信连接
    unordered_map<int,TcpConnectionPtr> _userConnMap;

    //定义互斥锁 保证_userConnMap的线程安全
    mutex _connMutex;

    UserModel _userModel;//数据操作类对象
    OfflineMessageModel _offlineMsgModel;//离线消息业务对象
    FriendModel _friendModel;//好友信息操作对象
    GroupModel _groupModel;//群组信息操作对象
    redis _redis;//redis数据库操作对象

public:
    static ChatService* instance();//获取单例对象的接口函数

    void login(const TcpConnectionPtr &conn,json &js,Timestamp time);//登录业务
    void reg(const TcpConnectionPtr &conn,json &js,Timestamp time);//注册业务
    void onechat(const TcpConnectionPtr &conn,json &js,Timestamp time);//一对一聊天业务
    void addfriend(const TcpConnectionPtr &conn,json &js,Timestamp time);//添加好友业务
    void creategroup(const TcpConnectionPtr &conn,json &js,Timestamp time);//创建群组业务
    void addgroup(const TcpConnectionPtr &conn,json &js,Timestamp time);//加入群
    void groupchat(const TcpConnectionPtr &conn,json &js,Timestamp time);//群聊业务
    void loginout(const TcpConnectionPtr &conn,json &js,Timestamp time);//处理注销业务
    void clientCloseException(const TcpConnectionPtr &conn);//客户端异常退出处理
    void handleredissubscribemessage(int userid,string msg);//从redis消息队列中拉取离线消息
    void reset();//服务器异常,业务重置方法
    msghandler getHandler(int msgid);//获取消息对应的处理器
    


};

#endif

业务头文件public.hpp

cpp 复制代码
#ifndef PUBLIC_HPP
#define PUBLIC_HPP
//server和client公共的头文件
enum EnMsgType{
    LOGIN_MSG=1,//登录消息
    LOGIN_ACK_MSG,//登录响应消息
    LOGINOUT_MSG,//注销消息
    REG_MSG,//注册消息
    REG_ACK_MSG,//注册响应消息
    ONE_CHAT_MSG,//单人聊天消息
    ADD_FRIEND_MSG,//添加好友消息

    CREATE_GROUP_MSG,//创建群组消息
    ADD_GROUP_MSG,//加入群组消息
    GROUP_CHAT_MSG//群组聊天消息

};

#endif

源文件(部分)

cpp 复制代码
#include"chatservice.hpp"
#include<string>
#include<muduo/base/Logging.h>
#include<vector>

using namespace std;
using namespace muduo;
#include"public.hpp"
ChatService* ChatService::instance(){
    static ChatService service;
    return &service;
}
ChatService::ChatService(){
    //注册消息以及对应的Handler

    //用户基本业务管理相关事件处理回调注册
    _msgHandlerMap.insert({LOGIN_MSG,bind(&ChatService::login,this,_1,_2,_3)});
    _msgHandlerMap.insert({LOGINOUT_MSG,bind(&ChatService::login,this,_1,_2,_3)});
    _msgHandlerMap.insert({REG_MSG,bind(&ChatService::reg,this,_1,_2,_3)});
    _msgHandlerMap.insert({ONE_CHAT_MSG,bind(&ChatService::onechat,this,_1,_2,_3)});
    _msgHandlerMap.insert({ADD_FRIEND_MSG,bind(&ChatService::addfriend,this,_1,_2,_3)});

    //群组业务管理相关事件处理回调注册
    _msgHandlerMap.insert({CREATE_GROUP_MSG,bind(&ChatService::creategroup,this,_1,_2,_3)});
    _msgHandlerMap.insert({ADD_GROUP_MSG,bind(&ChatService::addgroup,this,_1,_2,_3)});
    _msgHandlerMap.insert({GROUP_CHAT_MSG,bind(&ChatService::groupchat,this,_1,_2,_3)});

    if(_redis.connect()){
        //初始化消息回调
        _redis.init_notify_handler(bind(&ChatService::handleredissubscribemessage,this,_1,_2)); 
    }
}

void ChatService::reset(){
    //把online状态的用户,设置成offline
    _userModel.resetState();
}

msghandler ChatService::getHandler(int msgid){
    //记录错误日志,msgid没有对应的处理函数
    auto it=_msgHandlerMap.find(msgid);
    if(it==_msgHandlerMap.end()){
        //返回一个默认的处理器 空操作
        return [=](const TcpConnectionPtr &conn,json &js,Timestamp time){
            LOG_ERROR<<"msgid:"<<msgid<<" can not find handler!";
        };
    }
    else return _msgHandlerMap[msgid];
}

业务代码暂时不写

server的main函数

cpp 复制代码
#include"chatserver.hpp"
#include"chatservice.hpp"
#include<iostream>
#include<signal.h>
using namespace std;
//处理服务器ctrl+c结束后,重置user的状态信息
void resetHandler(int){
    ChatService::instance()->reset();
    exit(0);
}
int main(){
    signal(SIGPIPE,resetHandler);//重置信号处理函数,防止服务器崩溃

    EventLoop loop;//创建事件循环对象
    InetAddress addr("127.0.0.1",6000);//设置服务器端口号
    ChatServer server(&loop,addr,"ChatServer");//创建服务器对象
    server.start();//启动服务器
    loop.loop();//启动事件循环
    return 0;

}

JSON介绍

MYSQL表的设计

nginx集群聊天室(四)项目的mysql数据库表的设置及代码

撰写mysql类

cpp 复制代码
#ifndef MYSQL_HPP
#define MYSQL_HPP
#include<muduo/base/Logging.h>
#include<mysql/mysql.h>
#include<string>
using namespace std;

// 数据库操作类
class MySQL{
private:
    MYSQL *_conn;
    // 数据库配置信息
    string server = "127.0.0.1";
    string user = "yzy";
    string password = "770202";
    string dbname = "chat";

public:
    MySQL();// 初始化数据库连接
    ~MySQL();// 释放数据库连接资源
    bool connect();// 连接数据库
    bool update(string sql);// 更新操作
    MYSQL_RES* query(string sql);// 查询操作
    MYSQL* getConnection();// 获取连接
};
#endif
cpp 复制代码
#include"mysql.hpp"

MySQL::MySQL(){// 初始化数据库连接
    _conn = mysql_init(nullptr);
}
MySQL::~MySQL(){// 释放数据库连接资源
    if (_conn != nullptr)mysql_close(_conn);
}
bool MySQL::connect(){// 连接数据库
    MYSQL *p = mysql_real_connect(_conn, server.c_str(), user.c_str(),password.c_str(), dbname.c_str(), 3306, nullptr, 0);
    if (p != nullptr){
        //c和c++默认的编码格式是ascii,而中文使用gbk编码格式
       mysql_query(_conn, "set names gbk");
       LOG_INFO << "连接数据库成功!";
    }
    else{
        LOG_INFO << "连接数据库失败!";
    }
    return p ;
}
bool MySQL::update(string sql){// 更新操作
    if (mysql_query(_conn, sql.c_str())){
        LOG_INFO << __FILE__ << ":" << __LINE__ << ":" << sql << "更新失败! 错误: " << mysql_error(_conn);
        return false;
    }
    return true;
}
MYSQL_RES* MySQL::query(string sql){// 查询操作
    if (mysql_query(_conn, sql.c_str())){
        LOG_INFO << __FILE__ << ":" << __LINE__ << ":" << sql << "查询失败!";
        return nullptr;
    }
    return mysql_use_result(_conn);
}

MYSQL *MySQL::getConnection()
{
    return _conn;
}

Model数据层代码框架设计

设计user表(映射类 要和数据库表一一对应)

cpp 复制代码
#ifndef USER_HPP
#define USER_HPP
#include <string>
using namespace std;
#include<iostream>
//匹配user表的ORM类
class User {
private:
    int id;
    string name;
    string password;
    string state; // online offline

public:
    User(int id=-1,string name="",string password="",string state="offline") : id(id), name(name), password(password), state(state) {}
    void setId(int id) { this->id = id; }
    void setName(const string &name) { this->name = name; }
    void setPassword(const string &password) { this->password = password; }
    void setState(const string &state) { this->state = state; }

    int getId() const { return id; }
    string getName() const { return name; }
    string getPassword() const { return password; }
    string getState() const { return state; }
};

#endif
cpp 复制代码
#ifndef USERMODEL_HPP
#define USERMODEL_HPP
#include"user.hpp"
//user表的数据操作类
class UserModel {
public:
    //user表的增加方法
    bool insert(User &user);

    //根据用户号码查询用户信息
    User query(int id);

    //更新用户的状态信息
    bool updatestate(User &user);

    //重置用户的状态信息
    void resetState();
};

#endif

usermodel.cpp撰写

cpp 复制代码
#include"usermodel.hpp"
#include"mysql.hpp"
#include<iostream>
using namespace std;
//user表的增加方法
bool UserModel::insert(User &user){
    //1 组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "insert into User(name,password,state) values('%s','%s','%s')", user.getName().c_str(), user.getPassword().c_str(), user.getState().c_str());
    MySQL mysql;
    if (mysql.connect()) {
        if (mysql.update(sql)) {
            //获取插入成功的用户数据生成的主键id
            user.setId(mysql_insert_id(mysql.getConnection()));
            return true;
        }
    }
    return false;
}

User UserModel::query(int id){
    //1 组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "select * from User where id=%d", id);
    MySQL mysql;
    if (mysql.connect()) {
        MYSQL_RES* res = mysql.query(sql);
        if (res) {
            MYSQL_ROW row = mysql_fetch_row(res);
            if (row) {
                User user;
                user.setId(id);
                user.setName(row[1]);
                user.setPassword(row[2]);
                user.setState(row[3]);
                mysql_free_result(res);
                return user;
            }
        }
    }
    return User();
}

bool UserModel::updatestate(User &user){
    //1 组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "update User set state='%s' where id=%d", user.getState().c_str(), user.getId());
    MySQL mysql;
    if (mysql.connect()) {
        if (mysql.update(sql)) {
            return true;
        }
    }
    return false;
}

void UserModel::resetState(){
    //1 组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "update User set state='offline' where state='online'");
    MySQL mysql;
    if (mysql.connect()) {
        mysql.update(sql);
    }
}

处理登录,注册,注销,异常退出,一对一聊天 加好友等业务

cpp 复制代码
// 处理登录业务
void ChatService::login(const TcpConnectionPtr &conn,json &js,Timestamp time){
    int id=js["id"].get<int>();
    string password=js["password"];
    User user=_userModel.query(id);
    if(user.getId()==id&&user.getPassword()==password){
        if(user.getState()=="online"){
            //该用户已经登录,不能重复登录
            json response;
            response["msgid"]=LOGIN_ACK_MSG;
            response["errno"]=2;
            response["errmsg"]="该账号已经登录,不能重复登录";
            conn->send(response.dump());
            return;
        }
        else{
            //登录成功,记录用户连接信息
            {
                lock_guard<mutex> lock(_connMutex);
                _userConnMap.insert({id,conn});
            }
            //将用户id和通道号绑定,用于后续的消息推送
            _redis.subscribe(id);   
            
            //登录成功 更新用户状态信息 state offline=>online
            user.setState("online");
            _userModel.updatestate(user);

            json response;
            response["msgid"]=LOGIN_ACK_MSG;
            response["errno"]=0;
            response["id"]=user.getId();
            response["name"]=user.getName();
            //查询是否有离线消息
            vector<string> vec=_offlineMsgModel.query(id);
            if(!vec.empty()){
                response["offlinemsg"]=vec;
                //读取该用户的离线消息后,删除掉
                _offlineMsgModel.remove(id);
            }
            //查询该用户的好友信息并返回
            vector<User> vecFriend=_friendModel.query(id);
            if(!vecFriend.empty()){
                vector<string> vec2;
                for(User &user:vecFriend){
                    json js;
                    js["id"]=user.getId();
                    js["name"]=user.getName();
                    js["state"]=user.getState();
                    vec2.push_back(js.dump());
                }
                response["friends"]=vec2;
            }

            //查询用户的群组信息
            vector<Group> GroupuserVec=_groupModel.queryGroups(id);
            if(!GroupuserVec.empty()){
                vector<string> vec2;
                for(Group &group:GroupuserVec){ 
                    json grpjson;
                    grpjson["id"]=group.getId();
                    grpjson["groupname"]=group.getName();
                    grpjson["groupdesc"]=group.getDesc();
                    vector<string>userV;
                    for(Groupuser &user:group.getUsers()){
                        json userjson;
                        userjson["id"]=user.getId();
                        userjson["name"]=user.getName();
                        userjson["state"]=user.getState();
                        userjson["role"]=user.getRole();
                        userV.push_back(userjson.dump());
                    } 
                    grpjson["users"]=userV;
                    vec2.push_back(grpjson.dump());
                }
                response["groups"]=vec2;
            }
            conn->send(response.dump());
        }
    }
    else{
        //该用户不存在,登录失败
        json response;
        response["msgid"]=LOGIN_ACK_MSG;
        response["errno"]=1;
        response["errmsg"]="用户名或密码错误";
        conn->send(response.dump());
    }
    LOG_INFO<<"do login service!";
}

//处理注册业务
void ChatService::reg(const TcpConnectionPtr &conn,json &js,Timestamp time){
    string name=js["name"];
    string password=js["password"];
    User user;
    user.setName(name);
    user.setPassword(password);
    bool state=_userModel.insert(user);
    if(state){
        //注册成功
        json response;
        response["msgid"]=REG_ACK_MSG;
        response["id"]=user.getId();
        response["errno"]=0;
        conn->send(response.dump());
        
    }
    else{
        //注册失败
        json response;
        response["msgid"]=REG_ACK_MSG;
        response["errno"]=1;
        conn->send(response.dump());
    }
}

//处理注销业务
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()){
            //从map表删除用户的连接信息
            _userConnMap.erase(it);
            //更新用户的状态信息为offline
        }
    }
    //取消订阅通道
    _redis.unsubscribe(userid); 

    //更新用户的状态信息为offline
    User user(userid,"","offline");
    _userModel.updatestate(user);
}

//处理客户端异常退出
void ChatService::clientCloseException(const TcpConnectionPtr &conn){
    
    //遍历_userConnMap表,找到连接对应的用户id
    lock_guard<mutex> lock(_connMutex);
    User user;
    for(auto it=_userConnMap.begin();it!=_userConnMap.end();++it){
        if(it->second==conn){
            //从map表删除用户的连接信息
            user.setId(it->first);
            _userConnMap.erase(it);
            //更新用户的状态信息为offline
            User user=_userModel.query(it->first);
            user.setState("offline");
            _userModel.updatestate(user);
            break;
        }
    }
    //取消订阅通道
    _redis.unsubscribe(user.getId()); 
}

//处理一对一聊天业务
void ChatService::onechat(const TcpConnectionPtr &conn,json &js,Timestamp time){
    int toid=js["to"].get<int>();
    //加锁,保证_userConnMap的线程安全
    {
        lock_guard<mutex> lock(_connMutex);
        auto it=_userConnMap.find(toid);
        if(it!=_userConnMap.end()){
            //toid在线,转发消息 服务器主动推送消息给told用户
            it->second->send(js.dump());
            return;
        }
    }
    //查询toid是否在线
    User user=_userModel.query(toid);
    if(user.getState()=="online"){
        //toid在线,存储离线消息
        _redis.publish(toid,js.dump());
        return;
    }
    //toid不在线,存储离线消息
    _offlineMsgModel.insert(toid,js.dump());
}

//处理添加好友业务
void ChatService::addfriend(const TcpConnectionPtr &conn,json &js,Timestamp time){
    int userid=js["id"].get<int>();
    int friendid=js["friendid"].get<int>();

    //存储好友信息
    _friendModel.insert(userid,friendid);
}

处理离线消息业务代码

cpp 复制代码
#ifndef OFFLINEMESSAGEMODEL_HPP
#define OFFLINEMESSAGEMODEL_HPP
#include<string>
#include<vector>
using namespace std;
//提供离线消息表的操作接口方法
class OfflineMessageModel{
public:
    //存储用户的离线消息
    void insert(int userid,string msg);
    //删除用户的离线消息
    void remove(int userid);
    //查询用户的离线消息
    vector<string> query(int userid);
};
#endif
cpp 复制代码
#include"offlinemessagemodel.hpp"
#include"mysql.hpp"
//存储用户的离线消息
void OfflineMessageModel::insert(int userid,string msg){
    //1 组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "insert into OfflineMessage(userid,message) values(%d,'%s')", userid, msg.c_str());
    MySQL mysql;
    if (mysql.connect()) {
        mysql.update(sql);
    }
}
//删除用户的离线消息
void OfflineMessageModel::remove(int userid){
    //1 组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "delete from OfflineMessage where userid=%d", userid);
    MySQL mysql;
    if (mysql.connect()) {
        mysql.update(sql);
    }
}
//查询用户的离线消息
vector<string> OfflineMessageModel::query(int userid){
    //1 组装sql语句
    char sql[1024] = {0};   
    sprintf(sql, "select message from OfflineMessage where userid=%d", userid);
    vector<string> vec;
    MySQL mysql;
    if (mysql.connect()) {
        MYSQL_RES* res = mysql.query(sql);
        if (res != nullptr) {
            MYSQL_ROW row;
            while ((row = mysql_fetch_row(res)) != nullptr) {
                vec.push_back(row[0]);
            }
            mysql_free_result(res);
        }
    }
    return vec;
}

处理好友业务

cpp 复制代码
#ifndef FRIENDMODEL_HPP
#define FRIENDMODEL_HPP
//维护好友信息的操作接口方法
#include"user.hpp"
#include<vector>
using namespace std;
class FriendModel{
public:
    //添加好友关系
    void insert(int userid,int friendid);   
    //返回用户的好友列表
    vector<User> query(int userid);
    

};
#endif
cpp 复制代码
#include"friendmodel.hpp"
#include"mysql.hpp"
//添加好友关系
void FriendModel::insert(int userid,int friendid){
    //1 组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "insert into Friend(userid,friendid) values(%d,%d)", userid, friendid);
    MySQL mysql;
    if (mysql.connect()) {
        mysql.update(sql);
    }
}
//返回用户的好友列表
vector<User> FriendModel::query(int userid){
    //1 组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "select a.id,a.name,a.password,a.state from User a inner join Friend b on a.id=b.friendid where b.userid=%d", userid);
    vector<User> vec;
    MySQL mysql;
    if (mysql.connect()) {
        MYSQL_RES* res = mysql.query(sql);
        if (res != nullptr) {
            MYSQL_ROW row;
            while ((row = mysql_fetch_row(res)) != nullptr) {
                User user;
                user.setId(atoi(row[0]));
                user.setName(row[1]);
                user.setPassword(row[2]);
                user.setState(row[3]);
                vec.push_back(user);
            }
            mysql_free_result(res);
        }
    }
    return vec;
}

处理群组的业务

设计group表(映射类 要和数据库表一一对应)

cpp 复制代码
#ifndef GROUP_HPP
#define GROUP_HPP
using namespace std;
#include"groupuser.hpp"
#include<string>
#include<vector>
//User表的数据操作类ORM类
class Group{
private:
    int id;
    string name;
    string desc;
    vector<Groupuser> users; //群成员列表
public:
    Group(int id=-1,string name="",string desc=""):id(id),name(name),desc(desc){}

    void setId(int id){this->id=id;}
    void setName(const string &name){this->name=name;}
    void setDesc(const string &desc){this->desc=desc;}
    int getId()const{return id;}
    string getName()const{return name;}
    string getDesc()const{return desc;}

    vector<Groupuser>& getUsers(){return users;}
};
#endif
cpp 复制代码
#ifndef GROUPMODEL_HPP
#define GROUPMODEL_HPP
#include"group.hpp"
#include<vector>
#include<string>
using namespace std;
//维护群组信息的操作接口方法
class GroupModel{
public:
    //创建群组
    bool createGroup(Group &group);
    //加入群组
    void addGroup(int userid,int groupid,string role);
    //查询用户所在的群组信息
    vector<Group> queryGroups(int userid);
    //根据指定的群组id查询群组用户id列表,除userid自己,主要用于群聊业务给其他成员群发消息
    vector<int> queryGroupUsers(int userid,int groupid);
};

#endif

groupmodel.cpp

cpp 复制代码
#include"groupmodel.hpp"
#include"mysql.hpp"

//创建群组
bool GroupModel::createGroup(Group &group){
    //1 组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "insert into AllGroup(groupname,groupdesc) values('%s','%s')", group.getName().c_str(), group.getDesc().c_str());
    MySQL mysql;
    if (mysql.connect()) {
        if (mysql.update(sql)) {
            //获取插入成功的用户数据生成的主键id
            group.setId(mysql_insert_id(mysql.getConnection()));
            return true;
        }
    }
    return false;
}
//加入群组
void GroupModel::addGroup(int userid,int groupid,string role){
    //1 组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "insert into GroupUser(groupid,userid,role) values(%d,%d,'%s')", groupid, userid, role.c_str());
    MySQL mysql;
    if (mysql.connect()) {
        mysql.update(sql);
    }
}
//查询用户所在的群组信息
vector<Group> GroupModel::queryGroups(int userid){
    //1.先根据userid在GroupUser表中查询出该用户所属的群组信息
    //2.再根据群组信息,查询属于该群组的所有用户信息 并且和user表进行多表联合查询
    char sql[1024] = {0};
    sprintf(sql, "select a.id,a.groupname,a.groupdesc from AllGroup a inner join GroupUser b on a.id=b.groupid where b.userid=%d", userid);
    vector<Group> vec;
    MySQL mysql;
    if (mysql.connect()) {
        MYSQL_RES* res = mysql.query(sql);
        if (res != nullptr) {
            MYSQL_ROW row;
            while ((row = mysql_fetch_row(res)) != nullptr) {
                Group group;
                group.setId(atoi(row[0]));
                group.setName(row[1]);
                group.setDesc(row[2]);
                vec.push_back(group);
            }
            mysql_free_result(res);
        }
    }

    //查询群组的用户信息
    for(Group &group:vec){
        sprintf(sql, "select a.id,a.name,a.state,b.role from User a inner join GroupUser b on a.id=b.userid where b.groupid=%d", group.getId());
        MYSQL_RES* res = mysql.query(sql);
        if (res != nullptr) {
            MYSQL_ROW row;
            while ((row = mysql_fetch_row(res)) != nullptr) {
                Groupuser user;
                user.setId(atoi(row[0]));
                user.setName(row[1]);
                user.setState(row[2]);
                user.setRole(row[3]);
                group.getUsers().push_back(user);
            }
            mysql_free_result(res);
        }
    }
    return vec;
}

//根据指定的群组id查询群组用户id列表,除userid自己,主要用于群聊业务给其他成员群发消息
vector<int> GroupModel::queryGroupUsers(int userid,int groupid){
    //1 组装sql语句
    char sql[1024] = {0};
    sprintf(sql, "select userid from GroupUser where groupid=%d and userid!=%d", groupid, userid);
    vector<int> vec;
    MySQL mysql;
    if (mysql.connect()) {
        MYSQL_RES* res = mysql.query(sql);
        if (res != nullptr) {
            MYSQL_ROW row;
            while ((row = mysql_fetch_row(res)) != nullptr) {
                vec.push_back(atoi(row[0]));
            }
            mysql_free_result(res);
        }
    }
    return vec;
}
cpp 复制代码
//创建群组业务
void ChatService::creategroup(const TcpConnectionPtr &conn,json &js,Timestamp time){
    int userid=js["id"].get<int>();
    string groupname=js["groupname"];
    string groupdesc=js["groupdesc"];
    //存储新创建的群组信息
    Group group(-1,groupname,groupdesc);
    if(_groupModel.createGroup(group)){
        //存储群组创建人信息
        _groupModel.addGroup(userid,group.getId(),"creator");
    }
}

//加入群组业务
void ChatService::addgroup(const TcpConnectionPtr &conn,json &js,Timestamp time){
    int userid=js["id"].get<int>();
    int groupid=js["groupid"].get<int>();
    _groupModel.addGroup(userid,groupid,"normal");
}

//群组聊天业务
void ChatService::groupchat(const TcpConnectionPtr &conn,json &js,Timestamp time){
    int userid=js["id"].get<int>();
    int groupid=js["groupid"].get<int>();
    //查询群组成员列表,除userid自己外,其他成员转发消息
    vector<int> useridVec=_groupModel.queryGroupUsers(userid,groupid);
    lock_guard<mutex> lock(_connMutex);
    for(int id:useridVec){
        //加锁,保证_userConnMap的线程安全        
        auto it=_userConnMap.find(id);
        if(it!=_userConnMap.end()){
            //转发消息
            it->second->send(js.dump());
        }
        else{
            //查询toid是否在线
            User user=_userModel.query(id);
            if(user.getState()=="online"){
                //toid在线,存储离线消息
                _redis.publish(id,js.dump());
            }
            else{
                //toid不在线,存储离线消息
                _offlineMsgModel.insert(id,js.dump());
            }
        }
    }
}

客户端开发首页面

cpp 复制代码
#include"json.hpp"
#include<iostream>
#include<string>
#include<thread>
#include<vector>
#include<chrono>
#include<ctime>
using namespace std;
using json=nlohmann::json;

#include<unistd.h>
#include<sys/socket.h>
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"group.hpp"
#include"user.hpp"
#include"public.hpp"

//记录当前系统登录的用户信息
User g_currentUser;
//记录当前系统登录用户的好友列表信息
vector<User> g_currentUserFriendList;
//记录当前系统登录用户的群组列表信息
vector<Group> g_currentUserGroupList;
//显示当前登录成功用户的基本信息
void showCurrentUserData();
//控制主菜单页面程序
bool isMainMenuRunning=false;

//接收线程
void readTaskHandler(int clientfd);
//获取系统时间(聊天信息添加时间戳)
string getCurrentTime();
//主聊天页面程序
void mainMenu(int clientfd);

//聊天客户端程序实现,main线程用作发送线程 子线程用作接收线程
int main(int argc,char **argv){
    if(argc<3){
        cerr<<"command invalid! example: ./chatclient 127.0.0.1 6000"<<endl;
        exit(-1);
    }
    //解析获取服务器端的ip和端口号
    char* ip=argv[1];
    uint16_t port=atoi(argv[2]);
    //创建客户端的socket
    int clientfd=socket(AF_INET,SOCK_STREAM,0);
    if(clientfd==-1){
        cerr<<"socket create error!"<<endl;
        exit(-1);
    }
    //填写客户端需要连接的服务器信息
    struct sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(serveraddr));
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_port=htons(port);
    serveraddr.sin_addr.s_addr=inet_addr(ip);
    //连接服务器
    if(connect(clientfd,(struct sockaddr*)&serveraddr,sizeof(serveraddr))==-1){ 
        cerr<<"connect server error!"<<endl;
        close(clientfd);
        exit(-1);
    }
    cout<<"connect server success!"<<endl;
    //登录注册主菜单
    while(isMainMenuRunning){
        cout<<"======================"<<endl;
        cout<<"1. login"<<endl;
        cout<<"2. register"<<endl;
        cout<<"3. quit"<<endl;
        cout<<"======================"<<endl;
        cout<<"choice:";
        int choice=0;
        cin>>choice;
        switch (choice){
            case 1:{//登录业务
                int id=0;
                string password="";
                cout<<"userid:";
                cin>>id;
                cout<<"userpassword:";
                cin>>password;
                //组织登录json数据
                json js;    
                js["msgid"]=LOGIN_MSG;
                js["id"]=id;
                js["password"]=password;
                //发送登录数据
                string request=js.dump();
                int len=send(clientfd,request.c_str(),strlen(request.c_str())+1,0);
                if(len==-1){
                    cerr<<"send login msg error:"<<request<<endl;
                }
                else{
                    //接收服务器响应数据
                    char buffer[1024]={0};
                    int len=recv(clientfd,buffer,1024,0);
                    if(len==-1){
                        cerr<<"recv login response error!"<<endl;
                    }
                    else{
                        //解析响应数据
                        json response=json::parse(buffer);
                        if(response["errno"].get<int>()==0){
                            //登录成功
                            g_currentUser.setId(response["id"].get<int>());
                            g_currentUser.setName(response["name"]);
                            g_currentUser.setState("online");
                            //记录当前用户的好友列表信息
                            if(response.contains("friends")){
                                //初始化
                                g_currentUserFriendList.clear();
                                vector<string> vec=response["friends"].get<vector<string>>();
                                for(string &str:vec){
                                    json js=json::parse(str);
                                    User user;
                                    user.setId(js["id"].get<int>());
                                    user.setName(js["name"]);
                                    user.setState(js["state"]);
                                    g_currentUserFriendList.push_back(user);
                                }
                            }
                            //记录当前用户的群组列表信息
                            if(response.contains("groups")){
                                //初始化
                                g_currentUserGroupList.clear();
                                vector<string> vec=response["groups"].get<vector<string>>();
                                for(string &str:vec){
                                    json js=json::parse(str);
                                    Group group;
                                    group.setId(js["id"].get<int>());   
                                    group.setName(js["name"]);
                                    group.setDesc(js["desc"]);
                                    //记录群组的成员信息
                                    if(js.contains("users")){
                                        vector<string> vec2=js["users"].get<vector<string>>();
                                        for(string &userstr:vec2){
                                            json js2=json::parse(userstr);
                                            Groupuser groupuser;
                                            groupuser.setId(js2["id"].get<int>());
                                            groupuser.setName(js2["name"]);
                                            groupuser.setState(js2["state"]);
                                            groupuser.setRole(js2["role"]);
                                            group.getUsers().push_back(groupuser);
                                        }
                                    }
                                    g_currentUserGroupList.push_back(group);
                                }
                            }
                            //显示当前用户的基本信息
                            showCurrentUserData();

                            //显示当前用户的离线消息  个人聊天信息或者群组消息
                            if(response.contains("offlinemsg")){
                                vector<string> vec=response["offlinemsg"].get<vector<string>>();
                                for(string &str:vec){
                                    json js=json::parse(str);
                                    if(ONE_CHAT_MSG==js["msgid"].get<int>()){
                                        cout<<js["time"].get<string>()<<"["<<js["id"]<<"]"<<js["name"].get<string>()<<"said:"<<js["msg"].get<string>()<<endl;
                                    }
                                    else if(GROUP_CHAT_MSG==js["msgid"].get<int>()){
                                        cout<<js["time"].get<string>()<<"["<<js["id"]<<"]"<<js["name"].get<string>()<<"in group["<<js["groupid"].get<int>()<<"]said:"<<js["msg"].get<string>()<<endl;
                                    }
                                }
                            }
                            //登陆成功,启动接收线程负责接收数据 该线程只启动一次
                            static int threadnumber=0;
                            if(threadnumber==0){
                                thread readTask(readTaskHandler,clientfd);
                                readTask.detach();
                                threadnumber++;
                            }   
                            //进入主聊天页面
                            isMainMenuRunning=true;
                            mainMenu(clientfd);
                        }
                        else{
                            //登录失败
                            cerr<<response["errmsg"]<<endl;
                        }
                    }
                }
                break;
            }
            case 2:{//注册业务
                string name="";
                string password="";
                cout<<"username:";
                cin>>name;
                cout<<"userpassword:";
                cin>>password;
                //组织注册json数据
                json js;    
                js["msgid"]=REG_MSG;
                js["name"]=name;
                js["password"]=password;
                //发送注册数据
                string request=js.dump();
                int len=send(clientfd,request.c_str(),strlen(request.c_str())+1,0);
                if(len==-1){
                    cerr<<"send reg msg error:"<<request<<endl;
                }
                else{
                    //接收服务器响应数据
                    char buffer[1024]={0};
                    int len=recv(clientfd,buffer,1024,0);
                    if(len==-1){
                        cerr<<"recv reg response error!"<<endl; 
                    }
                    else{
                        //解析响应数据
                        json response=json::parse(buffer);
                        if(response["errno"].get<int>()==0){
                            //注册成功
                            cout<<"register success! userid is "<<response["id"]<<", please remember it!"<<endl;
                        }
                        else{
                            //注册失败
                            cerr<<"register failed! errno is "<<response["errno"].get<int>()<<endl;
                        }
                    }
                }
                break;
            }
            case 3:{//退出程序
                close(clientfd);
                isMainMenuRunning=false;
                break;
            }
            default:    
                cerr<<"invalid input!"<<endl;
                break;
        }
    }
    return 0;
}

//接收线程
void readTaskHandler(int clientfd){
    for(;;){
        char buffer[1024]={0};
        int len=recv(clientfd,buffer,1024,0);//阻塞了
        if(len==-1||len==0){
            close(clientfd);
            exit(-1);
        }
        //接收chatserver转发的数据,反序列化生成json数据对象
        json js=json::parse(buffer);
        int msgtype=js["msgid"].get<int>();
        if(ONE_CHAT_MSG==msgtype){
            cout<<js["time"].get<string>()<<"["<<js["id"]<<"]"<<js["name"].get<string>()<<"said:"<<js["msg"].get<string>()<<endl;
            continue;
        }
        else if(GROUP_CHAT_MSG==msgtype){
            cout<<js["time"].get<string>()<<"["<<js["id"]<<"]"<<js["name"].get<string>()<<"in group["<<js["groupid"].get<int>()<<"]said:"<<js["msg"].get<string>()<<endl;
            continue;
        }
        else if(LOGINOUT_MSG==msgtype){
            if(js["id"].get<int>()==g_currentUser.getId()){
                cout<<"you are loginout success!"<<endl;
                g_currentUser.setState("offline");
                continue;
            }
            else{
                cout<<js["name"].get<string>()<<"is loginout success!"<<endl;
                continue;
            }   
        }
        else{
            cout<<"recv unknown msgtype:"<<msgtype<<endl;
        }
    }   
}
//显示当前登录成功用户的基本信息
void showCurrentUserData(){
    cout<<"==================login user================="<<endl;
    cout<<"current login user => id:: "<<g_currentUser.getId()<<endl;
    cout<<"current login user => name:: "<<g_currentUser.getName()<<endl;
    cout<<"current login user => state:: "<<g_currentUser.getState()<<endl;
    cout<<"------------------friend list-----------------"<<endl;
    if(!g_currentUserFriendList.empty()){
        for(User &user:g_currentUserFriendList){
           cout<<user.getId()<<":"<<user.getName()<<":"<<user.getState()<<endl;
        }
    }
    else{
        cout<<"you have no friend!"<<endl;
    }
    cout<<"-----------------group list-----------------"<<endl;
    if(!g_currentUserGroupList.empty()){
        for(Group &group:g_currentUserGroupList){
            cout<<group.getId()<<":"<<group.getName()<<":"<<group.getDesc()<<endl;
            for(Groupuser &user:group.getUsers()){
               cout<<user.getId()<<":"<<user.getName()<<":"<<user.getState()<<":"<<user.getRole()<<endl;
            }
        }
    }
    else{
        cout<<"you have no group!"<<endl;
    }
    cout<<"================================================="<<endl;
}

//"help" commend handler
void help(int fd=0,string str="");
//"chat" commend handler
void chat(int,string);
//"addfrind" commend handler
void addfriend(int,string);
//"creategroup" commend handler
void creategroup(int,string);
//"addgroup" commend handler
void addgroup(int,string);
//"groupchat" commend handler
void groupchat(int,string);
//"loginout" commend handler
void loginout(int,string);

//系统支持的客户端命令列表
unordered_map<string,string>commandMap={
    {"help","显示所有支持的命令,格式help"},
    {"chat","一对一聊天,格式chat:friendid:message"},
    {"addfriend","添加好友,格式addfriend:friendid"},
    {"creategroup","创建群组,格式creategroup:groupname:groupdesc"},
    {"addgroup","加入群组,格式addgroup:groupid"},
    {"groupchat","群聊,格式groupchat:groupid:message"},
    {"loginout","注销,格式loginout"}
};

//注册系统支持的客户端命令处理
unordered_map<string,function<void(int,string)>> commandHandlerMap={
    {"help",help},
    {"chat",chat},
    {"addfriend",addfriend},
    {"creategroup",creategroup},
    {"addgroup",addgroup},
    {"groupchat",groupchat},
    {"loginout",loginout}
};

//主聊天页面程序
void mainMenu(int clientfd){
    help();

    char buffer[1024]={0};
    for(;;){
        cin.getline(buffer,1024);
        string commandbuf(buffer);
        string command;//存储命令
        int idx=commandbuf.find(":");
        if(idx==-1){
            command=commandbuf;
        }
        else{
            command=commandbuf.substr(0,idx);
        }
        auto it=commandHandlerMap.find(command);
        if(it==commandHandlerMap.end()){
            cerr<<"invalid input command!"<<endl;
            continue;
        }
        //调用相应命令的事件处理回调,mainmenu对修改封闭,添加新功能不需要修改该函数
        it->second(clientfd,commandbuf.substr(idx+1,commandbuf.size()-idx));//调用命令处理方法
        
    }
}

void help(int,string){
    cout<<"show command list>>>"<<endl;
    for(auto &p:commandMap){
        cout<<p.first<<":"<<p.second<<endl;
    }
    cout<<endl;
}

void addfriend(int clientfd,string str){
    int friendid=stoi(str);
    json js;
    js["msgid"]=ADD_FRIEND_MSG;
    js["id"]=g_currentUser.getId();
    js["friendid"]=friendid;
    string buffer=js.dump();
    int len=send(clientfd,buffer.c_str(),strlen(buffer.c_str())+1,0);
    if(len==-1){
        cerr<<"send addfriend msg error:"<<buffer<<endl;    
    }
}

void chat(int clientfd,string str){
    int idx=str.find(":");
    if(idx==-1){
        cerr<<"chat command format error!"<<endl;
        return;
    }
    int friendid=atoi(str.substr(0,idx).c_str());
    string message=str.substr(idx+1,str.size()-idx);
    json js;
    js["msgid"]=ONE_CHAT_MSG;
    js["id"]=g_currentUser.getId();
    js["name"]=g_currentUser.getName();
    js["toid"]=friendid;
    js["msg"]=message;
    js["time"]=getCurrentTime();
    string buffer=js.dump();
    int len=send(clientfd,buffer.c_str(),strlen(buffer.c_str())+1,0);
    if(len==-1){
        cerr<<"send chat msg error:"<<buffer<<endl;    
    }
}

void creategroup(int clientfd,string str){
    int idx=str.find(":");
    if(idx==-1){
        cerr<<"creategroup command format error!"<<endl;
        return;
    }
    string groupname=str.substr(0,idx);
    string groupdesc=str.substr(idx+1,str.size()-idx);
    json js;
    js["msgid"]=CREATE_GROUP_MSG;
    js["id"]=g_currentUser.getId();
    js["groupname"]=groupname;
    js["groupdesc"]=groupdesc;
    string buffer=js.dump();
    int len=send(clientfd,buffer.c_str(),strlen(buffer.c_str())+1,0);
    if(len==-1){
        cerr<<"send addgroup msg error:"<<buffer<<endl;    
    }
}
void addgroup(int clientfd,string str){
    int groupid=stoi(str.c_str());
    json js;
    js["msgid"]=ADD_GROUP_MSG;
    js["id"]=g_currentUser.getId();
    js["groupid"]=groupid;
    string buffer=js.dump();
    int len=send(clientfd,buffer.c_str(),strlen(buffer.c_str())+1,0);
    if(len==-1){
        cerr<<"send addgroup msg error:"<<buffer<<endl;    
    }
}

void groupchat(int clientfd,string str){
    int idx1=str.find(":");
    if(idx1==-1){
        cerr<<"groupchat command format error!"<<endl;
        return;
    }
    int groupid=stoi(str.substr(0,idx1));
    string message=str.substr(idx1+1,str.size()-idx1);
    json js;
    js["msgid"]=GROUP_CHAT_MSG;
    js["id"]=g_currentUser.getId();
    js["name"]=g_currentUser.getName();
    js["groupid"]=groupid;
    js["msg"]=message;
    js["time"]=getCurrentTime();
    string buffer=js.dump();
    int len=send(clientfd,buffer.c_str(),strlen(buffer.c_str())+1,0);
    if(len==-1){
        cerr<<"send groupchat msg error:"<<buffer<<endl;    
    }
}

void loginout(int clientfd,string str){
    json js;
    js["msgid"]=LOGINOUT_MSG;
    js["id"]=g_currentUser.getId();
    string buffer=js.dump();
    int len=send(clientfd,buffer.c_str(),strlen(buffer.c_str())+1,0);
    if(len==-1){
        cerr<<"send loginout msg error:"<<buffer<<endl;    
    }
    else{
        isMainMenuRunning=false;
    }
}

string getCurrentTime(){
    auto tt=chrono::system_clock::to_time_t(chrono::system_clock::now());
    struct tm *ptm=localtime(&tt);
    char date[60]={0};
    sprintf(date,"%d-%02d-%02d %02d:%02d:%02d",
        (1900+(int)ptm->tm_year),(1+(int)ptm->tm_mon),ptm->tm_mday,
        ptm->tm_hour,ptm->tm_min,ptm->tm_sec);
    return string(date);
}

为何要集群?为何还要引入负载均衡器?

为什么要做集群?(单机的致命问题)

如果聊天室只部署在一台服务器上,会遇到几个核心问题:

  1. 并发瓶颈:单台服务器的 CPU、内存、网络连接数有限(比如单机最多支撑 1 万并发连接),用户量超过阈值后,服务器会卡顿、崩溃;
  2. 单点故障:服务器宕机后,所有用户都无法使用聊天室,可用性为 0;
  3. 资源浪费:不同时段用户量波动大(比如晚上高峰、白天低峰),单机要么高峰扛不住,要么低峰闲置。

集群 就是把聊天室服务端部署在多台服务器(节点) 上(比如节点 A、B、C),理论上能支撑「单机并发数 × 节点数」的用户量,且一台节点宕机,其他节点还能工作。

二、集群的核心痛点:谁来分配用户连接?(负载均衡器的核心价值)

集群部署后,新的问题出现了:

  • 客户端该连接哪台节点?(总不能让用户手动选 "连接节点 A / 节点 B" 吧?)
  • 如何保证连接分配公平?(比如所有用户都连节点 A,节点 B/C 闲置,等于白做集群)
  • 如何处理节点故障?(节点 A 宕机后,新用户不能再连它)

这时候负载均衡器(Nginx) 就成了集群的 "总调度员",解决以上所有问题,核心作用可以总结为 4 点:

1. 流量分发:把用户连接均匀分配到集群节点

负载均衡器会对外暴露一个统一的入口地址 (比如 127.0.0.1:8000),所有客户端都连接这个地址,由负载均衡器决定把连接转发到哪个节点:

  • 比如用「轮询策略」:第 1 个用户连节点 A,第 2 个连节点 B,第 3 个连节点 C,第 4 个又回到节点 A;
  • 比如用「权重策略」:节点 A 配置高(8 核 16G),权重设为 2;节点 B/C 配置低(4 核 8G),权重设为 1,分配时 A 会收到两倍的连接数。

效果:避免单节点过载,所有集群节点的资源都能被充分利用,最大化集群的并发能力。

2. 屏蔽集群复杂度:对客户端透明

客户端只需要知道负载均衡器的地址,完全不用关心背后有多少个节点、节点的 IP / 端口是什么:

  • 比如节点 A 的 IP 是 192.168.1.100:6000,节点 B 是 192.168.1.101:6000,节点 C 是 192.168.1.102:6000;
  • 客户端只连 127.0.0.1:8000(负载均衡器),由负载均衡器转发到具体节点;
  • 后续新增 / 下线节点,客户端完全不用改代码,只需要调整负载均衡器的配置。

3. 故障自动剔除:提高集群可用性

负载均衡器会定期检测集群节点的健康状态(比如通过心跳包、端口检测):

  • 如果节点 A 宕机,负载均衡器会自动把它从 "可用节点列表" 中剔除,新的用户连接不会再转发到 A;
  • 等节点 A 恢复后,又会自动加回列表,继续接收连接;
  • 效果:避免用户连接到故障节点,保证聊天室的高可用(不会因为一台节点宕机导致服务不可用)。

4. 会话保持(可选但重要)

聊天室是长连接场景,需要保证「同一个用户的所有请求都落到同一个节点」(比如用户 1001 登录后,后续发的聊天消息也要到同一个节点,否则节点 B 不知道 1001 的连接状态):

  • 负载均衡器可以通过「IP 哈希」「Cookie」等策略实现会话保持;
  • 比如基于 IP 哈希:用户 1001 的 IP 哈希后指向节点 A,那么这个用户的所有连接都会转发到节点 A,保证状态一致性。

没有负载均衡器的集群会怎样?

如果直接让客户端连各个节点:

  • 客户端需要知道所有节点的 IP / 端口,配置复杂且易出错;
  • 节点负载不均(比如用户都连第一个节点),集群性能打折扣;
  • 节点宕机后,客户端无法感知,还会往故障节点发请求,导致服务异常;
  • 新增节点后,需要通知所有客户端更新配置,运维成本极高。

总结

  1. 负载均衡器是集群的「统一入口」,解决了 "客户端该连哪个节点" 的核心问题,对客户端屏蔽集群复杂度;
  2. 核心价值是流量均匀分发 (最大化集群性能)+ 故障自动剔除(提高可用性);
  3. 对于聊天室这种长连接、高并发的场景,负载均衡器是集群部署的 "必需品",而非 "可选品",它让集群真正具备 "高并发、高可用" 的能力。

服务器中间件:基于发布订阅的redis

1. 先看「坏设计」:为什么不能让服务器直连?

如果ChatServer 之间互相建立 TCP 连接,这种方式有致命问题:

  • 耦合度太高:新增 / 下线一台服务器,所有其他服务器都要改连接配置,运维爆炸;
  • 资源浪费 :6 台服务器要建 6×5=30 条连接,每台都要维护大量 socket,带宽和内存都被占满;
  • 扩展性差:服务器越多,连接数呈平方级增长,根本扛不住大规模集群。

所以这种 "全连接" 的方式绝对不能用,必须引入中间件解耦。


2. Redis 作为「消息队列中间件」的核心价值

Redis 在这里扮演的是 发布 - 订阅(Pub/Sub)消息队列 的角色,把集群通信从「服务器 - 服务器」变成「服务器 - 中间件 - 服务器」:

  • 解耦集群 :所有 ChatServer 只和 Redis 通信,不需要知道其他服务器的存在;
    • 新增节点:只需要让新节点连接 Redis,订阅自己关心的频道;
    • 下线节点:其他节点完全无感知,不会影响通信。
  • 节省资源:每台服务器只需要 1 条连接到 Redis,而不是和所有其他服务器建连接,带宽和内存压力骤减;
  • 异步通信:消息先存在 Redis 里,接收方不在线也不会丢失(配合离线消息表),避免同步阻塞。

3. 具体到聊天室业务:Redis 解决了什么问题?

跨节点聊天(用户 A 在节点 1,用户 B 在节点 2),核心场景就是:

  1. 跨节点消息转发
    • 节点 1 发现用户 B 不在自己的在线列表里,就把消息 发布(PUBLISH) 到 Redis 中对应用户 B 的频道;
    • 节点 2 订阅了用户 B 的频道,收到消息后,再转发给在线的用户 B。
  2. 在线状态共享 (可选)
    • 可以用 Redis 存储用户在线状态,让所有节点都能快速查询 "用户在哪台节点"。
  3. 离线消息缓存
    • 用户不在线时,消息先存在 Redis 或数据库,等用户登录后再推送。

4. 为什么选 Redis,而不是 Kafka/RabbitMQ?

  • 业务简单:聊天室并发量不算极高,不需要 Kafka 那样的高吞吐量;
  • 轻量易部署:Redis 安装配置简单,开箱即用,不像 RabbitMQ/Kafka 要维护复杂集群;
  • 功能足够:Pub/Sub 模式完全满足跨节点消息转发需求,同时还能做缓存、计数等其他业务;
  • 学习成本低:对于 C++ 项目,hiredis 客户端库轻量易用,集成成本远低于其他 MQ。

总结

在集群聊天室里,Redis 是用来做「集群间消息总线」的

  • 它把原本耦合的服务器间通信,变成了统一的发布 - 订阅模式;
  • 解决了跨节点聊天的核心问题,同时让系统更易扩展、更省资源;
  • 相比其他消息队列,它是最适合你这个项目的轻量选择。

nginx配置tcp服务器负载均衡

nginx配置tcp服务器负载均衡

redis订阅发布设计与封装

cpp 复制代码
#ifndef __REDIS_HPP__
#define __REDIS_HPP__

#include<hiredis/hiredis.h>
#include<thread>
#include<functional>
using namespace std;

class redis{
private:
    // hiredis同步上下文对象,负责publish消息
    redisContext *publish_context;

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

    // 回调操作,收到订阅的消息,给service层上报
    function<void(int, string)> _notify_message_handler;

public:
    redis();
    ~redis();

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

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

    //向redis指定的通道subscribe订阅消息
    bool subscribe(int channel);

    //向redis指定的通道unsubscribe取消订阅消息
    bool unsubscribe(int channel);

    //在独立线程中接收订阅通道中的消息
    void observer_channel_message();

    //初始化向业务层上报通道消息的回调对象
    void init_notify_handler(function<void(int, string)> fn);

};

#endif
cpp 复制代码
#include"redis.hpp"
#include<iostream>
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发布消息的上下文连接
    publish_context = redisConnect("127.0.0.1", 6379);
    if(publish_context == nullptr){
        cerr << "connect redis failed!" << endl;
        return false;
    }
    //负责subscribe订阅消息的上下文连接
    subscribe_context = redisConnect("127.0.0.1", 6379);
    if(subscribe_context == nullptr){
        cerr << "connect redis failed!" << endl;
        return false;
    }

    //在独立线程中,监听通道上的事件,有消息就给业务层上报
    thread t([&](){
        observer_channel_message();
    });
    t.detach(); 
    cout << "connect redis-server success!" << endl;
    return true;
}

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

//在redis指定的通道subscribe订阅消息
bool redis::subscribe(int channel){
    //subscribe命令会阻塞当前上下文,这里只做订阅通道,不接收通道消息
    //通道消息的接收专门在observer_channel_message函数中独立线程中运行
    //只负责发送命令,不阻塞接收redis server响应消息,否则和notify消息线程抢占响应资源
    if(REDIS_ERR == redisCommand(subscribe_context, "SUBSCRIBE %d", channel)){
        cerr << "subscribe command failed!" << endl;
        return false;
    }

    //redisBufferWrite可以循环发送缓冲区,直到缓冲区数据发送完毕(done被置为1)
    int done = 0;
    while(!done){
        if(REDIS_OK == redisBufferWrite(this->subscribe_context, &done)){
            cerr << "subscribe command failed!" << endl;
            return false;
        }
    }
    return true;
}

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

//在独立线程中接收订阅通道中的消息
void redis::observer_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(reply->element[1]->integer, reply->element[2]->str);
        }
        freeReplyObject(reply);
    }
    cerr << "observer_channel_message quit!" << endl;
}

void redis::init_notify_handler(function<void(int, string)> fn){
    this->_notify_message_handler = fn;
}

最后完善一下业务代码

cpp 复制代码
//从redis消息队列中拉取离线消息
void ChatService::handleredissubscribemessage(int userid,string msg){
    lock_guard<mutex> lock(_connMutex);
    auto it=_userConnMap.find(userid);
    if(it!=_userConnMap.end()){
        //转发消息
        it->second->send(msg);
        return;
    }
    //存储离线消息
    _offlineMsgModel.insert(userid,msg);
}
相关推荐
HAPPY酷2 小时前
Ubuntu 中如何启用 root 账户?—— 从 “su: 认证失败” 到成功切换 root 的完整指南
服务器·数据库·ubuntu
焦糖玛奇朵婷2 小时前
盲盒小程序一站式开发
java·大数据·服务器·前端·小程序
生活予甜2 小时前
2026AI智能体爆发,天翼云服务器成OpenClaw理想部署载体
运维·服务器
小生不才yz2 小时前
【Makefile 专家之路 | 基础篇】01. 万物起源:编译链接原理与 Makefile 的核心价值
linux
云飞云共享云桌面2 小时前
SolidWorks云电脑如何多人共享访问?
运维·服务器·人工智能·3d·自动化·云计算·电脑
PenguinLetsGo2 小时前
代码段的消失:页表异常清零引发的 ILL_ILLOPC 溯源
android·linux
AMoon丶2 小时前
C++基础-类、对象
java·linux·服务器·c语言·开发语言·jvm·c++
指尖在键盘上舞动2 小时前
Cannot find matching video player interface for ‘ffpyplayer‘.解决方案
linux·ubuntu·ffmpeg·psychopy·ffpyplayer
桌面运维家2 小时前
Linux/Windows终端密码设置:保护你的vDisk数据
linux·运维·服务器