AlexiaChen / AlexiaChen.github.io

My Blog https://github.com/AlexiaChen/AlexiaChen.github.io/issues
87 stars 11 forks source link

即时通信消息的可靠性保证---离线篇 #71

Closed AlexiaChen closed 1 year ago

AlexiaChen commented 4 years ago

title: 即时通信消息的可靠性保证---离线篇 date: 2017-10-29 21:26:00 tags:

前言

上一篇讲解了IM消息可靠性保证的双方在线情况,以及实现方案。那如果Client-B不在线,IM应用又是如何保证消息的可达性的呢? 这就是本文所要讨论的问题。

消息接收方不在线时的简化流程

注意,对于步骤4而言,无论是传统的PC客户端IM,比如QQ。还是主流的移动端IM,比如Wechat,消息发送出去以后,无论是对方实时在线收到还是对方不在线被IM-Server离线存储了,对于发送方而言只要消息没有因为网络等原因莫名消失,就应该被认为是对方收到消息了。

从技术角度讲,消息接收方是“收到消息发送消息Ack包”的真正发起者,但是实际有两种可能,一种是接收方发出,而另一种是由IM-Server代为发送(伪应答)。

典型离线消息表的设计以及拉取离线消息的过程

-- 消息接收者ID,一般是帐号,QQ号,微信号,手机号之类的
receiver_uid varchar(50), 

-- 消息的唯一指纹码(即消息ID),用于去重等场景,单机情况下此id可能是个自增值、分布式场景下可能是类似于UUID这样的东西
msg_id varchar(70), 

-- 消息发出时的时间戳(如果是个跨国IM,则此时间戳可能是GMT-0标准时间)       
send_time time, 

-- 消息发送者ID,也是帐号,QQ号,微信号,手机号这样等价的ID
sender_uid varchar(50), 

-- 消息类型(标识此条消息是:文本、图片还是语音留言等)
msg_type int, 

-- 消息内容(如果是图片或语音留言等类型,由此字段存放的可能是对应文件的存储地址或CDN的访问URL)
msg_content varchar(1024), 
…

接收方B要拉取发送方A给它发送的离线消息,只需在recceiver_uid(即接收方B的用户ID),sender_uid(即发送方A的用户ID)上查询,然后收到消息后,接收方B发送Ack:notify包通知IM-Server,这时候IM-Server才把离线消息删除。

SELECT msg_id, send_time, msg_type, msg_content 
FROM offline_msgs
WHERE receiver_uid = [接收方ID] and sender_uid = [发送方ID]

注意,因为接收方B,User-B一旦登录上线,就会首先拉取离线消息,遍历自己的好友列表,获取好友的用户ID,填入发送方ID,发送一条“获取所有好友离线的请求”到IM-Server,这样就知道谁昨天,前天,几天前给我发消息了。必须遍历好友列表,因为User-B不知道谁在它离线的时候给它发过消息。

步骤1,User-B开始拉取User-A发送给它的离线消息 步骤2,IM-Server从DB-Server中拉取离线消息 步骤3,IM-Server从DB-Server中把离线消息删除 步骤4,IM-Server把离线消息返回给User-B

上述流程中存在的问题以及优化方案


一旦User-B有很多好友,登录时客户端需要对所有好友进行离线消息拉取,客户端与IM-Server互交次数就会比较多

for(const auto& id : friend-id-list){
    getOffLineMessage(user-B-id, id); // RPC或者是http协议的封装或者自定义协议
}

优化方案一:

类似于lazy load的方式,当User-B对User-A感兴趣,点击User-A头像图标打开聊天对话框的时候,这时候再发送获取User-A发送给User-B的离线消息。这是按需拉取优化。 但是这个方案一般在实际场景中不用,因为聊天消息如果是按需才拉取,那么User-B可能永远也不会点击User-A的头像,因为你看各种IM App都不是这么做,你一旦登录,就会有消息框弹出来,XX给你发了消息,不然你很可能错过重要的消息,容易误事。一般登录都要主动提示拉取。

所以这里可以变通一下,还是lazy load的思想,User-A登录,拉取给User-A发送离线消息的好友的ID列表,也就是,弹出消息框,报告User-A,XXX,XXX给你发离线消息了,你点开哪一个,我就去请求IM-Server去拉取对应用户发过来的Message Content。如果把消息框给关闭了,那么直接就不去拉取了,这样流量就小了很多。特别适用于移动端。

优化方案二:

User-B在登录的时候,一次性拉取所有好友给它的离线消息,把这些离线消息在本地持久化,可以存储在SQLite中,也可以自己定义数据文件格式,当然,无论什么样的方式,数据总是类似以下二维表的方式:

sender_uid    send_time             message_content
User-A-id      xxxxx                    你好
User-C-id      xxxxx                 晚上来我家吃饭
User-F-id      xxxxx                 领导,明天请假

这样,开始流量是大一些,但是一劳永逸,到时候按需查询本地数据文件即可。

接收方一次性拉取大量离线消息导致速度慢,卡顿的解决方案

卡顿其实不存在的,速度慢倒是可能,卡顿其实就是UI线程被阻塞了一样,是把网络请求离线数据的工作放到了UI线程上,这样其实是不合适的,UI线程只需要关注用户点击,拖拽等UI事件。把计算量大,网络IO,文件IO等延迟性高的放到后台的worker线程中。现在有各种响应式编程框架,什么RxJava,RxJs等,已经不需要担心不同线程互交数据很难了,而且很轻松就可以做到不用显式加锁,完全异步。

这里的速度慢只是数据量大,传输到Client端需要很多时间,但是这个请求数据是后台线程来做的,从UI上用户是感觉不到速度慢的,用户自己也不知道有没有离线消息,如果要做到离线消息快点到达的话,做离线消息分页请求就可以了。

注意,响应式编程与信号槽机制还是有区别的,信号槽机制是对象之间观察者模式的一种实现,响应式编程与信号槽很相似,但是它在相似的基础上还加强了数据流的概念,而观察者模式非常适合描述这样的数据流。

解决重复拉取离线消息的问题

如果一次性拉取离线消息到本地持久化,那么不存在重复拉取的问题。