《即时消息技术剖析与实战》 学习笔记 day2

143 阅读6分钟

大家好,我是砸锅。一个摸鱼八年的后端开发。熟悉 Go、Lua。今天和大家一起学习 IM 系统😊

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 33 天,点击查看活动详情

即时通讯领域为了解决实时性问题,技术经历的迭代升级:

  1. 从简单、低效的短轮询升级为相对效率可控的长轮询
  2. HTML 5 的出现,全双工的 Websocket 彻底解决服务端推送的问题
  3. 基于 TCP 长连接衍生出的各种有状态的通信协议

消息丢失的场景

  1. 客户端发送到 IM 服务器的过程中,因为网络不通导致失败了
  2. IM 服务器接收到消息之后进行存储时失败
  3. 客户端发送请求之后,IM 服务器处理过慢一直没有返回结果
  4. 将消息推送给客户端时,服务端宕机了,导致消息不能成功推送
  5. 消息成功推送给客户端了,但是客户端处理消息过程出现异常,例如解析失败

解决消息丢失的方案

IM 服务推送后如何确认消息是否成功送达接收方:

IM 服务器在推送消息时,会携带一个 SID(安全标识符),推送消息之后就把这个消息加入到 “待 ACK 消息列表”,客户端成功收到消息之后,会给 IM 服务器回一个业务层的 ACK 包,包中会携带这条消息的 SID, IM 服务器接收到之后就从 “待 ACK 消息列表” 里删除这个消息,本次推送才算是真正结束

如果消息推送给客户端的过程中丢失了,可以在 IM 服务器的 “待 ACK 列表” 里维护一个超时计时器,一定时间内如果没有收到客户端返回的 ACK 包,则从 “待 ACK 列表” 里重新取出消息进行重推

利用时间戳机制对消息进行完整性检查:

IM 服务器每次推送消息都带上一个最新的时间戳 timestamp1,客户端收到消息之后将这个时间戳保存在本地。后续客户端重连上线的时候,携带本地最新的时间戳 timestamp1 给 IM 服务器,IM 服务器会将客户端暂存的消息中时间戳大于 timestamp1 的所有消息返回给客户端。客户端收到消息之后更新最新消息的时间戳(如果多机器时钟不同步的话,可以使用全局的自增序列作为版本号代替)

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ee5960c6de254016b913b8c793124f9d~tplv-k3u1fbpfcp-zoom-1.image

消息重复推送的解决方案

服务端推送消息的时候携带一个 Sequence ID,这个 ID 在本次会话里是唯一的,同一条消息的 Sequence ID 不变,客户端根据这个标识进行去重

消息的时序一致性

消息的时序一致性可以保证会话的语义连贯,逻辑清晰

要保证消息的时序性,需要找到一个时序基准,让消息可以具备”时序可比较性“,工程实现可以分为以下几步:

  1. 找到时序基准
  2. 时序基准的可用性
  3. 有了时序基准之后,还要尽可能减少误差

发送方的本地序号和本地时钟不能用于做时序基准,因为发送方可以随时调整时钟导致时序回退的问题,而且重装应用也会导致时序清零。以及多设备间存在时钟不同步的问题,不能保证设备的时间就是准确的

如果 IM 服务器是集群部署的话,服务器的本地时钟作为客户端消息排序的“时序基准”也不合适。虽然多态服务器之间可以通过 NTP 时间同步服务,能够降低服务集群机器间的时钟差异到毫秒级别,但还是存在一定误差,维护起来很复杂

所以全局的序列可以避免多服务器的时钟不同步问题,可以通过 Redis 的原子自增命令 incr,DB 自带的自增 id、Twitter 的 snowflake 算法、“时间相关”的分布式序号生成服务等实现。需要注意的是全局时序基准服务的可用性问题,如果是高并发的场景下,容易遇到性能瓶颈。从业务层面考虑,对于群聊和多点登录的场景,没有必要保证全局跨多个群的绝对时序性,只要保证某一个群的消息有序就可以

“时序基准”之外还有其他误差问题:例如 IM 服务器集群化部署,每台服务器性能上都会存在差异,处理效率都会有差别。处理快的会更快推送消息出去;IM 服务器收到发送方的消息之后,后续处理一般都是多线程处理,可能会导致流程处理速度不一致,消息顺序也会不一致

客户端整流优化:服务器发送消息时携带序号给客户端,客户端收到消息之后判断一下,如果当前消息序号大于前一条消息的序号就将当前消息追加到会话里

消息安全性

消息安全性分为:消息传输安全性、消息存储安全性、消息内容安全性

消息传输安全性。“访问入口安全”和“传输链路安全”是基于互联网的即时消息场景下的重要防范点。针对“访问入口安全”可以通过 HttpDNS 来解决路由器被恶意篡改和运营商的 LocalDNS 问题;而 TLS 传输层加密协议是保证消息传输过程中不被截获、篡改、伪造的常用手段

消息存储安全性。针对账号密码的存储安全可以通过“高强度单向散列算法”和“加盐”机制来提升加密密码可逆性;对于追求极致安全性的即时消息场景并且政策允许的情况下,服务端应该尽量不存储消息内容,并且采用“端到端加密”方式来提供更加安全的消息传输保护

消息内容安全性。针对消息内容的安全识别可以依托“敏感词库”“图片识别”“OCR 和语音转文字”“外链爬虫抓取分析”等多种手段,并且配合“联动惩罚处置”来进行风险识别的后置闭环

会话未读和总未读

会话总未读数和会话未读数是单独维护的。因为总未读在很多业务场景会高频使用,如果每次都通过聚合所有会话未读来获取,很容易出现性能瓶颈

用分布式锁来解决两个未读数量的原子更新,例如 Redis 的 setNX、DB 的插入固定记录成功与否等。锁的引入也会造成吞吐量降低,可以考虑原子化嵌入脚本,不需要额外的维护锁的资源,高并发场景下性能也较好,嵌入脚本的开发需要一些额外的学习成本

此文章为3月Day6学习笔记,内容来源于极客时间《即时消息技术剖析与实战》 这门课真的非常好,推荐大家看看