一些在设计IM通讯模块时的架构经验

前言

IM模块承载着复杂功能,需要提供通讯支持,数据支持,以及页面的支持。随着项目的进度推进,业务信令,以及数据类型会增加的非常快,对业务功能的快速响应尤为重要。

UI模块

思路可以沿用MVVM的思路,通过VM来驱动UI的更新,通过实现不同的VM协议来制定不同的UI展示规则。

需要注意的是尽量避免相似业务维护这多套代码。比如单选用户转发消息,多选用户创建群聊,从群员列表选择用户操作,这几个业务就存在相似的业务逻辑,区别可能在不同的数据源,不同的选择逻辑,不同的数据验证逻辑。这样就可以通过抽象ViewModel的方法,来共用一套UI选择展示界面。

通讯/数据模块

多使用协议来抽象功能模块逻辑,方便后期支持多种业务。比如,数据接收器的功能逻辑大致相同,启用的逻辑也大致相同,那么使用协议实现可以更容易的维护逐渐增多的接收器;

为功能模块提供插件注入口,通过加载不同的插件,使新功能在原有逻辑中也能生效,而不需要修改原有逻辑。比如,消息信令的处理,只需实现不同的消息信令处理插件,并注册到消息接收器中,新的信令协议就可以生效。

IM业务存在高并发的场景,在设计通讯/数据模块时,需要预先规划下线程的使用,避免后期由于线程资源问题引起的性能瓶颈。

UI模块设计

UI层的模块设计需要注意高并发情况下的性能优化。

比如在接收高流量的消息的时,如聊天详情页面很容易陷入瓶颈。

  1. 渲染频次过高会导致主线程阻塞,比如复杂的聊天气泡,会导致App卡顿,可以设计异步串型队列来限流高并发状态下的渲染频次。
  2. 不必要的在主线程的数据库操作也会造成主线程阻塞,可以通过缓存减少查询次数,也可以改为异步执行。
  3. 加载过多的资源,会导致App OOM,特别是对数据库资源的占用(Realm有这个问题)会有额外的资源消耗,需要制定合适的资源释放逻辑,来保证内存的占用情况,自动释放池也可以解决一些简单的内存管理问题。

通讯/数据模块设计

网络层

TCP连接管理模块(IMSocket)

IM最先要做的是建立与服务端的Socket链接,我选用了GCDAsyncSocket作为底层来搭建整个TCP管理模块。

  1. 网络状态监控使用AFNetworkReachabilityManager,需要根据网络状态来执行TCP的断连或者重连。
  2. 实现Socket的连接状态维护,当Socket连接状态变化时,发送通知。
  3. 提供Socket的配置API,方便后期做环境切换。
  4. 根据通信协议生成用于发送的数据包,并对包体内容进行AES加密。
  5. 接收到数据后,对数据进行预处理,先判断数据是否粘包,对数据包进行拆包。待包完整后,对数据进行AES解密。
  6. 心跳策略,根据不同的网络选择不同的心跳间隔。

TCP请求管理模块(IMEngine、IMRequestDispatcher)

完成TCP连接后,需要实现TCP的请求管理,如维护AskId,公共请求参数配置。

  1. 通过AskId维护请求与接收响应的回调。实现只响应一次的回调以及可多次响应可手动注销的回调。
  2. 接入protocol buffer与服务端定义通信协议。
  3. 实现基于protocol buffer的请求协议与响应协议,定义每个请求的请求类型,以及响应时用哪个模型解析数据。
  4. 实现请求超时逻辑与请求取消逻辑。

IM请求管理模块(IMConnectAssistant、IMHTTPAssistant)

对TCP、HTTP请求进行封装管理,配置公共请求参数,管理连接的重试逻辑。

  1. 在请求的生命周期中,增加注入口,方便业务使用时有更多的时机可以选。例如,在请求构造完成且未发送时,提供修改请求参数的回调。
  2. 维护请求的生命周期状态,如等待发起,发起中,完成响应,被取消等。
  3. 维护请求队列,当请求失败后,记录下请求,根据策略放入重试队列。

长链接通知分发模块(IMNotificationAssistant)

