IM 技术演进阶段
IM v1.0 阶段(简单、可用)

实现功能
-
用户/客服接入
-
消息收发
-
咨询列表管理
设计原理
-
通过「消息转发模块」中消息轮询协程操作 Redis 缓存进行消息分发
-
使用 Protobuf 解决数据传输问题
-
使用 Java NIO 的开源框架 Netty 异步非阻塞、事件驱动、高性能、高可靠、高可定制性的网络应用程序
技术选型
-
(v1.0 选型)基于 Scoket 原生:代表框架 CocoaAsyncSocket
-
(v2.0 选型)基于 WebScoket :代表框架 SocketRocket
-
基于 MQTT:代表框架 MQTTKit
-
基于 XMPP:代表框架 XMPPFramework
两种方案设计
-
集中式:简单的单通、双通,延迟率要求不高的消息发送。
-
分布式:实时的双通、多通,延迟率高要求的视频/语音通话。
问题
FAQ:TCP 选型,实现通讯信息的真正加密,复杂场景下更灵活的通信。
问题 1:分/黏包
-
分包(IP分片传输导致):指接受方没有接受到一个完整的包,只接受了部分。
-
黏包(TCP协议本身合并机制):指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
解决:
-
方案一:包头长度字段
-
方案二:包尾分隔标识
应用:消息头长度为2个字节,所以消息包的最大长度需要小于65536个字节,netty会把消息内容长度存放消息头的字段里,接收方可以根据消息头的字段拿到此条消息总长度。
问题 2:数据丢失
- 数据丢失,写 db 之前 App crash,虽然数据在网络层可靠抵达了,但没存进 db,下次用户打开 App 消息自然就丢失了,如果不在业务层再增加可靠性保障,网络层面不会重发,那么意味着这条消息对于 Receiver 永远丢失
解决:
-
方案一:应用层 Ack 消息(消息回调机制)
-
方案二:应用层 Seq ID(连续性编号)
IM v2.0 阶段

