.\redis-server.exe .\redis.windows.conf

sk-6d764c328eb8430eaf5b2effd44d6b2e

文本信息存在TEXTBUBBLE里,用QTextEdit管理

针对大文件传输过程,采用分块传输与环形缓冲区设计,优化分块传输协议提升传输效率,结合TCP长连接实现断点续传。传输进度通过双ACK机制保障,客户端发送分块后等待服务端ACK,服务端写入磁盘后返回客户端ACK;针对离线消息存储,采用Redis SortedSet(按时间戳排序),登录时通过ZRANGEBYSCORE拉取未读消息。消息状态同步使用版本号冲突检测,客户端本地维护版本号,与服务端版本号比对后增量同步。

global.h里1019,客户端tcpmgr处理服务器转发过来的消息,

客户端响应服务器返回的消息,包括两种:

  1. A给B发送文本消息,A所在的服务器会给A发送ID_TEXT_CHAT_MSG_RSP消息。
  2. B所在的服务器会通知B,告诉B有来自A的消息,通知消息为ID_NOTIFY_TEXT_CHAT_MSG_REQ

所以在tcpmgr的initHandlers中添加响应ID_TEXT_CHAT_MSG_RSP消息

chatdialog相当所有关系的缓冲,大部分信号连接都在这里

这是一个全栈的即时通讯项目,前端基于QT实现气泡聊天对话框,通过QListWidget实现好友列表,利用GridLayoutQPainter封装气泡聊天框组件,基于QT network模块封装httptcp服务。支持添加好友,好友通信,聊天记录展示等功能,仿微信布局并使用qss优化界面

后端采用分布式设计,分为GateServer网关服务,多个ChatServer聊天服务,StatusServer状态服务以及VerifyServer验证服务。

各服务通过grpc通信,支持断线重连。GateServer网关对外采用http服务,负责处理用户登录和注册功能。登录时GateServerStatusServer查询聊天服务达到负载均衡,ChatServer聊天服务采用asio实现tcp可靠长链接异步通信和转发, 采用多线程模式封装iocontext池提升并发性能,根据CPU的核数开辟n个这样的iocontext,跑在不同的线程,实现真正意义的一个并行

验证请求

GateServer收到Client发送的验证请求后,会调用grpc 服务 访问VarifyServer,此时网关服务要作为grpc服务的客户端,封装了grpc的连接池,取出连接后进行rpc调用,像在本地一样使用验证服务中获取验证码的方法,VarifyServer会随机生成验证码,调用邮箱模块发送邮件给指定邮箱。并且把发送的结果给GateServer,GateServer再将消息回传给客户端

验证码是要设置过期的,主要用redis管理过期的验证码自动删除,key为邮箱,value为验证码。所以服务里添加redis模块,封装redis操作在redis.js中,获取验证码之前可以先查询redis,如果没查到就生成uid并且写入redis。

注册请求

网关接受到注册请求的话,在逻辑层进行解析,先去redis看email对应的验证码是否合理,再去查数据库里面这个用户是否存在,尽管mysql提供了访问数据库的接口,但都是基于c风格的,为了便于面向对象设计,使用了mysql connector C++这个库来访问mysql。封装了mysql连接池,封装了DAO操作层。在注册的时候调用存储过程来存新用户信息。同时还有数据库管理者来实现服务层,对接逻辑层的调用。

数据存储采用mysql服务,并基于mysqlconnector库封装连接池,有了持久化存储必然还有缓存,同时封装redis连接池处理缓存数据,

各个服务之间采用grpc通信,但也不是一个连接就够用的,因为每个服务可能是多线程的,每个线程里可能都会使用这个grpc来通信,所以有grpc连接池保证多服务访问及并发访问的安全性。

登录请求

