13.10 本章小结:最终架构在学习完本章全部内容后,这里我们对整个IM服务做一个总结。
在用户之间建立聊天关系的渠道是会话,它被用作消息收发的容器,消息能被发送、存储、接收的核心是我们介绍的那些模式,其中负责消息存储的模式有如下两种。
读扩散模式:消息被发送到会话消息链,用户在读取新消息时需要拉取全部相关会话。此模式适合会话数较少的场景。
写扩散模式:消息被分别发送到每个会话中用户的用户消息链。此模式更适合会话参与用户较少的场景,如单聊和小群。
负责消息收发的模式有如下三种。
推模式:每条消息都经过长连接被直接推送给在线用户,消息的实时性强,但易丢失。
拉模式:客户端主动轮询访问服务端获取新消息,消息不易丢失,但实时性一般。
推拉结合模式:既将新消息推送给在线用户,客户端又周期性地拉取消息,消息的实时性强,也不易丢失。
本章设计了一个既支持读扩散模式,又支持写扩散模式消息存储的IM服务,且采用推拉结合模式收发消息。我们为数据库存储设计了多个数据表,它们分别负责如下事情。
消息表:存储消息本身。
会话表:存储会话元信息。
用户会话链:存储会话与用户的关联关系,以及每个用户在会 ...
高并发架构即时通信是一个典型的读多写多的场景,在讨论完IM服务如何满足即时通信的基本功能需求后,我们再将其放到海量用户场景中看看它是如何应对高并发请求的。
13.9.1 发送消息拥有上亿用户的即时通信软件,很容易发生大量用户同时发送消息的情况,尤其是在节假日期间,比如春节,可能有数百万用户同时发送拜年消息,所以我们需要设计一些策略来防止海量的发送消息请求击垮IM服务。
在 13.5.3节讨论过,为了保证会话中消息的有序性,我们为存储消息过程引入了消息队列,在IM服务内部由IM API服务接收消息,然后把消息投递到消息队列中就返回给用户了。当时的意图是保证消息基于会话ID有序和基于用户ID有序,其实使用消息队列还有另一层含义:消息队列可以对高并发发送消息的请求做削峰处理,IM服务内部可以按照自己的处理速度来平滑处理消息的存储与推送,在发送消息的过程中其实已经实现了异步写,可以较好地应对高并发发送消息请求的涌入。
接下来是消息的消费环节。如果消费者消费消息的速度较慢,或者消息队列的分区数量过少,则会造成消息在消息队列中堆积,无法被及时地存储到数据库中,进而导致消息迟迟无法触达接收者。为了尽 ...
13.8 阶段性汇总:存储设计在介绍完IM服务的主要逻辑后,我们对消息数据的存储进行正式设计。
存储消息本身的消息表:修改消息数据的场景极少见,更多的场景是使用消息ID获取消息数据,所以使用分布式KV存储系统或传统数据库都是合适的选型。如果使用分布式KV存储系统,则以消息ID为Key,以消息内容、发送时间、发送者、消息状态等信息组合的JSON格式为Value;如果使用传统数据库,则消息表的结构如表13-1所示。
消息表使用msg_id字段作为唯一索引,以提高根据消息ID获取消息数据的效率。
管理会话数据的会话表:其主要用途是根据会话ID获取会话信息。会话表的结构如表13-2所示。
会话表使用conversation_id字段作为唯一索引,且使用VARCHAR类型来表75,这是为了满足13.6.1节在创建单聊会话时使用字符串类型的会话ID的需要。
会话消息链:用于支持读扩散模式的消息存储,其重点是保证会话内消息有序。其数据表conversation_msg_list的结构如表13-3所示。
会话消息链主要用于按照会话查询有序的消息ID列表,所以需要为conversation_msg ...
13.7 消息回执很多产品的IM服务都支持消息回执功能,即消息发送者在单聊中可以看到对方是否已读取某条消息,以及在群聊中可以看到一条消息已被哪些成员读取。
13.7.1 上报已读消息要想知道一条消息是否已被读取,自然需要依赖消息接收者把已读事件上报给服务端,其重点是上报时机。
当一条消息被展示给用户时,如果客户端选择立刻上报已读事件,则可能会给服务端带来访问压力,尤其是群聊。假设一个大群有300人活跃在线,群内每下发1条消息,就会导致300个客户端的已读上报事件反向访问服务端。
实际上,对于一条消息是否已被读取并不需要很强的实时性,客户端没有必要在已读事件发生后就立刻上报给服务端,更好的做法是客户端周期性地(如每隔3s)汇总每个会话最后一条已读消息的Seq上报。从消息发送者的视角来说,也不需要实时知道对方是否已读取消息,所以发送者的客户端也可以周期性地从服务端查询消息是否已被读取。
接下来,我们讨论如何存储消息已读事件。
13.7.2 记录已读消息记录已读消息最直接的方式是为每条消息和每个已读用户保存关联关系,但是这种方式严重占用存储空间,并无实用性可言。实际上,我们可以利用消息有序性 ...
13.6 会话管理与命令消息目前我们只讨论了用户消息的收发问题,一条用户消息必然属于一个会话,会话本身也需要IM服务管理。
13.6.1 创建单聊会话我们先从发送者的视角介绍创建会话的流程。例如,用户A首次与用户B聊天时,用户A先打开与用户B的聊天框,此时对于用户A来说两者的会话就已经建立了,只不过服务端和用户B对此并不知情。当用户A发出第一条消息时,客户端才请求服务端创建会话,此时客户端将消息内容、接收者和值为0的会话ID传递给服务端,服务端发现请求的会话ID为0,于是需要依次创建如下数据。
生成唯一的会话ID,在会话表中创建新会话。
在用户会话链中建立会话与用户A、用户B的关联关系。
将消息存储到消息表中。
服务端将会话ID响应给客户端,表示会话创建成功、消息发送成功,但是此流程缺点明显,在如下场景中容易出现重复创建会话的情况。
服务端在存储消息时遇到网络错误,于是客户端被告知消息发送失败,然后用户A选择重发消息;当服务端再次处理此消息时,会创建新会话,以及会话与用户的关联关系,从而导致出现用户A与用户B之间存在两个单聊会话的情况。
当用户A打开聊天框准备与B首次聊天时,恰好 ...
13.5 消息的有序性保证即时通信就相当于现实生活中人与人之间的聊天,用户之间收发消息的有序性必然非常重要,因为如果出现不合乎逻辑的聊天内容,则会影响用户体验。本节我们将介绍在消息发送与消息接收的过程中哪些环节可能会发生乱序情况,以及针对各种乱序情况的处理思路。
13.5.1 消息乱序我们先来分析哪些情况可能会造成消息乱序。
客户端发送消息:如果客户端与服务端之间采用了短连接,即客户端每发送一条消息都与服务端建立一次连接,那么受限于公网环境的不确定性,用户发送3条消息A、B、C,最终到达服务端的消息顺序可能是C、B、A,从发送者的角度来说 ,消息发生了乱序。
服务端存储消息:即使用户发送的消息A、B、C按顺序到达服务端,但是由于这3条消息可能是由不同的IM服务实例处理的,每个服务实例处理消息的延迟不同,且与数据库网络通信的延迟不同,最终消息也可能会被按照C、B、A的顺序存储,于是消息接收者在拉取消息时发生了乱序。
服务端推送消息:即使按照发送者的本意消息被顺序存储起来,但是由于消息推送环节仍然要借助推送系统与目标用户客户端的网络连接,如果网络异常造成消息A丢失,只有消息B、C到达客户 ...
13.4 存储初探一个支持读扩散模式和写扩散模式的完整IM服务应该存储哪些数据?本节我们先做一个简单的介绍。本节内容可以作为后续章节的导论。
用户发送的消息显然应该被存储到消息表中,其具体属性如下。
消息ID:全局唯一标识一条消息。
发送时间:接收者可以看到消息是什么时间发送的。
内容:记录消息文本。
会话ID:指向消息所属的会话。
发送者ID:指向消息发送者的用户ID。
状态:记录消息是否对接收者可见,比如消息是否被屏蔽、被撤回。
用户聊天的渠道是会话,会话也应该被保存,尤其是群聊会话。
会话ID:全局唯一标识一个会话。
会话类型:会话是单聊会话还是群聊会话。
最新消息时间:记录会话中最新消息的发送时间。
会话人数:与会话相关的用户数量。单聊会话人数固定为2人 ,群聊会话人数为群组成员总人数。
其他群组信息:如群头像、群公告等。
为了支持读扩散模式,对于每个会话都应该存储此会话与其产生的消息的关联关系, 即“会话消息链”,这条链中的每条数据都应该包含如下属性。
会话ID:指代一个会话。
消息ID:指代属于此会话的一条消息。
同理,为了支持写扩散模式,对于每个用户也都应该存 ...
13.3 消息投递IM服务最重要的议题是消息投递,合格的消息投递应该具有如下特点。
准确性:比如用户1向用户2发送消息,消息只可能被用户2收到,不可能发生用户3收到这条消息的“串线”问题。
消息不可丢失:消息不可被漏发,更不可丢失,即使遇到故障(如机房断电、网络断开等),用户也应该能够在故障排除后接收到错过的消息。所有IM的用户都坚信消息是一定会被送达的,如果用户发送了重要的消息,而我们的IM服务却因为各种原因把它丢失了,那么用户会直接流失。
实时性:应该尽量保证一条发出的消息被实时地投递到接收方,保证用户通信的连贯性,这样才能达到即时通信所强调的“即时”。
有序性:消息需要按照发送时间严格排序,不能发生时空错乱。即时通信毕竟与日常交流一样有提问才有回答,也就是消息之间存在因果关系。比如用户1在群聊中问用户2“吃烤肉还是吃火锅”,用户2回答“吃火锅”,那么群聊中的其他用户看到的消息列表不应该是用户2先回答“吃火锅”,然后用户1再提问;否则,他们会疑惑用户2是怎么做到抢答的。
本节我们先围绕准确性、消息不可丢失和实时性来介绍消息投递的几个重点话题,而关于维护消息的有序性内容将在接下来 ...
13.2 IM相关概念与IM相关的概念如下。
消息:用户发出的任何内容。
用户状态:在线、离线、挂起。
设备/终端:用户使用IM的客户端,通常包括移动端和Web端。
单聊:两个用户一对一聊天的模式。
群聊:多个用户聊天的模式。
会话:描述用户通过聊天建立的关联关系。通俗地说,微信中的每个聊天框都是一个会话,比如你和一个好友聊天,你们就建立了会话;你被拉到某个群里聊天,你就和这个群建立了会话。会话是IM服务的核心概念,它真正指明了消息应该被发送给哪些用户。
信箱:对于用户来说,每个会话都有一个抽象的信箱,作为会话中每条消息按照时间顺序由远及近排列的存储容器。
IM (Instant Messaging,即时通信)是一种实时的通信方式,它可以使用户通过互联网快速、安全、低成本地相互交换即时信息。本章将详细讲解IM服务作为用户即时通信后台的功能与技术方案,具体的学习路径如下。
13.1节介绍IM的意义与核心能力。
13.2节介绍与IM相关的一些概念。
13.3节介绍IM服务合格的消息投递应该具有的特点,以及如何存储、接收消息。
13.4节初步介绍IM服务应该存储哪些数据。本节内容可以作为后续章节的导论。
13.5节介绍消息乱序的原因,以及在服务端和客户端分别保证消息有序收发的思路。
13.6节介绍如何创建一个新会话,以及如何处理用于管理会话的命令消息,比如撤回消息、管理群组成员等。
13.7节介绍消息回执功能的实现原理。
13.8节详细介绍存储系统的数据模型设计。
13.9节介绍在高并发架构下,IM服务在高并发收发消息场景中的应用,以及直播间弹幕模式应有的高并发设计。
13.10节介绍IM服务的最终架构,这一节内容也是本章的总结。
本章关键词:读扩散、写扩散、推拉结合、消息有序、消息回执、单聊、群聊、弹幕。
13.1 IM的意义与核心能力通 ...