开发要求
-
提升服务业务功能
-
增加可选配置,例如自动回复、FAQ
发展历程
- 第一阶段:短轮询 Polling(频繁的异步 JavaScript 和 XML (AJAX) 请求来实现轮循)
- 第二阶段:长轮询 Long polling(由服务端决定回执的长轮询机制)
- 第三阶段:基于 AJAX、Flash Socket 等流实现
- 第四阶段:WebSocket HTTP 5.0 全双工
区别整理
- WebSocket 是应用层的协议,Socket 是传输层的抽象
- WebSocket 是全双工的 HTTP 通讯协议
设计原理
-
建立一条可复用、可检索的消息线路(通过 HTTP 协议进行一个握手的动作,然后单独建立一条 TCP 的通信通道进行数据的传送)
-
分配服务优化:平均、权重、排队、AI
-
消息服务配置:风险检测、离线存储、消息队列、更新未读数、自动回复
保障性机制
-
方案一:应用层 Ack (确认字符) 消息
-
方案二:应用层 Seq ID(每个 Message 分配一个 Seq ID,这个 Seq ID 对于单个用户的接受消息队列来说是连续的)
心跳机制
定时心跳包
每隔若干时间发送一个固定信息给服务端,服务端收到后及时回复一个固定信息,如果服务端若干时间内没有收到客户端心跳信息则视客户端断开,同理如果客户端若干时间没有收到服务端心跳回值则视服务端断开。 ① 前台时,8秒发送一个心跳包; ② 切换到后台时,30秒发送一次,根据自己的实际情况修改一下即可。心跳包用于维持长连接以及检测长连接是否断开等。
智能心跳包
1)App 前台时:五次延迟测试确定网络情况良好,进入长心跳模式;在网络波动较大的情况,使用短心跳,保证收取消息相对及时。 2)App 后台时:先用几次最小心跳维持长链接,进入后台自适应心跳计算。(尽量选择用户不活跃的时间段,来减少心跳计算可能产生的消息不及时收取影响。)
重连流程
-
重连被触发时,如果该次连接成功,退出重连
-
反之重连失败后,会判断当前重连的次数是否超过预期值(这里设为6次),并对重连次数计数,如果超过就会退出重连
-
休眠预设的时间后再次进行重连操作
重连触发条件分为三种:
-
主动连接不成功(主动连接Socket,如果连接失败,会触发重连机制)
-
网络被主动断开(正常建立连接,操作过程中,网络被断开,通过系统广播触发重连)
-
服务器没响应,心跳没回值(服务端心跳预设时间内没回值,客户端认为服务端已经断开,触发重连)
发送消息类别
-
一类是 IM 相关数据的请求(HTTP),例如:历史消息列表,会话列表等
-
二类是 IM 消息的发送(Socket),主要是文字消息,包括上传到服务器的资源文件链接等
-
三类是辅助通信类型(混合),一般封装进 SDK 内部,例如:心跳包、回执、鉴权等
对应后端 API 设计: 2.1 保活心跳包(C->S) HeartbeatPackage【13】 2.2 响应心跳包(S->C) XXX【??】 2.3 探查客户端是否活跃(S->C) XXX【??】 2.4 拉取离线消息(C->S) ClientRequestMessage【13】 2.5 响应离线消息(S->C) Protobuf【22】 2.6 发送聊天消息(C->S) ChatMessage【10】 2.7 响应发聊天消息(S->C) Protobuf【29】 2.8 接收聊天通知(S->C) ChatMessage【10】 2.9 接收代办通知(S->C) TodoMessage【17】 2.10 漫游和搜索聊天历史(C->S) ClientRequestMessage【13】 2.11 响应漫游和搜索聊天历史(S->C) Protobuf【30】 2.12 获取好友列表(C->S) ClientRequestMessage【13】 2.13 响应好友列表(S->C) Protobuf【18】 2.14 获取圈子列表(C->S) ClientRequestMessage【13】 2.15 响应圈子列表(S->C) Protobuf【20】 2.16 获取代办列表(C->S) ClientRequestMessage【13】 2.17 响应代办列表(S->C) Protobuf【23】
业务缓存策略
(先查数据库后展示,没有再请求再显示)
1)首次会话:先查数据库 -> 显示UI -> 网络请求 -> 算出未读数据存入数据库 -> 显示UI 2)拉取历史数据:先查本地数据 -> 根据字段 msgid 是否为 0 请求数据并存储 -> 显示UI 3)单条消息发送:发送信息 -> 标记为插入失败 -> 插入数据库 -> 发送信息(成功接收回执:变为成功状态存储,发送失败或没有接收到回执:进入重发流程)-> 显示UI
IM v3.0 阶段
实现功能(服务拆分)
-
业务服务:客服群组/成员管理、搜索服务、质检服务、工单服务、优惠券、订单服务、自动回复、FAQ
-
用户服务:用户状态、行为轨迹(画像)、评价服务
-
IM 服务:单聊、群聊、通知、私信、联系人、文件服务、风控服务、敏感词过滤
-
数据服务:采集、分析、统计
功能模块主要分为 (现代 IM 系统中的消息系统架构——实现篇:www.infoq.cn/article/N6s…)
-
消息存储:读、写、持久化,单行写入,批量读取,多维检索、全文检索的模糊查询
-
关系维护:人与人的关系、人与群的关系以及人与会话的关系
-
即时感知:会话池方案,消息存储 > 第二类:同步库 > 新消息即时统计
-
多端同步:会话的未读消息数需要在应用服务侧维护、对自己的信息不计数在最新消息摘要中需要做更新
分页设计
-
规则:从数据库拉取
-
首次:sequenceId 范围 + 倒序
-
翻页:第一次请求的最小 sequencId 发起第二次请求
需求分析
-
可能在有些需求中,消息还会区分平台,比如ios/andoid/mac/windos等
-
关于信息的更新:某一个人的个人头像or昵称发生了变化,这时候,我们对应的消息页面的UI,朋友列表等需要更新。这种类型的通知,一般是通过tcp推送,后台通过查找该用户的uid然后查找到他的会话跟好友列表然后进行推送。
-
关于消息的设计:目前我所见是2种,一种是类型微信的常见,具有离线消息的概念,但是一旦客户端确认接收到,服务器将不会保存,也就是不能跨设备保存用户记录,一般用tcp实现。还有一种是类似钉钉那种,有一个同步消息的概念,这种情况下一般使用Tcp+Http实现。他们的逻辑相差还是比较大,会另外写一篇文章描述。
数据库改版
数据库设计
- 会话表
- 聊天详情表
- 群组表
- 群组信息表
- 群成员
- 联系人表
会话表
Id:自增长主键
Uid:该条消息所属消息,比如我登陆了,我发送/接收到消息入库的时候写入自己的uid,他的作用是多用户登陆的时候区分回话表
chatId:服务器生产回话 id当前的回话id,它作用是标识一个回话,比如我跟你聊天or 你跟我聊天,我们的回话id应该是一致的,对于群聊也是,在群中发送消息,每个人的回话id是一致的
c_id:他是标记一台设备上某个用户的唯一回话,用于更新避免插入多条数据的,可以用到sqlite的update or replace,他的值可以是hash(uid+chatid)或者其他,该字段可以只是客户端具有,客户端生成并且自己维护。
From:发送人id(自己发送就是自己的uid,不然就是别人的uid)
to:接收人id(uid/group_id)
last_msg:最后的一条消息内容
last_user_name:最后的发送者名称
last_time:最后消息发送时间
chat_type:回话类型(群组消息/个人聊天/系统消息)
msg_type:消息类型(文字/图片/文件/音乐等)
unread_count:改回话未读数目
聊天详情表
id:自增长主键
msg_id:消息唯一id,一般服务器生成,或者客户端本地使用UUID生成
uid:所属者uid
from:发送者uid
from_avatar:发送者头像
from_nam:发送者名称
to:接受者(uid/group_id)
chat_type:会话类型
msg_type:消息类型
msg:消息内容
file_info:文件信息json格式
send_time:发送时间
send_status:发送状态 发送中,发送完成,发送失败
extra:把人插入 一般可以为null,预留的额外字段,使用JOSN字符串存储
群组表
Id:自增长主键
group_id:群组唯一id
group_name:群组名称
group_name:群组头像
group_type:群组类型
group_num:群组数量
group_create_uid:群组创建者uid
群组信息表
Id:自增长主键
group_id:群组唯一id
group_name:群组名称
group_name:群组头像
group_type:群组类型
group_num:群组数量
group_create_uid:群组创建者uid
group_intrduce:群组简介
nick_name:个人的群昵称字段。
group_role:群组角色字段,比如你是管理员/群主/普通成员等(该字段的作用是可以用来做一个权限控制,比如在一些群里面,需要特定的人才可以拉人,需要群主才可以删除成员等,该字段是server下发的,根据不同的uid请求返回不同的role)
group_members:部分群成员的List的JSON数据,该字段看UI设计,可能有的UI在群信息页面默认显示几个群成员,然后点击进入通过另外的接口查看全部群成员。如果是这种情况下,可以在群信息接口下发该群的一些必要显示成员既可。当然,这个字段也可以不用,用一个或者群成员接口替换,查看群信息的时候也同时请求群信息和群成员接口也可以
群成员表
Id:自增长主键
group_id:群组唯一id
user_nam:用户名
user_avatar:varchar 头像
group_role:角色
联系人列表
Id:自增长主键
uid:所属者uid
sex:性别信息
birthday:生日信息
user_name:用户名
user_avatar:varchar 头像
Relation:关系(比如好友/陌生人)
数据库升级
- 顺序型策略:不能跨版本升级。一个从版本1到3,需要先升级到2再到3
- 阶梯型策略:能跨版本升级。设计嵌套数据库迁移功能,进行1-2-3自动检测,通过中间层进行升级
IM v3.5 重构

