聊天集群项目
文章目录
json
之前有一篇文章说过***Json***
CMake
之前有一篇文章说过CMake
muduo
文章链接
步骤
1.组合TcpServer对象
2.创建EventLoop事件循环对象的指针
3.明确TcpServer构造函数需要什么参数,输出ChatServer的构造函数
4.在当前服务器类的构造函数当中,注册处理连接的回调函数和处理读写时间的回调函数
5.设置合适的服务端线程数量,muduo库会自己分配I/O线程和worker线程
muduo的网络设计:
reactors in threads - one loop per thread
方案的特点是one loop per thread,有一个main reactor负载accept连接,然后把连接分发到某个subreactor(采用round-robin的方式来选择sub reactor),该连接的所用操作都在那个sub reactor所处
的线程中完成。多个连接可能被分派到多个线程中,以充分利用CPU。
Reactor poll的大小是固定的,根据CPU的数目确定。
一个Base IO thread负责accept新的连接,接收到新的连接以后,使用轮询的方式在reactor pool中找到合适的sub reactor将这个连接挂载上去,这个连接上的所有任务都在这个sub reactor上完成。
如果有过多的耗费CPU I/O的计算任务,可以提交到创建的ThreadPool线程池中专门处理耗时的计算任务。
数据库设计
表设计
User表 在线不在线
Friend表 好友关系
AllGroup表 群组描述
GroupUser表 管理员和普通用户(一个人可以属于多个群,一个群可以有多个人,user和group之间就是多对多的关系,所以groupuser事个中间表,群ID,用户ID 用户是属于那个群的)
OffLineMessage 离线信息
业务问题(解耦)
目录说明
- bin 可执行文件
- build CMakeList文件编译文件
- include 头文件
- include/server 客户端和服务端公用的公共文件
- include/server/model 映射类
- src/client 聊天客户端代码
- src/server 聊天服务器代码
- thirtyParty 第三方库
ORM(把业务层和数据库层剥离)
什么是ORM
对象关系映射(Object Relational Mapping,简称ORM)是通过使用描述对象和数据库之间映射的元数据,将面向对象语言程序中的对象自动持久化到关系数据库中。本质上就是将数据从一种形式转换到另外一种形式。 这也同时暗示着额外的执行开销;然而,如果ORM作为一种中间件实现,则会有很多机会做优化,而这些在手写的持久层并不存在(原引自百度解释)。一句话解释就是:你不用写sql命令,用对象的方式操作数据库。
为什么要使用这个呢?1、随着面向对象的软件开发而产生,在业务实体的内存表现为对象,在数据库中表现为关系数据。 2、内存中的对象有关联和继承关系,关系数据库无法表达这种关系,因此ORM系统就是以这种中间件的形式存在,表现为对象到关系数据库的映射。
问题
这里不想用switch case 或者 if else 这些东西去解析msgid,是注册的话就调用注册的方法,这样不好,这样就让网络模块的代码和业务模块的代码强耦合在一起了,直接调用服务层的方法,这样并没有解耦。
方法
通过msgid获取业务处理器,这个处理器事先绑定一个网络的方法来接偶网络模块的代码和业务模块的代码。
利用oop回调的思想,使用一种面向接口的编程,也即是抽象基类,也就是回调函数。
业务设计的核心,网络模块和业务模块解耦的核心(绑定器和回调)(OOP)就是把事件什么时候发生,以及发生之后干什么不在一起处理
// 处理消息的事件回调方法类型
chatservice.cpp
// 注册消息以及对应的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));
}
}
/*
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, // 群聊天
};
注册业务
usermodel数据操作类对象,只用model类不做相关数据库操作,数据库操作都封装到model类里面了,而且model给业务层提供的都是对象User,不是数据库的字段。
当服务器接收json字符串的时候,把name还有pwd通过json反序列化解析出来,得到注册的信息,然后写入User表里面,注册成功json就回应客户端REG_MSG_ACK–0,并把id返回。注册失败REG_MSG_ACK–1
登陆业务
当服务器接收到 json 字符串的时候,对其进行反序列化,得到用户传递过来的账号和密码信息。
首先就是检测这个账号和密码是否与服务器中的数据匹配,如果不匹配就把errno设置为1并返回 id or password error的错误信息。
如果匹配,就检测当前用户是否在线,因为有在别的设备登录的状况,如果在线就把errno设置为2,返回 id is online的错误信息
如果用户不在线,这个时候用户就是登陆成功了,这个时候服务器就把该用户的好友列表,群组列表以及离线消息都推送给该用户,读取完离线消息后,把离线消息删除。
连接(存储用户在线的通信连接)
1向2发消息 发送方 接收方 信息
// 登录成功,记录用户连接信息
{
lock_guard<mutex> lock(_connMutex);
_userConnMap.insert({id, conn});
}
异常退出
// 存储在线用户的通信连接
unordered_map<int, TcpConnectionPtr> _userConnMap;
遍历通信连接,然后从map表删除用户连接信息,更新用户状态信息
通过ctrl-c 强制退出 依旧是online
// 处理服务器ctrl+c结束后,重置user的状态信息
void resetHandler(int)
{
ChatService::instance()->reset();
exit(0);
}
// 服务器异常,业务重置方法
void ChatService::reset()
{
// 把online状态的用户,设置成offline
_userModel.resetState();
}
// 重置用户的状态信息
void UserModel::resetState()
{
// 1.组装sql语句
char sql[1024] = "update user set state = 'offline' where state = 'online'";
MySQL mysql;
if (mysql.connect())
{
mysql.update(sql);
}
}
一对一聊天业务
去userconmap里面去找toid在线与否。
如果toid在线,转发js.dump(),服务器主动推送消息给toid。
如果toid不在线,
添加好友业务
服务器得到反序列化的信息,然后将这个信息写入Friend表中即可。
返回好友列表,通过联合查询
sprintf(sql, "select a.id,a.name,a.state from user a inner join friend b on b.friendid = a.id where b.userid=%d", userid);
在登陆的时候查询用户的好友信息并返回
// 查询该用户的好友信息并返回
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;
}
创建群组业务
服务器接收到客户端的信息,把群组的信息写入到AllGroup表中,并将创建者的信息写入到GroupUser中,设置创建者为creator
加入群业务
服务器接收到客户端的信息,将用户数据写入到GroupUser表中,并将role角色设置为normal。
查询群组业务
1. 先根据userid在groupuser表中查询出该用户所属的群组信息
2. 在根据群组信息,查询属于该群组的所有用户的userid,并且和user表进行多表联合查询,查出用户的详细信息
群聊业务
服务器接收到客户端的信息,先去GroupUser查询到所有群员的id,然后一个个去本服务器的user_connection_map_接受信息的用户是否在本服务器在线,在线的话直接转发即可,不在线的话,看看数据库里面的信息是否是在线,如果在线,那么就是接收用户在其他服务器登录,将消息通过redis中间件转发即可。
如果均不在线,转储离线消息即可。
nginx
为什么要nginx
32位机器大概能支持2w并发量,如果要支持3-4w,提高并发量,就要采用集群或者分布式部署。
负载均衡器
引入三台聊天服务器,每一台服务器都会给用户提高服务,但是用户不可能自己去选链接那台服务器,客户不知道那台服务器繁忙或者不繁忙,所以在集群环境中我们肯定引入一个设备叫做负载均衡器,也叫做反向代理设备,统一接受客户端请求,然后根据配置的负载算法,把客户端的请求分发到我们的业务服务器上。
- 1能够承载客户端的请求以及链接服务器以及服务器的相应,得有高性能的网络设备,把client的请求俺找负载均衡算法分发到具体的业务服务器ChatServer上面
- 2可配置的多种负载算法,能够和ChatServer保持心跳机制,监测ChatServer故障
- 3不影响能够发现新添加的ChatServer设备,方便扩展服务器数量
长连接
聊天服务器属于长连接的业务,长连接的意思就是客户端和服务器通信以后,就一直保持通信,我们不仅要作为聊天的发起者还要作为聊天的接收者,不管是个人对个人聊天还是群组对我们个人聊天,服务器都会主动推消息到我们客户端上面。而如果我们是短链接,每发一次消息,就关掉了,服务器就无法给我们推送消息。
nginx LVS(。。)
可以直接把并发量提高到5-6w

Redis
为什么要用redis
- client1跟client2说话如果再client1的userConnMap找到client2通信用的connection,直接转发就可以了,就说明他们俩在一台服务器上登陆。如果没有找到,并不能说明client2是离线的,因为此使clinet2可能再chatServer2里面,所以得去数据库里面去查看如果是online就说明再其他服务器上登录,当然如果是offline就说明不在线。
- chat1得把client1的消息发到chat2上,然后chat2发给client2,但这样实现太复杂了

- 1.这样设计各个业务之间全是强耦合了
- 2.业务设计比较复杂
- 3.异常发生之后,难以发现
上面的设计,让各个ChatServer服务器互相之间直接建立TCP连接进行通信,相当于在服务器网络之间进行广播。这样的设计使得各个服务器之间耦合度太高,不利于系统扩展,并且会占用系统大量的socket资源,各服务器之间的带宽压力很大,不能够节省资源给更多的客户端提供服务,因此绝对不是一个好的设计。
引入中间件
集群部署的服务器之间进行通信,最好的方式就是引入中间件消息队列,解耦各个服务器,使整个系统松耦合,提高服务器的响应能力,节省服务器的带宽资源,如下图所示:

在集群分布式环境中,经常使用的中间件消息队列有ActiveMQ、RabbitMQ、Kafka等,都是应用场景广泛并且性能很好的消息队列,供集群服务器之间,分布式服务之间进行消息通信。限于我们的项目业务类型并不是非常复杂,对并发请求量也没有太高的要求,因此我们的中间件消息队列选型的是-基于发布-订阅模式的redis。
基于发布-订阅的消息队列redis

观察者模式,消息队列是观察者,观察事件的发生,把对该事件感兴趣的事情推给监听者(chat1,2,3),你要我给你推送消息,你事先得向我注册你对那些消息感兴趣。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)