目录
登录和登出逻辑实现
通过前面几篇文章,对 IM 系统的分层架构设计,以及入口层 Entry、路由层 Router、数据访问层 Das 等每一层的核心职责和关键逻辑进行了深度剖析。
今天开始分析 IM 的功能逻辑实现,通过落地系统功能,将前面剖析的每一块内容串联在一起,来体会每一模块在整体架构中的设计和实践。
一、用户登录
首先分析用户登录逻辑,见下图。
以用户 uid=101 为例说明,其登录流程为:
-
客户端在启动时,会通过 http 方式访问专门用于鉴权的 Passport 服务,获取用于登录的 token,token内嵌uid;
-
客户端在登录 IM 后端系统时,首先与入口层 Entry 通过三次握手建立 TCP 长连接;
-
基于该长连接,客户端携带 token,向 Entry 发送登录请求;
-
Entry 不处理业务逻辑,将该登录请求转发给业务逻辑层 Logic;
-
Logic 通过访问 Passport 服务,判断 token 的合法性,若 token 非法,Logic 回复 Entry,Entry 直接关闭与客户端之间的长连接;
-
若 token 合法,Logic 首先向 Router 中写入登录用户与 Entry 节点之间的映射关系 <uid, 192.168.1.1>;
-
然后 Logic 将用户登录事件封装成消息,发送到 MQ,MQ 下游的 Extlogic 通过访问数据访问层 Das,将用户登录事件写入到登录库;
-
最后 Logic 回复 Entry 用户登录成功,Entry 将回复包发送到客户端。
二、重复登录
对于已经成功登录的用户,如果通过其他设备再次登录,此时就会发生 “重复登录”,对于重复登录,我们通常的处理方式是将前一次登录的用户进行 “踢人” 操作,见下图。
用户 uid=101 已经成功登录 IP 是 192.168.1.2 的 Entry 节点,如果该用户此时通过其他移动设备重新发起登录,且连接的是 IP 是 192.168.1.1 的 Entry 节点,那么 Logic 在处理登录请求逻辑时,会通过访问 Router 判断出用户 101 已登录的状态,且连接的是另一个 Entry 节点,此时 Logic 会构造 kickout 包,发送到最初登录连接的 Entry 节点,该 Entry 节点向客户端发送 “踢人” 通知后,关闭与客户端之间的长连接;同时,Logic 会将最新的 <101, 192.168.1.1> 记录写入到 Router 中。
对于两次重复登录连接的是同一个 Entry 节点的情况,处理方式类似。
三、多端登录
如何实现用户的多端登录呢?即同一个用户在不同类型的客户端上同时在线。
实现多端登录的核心设计在于中央存储 Router 登录用户设备类型的支持,见下图。
同一个用户,可以在不同类型的客户端上与 Entry 建立连接,路由层 Router 需要存储 <uid、设备类型、Entry 节点> 这样的三元组记录,uid 可以唯一标识用户,uid + 设备类型 可以唯一标识连接的 Entry 节点。
所以 Router 的数据结构是一个嵌套的 key-value 结构:<uid , <type,entryIp>> , uid 表示用户,type 表示设备类型,entryIp 表示 Entry 节点。
四、用户登出
理解用户登录逻辑之后,用户登出逻辑就非常简单了。
-
客户端基于与 Entry 之间的长连接,发送登出请求到 Entry;
-
Entry 将登出请求转发给 Logic ;
-
Logic 访问 Router ,清理用户在线的相关数据;
-
Logic 将用户登出事件封装成消息,发送到 MQ,MQ 下游的 Extlogic 通过 Das,将用户登出事件写入到登录库;
-
Logic 回复 Entry 后, Entry 关闭与客户端之间的连接。
五、心跳
客户端成功登录后端 IM 系统后,会周期性发送心跳请求到后端,以保持长连接的活性;同时, Entry 也以客户端心跳为依据判断用户是否已经登出,毕竟在移动时代,很少有用户会有 “登出” 操作了。
-
客户端发送心跳请求到 Entry,Entry 通过心跳算法处理心跳请求;
-
同时,Entry 将心跳请求转发给 Logic,Logic 将心跳时间写入到 Router;
-
当 Entry 发现心跳失活的客户端后,会模拟客户端构造登出请求发给 Logic;
-
Logic 访问 Router ,清理用户在线的相关数据;
-
Logic 将用户登出事件封装成消息,发送到 MQ,MQ 下游的 Extlogic 通过 Das,将用户登出事件写入到登录库;
-
Logic 回复 Entry 后, Entry 关闭与客户端之间的连接。
消息收发逻辑实现
“消息收发” 是 IM 系统最最核心的业务逻辑模块,本篇文章是整个【IM专题】的核心!
IM,即 “即时通讯”,要求消息具备 “及时性” 和 “可靠性”:
及时性,要求消息的收发需要很低的延时,在线双方通过消息交流时,没有明显的滞后感。
可靠性,要求消息不能丢失;对于消息发送方来说,只要消息发送成功了,消息就会一直存在服务端,不会丢失(除非因产品策略,删除久远的历史消息);对于服务端来说,只要接收方在线,一定会把消息送达到接收方,如果接收方不在线,会通过其他通道来触达和通知到接收方用户,体现出 “即时通讯” 的优势 。
在 IM 单体架构中,我们分析过客户端不断对服务端进行轮询获取消息的方式,这种方式我们称之为 “信箱模型”;信箱模型虽然实现比较简单,但是消息的及时性不好,因为存在大量的无效请求,会造成服务端的资源浪费和负载,信箱模型比较适合用户规模比较小的场景,不适合分层架构的 IM 系统。
分层架构的 IM 系统,通常基于服务端直接推送的方式实现消息从服务端到客户端的传输,见下图。
客户端 A 发送消息到服务端后,服务端直接将消息推送到客户端 B,就好像服务端知道客户端 B 的电话号码一样,可以直接联系到客户端 B,我们把这样的消息推送模型叫做 “电话模型”,这里的 “电话” 往往就是 长连接。
分层架构 IM 系统的消息收发流程,为了做到 “及时性” 和 “可靠性”,我们划分为三个阶段:生产消息阶段、推送消息阶段,确认消息阶段,在这三个阶段中,通过识别用户假在线的三重保障加强了消息的及时可靠性。IM 分层架构消息收发流程见下图。
以用户 uid=101 发送消息给用户uid=102 为例,描述详细流程。
一、 生产消息阶段
生产消息阶段完成客户端成功发送消息到服务端的过程。
-
1-1:客户端 101 成功登录后,基于与入口层 Entry 之间的长连接,发送消息请求到 Entry;
-
1-2:Entry 是入口层组件,不负责处理业务请求,收到客户端的消息请求后,基于与业务逻辑层 Logic 之间的 RPC 连接,将消息请求转发到 Logic;
-
1-3:Logic 接收消息后的第一件事情,是对消息内容进行 “反作弊” 过滤,通过调用 Spam 服务识别出违反平台规则的消息;若消息违反平台规则,则整个流程止步于此;
-
1-4:对于正常的消息,Logic 做的第二件事情是通过调用数据访问层 Das 将消息落库,具体是分别写入 “消息库” 两条记录和 “联系人库” 两条记录;
-
1-5:消息成功落库后,Logic 构造回复包,通过 Entry 主动建立的 RPC 连接,将回复包发送给 Entry;
-
1-6:Entry 将回复包发送给客户端 101。
这是生产消息阶段所要完成的事情,有几个关键的设计点需要注意:
-
Logic 接收消息请求后,一定是 “先落库,再回复”,顺序不能反,很容易可以思考到为什么;
-
对于客户端来讲,消息发送后,如果在一定时间内没有收到 response,则表示消息发送失败;此时在客户端界面上,消息的旁边会出现红色的感叹号,提示用户消息发送失败,点击重发;根据网络的二将军原理,我们知道,客户端虽然没有收到 response,但消息可能已经落库成功,若重发消息,会造成消息的重复,如何幂等呢?通过客户端来生产在客户端范围内唯一的 client_msg_id 即可;(
-
Logic 接收消息落库时,为了消息定制化需要和读取的方便,同时也为了分库分表时同一用户的数据能落在一张表中,针对每一条消息会存两条消息记录和两条联系人记录。
二、推送消息阶段
推送消息阶段完成服务端推送消息到客户端的过程。
-
2-1:Logic 访问路由层 Router,获取消息接收方 uid=102 是否在线和连接的 Entry节点;若客户端 uid=102 不在线,Logic 会封装相关数据,发送到 MQ,由 Extlogic 进行离线用户的召回逻辑处理,包括向离线客户端 uid=102 推送手机push、发送微信公众号消息(对于关注公众号的用户)、发送短信、发送VoIP等;
-
2-2:若客户端 uid=102 在线,Logic 基于与 Entry 之间的 RPC 连接(对于这条 RPC 连接,Logic 是客户端,Entry 是服务端),将消息发送给 Entry;若用户 uid=102 处于假在线状态,即 Router 认为 102 在线,但 Entry 中已没有 102 的连接句柄,Entry 会直接回调 Logic 的 unreachabled 接口,在该接口中,Logic 一方面会写 Router,修复 uid=102 的假在线状态,一方面会封装相关数据发送到 MQ,由 Extlogic 进行离线用户的召回逻辑处理;
-
2-3:若 Entry 可以在内存中找到 uid=102 的连接句柄,则基于客户端与 Entry 之间的长连接,将消息推送给客户端。
这是推送消息阶段所要完成的事情,有几个关键的设计点需要注意:
-
Logic 分别通过访问 Router 和 Entry 识别出假在线用户,然后通过 Extlogic 完成离线用户的召回逻辑;对于真在线和真离线用户,都有相关的处理方式,如果不能识别出假在线用户,会极大影响 IM 系统的可靠性;
-
Logic 除了向 Entry 提供 msg_send 接口外,还需要提供 unreachabled 接口,从整体逻辑上简化流程实现。
访问 Router 和 Entry 是识别用户假在线的两重保障,第三重保障发生在第三阶段。
三、确认消息阶段
在第二阶段中,Entry 若从内存中找到客户端的连接句柄,会基于此将消息推送;在 Entry 内存中存在句柄的客户端,仍然有可能是假在线状态;确认消息阶段完成客户端接收到消息后向服务端回复确认的过程。
-
3-1:客户端 uid=102 接收到消息后,构造确认包,基于与 Entry 之间的长连接,发送给 Entry;
-
3-2:Entry 基于与 Logic 之间的 RPC 连接(Entry 是客户端,Logic 是服务端),调用 Logic 的 msg_ack 接口,向 Logic 确认消息已经被客户端 uid=102 成功接收;如果在一定时间内(比如15秒),Logic 没有接收到客户端发送的该条消息的确认请求时,根据消息的级别和产品规则,要么对该消息进行重复推送,要么认为客户端 uid=102 已经离线,此时 Logic 一方面写 Router,一方面封装相关数据发送给 MQ,由 Extlogic 进行离线用户的召回逻辑处理。
这就是识别假在线用户的第三重保障。确认消息阶段所要完成的事情不多,但有一个非常关键的设计。
大家想一下,对于 Logic 推送的每一条消息,Logic 都要等待(假设15秒)其被确认;对每一条消息单独开启一个线程,我们可以很容易落地实现;如果在15秒内,有10万条消息被推送,是不是要开启10万个线程来出来呢?有没有更轻量级的实现方案呢?(我们在下篇文章中解析该方案)
总结文中关键
-
用户登录时,Logic 完成四件事情:调用 Passport 进行用户合法鉴权、将客户端与 Entry 的映射关系写入到 Router 、将用户登录事件封装成消息发送到MQ、回复 Entry 是否要保留客户端连接;
-
判断用户是否重复登录的关键是访问 Router 获取用户登录状态,如果已经登录,通过发送 kickout 包进行踢人处理;
-
实现用户多端登录的核心是 Router 支持 三元组存储,即 <uid , <type,entryIp>> ;
-
用户登出时,Logic 完成三件事情:清理 Router 中用户在线数据、将用户登出事件封装成消息发送到MQ、回复 Entry;
-
当客户端心跳失活时, Entry 模拟客户端构造用户登出请求。
-
IM,要求消息具备 “及时性” 和 “可靠性”;
-
在用户量较大的分层架构的 IM 系统中,通常基于 “电话模型” 实现消息从服务端到客户端的传输;
-
分层架构 IM 系统的消息收发流程,包括三个阶段:
-
生产消息阶段,完成客户端成功发送消息到服务端的过程;
-
推送消息阶段,完成服务端推送消息到客户端的过程;
-
确认消息阶段,完成客户端接收到消息后向服务端回复确认的过程。
-
-
识别用户假在线状态的三重保障:
-
访问 Router;
-
访问 Entry;
-
客户端回复确认包。
-