处理基于IM协议的长链接回调,对响应进行识别处理,根据内容通知不同的模块触发动作,比如新消息提醒,会话配置变更通知。

为通知分发模块开一个并行队列,但需要限制队列的最大并发数,防止高并发时线程爆炸。

TCP消息通信协议(TCPMessage,HTTPRequest)

实现与服务端PB协议的TCP请求封装,与服务器的HTTP请求封装。

数据层

IM状态管理模块(IMManager)

主要负责网关鉴权流程,IM登录流程,IM用户信息管理,注册其他模块等。

  1. 实现网关鉴权逻辑,IM服务心跳包管理。
  2. 实现IM登录逻辑,登录失败后重连逻辑,Socket中断后重连逻辑。
  3. 实现用户注销逻辑,用户数据清理逻辑。
  4. 配置用户数据库,注册数据集合观察器。

举例:比如当用户的token变化时,需要重新链接IM。当网络出现抖动时,需要开启IM登录重试。当用户完成IM登录时,需要拉取离线消息,群组信息等。

IM数据库(IMDB)

我选用的是Realm作为数据库框架,数据集合的更新通知挺好用。

  1. 保证一人一库,实现用户数据隔离。
  2. 维护IM相关业务的数据模型管理。
  3. 维护数据库迁移。
  4. 维护数据库连接池。

模型与扩展(Model、Extension)

管理IM相关的数据模型,比如消息、会话、用户、群组等。

Model的Extension默认分为3个部分,Attributes、Database、Factory。

  1. Attrubtes用于类属性的扩展,如判断消息是否属于加密类型,会话是否属于群组会话等。
  2. Database用于数据库CRUD的扩展,如获取未读消息数,查询会话中的关联消息等。
  3. Factory用于模型特定形式的构建,如构建文件分享的消息,根据用户创建聊天回话等。

####数据接收器(IMDataFetcher)

使用协议实现IM数据的获取逻辑,分为主动获取与被动通知获取两种形式,包含HTTP与TCP API。

  1. 维护接收器的获取状态。
  2. 注册通知接收器,如当发生群组列表更新时,新消息提醒时,触发数据接收动作。
  3. 为存在高并发场景的数据接收器增设任务队列。当队列空闲时,直接触发任务。当队列繁忙时,任务会进入队列等待,由定时器触发任务。另外获取消息这样的API存在重复性,堆积在任务队列中请求所能响应的数据时相同的,所以当定时器触发任务时,可以将失效的任务直接丢弃处理,避免重复请求。

####消息处理器(IMMessageProcesser)

抽象接收消息逻辑,统一处理业务方提供的多条消息通道。

不同通道对消息的处理有所不同,需要提供功能插件,在消息完成模型构建和校验后,对消息进行处理。

举例:分包消息则对消息进行转存处理,撤回消息指令则直接执行后丢弃指令消息等。

数据集合监听器(IMDBObserver)

通过数据库通知自动触发,承担了大部分的数据更新需求。

  1. 这里用到了Realm的特性,Realm可以根据规则指定一个数据集合,比如所有isRead为False的未读消息集合。当数据集合中的发生数据新增,数据修改,数据删除时,均能通知给注册者。
  2. 我们对特定数据集合进行监听,当数据集合发生变化时,触发预设操作。比如,有新消息入库时,触发对应会话的最近消息属性更新。当群组信息(名称)变更时,触发对应会话的属性更新。
  3. 为存在高并发情况的DBObserver创建任务队列。当集合的更新频率过高时,也将任务存入队列,使用定时器限制任务并发量。

举例:当消息处理器将新消息入库后,监听器会获取到新消息,并判断是否需要更新对应会话,是否出发消息提醒,是否根据消息的类型来执行不同的任务。

模型数据源(IMDBSource)

主要用于为UI层提供数据。

  1. DBSource中配置了数据集合,数据集合的监听器,监听器的回调代理,监听器的回调线程。
  2. 监听器代理包含了数据CUD,UI层通过实现监听器代理,即可实现数据驱动的显示模式。

举例:在聊天详情页面,分页获取消息数据。当有新消息入库时,通过DBSource通知UI层进行更新。

封装UI层对数据的获取需求。