网络聊天服务器项目,该项目分为4个模块:
- 首先是网络模块:我使用了muduo高性能网络库,解耦合网络与业务之间这两部分代码,可以更加专注与业务的功能开发
- 其次是服务层模块:我使用了基于C++11的技术比如绑定器和map,绑定器实现了用户发送的消息id类型回调业务功能函数的机制(当网络i/o发送消息请求后,通过解析json消息获取消息id 回调业务功能函数)
- 然后是数据存储模块:我使用了MySQL数据库存储关键的信息(用户账户、离线消息列表、群组信息、好友列表等)由客户端的功能请求进行增删改查
以上是单机服务器的模块设计,但是单机服务器的并发数量是有限的,所以我采用了集群服务器经行并发能力扩展
4.最后在多台服务器部署,基于Tcp 协议搭建的C/S 通信,所以我使用了Nginx Tcp负载均衡,保持长连接状态,并且引入Redis 的发布订阅功能实现跨服务器间通信
1. 客户端:
1.1. 两个模块:
1.1.1. 负责用户登录、注册、退出功能
作为客户端连接服务器成功后的首界面展示:提供以上三种服务
客户端设计模式:主线线程专注于信息的发送、子线程负责接受信息
将具体功能的所需参数进行json(key-value键值对存储文本形式)序列化发送给服务器
个别服务需要服务器返回方法返回值状态信息,(登录成功、信息注册成功)?多线程协调实现
由主线程发送后,阻塞获取信号量,子线程此时接受服务器返回的json对象,由json中的服务方法枚举id,分类细化具体的功能实现业务逻辑,将服务方法调用的状态写入最后将信号量释放,主函数则可以通过子线程写入的状态进行下一步工作
登陆成功后进入功能菜单:
1.1.2. 负责聊天服务器业务功能:添加好友信息、创建群组、加入群组、单人聊天、群组聊天
将功能函数处理为map表形式<存储函数方法名称,方法绑定器>,循环处理客户端的输入的方法请求,在循环中未使用switch_case而是map+bind回调,适应于程序开发中的开闭原则,对修改关闭,对开发扩展开放;
该部分函数主要处理用户的功能以及输入的参数,封装为json 对象(服务器协商的通信规则:方法枚举id的标识,与方法请求一致的参数列表)发送给服务器
auto it = commandHandlerMap.find(command);
if (it == commandHandlerMap.end())
//error// 调用相应命令的事件处理回调,mainMenu对修改封闭,添加新功能不需要修改该函数
it->second(clientfd, commandbuf.substr(idx + 1, commandbuf.size() - idx));
// 调用命令处理方法
2. 服务器:
2.1. 服务器需要处理信息转发、功能性数据的存储、查询、更新服务
解决基于数据库的功能服务问题:MySQL
解决高并发场景下数据收发问题:muduo 静态网络库
解决跨服务器通信问题 :Redis发布订阅问题
解决负载均衡问题:Nginx_基于Tcp长连接的负载均衡问题
2.2. 主要基于以上内容将服务器划分出:
server、service、db、model、redis、public 六个模块
2.2.1. server——muduo网络:
基于epoll+多线程的机制实现高并发需求
在该模块中设置四个工作线程,其中一个处理连接回调,在连接异常的场景下关闭连接,余下三个线程为工作线程,完成将接收的字符基于json-parse反序列化发送给service,基于服务方法id,回调具体的处理函数
2.2.2. db:
数据库的接口API ,创建初始化、数据库连接、
数据更新、数据查询query
// 初始化数据库连接_conn = mysql_init(nullptr);// 释放数据库连接资源
if (_conn != nullptr)mysql_close(_conn);// 连接数据库MYSQL *p = mysql_real_connect(_conn, server.c_str(), user.c_str(),password.c_str(), dbname.c_str(), 3306, nullptr, 0);// 更新操作
mysql_query(_conn, sql.c_str())// 查询操作
mysql_query(_conn, sql.c_str())return mysql_use_result(_conn);
2.2.3. model:
该模块用来实现用户需求与服务器数据库交互增、删、改、查问题
以举例:
// 添加好友关系,在好友数据表中添加一条记录
sprintf(sql, "insert into friend values(%d, %d)", userid, friendid);// 返回用户好友列表,联合查询用户表和好友表,将结果插入vector并用返回
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);// 删除用户的离线消息,删除离线列表的一条信息
sprintf(sql, "delete from offlinemessage where userid=%d", userid);// 更新用户的状态信息
sprintf(sql, "update user set state = '%s' where id = %d",/user.getState().c_str(), user.getId());
2.2.4. redis:
基于Redis发布订阅问题,解决跨服务器通信问题:
初始化发布上下文、订阅上下文,进行连接、向指定chennel 发布、订阅,结束订阅、断开连接
2.2.5. service:
设置方法map集合<方法id,方法handler绑定器>
设置用户连接map集合<用户id,连接TcpConnectionPtr接口>,将所有方法的枚举值,函数方法绑定器插入map表,连接Redis服务器、MySQL 数据库
基于具体的方法选择,结合底层存储的用户数据(查询、插入、删除)完成登录、注册、注销用户;
结合model模块下的函数具体结合用户需求完善功能函数;
聊天服务:查询用户的连接信息,在本服务器时转发聊天信息,不在本台服务器,检查数据库的用户登陆状态,未登录存储离线信息表,登录则发布到Redis的发布队列
登录问题:(最复杂)
从json对象解析得到登录id、密码
在用户信息表中查询该id的所属对象并比对密码,不一致将封装json返回原因;
一致时检查登陆状态避免重复登陆,继而更新用户的登陆状态;
在用户登录map中插入其连接信息;
订阅Redis对该id 为chennel的消息;
打印该用户的离线消息;
将该对象的好友信息封装为vector存储的json序列化文本信息,作为返回的json-response对象key-value 存储的对象之一;群组信息如上;
最后返回最后的json-response序列化内容。
2.2.6. public:
保存服务方法的枚举信息,将每一服务名称与id一一对应