在即时消息的场景里,消息的多终端漫游是一个相对比较高级的功能,所谓的“多终端漫游”是指:用户在任意一个设备登录后,都能获取到历史的聊天记录。

如何实现多终端消息漫游

两个前置条件:一种是通过设备维度的在线状态来实现,一种是通过离线消息存储来实现。


设备维度的在线状态

对于在多个终端同时登录并在线的用户,可以让 IM 服务端在收到消息后推给接收方的多台设备,也推给发送方的其他登录设备。
这样的话,就要求能够按照用户的设备维度来记录在线状态,这个其实也是支持多端登录的一个前提。

离线消息存储

另外,如果消息发送时,接收方或者发送方只有一台设备在线,可能一段时间后,才通过其他设备登录来查看历史聊天记录,这种离线消息的多终端漫游就需要消息在服务端进行存储了。当用户的离线设备上线时,就能够从服务端的存储中获取到离线期间收发的消息。

image.png

image.png

离线消息同步的几个关键点

离线消息的存储和普通消息相比差别在哪?
应该怎么存?
每次上线怎么知道应该拉取哪些离线消息?

离线消息的存储也是在不超过大小限制和时效限制的前提下,采用 FIFO(先进先出)的淘汰机制。

多端消息同步机制

版本号机制

  • 每个用户拥有一套自己的版本号序列空间。
  • 每个版本号在该用户的序列空间都具备唯一性,一般是 64 位。
  • 当有消息或者信令需要推送给该用户时,会为每条消息或者信令生成一个版本号,并连同消息或者信令存入离线存储中,同时更新服务端维护的该用户的最新版本号。
  • 客户端接收到消息或者信令后,需要更新本地的最新版本号为收到的最后一条消息或者信令的版本号。
  • 当离线的用户上线时,会提交本地最新版本号到服务端,服务端比对服务端维护的该用户的最新版本号和客户端提交上来的版本号,如不一致,服务端根据客户端的版本号从离线存储获取“比客户端版本号新”的消息和信令,并推送给当前上线的客户端。

image.png

离线消息存储超过限额了怎么办?

如果离线消息存储容量超过限制,部分增量消息被淘汰掉了,会导致根据客户端最新版本号获取增量消息失败。

这种情况的处理方式可以是:直接下推所有离线消息或者从消息的联系人列表和索引表中获取最近联系人的部分最新的消息,后续让客户端在浏览时再根据“时间相关”的消息 ID 来按页获取剩余消息,对于重复的消息让客户端根据消息 ID 去重。

因为消息索引表里只存储消息,并不存储操作信令,这种处理方式可能会导致部分操作信令丢失,但不会出现丢消息的情况。因此,对于资源充足且对一致性要求高的业务场景,可以尽量提升离线消息存储的容量来提升离线存储的命中率。

离线存储写入失败了会怎么样?

在处理消息发送的过程中,IM 服务端可能会出现在获取到版本号以后写入离线消息存储时失败的情况,在这种情况下,如果版本号本身只是自增的话,会导致取离线消息时无法感知到有消息在写离线存储时失败的情况。

因为如果这一条消息写离线缓存失败,而下一条消息又成功了,这时拿着客户端版本号来取离线消息时发现,客户端版本号在里面,还是可以正常获取离线消息的,这样就会漏推之前写失败的那一条。

怎么避免这种离线存储写失败无感知的问题呢?

一个可行的方案是可以在存储离线消息时不仅存储当前版本号,还存储上一条消息或信令的版本号,获取消息时不仅要求客户端最新版本号在离线消息存储中存在,同时还要求离线存储的消息通过每条消息携带的上一版本号和当前版本号能够整体串联上,否则如果离线存储写入失败,所有消息的这两个版本号是没法串联上的。

这样,当用户上线拉取离线消息时,IM 服务端发现该用户的离线消息版本号不连续的情况后,就可以用和离线消息存储超限一样的处理方式,从消息的联系人列表和索引表来获取最近联系人的部分最新的消息。

消息打包下推和压缩

一般针对离线消息的下推会采用整体打包的方式来把多条消息合并成一个大包推下去,同时针对合并的大包还可以进一步进行压缩,通过降低包的大小不仅能减少网络传输时间,还能节省用户的流量消耗。

发送方设备的同步问题

版本号机制中,我们在下推消息时会携带每条消息的版本号,然后更新为客户端的最新版本号。而问题是发送方用于发出消息的设备本身已经不需要再进行当前消息的推送,没法通过消息下推来更新这台设备的最新版本号,这样的话这台设备如果下线后再上线,上报的版本号仍然是旧的,会导致 IM 服务端误判而重复下推已经存在的消息。

针对这个问题,一个比较常见的解决办法是:给消息的发送方设备仍然下推一条只携带版本号的单独的消息,发送方设备接收到该消息只需要更新本地的最新版本号就能做到和服务端的版本号同步了。