IM服务器设计与进化
一:概述
IM:instant messaging ,中文翻译”即时通讯“,干啥的呢?就是我发个消息,你即时的就能看见。
那么IM消息的整个系统都有哪些东西呢?
- 客户端。用来发送玩家的消息给服务器,接收并显示群内玩家的消息;
- 服务端。用来接收并处理玩家的消息,以及给某玩家推送消息
有人问客户端自己能不能干这事?不能,因为我想给老王发个消息,但老王在哪我是不知道的,因为老王和我没有连接,所以只好找个中间商,中间商就固定在某个位置,我俩分别于中间商连接,那我俩也就可以连接了。
二:仅支持单聊的IM消息服务器,IM服务器1.0版
还是我和老王,我俩都连接了服务器这个中间商,我俩想通个话,怎么整呢?
- 首先,我与服务器建立了连接,这个连接有一个唯一的标识,
- 然后,老王也与服务器建立了连接,这个连接也有一个唯一的标识
- 我想向老王发送一条消息:“吃了吗?”
- 那么我先向IM消息服务器发一条消息,说我想向老王发条消息。
- IM服务器接收到了这条消息,它看看在线的人里面有没有老王,发现有,然后把消息发送给老王,然后告诉我发送成功。
- 老王接收到这条消息,我本地在服务器告诉我成功之后,也显示了这条消息。
上边是通俗的讲法,下面是技术的讲法
- 我与im服务器建立长连接,如通过socketio建立了长连接 s1,登录信息与长连接的关系保存到服务器,如<playerId1: s1>
- 老王与im服务器建立长连接,如通过socketio建立了长连接 s2,登录信息与长连接的关系保存到服务器,如<playerId2: s2>
- 我发言,本质上是客户端通过s1,向服务器发送消息:
sendMessage: {playerId = playerId2, message = "吃了吗"}
- IM服务器接收到之后,在缓存中找有没有playerId2,如有,通过playerId2找到s2, 然后通过s2将message发送给playerId2, 同时将此信息发送给playerId1一份。
- 双方收到服务器推送的消息,分别展示这条消息
如此设计,一个简单的单聊IM服务器,就成型了。
那么它有什么问题?
- 只能单聊,群聊暂不支持。我想8个人一起聊,不能实现。
- 如果有人掉线了,发向此人的消息他接收不到,那人重新上线,也无法看到,简单来说这条消息丢了。
三:支持群聊和消息历史的IM服务器,IM消息服务器2.0版
1.0版本解决只能单聊和消息丢失的问题,那么如何解决?
通俗来讲:
- 通过发言前建立群组,把想群聊的这些人的信息归于一处,来支持群聊,这样就能知道你的发言应该发给谁谁谁
- 通过把发言备份到一个可靠的地方,作为历史发言记录,解决消息丢失的问题
下面是技术的讲法
- playerId1,playerId2,playerId3分别于im服务器通过socketio建立了长连接 s1,s2,s3
- playerid1想组建个群聊,成员分别是playerid1,playerId2,playerId3,因此向IM服务器发送组建群聊请求
- IM收到该请求,创建个群组的数据group: {playerIds:[playerId1,playerId2,playerId3]},然后告知playerId1成功,并且通知到playerId2和playerId3这个组建群聊成功
- playerId1,2,3接收到组建群聊成功消息后,在UI界面展示群组信息
- playerId1发言:"干啥去",playerId 向IM发送向某群聊发送消息请求
- IM收到该请求,查找到该群聊,查找到群聊中的playerid1,2,3,然后分别向playerId1,2,3发送此条消息,并且存储此条群聊的消息到数据库(如redis,mongodb)
- playerId1,2,3接收到IM的消息推送后,在各自的UI中展示这条消息
如果playerId3 断线之后,playerId2说了句“去吃饭”呢?
- 客户端保存有群聊最大的消息序号x,与im服务器建立长连接后,先问IM服务器询问目前最新的消息序号y,如x<y,说明客户端存着的消息是滞后的,因此通过请求IM服务器的方式拿到x之后的群聊消息,并及时更新自己的最大消息序号到y
看起来目前的版本已经支持了群聊,也解决了消息丢失问题。那么它还有什么问题?
- 当同时发言的人数多的时候,这台服务器会处理不过来,表现为消息发出去以后老半天不见动静
四,理论上支持无限人聊天的IM服务器,IM服务器3.0版
2.0版本的单台服务器在人多时的性能问题,该如何解决?
答曰,分布式。分布式也就是多服务的意思,这些服务可能在一个服务器,也可能在多个服务器,这种方式理论上能近乎无限的提升服务的性能,但他会带来一些挑战:
- 群聊里的人可能来自不同的服务器,因此消息发送将更复杂
- 当多人近乎同时发送消息时,需要保证群聊里的所有人完整,准确,顺序相同的拿到这些人的发言。不能顺序不一样,更不能缺失。
那么服务器该如何设计呢?
通俗来讲:
- 客户端创建的群组信息,要所有服务器都能看到,且都能获取,并且群组信息里除了要有用户的ID之外,还得有他在哪台服务器,这样才能找到这个用户在哪
- 用户上线时,需要更新一下群组信息,说明我目前连的是这台服务器
- 对于来自同一个群组内同时发言的这些请求,不管它有多少,整个服务器部分同时只处理一个
下面是技术的讲法
- playerId1,playerId2,playerId3分别于im服务器A,B,C 通过socketio建立了长连接 s1,s2,s3
- playerId1,2,3,分别更新自己在数据库中的数据,使server=A/B/C
- playerid1想组建个群聊,成员分别是playerid1,playerId2,playerId3,因此向IM服务器发送组建群聊请求
- IM收到该请求,查找数据库,找到playerId1,2,3的数据,拿到各自的server,并创建群组的数据
group: {players:[
{id:playerId1,server:A,socket:s1},
{id:playerId2,server:B,socket:s2},
{id:playerId3,server:C,socket:s3},
]},
然后告知playerId1成功,并且通知发送消息给B和C,中的playerId2和playerId3这个组建群聊成功
- IM的全局kafka消费者实例,订阅群聊的topic : "群聊ID",xxxxx.subscribe('xxxx');
- playerId1,2,3接收到组建群聊成功消息后,在UI界面展示群组信息
- playerId1发言:"干啥去",playerId 向IM发送向某群聊发送消息请求
- IM接收到kafka的消息后,取出群聊消息,查找到该群聊,查找到群聊中的playerid1,2,3,拿到各自的server,然后向playerId1,2,3发送此条消息,并且存储此条群聊的消息到数据库(如redis,mongodb)
- playerId1,2,3接收到IM的消息推送后,在各自的UI中展示这条消息
对于多个服务来说,各服务的在线状态需要同步,如果当前的流量较大,还需要动态的新增服务,简单来说服务需要管理。kubernetes集群是个可行的方案。
看起来目前的版本已经支持了分布式的服务,那么它还有什么问题?
- 逻辑更复杂,客户端连接与消息处理的逻辑耦合在一起,IM服务器的代码将更加复杂,系统的可维护性降低
- 可测试性下降,单元测试和集成测试可能需要启动整个im服务器来进行
- 耦合性强,难以将消息的处理部分抽离出来,用于其他功能或服务
- 代码维护困难,很好理解,越复杂,就越难维护。
五,架构更优的IM服务器,IM服务器4.0版
3.0版本的高耦合问题,该如何解决?
通俗来讲:
- 将IM服务器的管理客户端连接,群组管理,发送消息的功能;与对消息的处理功能,分开,各自干各自的事情
下面是技术的讲法:
- 新建若干消息消费者服务器,这些服务器用于消息处理
- IM服务器只负责管理与客户端的连接,群组管理,向客户端发送消息,以及接收来自消费者服务器的消息
当玩家创建群组时:
- playerId1,playerId2,playerId3分别于im服务器A,B,C 通过socketio建立了长连接 s1,s2,s3
- playerId1,2,3,分别更新自己在全局redis集群中的数据,即playerId1的server=A,
- playerid1想组建个群聊,成员分别是playerid1,playerId2,playerId3,因此向IM服务器发送组建群聊请求
- IM收到该请求,查找数据库(redis),找到playerId1,2,3的server,并创建群组的数据,并保存到数据库(如redis)
group: {players:[
{id:playerId1},
{id:playerId2},
{id:playerId3},
]},
然后告知playerId1成功,并且通知发送消息给B和C,中的playerId2和playerId3这个组建群聊成功
- IM通知消息服务,消息服务接到通知后开始subscribe 此topic, topic名字是群组的ID
- playerId1,2,3接收到组建群聊成功消息后,在UI界面展示群组信息
当玩家发言时:
- playerId1发言:"干啥去",playerId 向IM发送向某群聊发送消息请求
- IM接收到请求,验证playerId1的身份,将消息存入数据库,并向kafka发送一条消息。topic为群聊ID
3.消息消费者拿到来自kafka的消息后,取出群聊的消息,在redis中找到该群聊,取出其中的playerId, 然后从全局redis中找到playerId对应的server,然后给对应的IM服务器发送消息,类似如{server:A,playerId:'playerId1',"message":"干啥去"} - IM服务器接收到来自消息消费者的消息后,验证是否连接有playerId, 如果有,通过socketio向该用户发消息
- playerId1,2,3接收到IM的消息推送后,在各自的UI中展示这条消息
目前的架构,有如下特征:
- IM服务可以是任意多个,负责处理连接,管理群组,发送消息
- 消息消费者服务可以是任意多个,负责处理消息,形成明确的消息发送指令,并给到IM服务
- kafka作为中间件存在,负责异步转同步,流量削峰
- IM和消息消费者服务职责明确,有利于维护
看起来似乎不错了,那么它还有哪些问题?
- 鉴权,不是什么人都能发消息,因此需要增加鉴权
- 全局的redis来储存用户所在的server信息,一旦redis挂了,所有人都不能聊天了,因此可以考虑redis与mongodb结合,缓存+DB的形式,这将增加开发的复杂度
但主体的架构已经完成,总体来说是个还不错的IM服务器架构。
这里先空着,我将给出架构图:
// empty
六:下一代IM服务器的方向在哪里
本着大胆想象的原则,让我们畅想一下下一代的IM服务器。
但我这里还没有具体的答案,但大概有一些关键词,如AI,去中心化和边缘计算,有待思考和讨论