IM即时通讯

1 IM系统体系

主要模块
《IM即时通讯》
客户端:一般用于用户收发消息的终端设备,内置的客户端程序和服务端进行网络通讯,用来承载用户的互动请求和消息接收到功能,
接入服务:接入服务是服务端的门户,为客户端提供收发消息的出入口,发送消息一般客户端发送消息到接入服务,然后再由接入服务交到业务层进行处理,
接入服务分为,连接保持、协议解析、session维护和消息推送一般使用的socket保持长连接
连接保持:一般我们使用socket保持长链接
协议解析:提供协议的编解码工作,为了节省流量系统会对传输的内容进行紧凑的编码(protobuf)
session维护:标示用户在那个tcp连接(socket可以基于tcp连接来实现通讯),
消息退送:把消息从服务器传输到用户设备上
业务处理服务:业务处理服务是消息业务逻辑处理层,对于的是服务器,如消息的存储,未读数变更等,包含存储服务,外部接口服务,
存储服务:数据的持久化存储,在服务器储存的好处;

  • 会话一方用户不在线,上线时进行消息推送。
  • 内容审查,监管,电子证据,法律要求。
  • 数据分析

三方外部接口服务:当app没有打开或者在后台运行,消息给到第三方外部接口服务,来通过手机操作系统自身的公共连接服务来进行操作系统级的“消息推送”,通过这种方式下发的消息一般会在手机的“通知栏”对用户进行提醒和展示

即时通讯的特性:

实时性 :立马接受到消息,怎么保证消息的实时性是要解决的问题?
可靠性:分为两块
1.不丢消息:发送的消息不会丢失
2.消息不重复:消息不会重复发送
一致性:同一条消息在不能端接受的消息顺序是一致的。怎么解决消息的一致性? 消息序号生成器
安全性:消息数据的安全,“数据传输安全” “数据储存安全”,“消息内容安全”,怎么保证消息的安全性?

app添加IM模块

整体分为:制订好消息内容、消息存储、未读消息的存储,需要建立高效的实时消息收发通道等
消息存储:历史消息或者用于暂存离线消息,都需要对消息进行服务端存储。也会根据业务进行本地存储,
消息内容:对消息进行分类(根据业务进行划分)
消息发送通道:一般有两种

  1. IM 服务端提供一个 HTTP 协议的 API 接口,客户端需要发送消息时,调用这个接口把消息发给 IM 服务端。
  2. 客户端和 IM 服务端维护一个 TCP 长连接,客户端有消息发送时,会以私有协议来封装这条要发送的消息,然后通过这个 TCP 长连接把消息发给 IM 服务端。

接受消息通道
结构图
《IM即时通讯》
IM服务器端和消息接受设备之间维护一条tcp长连接(准备使用websocket)tcp全双工能力,可以同时接受和发送数据。当对放不在线的时候,可以通过三方操作系统辅助通道,重点在于维护可靠的长连接,可靠的长连接,

  1. IM 服务端和接收方能较为精确地感知这个长连接的可用性,当由于网络原因连接被中断时,能快速感知并进行重连等恢复性操作。
  2. 通过这个长连接投递的消息不能出现丢失的情况,否则会比较影响用户体验

消息未读数:如果消息接收方当前不在线,还可以通过第三方操作系统级别的辅助通道,来实时地将消息通过手机通知栏等方式推送给接收方。但是三方服务通道受限制比较大,

总结

  1. 消息的发送方通过发送通道来把消息提交到 IM 服务端。
  2. IM 服务端接收到发送的消息后,会进行消息的存储以便于后续历史消息的查看,消息的存储从实现上可以分为:消息内容存储、消息索引存储、最近联系人列表存储。
  3. IM 服务端接收到发送的消息后,还会针对接收方进行未读数的变更,以提醒用户查看未读的消息,消息未读数的实现上一般分为:用户维度的总未读和会话维度的会话未读。
  4. IM 服务端进行完消息存储和未读变更后,会通过接收通道把消息推送给接收方,接收通道一般是通过 IM 服务端和消息接收方之间维护的长连接来实现,还会使用第三方操作系统级别的辅助通道,来提升“自建的长连接不可用“时,实时触达的能力

轮询和长连接,

