前言
了解对应的java知识可看我这篇文章:
java框架零基础从入门到精通的学习路线(超全)
设计该系统的业务逻辑,之后针对性的优化
从整体框架掌握各个深层次的框架知识点,以此查漏补缺
其他系统设计如下:
以下的文章知识点主要来源于:
系统设计面试题-如何设计10亿流量的即时通讯(IM)系统?
记笔记的同时融入了一些自已的见解和拓展
1. 背景
所需的功能?
- 添加好友
- 聊天会话列表
- 单聊、群聊
- 多终端登录(从数据库中拉取)
- 消息漫游(一个终端收到很多消息后,登录另外一个终端实现同步)
- 消息已读以及已读未读列表
需考虑的约束条件?
- qps存储量
- 可靠性
- 收发消息的延迟
- 消息时序的一致性(接收发消息的顺序、消息不重不漏)
- 多人群聊
- 可维护可运维
2. 高性能
接入层优化
:
-
轮询拉模式无法保证实时性要求
可通过TCP的长连接push给客户 -
客户端如何与服务端建立长连接
a. 客户端通过公网IP socket编程直接连接服务器
b. 通过IPConf服务发送公网IP给客户端,灵活使用调用长连接(客户端自已选择的话,可以选择一个最优。将其缓存起来,如果连接失败,选择其他ip进行连接,减少ipconfig的压力)
c. 建立长连接之后,业务逻辑层与uid进行映射
d. IPConf通过协调服务与业务逻辑层进行交互,根据机器来负载均衡 -
长连接占用资源,而且跟DB结合在一起,读写会比较慢
拆分服务,长连接负责收发消息,业务服务器负责业务块 -
调度资源优化网络通信会 频繁迭代消息来支持 业务开发,本身有状态的业务重启比较缓慢
变化以及不变化的业务进行拆开,大致通过状态机服务
长连接服务专门收发消息,并且更新状态机,ip服务通过调取状态机来调度策略
频繁的变化(比如消息登入登出)通过控制长连接的断开,通过mq发送close用于长连接的调度 -
建立长连接的时候,客户端如何知道接入层服务器在哪
方案 | 优点 | 缺点 |
---|---|---|
广播 1. 服务层将其消息发送给所有接入层 2.接入层只会处理自已的uid连接(通过map进行配对有无自已的uid) |
实现简单,适合超大聊天室 |
1.单聊场景过多的无效通信 2.消息风暴系统崩溃 |
一致性哈希 1.ipconf与业务逻辑使用同一个hash,按照uid进行分片落入服务器 2.通过统一的服务注册发现,水平扩展使用虚拟节点迁移,迁移的通过断线重连 |
计算简单无性能消耗 |
1.过度依赖服务发现 2.水平扩展需要连接迁移 3.一致性哈希的均匀性限制 |
路由服务层 1. 底层使用kv存储uid以及接入机器的映射关系 2.根据不同会话更新映射关系 3.使用路由服务以及业务逻辑来维护消息队列 4.任意路由服务消费mq解析此消息的路由信息确定接入层机器 5.根据接入层机器标志创建的routeKey,将消息发送给接入层专用的队列 |
1.mq可靠,解耦削峰 2.路由服务无状态可水平扩展 3.路由存储长连接以及映射关系 |
1.需要独立维护路由层集群 2.服务底层依赖kv以及mq稳定性 |
补充对应的知识点:
一致性哈希算法的原理与应用剖析
存储层优化
:
-
读少写多,如何控制消息分发不让资源耗尽?
尽可能将其控制消息并发(不是按照一个消息一个线程,而是启动个别线程通过分发的形式),保证群发消息的时候系统不会崩溃。 -
上面的情况可能会出现消息挤压,延迟增大?
a.cache打包压缩,让router一次发送,当窗口10ms发送一次,整体性能提升一次。而且b.通过推拉结合将其压缩打包(比如服务端发送一个请求让客户端专门去pull请求,减少消息的轮询) -
存储系统有严重的写放大(也就是本身写A对BCD写消息,但是三者的通道都要写一遍),如何降低成本?
a.超大群降级为放大模式进行存储,消息仅同步写入收件箱(A读取BCD的消息的时候,只需要对应读取BCD的收件箱,读的复杂度为o(n),写只要一次)
b.群状态的消息异步存储 -
消息如何处理?如何保证万人群聊中的已读/未读一致性?
a.实时流处理,接收者已读消息下发同步,接收者消息已读通过异步落库
b.超大群消息,通过群状态变更服务进行降级
c.异步写入通过重试保证最终一致
写磁盘如何优化延迟?
:
(同步数据的时候最好在磁盘处理)
分级存储:
- 按照uid的维度,在数据库上维护可排序的列表,根据活跃程度调节最大的条数。
- 对于超大群聊退化为读扩散模式,维护会话级别中list并异步缓存消息状态。群聊中的会话根据ID对消息列表本地缓存LRU(本地的LRU可用HotRing算法,测热点数据)
- 用protobug序列化压缩,减少存储空间
- 消息队列广播状态给本地缓存 (也可使用Gossip算法)
- 超过一定时间数据,压缩在文件系统中,提供OLAP查询
该方案的优点有如下:
按照请求读取热度分级处理,而且对离线消息同步的range操作良好,也可支持群聊的推拉结合。
缺点如下:
本地缓存预热慢,服务重启会有抖动。
大量使用内存,运维难度高
需关注缓存命中率等问题
多元DB存储
:(不同字段不同存储)
- rocksDB存储kv,key 为会话ID value 是序列化消息列表
- 消息列表过长可对列表进行分列,key为会话ID,value为meta 索引信息
- 按会话ID+分段seqID作为key,value存储消息列表,读取时做合并
- 等等
优点就是利用DB的又是补长取短,基于磁盘解决存储容量问题
缺点就是ROcksDB单机数据库,需要有自研的分布式代理层。磁盘kv读写磁盘,拉取消息性能收到影响
图数据库
:
存储数据库关系的关系,会话消息列表的各种拉链关系
可运行近线OLAP查询快速识别的热点消息等,利用消息快速处理,准确命中
优点:
- 低延迟
- 可提供丰富的查询功能
缺点就是图数据库运维成本比较高,不好维护
存储层代理服务
:
增加这一层来屏蔽底层细节,代理层基于key做hash分片,基于一致性协议进行复制kv
超大群聊多个请求可能会拉取一份消息列表(做自旋cache,减少下游DB的数据库访问)
优点:
- 业务隔离
- 代理层无状态可水平扩展
- 消息列表缓存在代理层,减少底层内存负担
缺点
就是增加了一层逻辑,复杂度增大
3. 高一致
收发者的消息顺序一致以及消息不丢失
要保证消息不丢失?
TCP网络传输的数据是不会丢失,但是数据包是可能会丢失(TCP网络断、延迟)
明白其背景之后,可通过重试机制
- 上游客户端重试(数据获取不到的时候,通过重试并且服务端返回ack机制)
- 下游服务端重试(同样通过重试并且客户端返回ack机制)
但重试也有bug,如果ack丢失,消息无限重试发送,也会造成顺序的不一致(比如原本发送的答案是ABCD,结果消息为ABCCD,那样就会造成混乱)
解决上面的重复发送问题,可通过UUID的去重?
× ,此处使用UUID来对ack去重判断,是不合适的
发送的ack包(使用UUID),还需要通过一个全局表配对是否有重复
本身如果流量比较小,可以跟全局变量进行判断(使用一张表存储)
如果流量比较多,可以采取在很小的时间段进行判断
但是本身是亿级流量,即使很小的时间段,变量也是特别多
解决重复发送的问题?正确方式是?
借鉴TCP的三次握手机制,通过ack+1判断
-
上游:客户端在发送会话中生成一个消息,并且是自增的cid,服务端本身存储的是上次最大的cid。
如果发送的消息不是cid+1,则将其丢弃(不可能一直等待,因为后续消息发不出去,服务会造成很大的浪费)
此处的cid是单个客户端的,不同客户端可以重复
-
下游(类似上游的处理方式):服务端每个发送的消息分配seqid,客户端本身存储的是上次最大的seqid。
如果发送的消息不是seqid+1,则将其丢弃(类似上游的处理方式)
此处的seqid是服务端的,需确保不重复而且递增
保证消息有序?
通过上面的知识,已经知道消息不会重复且不会丢失
那可通过递增的ID进行排序
- 上游客户端 按照cid为每个消息分配seqid(之所以要进行排序,是因为服务端存储的不止一个客户端的seqid)
- 下游按照seqid进行排序
特别是服务器宕机之后,本身的seqid在进行主从复制的时候,复制过程中会存在延迟
(重启的时候怎么保证ID不重复?)
每次Redis重启的时候,都会通过实例ID是否一样,如果不一样说明重启过。重启过则通过哈希加上时间戳等办法保证消息不会重复(保证消息递增,但是不会单调)
如何生成递增的消息ID?
可看这篇文章:
分布式ID生成方法的超详细分析(全)
具体方案设计
方案 | 优点 | 缺点 |
---|---|---|
纯拉数据 : 只保证上行(发送方)消息一致性 |
实现比较简单 | 实时性差(只是按照服务器的标准) |
单调递增ID : 通过Redis+lua |
实现比较简单 |
1.通信次数过多,群聊性能差。 2.依赖分布实ID生成系统可靠性 |
双ID方法 : 发送方发送当前id以及上次消息id,接收方保留上次的id(接收双方通过对比这个id判断是否有消息遗漏) |
不依赖id生成的单调性 |
1.通信次数过多,群聊性能差。 2.下行消息(接收方)实现机制复杂 |
推拉结合 : 服务端通知客户端拉取消息,客户端pull消息(本身pull可做ack) |
1.下行消息不需ack机制,服务端不需维护超时重发。 2.可一次性拉去所有会话,减少调用次数。 3.批量拉取有利于消息压缩,提高带宽利用率。 |
无法解决上行消息的时序性 |
通过以上方案,找出更好的解决方法
- 上行消息(客户端)通过先前的ID配对策略(双ID方法),可保证消息的时序一致性
- 下行消息(服务端)通过推拉结合,可保证高吞吐量
4. 高可用
链路复杂过长容易造成瓶颈,导致没有高可用
4.1 连接断路
整个链路跨越公网(运营商),TCP如果开启Keepalive(心跳机制是2h),长连接如果超时没有回馈,就会将其断开。心跳机制应该取决于整个链路,要想维护一个端到端的有效性(内核间有效是可以维护的,但是业务逻辑不好维护),应该维护业务逻辑上的心跳机制。
1. 心跳机制:
心跳机制应该放在业务逻辑层中
- 通过服务端push给客户端(×),本身是即时性,数据量大的话也很不现实
- 通过客户端push给服务端(√),将其心跳周期性的发送给服务端的网关,通过网关重置内部的定时器
包的大小:
包的心跳控制包不可过大,需控制在0.5kb以下
心跳的时间:
- 心跳过长:断线客户端太多,效率低,资源利用率低
- 心跳过短:心跳请求太多,造成网关流量压力过大
最好的方式是自适应心跳:前端通过固定心跳(没有链路固定好久可),后端通过测算NAT淘汰时间(自适应估算时间,取最小最大的临界值中间)
2. 断线重连
:
背景1
:坐高铁或者火车的时候,客户端网络原因频繁切换,会导致服务创建销毁,过度清空资源。如何更稳定快速的建立长连接?
解决方式
:断开的一瞬间,启动一个session超时器,如果在这之后能连接上来,资源就不会被清空,就可进行重连,保证链接的稳定性(直接复用)
本身会创建一个TCP通路,通过fid与session进行关联即可
以上方式可以防止频繁的创建和销毁,不会让Redis雪崩
背景2
:服务器如果崩盘导致客户端重连,请求过多造成雪崩如何处理?
解决方式
: IPConf服务通过发现机制快速识别服务端节点故障,进行调度,客户端断线后,通过随机策略重连请求,获取到服务重新调度,如果原有服务器还在则优先选中(本身有服务故障自我发现,负载均衡机制)
3. 消息风暴
:(如何保证消息的可靠性)
背景3
:长连接下服务奔溃未发送的消息如何再次发送?
解决方式
:
- 建立连接后,需要调用离线消息同步接口,主动拉取消息(主要为了同步网关的状态消息)
- 连接的状态信息单独存在在状态服务器中,和整个通讯系统可使用RPC或者共享内存(状态服务器可做持久化处理,类似快照处理)
背景4
:心跳的计数超时太多,导致大量的定时器占用内存资源,会造成整个系统卡顿造成消息超时?
解决方式
:使用二叉堆(定时时间复杂度为logn),大量的创建和消除,瓶颈在于数据结构中,所以改为时间轮算法(但是定时精度有所缺失)
4.2 弱网
通过快链路优化TCP连接:
- 减少TCP数据包,IP会分片(超过1400字节),将其数据包控制在1400下
- 拥塞控制窗口放大,避免收到拥塞
- socket读写缓冲区,避免数据包溢出
- 调整RTO初始时间,避免重试的网络拥塞
- 禁用Nagle算法延迟,减少更少的数据包被缓存(TCP数据比较小的时候,会进行堆积到一定程度在发送,此处为了防止堆积)
通过策略优化:
- 多个ip测速 选择最优的连接线路
- 不同网络环境选择 不同超时时间,超时参数进行 动态下发 来 计算策略
- 短链退化,链接状态数太多导致频繁链接,可以退化为轮询手法消息
协议优化:
- 二进制协议
- QUIC协议(基于UDP)
4.3 异地多架构
多数据中心通信,广域请求(跨数据中心请求,延迟比较高)
对此应该更大程度的减少广域请求(保证延迟比较少),这也是核心思想
5. 高可靠
客户端与服务端的通信,通过TCP的全双工进行处理
基本概念
:
短连接与长连接
概念 | 大致情况 |
---|---|
短连接 | 客户端第一时间拉取服务端的数据,但过多的轮询,造成服务端端的过载(不选此方案)。但是在弱网情况下会选择短连接 |
长连接 | 可以减少这种轮询方式,更快的push给客户端(√) |
- connID:客户端与服务端建立连接,通过长连接,可以通过全局id分配、雪花算法等来分配这个connID(只要保证全局唯一即可)
- sessionID:业务逻辑的处理。标记A与B之间的聊天框,主要是会话ID
- msgID:也是业务逻辑的处理
推拉模式
服务端将其请求push给客户端,客户端的应答通过pull模式
网络调用在整个系统是损耗是最大的,要考虑到消息风暴中(带宽容易被打满)
一般可靠性会与一致性来调和
- 可靠性:消息成功发送后,端对端的到达
- 一致性:任意时刻,消息的发送与接受顺序一致
背景
:
设计一个即时通讯系统,底层的可靠与一致性只能保证底层的通信,但是不能保证上层系统,怎么设计这个架构是很大的学问?
传输过程中,整体架构的设计每一步都要保证可用性
可靠性:上行消息可靠 + 服务端业务可靠 + 下行消息可靠
一致性:上行消息一致 + 服务端业务一致 + 下行消息一致
整个架构传输过程中,可能会出现的问题:
客户端发送两条msg消息给服务端的时候,两条消息都在同一个TCP链路中到达服务端。
发送消息的时候一般会引发这三种情况:
- 客户端将其消息发送到服务端的时候(TCP层面的可靠性),之后发往业务逻辑层的时候,业务逻辑层崩溃造成了丢失,但服务端业务层未知,而客户端以为消息已经收到了
- 业务逻辑层的消息成功处理后。服务端多线程,处理分别进来的两个消息,消息体大小处理速度不一样,导致消息可能乱序
- 服务端的数据处理完毕到达客户端后。某一条消息存储失败,导致消息丢失乱序
通过上面的例子可看到,TCP/UDP只能保证底层数据的可靠性,并不能保证业务逻辑的可靠性。
特别的挑战难点在于:
- 不同操作系统分发的消息不同,导致网络层面无法保证消息的成功到达
- 无法确定消息的顺序,没有全局时钟来确定唯一顺序
- 多个客户端/多个服务端/多线程多协程 的处理消息,顺序更难以确定
方案选型
:
为了解决上面的问题,而且要保证系统的及时性、可达性、幂等性以及时序性
-
及时性
:端对端及时的接收发消息并且实时(即使在高峰期延迟也不应该过大) -
可达性
:超时重试、ACK确认。
所谓的超时重试,通过设置一个定时。
客户端将其消息发送给服务端的时候,当服务端返回ack确认收到,客户端的定时器就会结束。
服务端将其消息发送给客户端的时候,当客户端返回ack确认收到,服务端的定时器就会结束。 -
幂等性
:分配seqID、服务端存储seqID
每个消息只能被存储一次,通过分配ID存在map里,确保消息接收不到在重发一次 -
有序性
:seqID可比较,服务端按照发送端的消息对消息进行排序
ID不仅要保证唯一,还要保证有序(后续发送消息的ID比前面发送的ID要大)
通过客户端ID与服务端ID,客户端发送的ID大,则服务端发送的ID也要大(保持一致)
5.1 上行消息
客户端将其消息发送给服务端(保证此通路是可用的)
补充:
- 严格递增:ID 后一个比前一个大
- 趋势递增:ID的步长 后一个比前一个大
- clientID是单个客户端的ID,seqID是服务端的ID
大致方案如下:(流程一个个递进)
-
单纯使用客户端的ID来进行配对(
不选用
)。 应该依赖全局会话ID,而不是单独的客户端ID。 -
clientID使用UUID,可保证有序性,但不能保证唯一性(唯一性的保证需要,将其映射存储在多个内存中,以map形式,会浪费空间(N多个客户端就要存个map))(
不选用
) -
服务端的映射应该为 connID -> preMaxCID,代表客户端传输过来的CID,要配对前一个最大的ID(保证现在传进来的ID比原先的要大,也就是preMaxCID + 1 = CID),此种情况,在弱网情况下,单独一个数据包丢失一直重试,会造成数据库崩溃。(
选用
),只不过弱网会特殊处理。 -
为了解决这种情况,可以使用滑动窗口,如果窗口没满(前面的ID未传,线程池就不会分配线程给予)。此种情况会造成长连接的资源维护(长连接状态)(
不选用
) -
趋势递增(链表的方式,但这会浪费协议的消息带宽)。客户端与服务端都会存储preID,两者配对成功之后,服务端就会存储客户端发送的ID,并且将其ID存储为preID。这是存储两个ID来比较。(
不选用
)
设计一个 内存占用率比较小的方案 以及可靠的方案
对于以上讲到的方法,将其制作成表格:
方案 | 优点 | 缺点 |
---|---|---|
clientID严格递增 1.客户端创建会话与服务器建立长连接,刚开始的clientID初始为0 2.发送第一条消息分配一个clientID,该值会在该会话内(表示一个A与B聊天的框)严格递增 3.服务端存储的消息通过 clientID = preClientID + 1判断 4. 服务端接收到消息后,返回给客户端ACK (如果没有接收到,最多三次重试) |
1. 长连接通信延迟低 2.发送端顺序为准,保证严格有序 |
弱网情况下,单条数据丢失会造成重试导致数据瘫痪无法保证及时性 |
clientID 链表 1.客户端通过本地时间戳作为clientID + 上一个消息的clientID 服务端 2. 服务端也是存储preClientID以及clientID,只有当前preClientID配对了才接收clientID |
同上 | 浪费协议消息带宽 |
clientID 列表 1. 服务端针对每个连接存储多个clientID,形成clientID list 2.多个clientID 的列表作为滑动窗口,保证消息幂等 |
减少弱网重传的消息风暴 |
1.实现复杂 2.网关层面需要更多内存,来维护连接状态 3.本身传输层TCP对弱网有所优化,应用层维护窗口收益不大 |
对于上面的方案,选用的方案只要保证clientID单调递增,特别是弱网情况下,通过优化传输层协议(QUIC)。本身长连接就不适合在弱网情况下工作,丢包和断线是传输层的问题。
上面的前提都是要在同一个sessionID,所以每次存储的时候不仅仅要存储clientID还需要存储sessionID
5.2 消息转发 + 下行消息
- 分配seqID
- 异步存储消息
- 处理业务逻辑
- 将其消息转发给其他的客户端
补充:
上面一直讲到clientID,还需要一个seqID(服务端分配的ID),本身会话中分为单聊和群聊,任何一个客户端的ID都不能作为整个会话的消息ID,否则会产生顺序冲突,必须要以服务端为主。
服务端需要在会话范围内分配一个全局递增的ID(比如客户端发出的msg1, msg2,可能由两个不同的终端发出。所以服务端发出的seq1,seq2,一大一小要跟客户端一样有一大一小,保证消息的有序性)比如msg1,cid1,seq1。下一条消息为msg2,cid2,seq2 等等(每条消息都有它的生命周期,生命周期到了就会丢弃,减少带宽的存储)。保证同一个session的有序性
如何存储seqID:
redis 存储seqID,存在一个Redis(incrby),而且还要主从复制,如果一个会话的qps过高,还不能这么存储,Redis10w的数据会满。
从业务上处理,只保证会话内的每个消息有序,那么msgID sessionID seqID 拼接作为key,value为int的64位,通过哈希key变量来分组到不同的Redis。保证消息的唯一性可通过时间戳(比如雪花算法左移右移)。
如何让seqID持久化:
服务端的seqID单调递增跟客户端的处理方式一样。但有一点不一样的地方在于,客户端如果断掉的话,ID可以从零开始递增,但是服务端是全局的ID,从零递增会导致消息不一致。
如何保证消息的持久化,sessionID 业务逻辑永远是递增的(断电故障)。即使主从复制,在主节点故障的时候,从节点选举为主节点,也会有消息慢一拍,导致消息不一致,消息的回退。
为了解决上面的情况,需要将其Redis与lua结合在一起。每次取key节点,还需要比较ruaID(通过lua来保证一致性)。但是如果比较的ruaID真的不一致了(就像上面的主从复制的时候,主节点发生了故障),还是会回退处理。为了解决这种情况,seqID结合时间戳,该ID也是趋势递增。
趋势递增也会有bug(解决方案如下):
客户端可能收到的msg为1,10。中间的跳变缺失不好判断是真缺还是假缺,客户端会pull服务端(分页查询【1,10】的消息),如果确认有消息跳变,则进行补充(结合推拉的消息,此时会有网络异动。正常网络不会有这种现象,弱网比较多)
通过上面的文字版,将其整理成表格,具体如下:
消息转发保证可靠性
- 将其消息转发给mq异步存储,保证消息不丢失
- seqID无需全局有序,只要保证当前的session有序即可,解决单点瓶颈
方案 | 优点 | 缺点 |
---|---|---|
服务在分配seqID前,请求失败或者进程崩溃? 分配seqID之后在回馈ACK |
保证seqID可用 |
1.ack回复慢,收发消息慢 2.seqID瓶颈 3.消息存储失败,消息将丢失 |
服务端存储消息、业务逻辑等失败处理? 1.消息存储后在回馈ACK,ACK失败则客户端重发 2.处理服务逻辑的时候断开,则客户端可通过pull拉取,补充消息的漏洞,保证消息可靠 3.消息存储后,但业务逻辑失败。(可通过异常捕获,并客户端pull拉取历史消息) |
1.保证业务可用性 2.出现异常,处理瞬时(比较接近异常) |
1.上行消息延迟增加 2.整体复杂度搞 3.弱网协议需要协议升降级 |
对应的下行消息保证可靠性,整体表格如下:
方案 | 有点 | 缺点 |
---|---|---|
客户端定期轮询发送pull请求 | 实现简单,可靠 |
1.客户端耗电高(用户体验差) 2.消息延迟高,不及时 |
seqID的严格递增 1.按照消息到达服务端顺序,来分配seqID(用服务端来弄seqID,有全局性)。特别是使用redis incrby生成seqID,key是sessionID或者connID 2.服务端的seqID严格递增,回馈给客户端,客户端按照preSeqID = seqID + 1做到幂等性 3. 服务端等待客户端的ACK回馈,如果超时则重传 |
可最大程度保证严格单调递增 |
1.弱网重传问题 2.Redis单点问题,无法保证严格单调递增 3.需要维护超时重传以及定时器 4.无法解决另外一个客户端不存在的传递消息 |
seqID趋势递增 1.使用lua脚本存储maxSeqID,每次获取该ID的时候,都会检查是否一致,如果不一致,则发生主从切换 2. 客户端发现消息不一致,则通过pull拉取命令,拉取不到,则说明屎seqID跳变。如果是另外一个终端不在线,则查询状态后仅存储而不推送 |
1.保证连续,任意时刻单调和递增 2. 会话级别seqID,不需要全局分布式ID,redis可用cluster水平扩展 3.可识别用户是否在线,减少网络带宽 |
群聊场景,有消息风暴 |
推拉结合 + 服务端打包 |
解决消息风暴 | |
seqID 链表 服务端与客户端都通过存储seqID以及preSeqID,通过比较前一个消息是否满足,如果不满足再通过pull拉取 |
屏蔽对seqID趋势递增依赖 | 存储的时候还要多存储一个preSeqID |
群聊点对点已经无法处理,可以通过批处理进行处理。
- 将其多个msg存储在一个窗口中(窗口进行排序,发送给客户端),客户端同时对这么多个消息进行发送ack
- 将其消息压缩(减少卡顿)
上面的消息如果过大,反而影响TCP的拆包,可以使用长短连接,群聊用短连接(用其优化)
服务端长连接push给客户端,让客户端主动pull,服务端主动发送短连接的http请求,减少服务端负载
plato 的总体大致流程如下:
- 客户端A创建连接之后,发送消息的时候分配一个clientID(从0开始,而且是递增)
- 启动消息定时器(ack回馈清除 或者 超时就会重传),将其消息发送给服务端
- 服务端通过redis将其session进行分片(可使用incryby)。异步写入mq,保证可靠传输
- 消息处理完成之后回馈给客户端A 一个ack,A客户端收到之后就会取消定时器(这一步的过程可以异步)
- 启动下行消息定时器,将其消息发送给客户端B,客户端B通过session的maxSeqID + 1来判定
- 客户端B回馈服务端来接收或者拒绝,以此判定是否关闭定时器