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中间件转发即可。
如果均不在线,转储离线消息即可。
|