网关收到客户端的登录请求,在逻辑层先去查询数据库看账户密码是否匹配,再通过状态服务器找到一个合适的聊天的服务器,同样是作为grpc客户端调用状态服务的获取服务器方法。在DAO层根据用户名查询sql,并判断密码是否匹配,在取连接的时候通过自己封装的defer类实现连接的正常归还,体现RAII的思想。

状态服务主要是用来监听其他服务器的查询请求,维护一个服务器列表,负载均衡主要是通过存在redis’里面的服务器登陆的用户数量去选择负载更小的服务器,返回该服务器主机和端口,每个服务器在启动的时候便重置了本服务器的登陆数量,同时构建并启动grpc的服务端

聊天服务

聊天服务要保持一个长连接,方便客户端和服务器端进行通信,所以在客户端用tcpmgr去管理tcp连接,在服务器端主要是基于asio完成这个服务的搭建,cserver构造函数异步监听对端链接,当新连接到来的时候从asioIOServicePool中返回一个可用的iocontext构造new_session,然后将新连接的socket写到这个session保管,触发绑定的回调函数进行连接的处理:异步读取消息头和把session和id存在map里面管理。

​ 读取消息头后分离出消息id和消息长度,判断合法性后构造接收节点,接着去异步读取消息体。接收节点填充好后调用逻辑系统单例将解析好的消息封装为逻辑节点投递到逻辑队列中,进行一个网络线程和逻辑线程的解耦,继续监听头部信息接受事件。以此循环往复直到读完所有数据。如果对方不发送数据,则回调函数就不会触发。不影响程序执行其他工作,因为我们采用的是asio异步的读写操作。

逻辑系统有一个工作线程,由它对消息队列中的消息进行处理,根据消息id触发事先注册好的不同的业务函数,比如聊天服务器登录,查找用户请求,搜索用户请求,消息传递请求。

登录逻辑:登录请求先到网关,网关通过grpc调用状态服务器中的负载均衡方法获取服务器信息返回给客户端,同时将token和用户uid绑定写进缓存。客户端拿网关给的服务器信息向聊天服务器建立连接,成功后发送tcp请求登录,这里要先去redis验证用户token是否正确,如果合理再从redis中寻找用户信息,没找到则从数据库加载一份,再从数据库获取申请列表,好友列表,将登录数量增加,同时将用户uid和session绑定管理。至此该用户就完全登陆上一个该聊天服务器了。

好友搜索

客户端点击搜索列表的搜索item之后,根据bool值判断发送是否阻塞,如果阻塞就弹出一个加载框。服务器收到后判断是否存在,如果不存在则显示未找到,如果存在则显示查找到的结果;客户端在收到服务器回包后的需要进行解析处理,将搜索到的结果封装为searchinfo发送给搜索列表做展示,此时又分两种情况,一个是已经是好友了:在用户的朋友表中找到了该id后就跳转到聊天界面,不是好友的话,构造查找成功dialog,用户信息展示上去。

服务器方向:根据用户发过来的用户id(或者name)进行查找,在逻辑层根据请求id回调搜索用户信息的函数,优先在redis里面找,没有再去数据库,同时把搜索用户信息写入缓存,向客户端返回数据。

好友添加

当Client1搜索到好友后,点击添加弹出信息界面,然后点击确定即可向对方Client2申请添加好友,这个请求要先发送到Client1所在的服务器Server1,服务器收到后判断Client2所在服务器,如果Client2在Server1则直接在Server1中查找Client2的连接信息,没找到说明Client2未在内存中,找到了则通过Session发送tcp给对方。如果Client2不在Server1而在Server2上,则需要让Server1通过grpc接口通知Server2,Server2收到后继续判断Client2是否在线,如果在线则通知。

另一个客户端会收到这个添加好友的请求,通过申请方信息构造展示到新的朋友页面,