开发要求
-
通用性
-
可用性
-
降级策略
设计原理
-
IM 服务抽取成独立的模块,对外提供统一的集成和调用方式
-
监控管理提高线上质量

-
IMSocketConfig: 是 SDK 的配置入口,主要完成服务器配置及相关参数设置的入口。
-
IMSocketManager: 管理登录、退出、连接管理等功能。
-
IMAPIManager: 负责消息的收发,完成会话管理、好友的添加删除,黑名单的管理。
-
IMResponseHandle: 负责接收多种类型消息回调及 delegate 事件分发等功能。
问题:消息丢失和重复
FAQ:解决消息丢失和重复问题?
-
长连接轮询方式:带上客户端已读消息的 ID,由服务端计算出差值消息然后返回
-
WebSocket 方式:服务端会在推送给客户端消息后,等待客户端的 ACK,如果客户端没有 ACK,服务端会尝试多次推送
前端 UIKit 实现
1)基于 UICollectionView 预加载和缓存气泡的大小计算 2)自动增长输入视图 3)多种包含的气泡类型
-
文本
-
大型表情符号
-
图像
-
视频
4)内置 SFSafariViewController 访问 Web 页面 5)内置一套基本的 iMessenges 风格示例主体 6)自定义主题系统,用于显示 UICollectionViewCells 的页眉,页脚和输入等视图
Protocol Buffer 数据结构
-
利用 varint 原理压缩数据以后,二进制数据非常紧凑
-
没有数据描述 .proto 文件是无法理解二进制数据流,加密性更好
-
向后兼容,更新数据结构以后,老版本依旧可以兼容
IM v4.0 阶段
可插拔跨平台架构
-
Native 层:负责业务请求封装和数据解析,与原生进行交互
-
Chat 层:负责提供底层通信使用的 c 接口,包含连接、读写和关闭
-
WebSocket 层:实现 WebSocket 协议及维护心跳
-
TLS 层:基于 mbedTLS 实现 TLS 协议及数据加解密
-
TCP 层:基于 libuv 实现 TCP 连接和数据的读写
层级解耦,提高组件的灵活性,实现可插拔
C 语言调用方式
-
Android 采用 JNI 的方式
-
iOS 采用 runtime 的方式
插件化改造
-
将插件行为抽象出一个结构体,利用双向链表将前后插件绑定在一起,使用函数指针调用具体插件的函数或回调
-
通过修改插件的注册提高了灵活性,使得组件具有可插拔性(冷插拔)
存储库的数据建立多元索引:公开群的检索,需要对群 ID、消息发送人、消息类型、消息内容、以及时间建立索引,其中消息内容需要使用分词字符串类型,从而提供模糊查询的能力。做消息的权限管理需要扩展接收人 ID 数组。