在上一篇技术短文(单体架构的 IM 系统设计)中,我们讨论了在 "用户规模小、开发人员少、开发时间短" 的业务背景下,采取 "怎么简单怎么做,怎么快怎么来" 的研发策略,于是设计了 单体架构的IM系统,并分析了 "通讯协议、编程语言和数据库" 的技术选型。我们快速复习一下单体架构的 IM 系统,见下图。
单体架构 IM 系统的每个组件描述如下:
-
客户端是一个业务组件,嵌入在游戏 APP 中运行;
-
客户端通过 http 这种非常简单的短连接的协议方式访问后端的 server;
-
一个 server 程序实现了后端所有的业务逻辑,不过是多个 server 节点集群化部署的(这就是典型的单体架构);
-
server 节点直接访问数据库和缓存;在数据库中分别创建消息表、离线消息表、联系人表和用户表,消息表用来存储用户的历史记录消息,离线消息表用来存储用户不在线时收到的消息; 缓存用来记录用户是否在线的状态。
在该单体架构中,前端与后端的通讯协议是 http,也就是说所有的业务流程动作都是由前端触发的,server 端只是被动响应即可;从这一点出发也足以看出,为了降低 server 逻辑的复杂性,选择 http 通讯协议的必要性。基于该架构,我们讨论一下 IM 核心业务逻辑的实现,包括:用户状态维护、点对点消息收发、云消息。
一、 用户状态维护
用户状态是什么状态呢?就是用户的 "在线" 和 "离线" 状态。很多同学可能会有疑惑,不是基于 "长连接协议" 的客户端才会有 "在线" 和 "离线" 这一回事吗? http 是无状态化的短连接,难道也有 "在线" 一说? 是的,客户端在线与否,其实与通讯协议并不是强相关的,协议仅仅是传递数据的方式而已,甚至我们用 http 比用 tcp 更能精准表达出用户的状态。
通过 http 协议表达用户的在线状态,这一块技术实现应该非常成熟了;大家不妨思考一下,当我们登录163邮箱,把浏览器关闭,10分钟后再次访问 163 邮箱,是不需要重新登录的;这就是 http 实现用户在线状态的关键。
单体架构 IM 系统实现用户状态维护的核心逻辑,见下图。
- 登录
-
客户端向 server 端发送 http 登录请求;
-
因为客户端是嵌入在游戏 APP 中运行的,游戏会有登录逻辑,所以 server 只需要调用游戏侧的登录服务进行鉴权校验即可;
-
登录成功后,server 向 redis 中写入 <uid, {type, cmd, time}> 这样一个 kv 的 session 数据,并设置该 session 的有效期 10秒; type 描述客户端的类型,cmd 描述客户端请求server的接口,time描述写记录的时间。
-
后续,客户端访问 server 端其他所有接口时,server 读 redis ,如果对应的 session 不存在,说明用户未登录或登录已失效,需要重定向引导客户端重新登录。
-
心跳
-
客户端向 server 端周期性(2秒)发送 http 心跳请求;
-
server 延长 redis 中对应 session 的有效期,并修改 session 的 cmd 和 time 属性。
-
-
登出
-
客户端向 server 端发送 http 登出请求;
-
server 直接删除 redis 中对应的 session 数据。
-
在该单体架构的 IM 系统中,对用户状态的维护,就是对 redis 缓存中 session 数据的维护; 在客户端访问 server 其他接口时,server 也会对 session 的有效期和 cmd、time 属性进行修改。
二、 点对点消息收发
消息收发是 IM 系统最核心的功能;基于 http 协议的单体架构 IM 系统的 "点对点消息收发" 逻辑见下图。
- 消息收发
-
client1 向 server 端发送 http 消息请求;
-
server 端向数据库中分别写入 "离线表" 和 "云消息表";(离线表和云消息表在同一个数据库中,通过数据库保证其事务性)
-
client2 向 server 端周期性(2秒)发送 http 拉取消息请求;(拉取消息复用心跳请求)
-
server 端从数据库 "离线表" 中读取 client2 相关记录后返回;
-
client2 再次发送拉取消息请求时,携带上次已经成功拉取的消息msgid,server 端删除 "离线表" 中相关记录。
-
整个消息的收发逻辑并不复杂,【发消息】和【收消息】动作完全由客户端触发,server 端被动响应即可;我们把这样的消息收发模型叫做 【信箱模型】。很明显,信箱模型的实现非常简单,缺点是消息的及时性不高,取决于客户端的心跳动作。
在整个消息的收发逻辑中,有两个细节点需要注意:"离线表" 通常在消息接收方离线时存储其消息,而在该实现逻辑中, server 端不管 client2 是否在线都会直接将消息持久化在 "离线表" 中, 这样处理的原因在于防止 clien2 没有及时拉取消息而造成消息丢失,提高了消息的可靠性; 再一个,当 client2 从 "离线表" 中拉取消息时,不能立刻将其删除,必须在下次拉取时进行删除,这也是消息可靠性的体现。
三、 云消息
所谓 "云消息",指存储在云端的消息,这样的消息可以被反复拉取,用来查看历史记录和实现消息漫游;在上述的消息收发逻辑中,每次客户端发消息时,server 端都会在 "云消息表" 中保存一条记录,所以客户端随时随地都能实现对云消息的读取。见下图。
- 云消息
-
client 向 server 端发送 http 拉取历史消息请求;
-
server 端从云消息表中读取相关记录返回。
-
最后,总结文中关键:
1、"信箱模型" 由客户端主动向服务端发送请求,服务端被动响应即可;该模型实现简单,但作为 IM 系统来说,消息的实时性不高;
2、 基于 "信箱模型" 的单体架构 IM 系统,用户的在线状态通过在 redis 中保存有有效期的 session 来实现,并通过客户端的周期心跳实现 session 的持续有效;
3、 基于 "信箱模型" 的单体架构 IM 系统,发消息的逻辑是发送方向 "离线表" 和 "云消息表" 写入消息记录,收消息的逻辑是接收方从 "离线表" 中读取消息记录; 云消息表用来实现消息的历史记录读取和消息漫游。
在该单体架构的 IM 系统中, server 端完全是无状态化的,在 server 节点负载较高时,可以通过增加 server 节点轻松实现集群扩容,应对不断增长的同时在线用户数。
大家思考一下:在该架构中,消息的实时性不高,应该如何优化呢?优化时需要解决什么问题呢?