本文主要介绍APP功能中的IM模块的设计方案
1 设计原则
- 合适原则——合适优于业界领先
- 简单原则——简单优于复杂
- 演化原则——演化优于一步到位
架构设计的目的在于解决系统复杂度问题,真正优秀的架构都是企业当前人力、条件、业务等各种约束下设计出来的,能够合理地将资源整合在一起并发挥出最大功效、并且能够快速落地。
2 系统复杂度分析
2.1 优先重点考虑
- 高性能
消息尽量低延迟 - 成本
人力,服务器资源有限,需要做到相比之前使用付费环信更节约成本
2.2 相对重点考虑
- 高可用
因为IM不是非常关键功能,只是APP的附属功能,目前阶段可暂时不考虑故障自动恢复,先采用故障手工恢复 - 可扩展性
功能相对固定,后期功能扩展较少 - 规模
使用该功能的用户规模短期不会爆炸增长
2.3 其他
- 旧APP版本兼容
旧版本基于环信SDK,需要支持有环信和无环信用户互聊 - 消息的有序性
- 消息已读未读状态更新
3 设计备选方案
3.1 通信方案
- 方案1:个推+http接口方案
- 方案图
优点
技术成熟,基于现有技术,不用额外引入新技术缺点
用户发消息频繁建立连接,服务器压力较大
消息时延较大,个推推送消息时延较大,存在丢消息的可能性
- 方案2:TCP长连接方案
- 方案图
优点
消息实时性较好
基于单条长连接,消息乱序的可能性较小缺点
客户端和服务端需要学习使用新的技术方案维持长连接
增加新的机器成本
3.2 存储方案
方案1: 聊天记录服务端存储
- 优点
支持聊天记录漫游 - 缺点
增加服务器成本
增加服务端开发成本
增加客户端,服务端接口对接
方案2: 聊天记录客户端存储
- 优点
节省存储成本 - 缺点
聊天记录不支持漫游,增加客户端开发成本
具体存储设计如下:
自研IM系统存储设计
4 方案评估和选择
基于成本和快速开发的考虑,选择个推+http接口方案,聊天消息保存服务端
5 关键难点解决方案
字段名定义
clientMsgId 消息的客户端UUID,不重复,由客户端生成
serverMsgId 消息的服务端ID,不重复,由服务端递增规则生成,
msgIsResend 消息是否是重发消息,1-是,0-否
hasMsgToReceive 是否有消息待接收,1-是,0-否
msgAccept 消息是否成功被服务端接收,1-是,0-否
5.1 APP新旧版本兼容
问题
旧版本APP使用环信通信,新版本使用自研IM通信,需要做到兼容使得新旧版本客户端可以互聊解决
新版APP做兼容,可以兼容收环信消息,发环信消息,根据新版本发消息时,根据接收方用户的版本号,判断发环信消息,还是发自研IM消息
5.2 发送消息与接收到的消息乱序问题
问题
用户发送消息的前,有可能有其他旧的未读消息未到达,
消息发出去后,未读消息才到达,导致聊天界面展示消息乱序。解决
采用客户端存储聊天记录的方案,发送消息的同时获取未读消息,再一并展示给用户看
5.3 消息发送传输失败重发问题
问题
消息已经发送给服务端,服务端处理成功并返回,但是因为网络问题返回失败,客户端重发消息,导致服务端消息重发解决
服务端基于客户端上报的消息的clientMsgId,msgIsResend,进行消息重复校验,如果消息已发的是重复消息,不再重复发送
5.4 发消息时拉取消息消息包过大问题
- 问题
客户端在发消息时服务端一并返回未收消息,假定每次限定最多返回1000条消息,当服务端积压超过1000条消息,消息无法一次返回
- 解决
客户端获取消息的有序性
在一个聊天对话中,客户端本地已有消息的时间戳(服务端创建),总是处于服务端的所有待收消息的时间戳的过去时间。
下图只有A情况满足有序性条件
实际使用上,积压大量消息未接收发生的概率极小,初步解决如下
- 服务端返回消息报文中带一个字段,hasMsgToReceive,代表是否有消息待接收
- 当未收过多未能一次返回,需要客户端再请求一次才能全部返回,hasMsgToReceive=1,msgAccept =0
- 已经全部返回,hasMsgToReceive=0,msgAccept =1
- 为了保证客户端获取消息的有序性,当服务端消息未能正常一次返回,服务端不发送本条消息,需要客户端再次重发消息
5.5 拉取未读消息传输失败问题
问题
APP拉取未读消息时,服务端成功处理并返回消息,在消息网络传输到达到达APP之前,网络中断,消息传输失败,导致丢消息。解决
拉取消息时请求参数待最新已收消息ID(serverMsgId),服务端根据这个ID获取未读消息和删除历史已读消息
6 其他待解决问题
6.1 敏感内容,信息过滤监控
目前阶段暂时不做
6.2 消息“已读”、“未读”的标记展示
目前阶段暂时不做
6.3 客户端删除聊天消息
目前阶段暂时不做