相关介绍
技术栈
- Json序列化和反序列化
- muduo网络库开发
- nginx源码编译安装和环境配置
- redis缓存服务器编程实践
- 基于发布-订阅的服务器中间件redis消息队列编程实践
- MySQL数据库编程
- CMake构建编译环境
- Github托管项目
项目需求
1. 客户端新用户注册
2. 客户端用户登录
3. 添加好友和添加群组
4. 好友聊天
5. 群组聊天
6. 离线消息
7. nginx配置tcp负载均衡
8. 集群聊天系统支持客户端跨服务器通信
项目目标
1. 掌握服务器的网络I/O模块,业务模块,数据模块分层的设计思想。
2. 掌握C++ muduo网络库的编程以及实现原理
3. 掌握Json的编程应用
4. 掌握nginx配置部署tcp负载均衡器的应用以及原理
5. 掌握服务器中间件的应用场景和基于发布-订阅的redis编程实践以及应用原理
6. 掌握CMake构建自动化编译环境
7. 掌握Github管理项目
开发环境
ubuntu+Json+boost+muduo+redis+mysql+nginx+cmake
项目内容
网络模块直接使用了moduo库基于reactor模型(基于事件驱动、IO复用,epoll加线程池的网络模型。),有一个主reactor是IO线程,三个sub reactor是工作线程,主reactor主要负责新用户的连接,子reactor负责连接用户的读写事件的处理。
public.hpp
#ifndef PUBLIC_H
#define PUBLIC_H
/*
server和client的公共文件
*/
enum EnMsgType
{
LOGIN_MSG = 1, // 登录消息
LOGIN_MSG_ACK, // 登录响应消息
LOGINOUT_MSG, // 注销消息
REG_MSG, // 注册消息
REG_MSG_ACK, // 注册响应消息
ONE_CHAT_MSG, // 聊天消息
ADD_FRIEND_MSG, // 添加好友消息
CREATE_GROUP_MSG, // 创建群组
ADD_GROUP_MSG, // 加入群组
GROUP_CHAT_MSG, // 群聊天
};
#endif
网络模块和业务模块解耦,通过绑定器和回调函数。大致思路如下
// 表示处理消息的事件回调方法类型(function表示存储函数的通用容器)
using MsgHandler = std::function<void(const TcpConnectionPtr &conn, json &js, Timestamp)>;
//用于将消息映射到对应的处理函数
unordered_map<int, MsgHandler> _msgHandlerMap;
//std::bind 是一个函数适配器,它将成员函数ChatService::login绑定到特定的对象this上,同时为这个函数预留占位符_1、_2、_3,表示这个函数可以接受三个参数。
//&ChatService::login:这是一个指向ChatService类的login成员函数的指针。login函数的签名应该是一个能够接受三个参数的成员函数。
//将ChatService类的login函数与LOGIN_MSG消息类型关联起来,并将这个关联存储在消息处理映射表_msgHandlerMap中。之后,当LOGIN_MSG类型的消息到达时,系统可以通过查找_msgHandlerMap来调用login函数并传递相应的参数。
_msgHandlerMap.insert({LOGIN_MSG, std::bind(&ChatService::login, this, _1, _2, _3)});
获取消息对应的处理器
// 获取消息对应的处理器
MsgHandler getHandler(int msgid);
MsgHandler ChatService::getHandler(int msgid)
{
// 记录错误日志,msgid没有对应的事件处理回调
auto it = _msgHandlerMap.find(msgid);
if (it == _msgHandlerMap.end())
{
//使用moduo库的日志
// 返回一个默认的处理器,空操作
return [=](const TcpConnectionPtr &conn, json &js, Timestamp) {
LOG_ERROR << "msgid:" << msgid << " can not find handler!";
};
}
else
{
return _msgHandlerMap[msgid];
}
}
//lambda表达式的捕获列表,[=]表示捕获外部作用域中的所有变量(包括msgid)并以值传递的方式在lambda表达式中使用。
chatserver.hpp
#ifndef CHATSERVER_H
#define CHATSERVER_H
#include <muduo/net/TcpServer.h>
#include <muduo/net/EventLoop.h>
using namespace muduo;
using namespace muduo::net;
// 聊天服务器的主类
class ChatServer
{
public:
// 初始化聊天服务器对象
ChatServer(EventLoop *loop,
const InetAddress &listenAddr,
const string &nameArg);
// 启动服务
void start();
private:
// 上报链接相关信息的回调函数
void onConnection(const TcpConnectionPtr &);
// 上报读写事件相关信息的回调函数
void onMessage(const TcpConnectionPtr &,
Buffer *,
Timestamp);
TcpServer _server; // 组合的muduo库,实现服务器功能的类对象
EventLoop *_loop; // 指向事件循环对象的指针
};
#endif
chatserver.cpp
chatservice.hpp
#ifndef CHATSERVICE_H
#define CHATSERVICE_H
#include <muduo/net/TcpConnection.h>
#include <unordered_map>
#include <functional>
#include <mutex>
using namespace std;
using namespace muduo;
using namespace muduo::net;
#include "redis.hpp"
#include "groupmodel.hpp"
#include "friendmodel.hpp"
#include "usermodel.hpp"
#include "offlinemessagemodel.hpp"
#include "json.hpp"
using json = nlohmann::json;
// 表示处理消息的事件回调方法类型
using MsgHandler = std::function<void(const TcpConnectionPtr &conn, json &js, Timestamp)>;
// 聊天服务器业务类
class ChatService
{
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 reset();
// 获取消息对应的处理器
MsgHandler getHandler(int msgid);
// 从redis消息队列中获取订阅的消息
void handleRedisSubscribeMessage(int, string);
private:
ChatService();
//一个消息id映射一个事件处理
// 存储消息id和其对应的业务处理方法
unordered_map<int, MsgHandler> _msgHandlerMap;
// 存储在线用户的通信连接
unordered_map<int, TcpConnectionPtr> _userConnMap;
// 定义互斥锁,保证_userConnMap的线程安全
mutex _connMutex;
// 数据操作类对象
UserModel _userModel;
OfflineMsgModel _offlineMsgModel;
FriendModel _friendModel;
GroupModel _groupModel;
// redis操作对象
Redis _redis;
};
#endif
chatservice.cpp
#include "chatservice.hpp"
#include "public.hpp"
#include <muduo/base/Logging.h>//muduo库的日志
#include <vector>
using namespace std;
using namespace muduo;
// 获取单例对象的接口函数
ChatService* ChatService::instance()
{
static ChatService service;
return &service;
}
// 注册消息以及对应的Handler回调操作
ChatService::ChatService()
{
// 用户基本业务管理相关事件处理回调注册
_msgHandlerMap.insert({LOGIN_MSG, std::bind(&ChatService::login, this, _1, _2, _3)});
_msgHandlerMap.insert({LOGINOUT_MSG, std::bind(&ChatService::loginout, this, _1, _2, _3)});
_msgHandlerMap.insert({REG_MSG, std::bind(&ChatService::reg, this, _1, _2, _3)});
_msgHandlerMap.insert({ONE_CHAT_MSG, std::bind(&ChatService::oneChat, this, _1, _2, _3)});
_msgHandlerMap.insert({ADD_FRIEND_MSG, std::bind(&ChatService::addFriend, this, _1, _2, _3)});
// 群组业务管理相关事件处理回调注册
_msgHandlerMap.insert({CREATE_GROUP_MSG, std::bind(&ChatService::createGroup, this, _1, _2, _3)});
_msgHandlerMap.insert({ADD_GROUP_MSG, std::bind(&ChatService::addGroup, this, _1, _2, _3)});
_msgHandlerMap.insert({GROUP_CHAT_MSG, std::bind(&ChatService::groupChat, this, _1, _2, _3)});
// 连接redis服务器
if (_redis.connect())
{
// 设置上报消息的回调
_redis.init_notify_handler(std::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())
{
//使用moduo库的日志
// 返回一个默认的处理器,空操作
return [=](const TcpConnectionPtr &conn, json &js, Timestamp) {
LOG_ERROR << "msgid:" << msgid << " can not find handler!";
};
}
else
{
return _msgHandlerMap[msgid];
}
}
// 处理登录业务 id pwd pwd
void ChatService::login(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
int id = js["id"].get<int>();
string pwd = js["password"];
User user = _userModel.query(id);
if (user.getId() == id && user.getPwd() == pwd)
{
if (user.getState() == "online")
{
// 该用户已经登录,不允许重复登录
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 2;
response["errmsg"] = "this account is using, input another!";
conn->send(response.dump());
}
else
{
// 登录成功,记录用户连接信息
{
lock_guard<mutex> lock(_connMutex);
_userConnMap.insert({id, conn});
}
// id用户登录成功后,向redis订阅channel(id)
_redis.subscribe(id);
// 登录成功,更新用户状态信息 state offline=>online
user.setState("online");
_userModel.updateState(user);
json response;
response["msgid"] = LOGIN_MSG_ACK;
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> userVec = _friendModel.query(id);
if (!userVec.empty())
{
vector<string> vec2;
for (User &user : userVec)
{
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())
{
// group:[{groupid:[xxx, xxx, xxx, xxx]}]
vector<string> groupV;
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 js;
js["id"] = user.getId();
js["name"] = user.getName();
js["state"] = user.getState();
js["role"] = user.getRole();
userV.push_back(js.dump());
}
grpjson["users"] = userV;
groupV.push_back(grpjson.dump());
}
response["groups"] = groupV;
}
conn->send(response.dump());
}
}
else
{
// 该用户不存在,用户存在但是密码错误,登录失败
json response;
response["msgid"] = LOGIN_MSG_ACK;
response["errno"] = 1;
response["errmsg"] = "id or password is invalid!";
conn->send(response.dump());
}
}
// 处理注册业务 name password
void ChatService::reg(const TcpConnectionPtr &conn, json &js, Timestamp time)
{
string name = js["name"];
string pwd = js["password"];
User user;
user.setName(name);
user.setPwd(pwd);
bool state = _userModel.insert(user);
if (state)
{
// 注册成功
json response;
response["msgid"] = REG_MSG_ACK;
response["errno"] = 0;
response["id"] = user.getId();
conn->send(response.dump());
}
else
{
// 注册失败
json response;
response["msgid"] = REG_MSG_ACK;
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())
{
_userConnMap.erase(it);
}
}
// 用户注销,相当于就是下线,在redis中取消订阅通道
_redis.unsubscribe(userid);
// 更新用户的状态信息
User user(userid, "", "", "offline");
_userModel.updateState(user);
}
// 处理客户端异常退出
void ChatService::clientCloseException(const TcpConnectionPtr &conn)
{
User