【IM】如何保证消息有序性?

即时通讯系统引入保证消息有序的机制,务必会影响性能,增加开发复杂度。 引入 IM 大佬的一句话: 1. 理想状态 服务端接收到消息的顺序与用户点击发送消息的按钮顺序完全一致,用户接收到的消息的顺序也和发送时的顺序完全一致。 这是几乎不可能实现的!为什么? 1. 网络延迟 不可能按下发送消息的按钮后,消息就会马上到达服务器。每个用户的网速可能不一样。 OK,那我按发送时的时间戳来排序不就行了? 2. 时钟不一致 其实每个人本地的时钟不一定都是同一个,任何物理时钟都有漂移,任何时钟同步协议都受制于网络延迟。这导致我们对时间的测量永远存在一个不确定的误差区间,区间内的事件顺序无法判定。 既然如此,那我们退一步,一个即时通讯系统,以微信举例,是不是真的需要保证消息的全局有序? 答案是不用。我们只需要保证每个会话内的消息有序就行,也就是用户1和用户2的这个聊天会话中,我们看到的消息是一样的,保证两个人发送的消息在这个会话内有序! 再退一步,那我只保证每个人发送的消息有序行不行? 答案是不行!比如用户A发送了两条消息MSG1和MSG2,用户B发送了MSG3,如果在页面上显示了MSG3 -> MSG1 -> MSG2,这样也算是乱序。 2. 如何实现会话内有序? 路径1:客户端 -> 服务端 1. 建立 TCP 长连接 我们都知道,TCP 长连接保证消息可靠性、有序性。 但是,实际并不能只依靠 TCP 连接来保证消息的发送顺序!在弱网环境,比如火车、电梯,可能会快速断网、切网,原来的 TCP 连接断开,连上一条新的TCP 连接,我发送消息MSG1在旧连接上没发出去,消息MSG2 在新的TCP 连接上发送成功了,这就导致了消息乱序。 2. 客户端生成序列号 学习 TCP 保证消息有序的方案,发送消息前,客户端生成本地递增序列号localSeq,这个序列号在本地存储的,服务端也会记录上一条接收成功了localSeq,然后在服务端判断当前消息是不是有序的。 如果乱序,可以通知客户端重发消息。 也就是:客户端发消息 ->服务端接收并校验localSeq -> 服务端发送ack或要求重发 如果一来一回严重影响性能,可以参考使用批量ack等。 路径2:服务端处理入库 因为我们的目标是保证会话有序,所以消息到达服务端时的第一件事应该是分发消息,按照会话的 ID 进行分发,在我的项目中,私聊会话格式:private_{user_id1}_{user_id2},user_id1 小于 user_id2。 分发最简单的就是把相同会话的消息放到一个队列,按顺序处理。 这里可以引入消息队列这个中间件,像 Kafka 的 Partition 分区机制 和 RocketMQ Queue 机制。 ...

九月 6, 2025 · 1 分钟

im:私聊消息端到端加密

概括:通过非对称加密算法获取共享密钥,通过对称加密算法对消息进行加密解密。 一、(非对称算法)获取共享密钥 采用 ECDH 算法获取共享密钥。 1. 公钥私钥什么时候生成? 当前设备第一次登录的时候,会生成公钥私钥,公钥保存到服务端,私钥保存在本地。 注册的时候 2. 共享密钥什么时候生成? 刚加好友的时候/刚进一个群的时候。 发现本地没有与一个好友/群的共享密钥的时候。 1. 私聊 发送消息时,需要对消息加密,加密用的是共享密钥。 共享密钥生成过程: A和B 都会生成各自的公钥和私钥,公钥存在服务端数据库中,私钥保存在本地。 根据 ECDH 算法,A 只需要获取到 B 的公钥,可以算得一个共享密钥,这个共享密钥和 B 用 A 的公钥算到的共享密钥是一样的。 共享密钥生成后,转为 AES 算法加密的密钥格式 2. 群聊 用户加入一个群的时候,会与这个群交换共享密钥,而不是与群内的所有用户交换共享密钥。服务端保存着 群与每个群用户的共享密钥,所以说,一个群有多少个用户,就会生成多少条共享密钥。 创建群聊的时候,群聊的公钥和私钥都存在服务端数据库。 用户A给群G发送消息时,用A与G的共享密钥加密;服务端获取到消息后,用 A与G 的共享密钥解密,然后把原文存到数据库;转发给群内用户的时候,用G与其他用户的共享密钥加密后再发送。 二、(对称算法)消息加密 采用对称加密算法 AES 进行加密,即用什么加密,就用什么解密。 服务端获取到私聊消息的时候,不用加密解密,因为服务端没有用户的共享密钥,直接存储消息加密后的二进制格式。 服务端收到群聊消息的时候,需要加密解密,存的是消息原文。

八月 31, 2024 · 1 分钟

IM 系统:RabbitMQ + Redis 实现消息重发机制

【消息未收到 ack 的情况】 发送一条消息后,将消息体存入到 redis 中,并将消息的【uniqueId 和 重发次数】放入 RabbitMQ 消息队列中。 延迟队列中的 【uniqueId 和 重发次数】在规定客户端ack时间内, 会被放入到死信队列,用于检查是否 收到ack消息。 死信队列收到 【uniqueId 和 重发次数】 到 redis 中检查这条消息体是否还存在,存在则说明还没有收到ack 再检查 重发次数,如果重发次数已达上限,报异常【重试发送多次消息失败】 如果还没到重发次数上限,就进入消息重发。 【消息收到 ack 后的处理】 (私信) 删除 redis 中的等待ack 的消息体,这样死信队列在检查的时候没有查到这条消息体,说明消息已经ack 修改消息状态 为【已送达】。 (群聊) 修改用户的 last_ack_id 删除消息体缓存

八月 30, 2024 · 1 分钟