IM系统发送一条消息到底经历了什么?

3,492 阅读5分钟

1:为什么说IM系统发送一条消息并不是简单的

“ 本文正在参加「金石计划」 ”我在网上看到很多的文章使用Netty来编写聊天室,或者是聊天相关的,但是大都是是在demo阶段,并不能使用在生产环境上使用,比如用户A给用户B发送一条消息,大都是让你使用


protected void channelRead0(ChannelHandlerContext ctx, Message msg) throws Exception {
   ctx.writeAndFlush("你要发送的消息");
}

但是发送一条消息真的有这么简单吗? 在我们使用微信聊天的时候,首先会校验我们是否是好友,对方是否把我们拉黑,如果是群聊的时候,还要判断自己是否被禁言,或者群是否被禁言,这些前置的校验都是必不可少的

当我们网络不好的时候,发送一条消息会一直转圈圈,直到过了一会这个圈圈才会消失,这个就是消息确认接收的ack机制了,当你的IM系统收到这条消息,并成功保存到数据库中了,这时候IM系统会返回一个ack的确认机制给你,然后前端收到这个请求之后就把这个圈圈去掉,就代表我们这条消息已经发送成功了

还有虽然我们发送消息是使用TCP的,TCP本身是安全的,但是它只保证了传输层的安全,在应用层是不保证数据安全的,也就是有可能会导致数据的丢失,所以我们又应该如何保证聊天的消息不丢失呢

当然IM系统还有一个重要的地方,就是我们如何保证消息的顺序性,如果你发送的消息是乱序的,那就有可能造成误会了

.........

等等一系列的问题都需要我们去解决,所以别小看通讯系统简单的发送一条消息,其实需要解决的问题有很多

2:一条消息发送的大致流程

image.png

3:如何保证我们的消息已经成功发送到IM服务器了

当我们发送一条消息给对方之后,如果没发送出去就会有一个这样的圈圈,发送成功了就不会有。

image.png

消息会转圈圈就代表着对方还没有接收到这条消息,或者说这条消息还没有持久化到数据库中,这时候当服务端已经将这条消息持久化到数据库了,这时候就可以发送一个ack给客户端,告诉客户端这条消息已经发送成功了

4:我们消息发送到IM服务器失败了怎么办

网络是不稳定的,有时候我们发送一条消息给对方,但是这条消息根本没发送到IM服务器,或者说我们消息已经成功发送到IM服务器了,但是返回ack的时候,客户端没有接收到这条消息的ack,所以呢,对于发送这条消息的用户来说,这条消息就是没有发送成功的。所以这时候就会重发。但是也不能无限制的重发,要有限制次数,超过次数就要提示客户端这条消息发送失败了

5:重复发送消息如何保证幂等性

当我们发送一条消息给IM服务器。发送成功之后IM服务器会返回一个ack消息给客户端,如果这时候客户端没有接收到这个ack,那么就会重发。这时候IM服务器就会有二条一摸一样的消息了,但是消息接收方是只能接收一条消息的,所以这时候就要保证消息的幂等性了

6:如何保证消息的有序性

无论是发送消息还是接收到的消息,都应该是有顺序的,不能乱序的,不然很可能意思全变了,我们发送消息是 A,B,C,那么接收消息的一方也要是A-B-C这样的顺序。我们可以给消息设置一个序列号,这个序列号是自动递增不会重复的,比如时间戳,或者借用redis等一些工具

7:消息该如何存储,读扩散还是写扩散

读扩散: 一条消息在数据库中只存一条记录,比如用户A给用户B发送一条消息,那么这时候数据库就存一条记录即可,如下

                                                  key         fromId   toId   body
  读扩散:A发送一条消息给B  数据库就存一条记录就行   messageKey-1    A      B      messageBody
         B发送一条消息给A  数据库就存一条记录就行   messageKey-2    B      A      messageBody       

但是此时有个需求,就是查询用户A和用户B的聊天记录,那么SQL就要这样写: (select * from table where fromId = A and toId = B) or (select * from table where fromId = B and toId = A),也就是查询的时候变复杂了。

写扩散:

*                                                   key        fromId   toId  ownerId  body
* 写扩散:A发送一条消息给B  数据库就要存二条记录就行   messageKey-1    A      B       A      messageBody
*                                                  messageKey-1    A      B       B      messageBody
*                                                  messageKey-2    B      A       B      messageBody
*                                                  messageKey-2    B      A       A      messageBody

相对于读扩散,写扩散增加了写的压力,一条消息会在数据库存二份,甚至多份,

所以一般来说对于单聊和群聊我们的消息存储是不一样的,单聊我们完全可以使用写扩散,因为一条消息存储二份对于数据库而言压力不大,这样对于查询而言就简单了

但是对于群聊而言,如果一个群有1万个用户,那么就要存2万条记录,那么对于数据库而言写压力就大了,所以对于群聊一般而言,我们一般使用的是读扩散,查询的时候麻烦点,减少写的压力

但是写扩散真的不适合群聊嘛?也不绝对,对于钉钉那些要显示某些人已读或者未读的。那么就要用读扩散了

所以呀,一个简单的发送消息并不是简单的就是发送一条消息而已,要考虑的问题有很多