为了解决消息的实时到达问题
短轮询场景:早起使用的方式,一般是”请求响应式”的模式,y一般的请求都是这种请求响应式,不太适合实时性要求比较高的场景,采用”短轮询”,来定期、高频地轮训服务器的消息会给服务器造成的压力比较大,而且即费电又费流量
长轮询场景:短轮询模式下,服务端不管本轮有没有新消息产生,都会马上响应并返回。而长轮询模式当本次请求没有获取到新消息时,并不会马上结束返回,而是会在服务端“悬挂(hang)”,等待一段时间;如果在等待的这段时间内有新消息产生,就能马上响应返回。这种场景用于对实时性要求比较高,但是整体用户量不太大。但是长轮询还是有问题的,长轮询在超时时间内没有获取到消息时,会结束返回,没有解决无效请求,而且对后段的压力并没有减少
不管是短轮询还是长轮询都是基于HTTP 协议实现的,由于 HTTP 是一个无状态协议,同一客户端的多次请求对于服务端来说并没有关系,也不会去记录客户端相关的连接信息。

WebSocket:WebSocket 是一种服务端推送的技术代表,不同于轮训的客户端推送,基于 WebSocket 实现的 IM 服务,客户端和服务端只需要完成一次握手,就可以创建持久的长连接,并进行随时的双向数据传输。当服务端接收到新消息时,可以通过建立的 WebSocket 连接,直接进行推送,真正做到“边缘触发”(当状态变化时,发生一个 IO 事件),也保证了消息到达的实时性。
WebSocket 的优点是:

  1. 支持服务端推送的双向通信,大幅降低服务端轮询压力;
  2. 数据交互的控制开销低,降低双方通信的网络开销;
  3. Web 原生支持,实现相对简单。

消息的可靠传递

消息的可靠投递主要是指:消息在发送接收过程中,能够做到不丢消息、消息不重复两点。
《IM即时通讯》
第一种:用户 A 发消息是一个“请求”和“响应”的过程,如果用户 A 在把消息发送到 IM 服务器的过程中,由于网络不通等原因失败了;或者 IM 服务器接收到消息进行服务端存储时失败了;或者用户 A 等待 IM 服务器一定的超时时间,但 IM 服务器一直没有返回结果,那么这些情况用户 A 都会被提示发送失败。
第二种:消息在 IM 服务器存储完后,响应用户 A 告知消息发送成功了,然后 IM 服务器把消息推送给用户 B 的在线设备。在推送的准备阶段或者把消息写入到内核缓冲区后,如果服务端出现掉电,也会导致消息不能成功推送给用户 B。这种情况实际上由于连接的 IM 服务器可能已经无法正常运转论。即使我们的消息成功通过 TCP 连接给到用户 B 的设备,但如果用户 B 的设备在接收后的处理过程出现问题,也会导致消息丢失。比如:用户 B 的设备在把消息写入本地 DB 时,出现异常导致没能成功入库,这种情况下,由于网络层面实际上已经成功投递了,但用户 B 却看不到消息。所以比较难处理。
对于消息丢失会有两种方案:

  1. 针对第一部分,我们通过客户端 A 的超时重发和 IM 服务器的去重机制,基本就可以解决问题
  2. 针对第二部分,业界一般参考 TCP 协议的 ACK 机制,实现一套业务层的 ACK 协议。

业务层 ACK 机制:在 TCP 协议中,默认提供了 ACK 机制,通过一个协议自带的标准的 ACK 数据包,来对通信方接收的数据进行确认,告知通信发送方已经确认成功接收了数据。那么,业务层 ACK 机制也是类似,解决的是:IM 服务推送后如何确认消息是否成功送达接收方。具体实现如下图:
《IM即时通讯》
IM 服务器在推送消息时,携带一个标识 SID(安全标识符,类似 TCP 的 sequenceId),推送出消息后会将当前消息添加到“待 ACK 消息列表”,客户端 B 成功接收完消息后,会给 IM 服务器回一个业务层的 ACK 包,包中携带有本条接收消息的 SID,IM 服务器接收后,会从“待 ACK 消息列表”记录中删除此条消息,本次推送才算真正结束

ACK 机制中的消息重传
消息推给用户 B 的过程中丢失了怎么办?比如:

  • B 网络实际已经不可达,但 IM 服务器还没有感知到;
  • 用户 B 的设备还没从内核缓冲区取完数据就崩溃了;
  • 消息在中间网络途中被某些中间设备丢掉了,TCP 层还一直重传不成功等。

