项目描述
这是一个基于C/S架构实现的聊天系统,可以进行 用户注册,用户登录,用户聊天,群组聊天,添加好友,创建群组,加入群组,用户退出等功能。通过数据模块 业务模块 网络模块对整体的项目框架进行解耦,数据通信格式为Json格式。服务器使用集群式服务,负载均衡器使用Nginx tcp长连接负载均衡算法,服务器之间使用基于发布-订阅模式的redis消息中间件进行通信。
项目需求
1. 客户端新用户注册2. 客户端用户登录3. 添加好友和添加群组4. 好友聊天5. 群组聊天6. 离线消息7. nginx配置tcp负载均衡8. 集群聊天系统支持客户端跨服务器通信
开发环境
操作系统:Centos7.5
数据库:MySQL
编译器:g++
Json:JSON for Modern C++
模块划分
网络模块
服务端使用muduo网络库
muduo的网络设计
是one loop per thread,有一个main reactor负载accept连接,然后把连接分发到某个subreactor(采用round-robin的方式来选择sub reactor),该连接的所用操作都在那个sub reactor所处的线程中完成。多个连接可能被分派到多个线程中,以充分利用CPU。
1. 事件驱动(event handling)2. 可以处理一个或多个输入源(one or more inputs)3. 通过Service Handler同步的将输入事件(Event)采用多路复用分发给相应的RequestHandler(多个)处理
业务模块
1.登录业务 msgid:id:passwd
通过接收到的id在数据库中查询是否有id
没有 直接退出
若有 连同pwd返回 是否是在线在线 报错退出
不在线进行密码匹配 匹配成功ok
推送离线消息 好友列表 群组列表
将数据库状态设置为在线
向redis中订阅channal
2.注册业务 msgid:name:passwd
将name passwd直接进行用户表的插入 通过返回的id将id号返回
3.注销业务 msgid
在连接map中删除,redis取消订阅 在user表状态设置为离线
4.聊天 msgid:id:name:toid:message:time
在连接map中查找toid-conn 有 进行消息转发
没有 在数据库中查询toid状态 离线进行离线消息转发
在线 向redis消息队列中发布消息
5.添加好友 msgid:id:friendid
在数据库中查找friendid,没有退出
有,将id--friendid与 friendid---id 写入数据库中
6.创建群组 msgid:id:groupname:groupdesc
在allgroup数据库中插入groupname-groupdesc字段,并且将allgroup表中生成的groupid返回
在groupuser表中插入id--groupid--'creator'
创建群组成功后,该用户就是该群组的创建者
7.加入群组 msgid:id:groupid
在groupuser表中插入id--groupid--'normal'就可以了
实际上加入群组这只是一个业务,对于实际操作中肯定是先进行查询群组操作,然后加入群组
8.群组聊天 msgid:id:groupid:msg
先使用groupid在groupuser中查出除了id外所有在groupid中的用户
使用vector<int>保存用户id
然后通过_idConnMap中查询本地在线用户是否有id 有就转发消息
没有然后去查询user表中的用户state是否在线,
在线那就向redis消息队列中发布userid--js 让订阅userid的主机会去获取订阅消息,
不在线就会进行离线消息存储
9.从redis消息队列中获取订阅的消息
订阅userid的主机会去获取订阅消息,判断此时用户是否在线,
在线其进行转发,不在线就进行离线消息存储
数据模块
所有的数据表
用户表
id--用户账号,自动生成的,唯一主键
name---用户名,名字唯一
password----用户密码
state----用户状态:在线 离线
好友表
userid---本人账号
friendid-----好友账号
所有群组表
id--群组号,自动生成,唯一主键
groupname---群组名,不允许重复
groupdesc---群组描述
群组与用户表
记录加入群组的用户及该群组的表
一个群可以有多个用户,一个用户也可以有多个群,这是一个多对多的关系
groupid---群组号
userid---用户号
grouprole---用户角色
离线消息表
每条数据对应的是某个用户的一条离线消息
userid----用户名
message---离线消息
服务器集群
nginx.conf文件,配置tcp长连接负载均衡
跨服务器通信
由于不同客户端的连接可能在不同的主机上,这就会牵扯到一个问题,在不同服务器上的用户之间如何进行通信呢?也就是说服务器与服务器之间如何进行通信呢?
1.服务器与服务器之间两两在通过tcp连接通信?
这里引入了消息中间件,基于发布-订阅模式的redis消息队列
项目中的问题
1.添加好友业务
添加完好友后,对方却没有显示我为对方的好友
原因:在进行数据库插入操作时只进行了id--friendid,没有插入friendid---id操作,这应该是双向的
2.离线消息存储
当一个用户有多条离线消息时,登陆成功后只显示一条消息
原因:在设计offlinemessage数据表时对id字段设置为了主键,导致表中不能有重复id,这样使得每个用户只能有一条离线数据。解决也很简单将id字段的主键属性去掉,使得id可以重复。
3.向消息中间件PUBLISH发布消息不成功,导致无法跨服务器通信
原因:对于redis客户端进行publish或者 subscribe 时,一个客户端只能进行某一种操作。因此对于业务层应当使用两个redis对象,一个用于publish,一个用于subscribe
4.多次向中间件SUBSCRIBE订阅消息,出现无响应,客户端也无响应
原因:记C++集群服务器项目消息中间件编程大家遇到的两个Bug_大秦坑王的专栏-CSDN博客
项目缺陷及扩展
业务方面
由于这个项目业务比较简单,中间有很多设计会不符合我们日常聊天的场景和业务,这里就不一一赘述,其实业务方面没有什么,要扩展也很容易,根据需求来自己定制很多业务。
服务器设计方面
1.由于所有的连接都会经过nginx负载均衡,因此对于nginx会有很大的IO负载压力。
2.所有服务器都会提供整套的服务,这对于服务器的性能来说得不到提升。
3.对于请求数据库连接方面,频繁的去数据库申请连接断开连接这也是一种性能的损耗
因此:对于服务器设计方面可以采用分布式集群的方式进行设计
多台服务器组合而成的一台超级性能的服务器,这些服务器形成一个小集合,部署一整套对外的服务。set模型弥补了单机能力的不足,对业务组合搭配成一个单元。本质上是对服务的一个高内聚的封装。
对内不同主机执行一种不同的业务,这些主机构成一个超级服务器提供一整套服务,对外通过一台主机对这个超级服务器的抽象作为接口
建立多个这样的服务集群,这些服务集群是通过一个代理服务器 根据哈希算法和最小负载进行分发业务。但是这样也会造成代理服务器的IO负载压力过大,因此可以将这样的压力分配给客户端。
建立一个注册信息服务单元,这里只会注册每个服务器集群的IP及负载压力,客户端主动拉取这些信息,然后根据负载均衡算法以及最小负载算法去进行连接服务器。
参考
腾讯课堂 施磊 基于muduo实现集群服务器项目