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

谁能懂一下这个项目恶心的架构 好吧 看着很复杂 其实我们只用关心这两个部分
架构
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);
}
为何要集群?为何还要引入负载均衡器?
为什么要做集群?(单机的致命问题)
如果聊天室只部署在一台服务器上,会遇到几个核心问题:
- 并发瓶颈:单台服务器的 CPU、内存、网络连接数有限(比如单机最多支撑 1 万并发连接),用户量超过阈值后,服务器会卡顿、崩溃;
- 单点故障:服务器宕机后,所有用户都无法使用聊天室,可用性为 0;
- 资源浪费:不同时段用户量波动大(比如晚上高峰、白天低峰),单机要么高峰扛不住,要么低峰闲置。
而集群 就是把聊天室服务端部署在多台服务器(节点) 上(比如节点 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 / 端口,配置复杂且易出错;
- 节点负载不均(比如用户都连第一个节点),集群性能打折扣;
- 节点宕机后,客户端无法感知,还会往故障节点发请求,导致服务异常;
- 新增节点后,需要通知所有客户端更新配置,运维成本极高。
总结
- 负载均衡器是集群的「统一入口」,解决了 "客户端该连哪个节点" 的核心问题,对客户端屏蔽集群复杂度;
- 核心价值是流量均匀分发 (最大化集群性能)+ 故障自动剔除(提高可用性);
- 对于聊天室这种长连接、高并发的场景,负载均衡器是集群部署的 "必需品",而非 "可选品",它让集群真正具备 "高并发、高可用" 的能力。
服务器中间件:基于发布订阅的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 发现用户 B 不在自己的在线列表里,就把消息 发布(PUBLISH) 到 Redis 中对应用户 B 的频道;
- 节点 2 订阅了用户 B 的频道,收到消息后,再转发给在线的用户 B。
- 在线状态共享 (可选)
- 可以用 Redis 存储用户在线状态,让所有节点都能快速查询 "用户在哪台节点"。
- 离线消息缓存
- 用户不在线时,消息先存在 Redis 或数据库,等用户登录后再推送。
4. 为什么选 Redis,而不是 Kafka/RabbitMQ?
- 业务简单:聊天室并发量不算极高,不需要 Kafka 那样的高吞吐量;
- 轻量易部署:Redis 安装配置简单,开箱即用,不像 RabbitMQ/Kafka 要维护复杂集群;
- 功能足够:Pub/Sub 模式完全满足跨节点消息转发需求,同时还能做缓存、计数等其他业务;
- 学习成本低:对于 C++ 项目,hiredis 客户端库轻量易用,集成成本远低于其他 MQ。
总结
在集群聊天室里,Redis 是用来做「集群间消息总线」的:
- 它把原本耦合的服务器间通信,变成了统一的发布 - 订阅模式;
- 解决了跨节点聊天的核心问题,同时让系统更易扩展、更省资源;
- 相比其他消息队列,它是最适合你这个项目的轻量选择。
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);
}