解决这个问题的常用策略其实也是参考了 TCP 协议的重传机制。类似的,IM 服务器的“等待 ACK 队列”一般都会维护一个超时计时器,一定时间内如果没有收到用户 B 回的 ACK 包,会从“等待 ACK 队列”中重新取出那条消息进行重推

消息重复推送的问题
如果在一定时间内没有收到 ACK 包,就会触发服务端的重传。收不到 ACK 的情况有两种,除了推送的消息真正丢失导致用户 B 不回 ACK 外,还可能是用户 B 回的 ACK 包本身丢了

一般的解决方案是:服务端推送消息时携带一个 Sequence ID,Sequence ID 在本次连接会话中需要唯一,针对同一条重推的消息 Sequence ID 不变,接收方根据这个唯一的 Sequence ID 来进行业务层的去重,这样经过去重后,对于用户 B 来说,看到的还是接收到一条消息,不影响使用体验。

补救措施:消息完整性检查
针对服务器宕机可能导致的重传失效的问题我们来分析一下,这里的问题在于:服务器机器宕机,重传这条路走不通了

《IM即时通讯》

  1. IM 服务器给接收方 B 推送 msg1,顺便带上一个最新的时间戳 timestamp1,接收方 B 收到 msg1 后,更新本地最新消息的时间戳为 timestamp1。
  2. IM 服务器推送第二条消息 msg2,带上一个当前最新的时间戳 timestamp2,msg2 在推送过程中由于某种原因接收方 B 和 IM 服务器连接断开,导致 msg2 没有成功送达到接收方 B。
  3. 用户 B 重新连上线,携带本地最新的时间戳 timestamp1,IM 服务器将用户 B 暂存的消息中时间戳大于 timestamp1 的所有消息返回给用户 B,其中就包括之前没有成功的 msg2。
  4. 用户 B 收到 msg2 后,更新本地最新消息的时间戳为 timestamp2。

有剋TCP 协议本身的 ACK 机制为什么还需要业务层的ACK 机制?:

  1. 操作系统在TCP发送端创建了一个TCP发送缓冲区,在接收端创建了一个TCP接收缓冲区;
  2. 在发送端应用层程序调用send()方法成功后,实际是将数据写入了TCP发送缓冲区;
  3. 根据TCP协议的规定,在TCP连接良好的情况下,TCP发送缓冲区的数据是“有序的可靠的”到达TCP接收缓冲区,然后回调接收方应用层程序来通知数据到达;
  4. 但是在TCP连接断开的时候,在TCP的发送缓冲区和TCP的接收缓冲区中可能还有数据,那么操作系统如何处理呢?
    首先,对于TCP发送缓冲区中还未发送的数据,操作系统不会通知应用层程序进行处理(试想一下:send()函数已经返回成功了,后面再告诉你失败,这样的系统如何设计?太复杂了…),通常的处理手段就是直接回收TCP发送缓存区及其socket资源;
    对于TCP接收方来说,在还未监测到TCP连接断开的时候,因为TCP接收缓冲区不再写入数据了,所以会有足够的时间进行处理,但若未来得及处理就发现了连接断开,仍然会为了及时释放资源,直接回收TCP接收缓存区和对应的socket资源。

总结一下就是: 发送方的应用层程序,调用send()方法返回成功的时候,数据实际是写入到了TCP的发送缓冲区,而非已经被接收方的应用层程序处理。怎么办呢?只能借助于应用层的ACK机制。

保证消息不会乱序

消息的一致性是很重要的,在我们的聊天过程和后续聊天记录的保存都需要保证正确的顺序,

  1. 以发送方的本地需要或者本地时间戳为序号,但是这样有比较大的问题,发送方的序号或者时间戳是可以被改动的,
  2. 以服务器的本地时钟是否可以作为时序基准,IM服务器是集群部署,也不太适合
  3. IM服务器自定义一些序号,在接收到方收到信息的时候判断需要追加到合适的时候

解决网络的不确定性
我们通过长连接来实现投递,“长连接”底层使用的 TCP 连接并不是一个真正存在的物理连接。我们需要用心跳机制维护,当网络出问题的时候,可能还维护这长连接,
心跳机制:TCP Keepalive ,应用层心跳,智能心跳,智能心跳。IM 都采用了应用层心跳方案来解决连接保活和可用性探测的问题

    原文作者:lin_henry
    原文地址: https://blog.csdn.net/lin_henry/article/details/106568480
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