服务器要处理客户端发来的添加好友的请求,并决定是否调用rpc通知其他服务。主要是更新数据库中朋友申请表,查询redis找到touid对应的server ip,如果在同一个服务器则直接通过uid查找session,判断用户是否在线,如果在线则直接通知对端如果不在的话,让服务器作为grpc客户端通知其他服务器,每个服务器不光监听tcp连接,还作为grpc服务端监听着grpc连接,还是从池子里取一个stub,调用通知添加好友的方法,其他服务器收到后继续判断用户是否在本服务器,是否在线,如果在线则直接发消息通知。

总之对方客户端会收到这个添加好友的请求,在用户管理类加载这个申请信息,进而在新的朋友列表去加载条目。

同意好友申请

收到好友请求之后点击同意也会去填写认证信息发给服务器,服务器再通知申请方,告诉对方被申请人已经同意加好友了。

服务器接收客户端发送过来的好友认证请求,

asio 网络库grpc分布式通信技术,Node.js做插件,多线程,Redis, MySql,Qt 信号槽与一些组件,网络编程,设计模式

解决了高并发场景下单个服务连接数吃紧的情况,所以采用了分布式的这样一个设计

1 如何利用asio实现的tcp服务

利用asio 的多线程模式,根据cpu核数封装iocontext连接池,每个连接池跑在独立线程,通信采用异步async_readassync_write方式读写,处理就绪事件的话是通过消息回调完成数据收发,读就绪通过读回调触发,写就绪通过写回调触发。整个项目采用的网络模式是Proactor模式.

每个连接通过Session类管理,通过智能指针管理Session,b保证回调之前Session可用,底层绑定用户id和session关联,所以在登陆的时候是能够找到用户对应的session的,回调函数可根据session反向查找用户进行消息推送(这也就是说聊天的时候,做跨服务通信的时候,通过session找到对应的用户进行通信)。

客户端和服务器通信采用json, 通过tlv方式(消息头(消息id+消息长度)+消息内容)封装消息包防止粘包。通过心跳机制检测连接可用性。

(为什么要有心跳机制)在网络情况下,会出现各种各样的中断,有些是网络不稳定或者客户端主动断开连接,这种服务器是可以检测到的。PC拔掉网线,还有一种情况客户端突然崩溃,有时候服务器会检测不到断开连接,那么你这个用户就相当于僵尸连接。当服务器有太多僵尸连接就会造成服务器性能的损耗。

另外心跳还有一个作用,保证连接持续可用,比如mysql,redis这种连接池,如果不设计心跳,时间过长没有访问的时候连接会自动断开。

2 为何封装Mysql连接池

首先多个线程使用同一个mysql连接是不安全的,所以要为每个线程分配独立连接,而连接数不能随着线程数无线增加,所以考虑连接池,每个线程想要操作mysql的时候从连接池取出连接进行数据访问。

Mysql连接池封装包括Mgr管理层和Dao数据访问层,Mgr管理层是单例模式,保证在任何地方都可以进行访问这样一个操作,具体怎么操作,Dao层包含了一个连接池,采用生产者消费者模式管理可用连接,并且通过心跳定时访问mysql保活连接。

对端离线消息是如何存储的

使用redis的sorted set数据结构来存储用户的离线信息,每个用户对应一个Sorted set,成员是消息的时间戳,分值是消息的内容。当用户上线时,通过ZRangeByScore命令来获取用户离线期间收到的信息,并将其发送给用户。这种方式简单高效,能有效解救离线消息的管理问题。

单例模式

http管理类和tcp管理类都做成了一个单例类,这样方便在任何需要发送请求的时候调用

自定义组件

增加定时按钮,倒计时结束之后可以再次点击,重写鼠标释放事件,释放之后开始计时,在定时器停止之前该按钮都不可点击;

实现了可点击标签,用在比如说输入密码时显示或隐藏密码,把图片先放入资源,要实现一个点击后有状态切换,以及浮动显示不一样的效果等,该组件有六种状态,用qss写好,只需要根据鼠标事件切换不同的qss实现样式变换