Jack Jiang

我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
posts - 490, comments - 13, trackbacks - 0, articles - 1

     摘要: 本文由微信开发团队工程师“ qiuwenchen”分享,发布于WeMobileDev公众号,有修订。1、引言全文搜索是使用倒排索引进行搜索的一种搜索方式。倒排索引也称为反向索引,是指对输入的内容中的每个Token建立一个索引,索引中保存了这个Token在内容中的具体位置。全文搜索技术主要应用在对大量文本内容进行搜索的场景。微信终端涉及到大量文本搜索的业务场景主要包括:im联...  阅读全文

posted @ 2022-02-28 17:48 Jack Jiang 阅读(144) | 评论 (0)编辑 收藏

本文由融云技术团队原创分享,有修订和改动。

1、引言

在视频直播场景中,弹幕交互、与主播的聊天、各种业务指令等等,组成了普通用户与主播之间的互动方式。

从技术的角度来看,这些实时互动手段,底层逻辑都是实时聊天消息或指令的分发,技术架构类比于IM应用的话,那就相当于IM聊天室功能。

本系列文章的上篇《百万人在线的直播间实时聊天消息分发技术实践》主要分享的是消息分发和丢弃策略。本文将主要从高可用、弹性扩缩容、用户管理、消息分发、客户端优化等角度,分享直播间海量聊天消息的架构设计技术难点的实践经验。

学习交流:

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK 

本文已同步发布于:http://www.52im.net/thread-3835-1-1.html

2、系列文章

本文是系列文章中的第7篇:

直播系统聊天技术(一):百万在线的美拍直播弹幕系统的实时推送技术实践之路

直播系统聊天技术(二):阿里电商IM消息平台,在群聊、直播场景下的技术实践

直播系统聊天技术(三):微信直播聊天室单房间1500万在线的消息架构演进之路

直播系统聊天技术(四):百度直播的海量用户实时消息系统架构演进实践

直播系统聊天技术(五):微信小游戏直播在Android端的跨进程渲染推流实践

直播系统聊天技术(六):百万人在线的直播间实时聊天消息分发技术实践

直播系统聊天技术(七):直播间海量聊天消息的架构设计难点实践》(* 本文

3、直播间的主要功能和技术特征

如今的视频直播间早已不单纯是视频流媒体技术问题,它还包含了用户可感知的多类型消息发送和管理、用户管理等任务。在万物皆可直播的当下,超大型直播场景屡见不鲜,甚至出现了人数无上限的场景,面对如此海量实时消息和指令的并发挑战,带来的技术难度已非常规手段所能解决。

我们先来归纳一下如今的典型视频直播间,相较于传统直播间所包含的主要功能特征、技术特征等。

丰富的消息类型和进阶功能:

  • 1)可发送文字、语音、图片等传统聊天功能;
  • 2)可实现点赞、礼物等非传统聊天功能的消息类型;
  • 3)可管理内容安全,包括敏感词设置,聊天内容反垃圾处理等。

聊天管理功能:

  • 1)用户管理:包括创建、加入、销毁、禁言、查询、封禁(踢人)等;
  • 2)用户白名单:白名单用户处于被保护状态不会被自动踢出,且发送消息优先级别最高;
  • 3)消息管理:包括消息优先级、消息分发控制等;
  • 4)实时统计及消息路由等能力。

人数上限和行为特征:

  • 1)人数没有上限:一些大型直播场景,如春晚、国庆大阅兵等,直播间累计观看动辄上千万人次,同时观看人数也可达数百万;
  • 2)用户进退行为:用户进出直播间非常频繁,高热度直播间的人员进出秒并发可能上万,这对服务支撑用户上下线以及用户管理的能力提出了非常大的挑战。

海量消息并发:

  • 1)消息并发量大:直播聊天室人数没有明显上限,带来了海量并发消息的问题(一个百万人数的聊天室,消息的上行已是巨量,消息分发量更是几何级上升);
  • 2)消息实时性高:如果服务器只做消息的消峰处理,峰值消息的堆积会造成整体消息延时增大。

针对上述第 2) 点,延时的累积效应会导致消息与直播视频流在时间线上产生偏差,进而影响用户观看直播时互动的实时性。所以,服务器的海量消息快速分发能力十分重要。

4、直播间聊天室的架构设计

高可用系统需要支持服务故障自动转移、服务精准熔断降级、服务治理、服务限流、服务可回滚、服务自动扩容 / 缩容等能力。

以服务高可用为目标的直播间聊天室系统架构如下:

如上图所示,系统架构主要分三层:

  • 1)连接层:主要管理服务跟客户端的长链接;
  • 2)存储层:当前使用的是 Redis,作为二级缓存,主要存储聊天室的信息(比如人员列表、黑白名单、封禁列表等,服务更新或重启时,可以从 Redis 中加载出聊天室的备份信息);
  • 3)业务层:这是整个聊天室的核心,为了实现跨机房容灾,将服务部署在多个可用区,并根据能力和职责,将其分为聊天室服务和消息服务。

聊天室服务和消息服务的具体职责:

  • 1)聊天室服务:主要负责处理管理类请求,比如聊天室人员的进出、封禁 / 禁言、上行消息处理审核等;
  • 2)消息服务:主要缓存本节点需要处理的用户信息以及消息队列信息,并负责聊天室消息的分发。

在海量用户高并发场景下,消息分发能力将决定着系统的性能。以一个百万级用户量的直播间聊天室为例,一条上行消息对应的是百万倍的分发。这种情况下,海量消息的分发,依靠单台服务器是无法实现的。

我们的优化思路是:将一个聊天室的人员分拆到不同的消息服务上,在聊天室服务收到消息后向消息服务扩散,再由消息服务分发给用户。

以百万在线的直播间聊天室为例:假设聊天室消息服务共 200 台,那平均每台消息服务管理 5000 人左右,每台消息服务在分发消息时只需要给落在本台服务器上的用户分发即可。

服务落点的选择逻辑:

  • 1)在聊天室服务中:聊天室的上行信令是依据聊天室 ID 使用一致性哈希算法来选择节点的;
  • 2)在消息服务中:依据用户 ID 使用一致性哈希算法来决定用户具体落在哪个消息服务。

一致性哈希选择的落点相对固定,可以将聊天室的行为汇聚到一个节点上,极大提升服务的缓存命中率。

聊天室人员进出、黑 / 白名单设置以及消息发送时的判断等处理直接访问内存即可,无须每次都访问第三方缓存,从而提高了聊天室的响应速度和分发速度。

最后:Zookeeper 在架构中主要用来做服务发现,各服务实例均注册到 Zookeeper。

5、直播间聊天室的扩缩容能力

5.1 概述

随着直播这种形式被越来越多人接受,直播间聊天室面对人数激增致使服务器压力逐步增大的情况越来越多。所以,在服务压力逐步增大 / 减少的过程中能否进行平滑的扩 / 缩容非常重要。

在服务的自动扩缩容方面,业内提供的方案大体一致:即通过压力测试了解单台服务器的瓶颈点  通过对业务数据的监控来判断是否需要进行扩缩 → 触发设定的条件后报警并自动进行扩缩容。

鉴于直播间聊天室的强业务性,具体执行中应该保证在扩缩容中整体聊天室业务不受影响。

5.2 聊天室服务扩缩容

聊天室服务在进行扩缩容时,我们通过 Redis 来加载成员列表、封禁 / 黑白名单等信息。

需要注意的是:在聊天室进行自动销毁时,需先判断当前聊天室是否应该是本节点的。如果不是,跳过销毁逻辑,避免 Redis 中的数据因为销毁逻辑而丢失。

聊天室服务扩缩容方案细节如下图所示:

5.3 消息服务扩缩容

消息服务在进行扩缩容时,大部分成员需要按照一致性哈希的原则路由到新的消息服务节点上。这个过程会打破当前的人员平衡,并做一次整体的人员转移。

1)在扩容时:我们根据聊天室的活跃程度逐步转移人员。

2)在有消息时:[消息服务会遍历缓存在本节点上的所有用户进行消息的通知拉取,在此过程中判断此用户是否属于这台节点(如果不是,将此用户同步加入到属于他的节点)。

3)在拉消息时:用户在拉取消息时,如果本机缓存列表中没有该用户,消息服务会向聊天室服务发送请求确认此用户是否在聊天室中(如果在则同步加入到消息服务,不在则直接丢掉)。

4)在缩容时:消息服务会从公共 Redis 获得全部成员,并根据落点计算将本节点用户筛选出来并放入用户管理列表中。

6、海量用户的上下线和管理

聊天室服务:管理了所有人员的进出,人员的列表变动也会异步存入 Redis 中。

消息服务:则维护属于自己的聊天室人员,用户在主动加入和退出房间时,需要根据一致性哈希算出落点后同步给对应的消息服务。

聊天室获得消息后:聊天室服务广播给所有聊天室消息服务,由消息服务进行消息的通知拉取。消息服务会检测用户的消息拉取情况,在聊天室活跃的情况下,30s 内人员没有进行拉取或者累计 30 条消息没有拉取,消息服务会判断当前用户已经离线,然后踢出此人,并且同步给聊天室服务对此成员做下线处理。

7、海量聊天消息的分发策略

直播间聊天室服务的消息分发及拉取方案如下图:

7.1 消息通知的拉取

在上图中:用户 A 在聊天室中发送一条消息,首先由聊天室服务处理,聊天室服务将消息同步到各消息服务节点,消息服务向本节点缓存的所有成员下发通知拉取(图中服务器向用户 B 和用户 Z 下发了通知)。

在消息分发过程中,server 做了通知合并。

通知拉取的详细流程为:

  • 1)客户端成功加入聊天,将所有成员加入到待通知队列中(如已存在则更新通知消息时间);
  • 2)下发线程,轮训获取待通知队列;
  • 3)向队列中用户下发通知拉取。

通过这个流程可保障下发线程一轮只会向同一用户发送一个通知拉取(即多个消息会合并为一个通知拉取),有效提升了服务端性能且降低了客户端与服务端的网络消耗。

7.2 消息的拉取

用户的消息拉取流程如下图:

 

如上图所示,用户 B 收到通知后向服务端发送拉取消息请求,该请求最终将由消息节点 1 进行处理,消息节点 1 将根据客户端传递的最后一条消息时间戳,从消息队列中返回消息列表(参考下图 )。

客户端拉取消息示例:

用户端本地最大时间为 1585224100000,从 server 端可以拉取到比这个数大的两条消息。

7.3 消息控速

服务器应对海量消息时,需要做消息的控速处理。

这是因为:在直播间聊天室中,大量用户在同一时段发送的海量消息,一般情况下内容基本相同。如果将所有消息全部分发给客户端,客户端很可能出现卡顿、消息延迟等问题,严重影响用户体验。

所以服务器对消息的上下行都做了限速处理。

消息控速原理:

具体的限速控制策略如下:

  • 1)服务器上行限速控制(丢弃)策略:针对单个聊天室的消息上行的限速控制,我们默认为 200 条 / 秒,可根据业务需要调整。达到限速后发送的消息将在聊天室服务丢弃,不再向各消息服务节点同步;
  • 2)服务器下行限速(丢弃)策略:服务端的下行限速控制,主要是根据消息环形队列的长度进行控制,达到最大值后最“老”的消息将被淘汰丢弃。

每次下发通知拉取后服务端将该用户标记为拉取中,用户实际拉取消息后移除该标记。

如果产生新消息时用户有拉取中标记:

  • 1)距设置标记时间在 2 秒内,则不会下发通知(降低客户端压力,丢弃通知未丢弃消息);
  • 2)超过 2 秒则继续下发通知(连续多次通知未拉取则触发用户踢出策略,不在此赘述)。

因此:消息是否被丢弃取决于客户端拉取速度(受客户端性能、网络影响),客户端及时拉取消息则没有被丢弃的消息。

8、直播间聊天室的消息优先级

消息控速的核心是对消息的取舍,这就需要对消息做优先级划分。

划分逻辑大致如下:

  • 1)白名单消息:这类消息最为重要,级别最高,一般系统类通知或者管理类信息会设置为白名单消息;
  • 2)高优先级消息:仅次于白名单消息,没有特殊设置过的消息都为高优先级;
  • 3)低优先级消息:最低优先级的消息,这类消息大多是一些文字类消息。

具体如何划分,应该是可以开放出方便的接口进行设置的。

服务器对三种消息执行不同的限速策略,在高并发时,低优先级消息被丢弃的概率最大。

服务器将三种消息分别存储在三个消息桶中:客户端在拉取消息时按照白名单消息  高优先级消息  低优先级消息的顺序拉取。

9、客户端针对大量消息的接收和渲染优化

9.1 消息的接收优化

在消息同步机制方面,如果直播间聊天室每收到一条消息都直接下发到客户端,无疑会给客户端带来极大性能挑战。特别是在每秒几千或上万条消息的并发场景下,持续的消息处理会占用客户端有限的资源,影响用户其它方面的互动。

考虑到以上问题,为聊天室单独设计了通知拉取机制,由服务端进行一系列分频限速聚合等控制后,再通知客户端拉取。

具体分为以下几步:

  • 1)客户端成功加入聊天室;
  • 2)服务端下发通知拉取信令;
  • 3)客户端根据本地存储的消息最大时间戳,去服务端拉取消息。

这里需要注意的是:首次加入直播间聊天室时,本地并没有有效时间戳,此时会传 0 给服务拉取最近 50 条消息并存库。后续再次拉取时才会传递数据库里存储的消息的最大时间戳,进行差量拉取。

客户端拉取到消息后:会进行排重处理,然后将排重后的数据上抛业务层,以避免上层重复显示。

另外:直播间聊天室中的消息即时性较强,直播结束或用户退出聊天室后,之前拉取的消息大部分不需要再次查看,因此在用户退出聊天室时,会清除数据库中该聊天室的所有消息,以节约存储空间。

9.2 消息的渲染优化

在消息渲染方面,客户端也通过一系列优化保证在直播间聊天室大量消息刷屏的场景下仍有不俗的表现。

以Andriod端为例,具体的措施有:

  • 1)采用 MVVM 机制:将业务处理和 UI 刷新严格区分。每收到一条消息,都在 ViewModel 的子线程将所有业务处理好,并将页面刷新需要的数据准备完毕后,才通知页面刷新;
  • 2)降低主线程负担:精确使用 LiveData 的 setValue() 和 postValue() 方法:已经在主线程的事件通过  setValue() 方式通知 View 刷新,以避免过多的 postValue() 造成主线程负担过重;
  • 3)减少非必要刷新:比如在消息列表滑动时,并不需要将接收到的新消息刷新出来,仅进行提示即可;
  • 4)识别数据的更新:通过谷歌的数据对比工具 DiffUtil 识别数据是否有更新,仅更新有变更的部分数据;
  • 5)控制全局刷新次数:尽量通过局部刷新进行 UI 更新。

通过以上机制:从压测结果看,在中端手机上,直播间聊天室中每秒 400 条消息时,消息列表仍然表现流畅,没有卡顿。

10、针对传统聊天消息外的自定义属性优化

10.1 概述

在直播间聊天室场景中,除了传统的聊天消息收发以外,业务层经常需要有自己的一些业务属性,如在语音直播聊天室场景中的主播麦位信息、角色管理等,还有狼人杀等卡牌类游戏场景中记录用户的角色和牌局状态等。

相对于传统聊天消息,自定义属性有必达和时效的要求,比如麦位、角色等信息需要实时同步给聊天室的所有成员,然后客户端再根据自定义属性刷新本地的业务。

10.2 自定义属性的存储

自定义属性是以 key 和 value 的形式进行传递和存储的。自定义属性的操作行为主要有两种:即设置和删除。

服务器存储自定义属性也分两部分:

  • 1)全量的自定义属性集合;
  • 2)自定义属性集合变更记录。

自定义属性存储结构如下图所示:

针对这两份数据,应该提供两种查询接口,分别是查询全量数据和查询增量数据。这两种接口的组合应用可以极大提升聊天室服务的属性查询响应和自定义分发能力。

10.3 自定义属性的拉取

内存中的全量数据,主要给从未拉取过自定义属性的成员使用。刚进入聊天室的成员,直接拉取全量自定义属性数据然后展示即可。

对于已经拉取过全量数据的成员来说,若每次都拉取全量数据,客户端想获得本次的修改内容,就需要比对客户端的全量自定义属性与服务器端的全量自定义属性,无论比对行为放在哪一端,都会增加一定的计算压力。

所以:为了实现增量数据的同步,构建一份属性变更记录集合十分必要。这样:大部分成员在收到自定义属性有变更来拉取时,都可以获得增量数据。

属性变更记录采用的是一个有序的 map 集合:key 为变更时间戳,value 里存着变更的类型以及自定义属性内容,这个有序的 map 提供了这段时间内所有的自定义属性的动作。

自定义属性的分发逻辑与消息一致:均为通知拉取。即客户端在收到自定义属性变更拉取的通知后,带着自己本地最大自定义属性的时间戳来拉取。比如:如果客户端传的时间戳为 4,则会拉取到时间戳为 5 和时间戳为 6 的两条记录。客户端拉取到增量内容后在本地进行回放,然后对自己本地的自定义属性进行修改和渲染。

11、多人群聊参考资料

[1] IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?

[2] IM群聊消息如此复杂,如何保证不丢不重?

[3] 移动端IM中大规模群消息的推送如何保证效率、实时性?

[4] 现代IM系统中聊天消息的同步和存储方案探讨

[5] 关于IM即时通讯群聊消息的乱序问题讨论

[6] IM群聊消息的已读回执功能该怎么实现?

[7] IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?

[8] 一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践

[9] IM群聊机制,除了循环去发消息还有什么方式?如何优化?

[10] 网易云信技术分享:IM中的万人群聊技术方案实践总结

[11] 阿里钉钉技术分享:企业级IM王者——钉钉在后端架构上的过人之处

[12] IM群聊消息的已读未读功能在存储空间方面的实现思路探讨

[13] 企业微信的IM架构设计揭秘:消息模型、万人群、已读回执、消息撤回等

[14] 融云IM技术分享:万人群聊消息投递方案的思考和实践

本文已同步发布于:http://www.52im.net/thread-3835-1-1.html

posted @ 2022-02-23 12:50 Jack Jiang 阅读(253) | 评论 (0)编辑 收藏

本文由cxuan分享,原题“原来这才是 Socket”,有修订。

1、引言

本系列文章前面那些主要讲解的是计算机网络的理论基础,但对于即时通讯IM这方面的应用层开发者来说,跟计算机网络打道的其实是各种API接口。

本篇文章就来聊一下网络应用程序员最熟悉的Socket这个东西,抛开生涩的计算机网络理论,从应用层的角度来理解到底什么是Socket。

对于 Socket 的认识,本文将从以下几个方面着手介绍:

  • 1)Socket 是什么;
  • 2)Socket 是如何创建的;
  • 3)Socket 是如何连接的;
  • 4)Socket 是如何收发数据的;
  • 5)Socket 是如何断开连接的;
  • 6)Socket 套接字的删除等。

特别说明:本文中提到的“Socket”、“网络套接字”、“套接字”,如无特殊指明,指的都是同一个东西哦。

 

学习交流:

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK 

本文已同步发布于:http://www.52im.net/thread-3821-1-1.html

2、Socket 是什么

一个数据包经由应用程序产生,进入到协议栈中进行各种报文头的包装,然后操作系统调用网卡驱动程序指挥硬件,把数据发送到对端主机。

整个过程的大体的图示如下:

我们大家知道,协议栈其实是位于操作系统中的一些协议的堆叠,这些协议包括 TCP、UDP、ARP、ICMP、IP等。

通常某个协议的设计都是为了解决特定问题的,比如:

  • 1)TCP 的设计就负责安全可靠的传输数据;
  • 2)UDP 设计就是报文小,传输效率高;
  • 3)ARP 的设计是能够通过 IP 地址查询物理(Mac)地址;
  • 4)ICMP 的设计目的是返回错误报文给主机;
  • 5)IP 设计的目的是为了实现大规模主机的互联互通。

应用程序比如浏览器、电子邮件、文件传输服务器等产生的数据,会通过传输层协议进行传输。而应用程序是不会和传输层直接建立联系的,而是有一个能够连接应用层和传输层之间的套件,这个套件就是 Socket

在上面这幅图中,应用程序包含 Socket 和解析器,解析器的作用就是向 DNS 服务器发起查询,查询目标 IP 地址(关于DNS请见《理论联系实际,全方位深入理解DNS)。

应用程序的下面:就是操作系统内部,操作系统内部包括协议栈,协议栈是一系列协议的堆叠。

操作系统下面:就是网卡驱动程序,网卡驱动程序负责控制网卡硬件,驱动程序驱动网卡硬件完成收发工作。

在操作系统内部有一块用于存放控制信息的存储空间,这块存储空间记录了用于控制通信的控制信息。其实这些控制信息就是 Socket 的实体,或者说存放控制信息的内存空间就是Socket的实体。

这里大家有可能不太清楚所以然,所以我用了一下 netstat 命令来给大伙看一下Socket是啥玩意。

我们在 Windows 的命令提示符中输入:

netstat-ano

# netstat 用于显示Socket内容 , -ano 是可选选项

# a 不仅显示正在通信的Socket,还显示包括尚未开始通信等状态的所有Socket

# n 显示 IP 地址和端口号

# o 显示Socket的程序 PID

我的计算机会出现下面结果:

如上图所示:

  • 1)每一行都相当于一个Socket;
  • 2)每一列也被称为一个元组。

所以,一个Socket就是五元组:

  • 1)协议;
  • 2)本地地址;
  • 3)外部地址;
  • 4)状态;
  • 5)PID。

PS:有的时候也被叫做四元组,四元组不包括协议。

我们来解读一下上图中的数据,比如图中的第一行:

1)它的协议就是 TCP,本地地址和远程地址都是 0.0.0.0这表示通信还没有开始,IP 地址暂时还未确定)。

2)而本地端口已知是 135,但是远程端口还未知,此时的状态是 LISTENING(LISTENING 表示应用程序已经打开,正在等待与远程主机建立连接。关于各种状态之间的转换,大家可以阅读《通俗易懂-深入理解TCP协议(上):理论基础)。

3)最后一个元组是 PID,即进程标识符,PID 就像我们的身份证号码,能够精确定位唯一的进程。

3、Socket 是如何创建的

通过上节的讲解,现在你可能对 Socket 有了一个基本的认识,先喝口水,休息一下,让我们继续探究 Socket。

现在我有个问题,Socket 是如何创建的呢?

Socket 是和应用程序一起创建的。

应用程序中有一个 socket 组件,在应用程序启动时,会调用 socket 申请创建Socket,协议栈会根据应用程序的申请创建Socket:首先分配一个Socket所需的内存空间,这一步相当于是为控制信息准备一个容器,但只有容器并没有实际作用,所以你还需要向容器中放入控制信息;如果你不申请创建Socket所需要的内存空间,你创建的控制信息也没有地方存放,所以分配内存空间,放入控制信息缺一不可。至此Socket的创建就已经完成了。

Socket创建完成后,会返回一个Socket描述符给应用程序,这个描述符相当于是区分不同Socket的号码牌。根据这个描述符,应用程序在委托协议栈收发数据时就需要提供这个描述符。

4、Socket 是如何连接的

Socket创建完成后,最终还是为数据收发服务的。但是,在数据收发之前,还需要进行一步“连接”(术语就是 connect),建立连接有一整套过程。

这个“连接”并不是真实的连接(用一根水管插在两个电脑之间?不是你想的这样。。。)。

实际上这个“连接”是应用程序通过 TCP/IP 协议标准从一个主机通过网络介质传输到另一个主机的过程。

Socket刚刚创建完成后,还没有数据,也不知道通信对象。

在这种状态下:即使你让客户端应用程序委托协议栈发送数据,它也不知道发送到哪里。所以浏览器需要根据网址来查询服务器的 IP 地址(做这项工作的协议是 DNS),查询到目标主机后,再把目标主机的 IP 告诉协议栈。至此,客户端这边就准备好了。

在服务器上:与客户端一样也需要创建Socket,但是同样的它也不知道通信对象是谁,所以我们需要让客户端向服务器告知客户端的必要信息:IP 地址和端口号

现在通信双方建立连接的必要信息已经具备,可以开始“连接”过程了。

首先:客户端应用程序需要调用 Socket 库中的 connect 方法,提供 socket 描述符和服务器 IP 地址、端口号。

以下是connect的伪码调用:

connect(<描述符>、<服务器IP地址和端口号>)

这些信息会传递给协议栈中的 TCP 模块,TCP 模块会对请求报文进行封装,再传递给 IP 模块,进行 IP 报文头的封装,然后传递给物理层,进行帧头封装。

之后通过网络介质传递给服务器,服务器上会对帧头、IP 模块、TCP 模块的报文头进行解析,从而找到对应的Socket。

Socket收到请求后,会写入相应的信息,并且把状态改为正在连接。

请求过程完成后:服务器的 TCP 模块会返回响应,这个过程和客户端是一样的(如果大家不太清楚报文头的封装过程,可以阅读《快速理解TCP协议一篇就够)。

在一个完整的请求和响应过程中,控制信息起到非常关键的作用:

  • 1)SYN 就是同步的缩写,客户端会首先发送 SYN 数据包,请求服务端建立连接;
  • 2)ACK 就是相应的意思,它是对发送 SYN 数据包的响应;
  • 3)FIN 是终止的意思,它表示客户端/服务器想要终止连接。

由于网络环境的复杂多变,经常会存在数据包丢失的情况,所以双方通信时需要相互确认对方的数据包是否已经到达,而判断的标准就是 ACK 的值。

上面的文字不够生动,动画可以更好的说明这个过程:

PS:这个“连接”的详细理论知识,可以阅读《理论经典:TCP协议的3次握手与4次挥手过程详解》、《跟着动画来学TCP三次握手和四次挥手》,这里不再赘述。

当所有建立连接的报文都能够正常收发之后,此时套接字就已经进入可收发状态了,此时可以认为用一根管理把两个套接字连接了起来。当然,实际上并不存在这个管子。建立连接之后,协议栈的连接操作就结束了,也就是说 connect 已经执行完毕,控制流程被交回给应用程序。

另外:如果你对Socket代码更熟悉的话,可以先读读这篇《手把手教你写基于TCP的Socket长连接》。

5、Socket 是如何收发数据的

当控制流程上节中的连接过程回到应用程序之后,接下来就会直接进入数据收发阶段。

数据收发操作是从应用程序调用 write 将要发送的数据交给协议栈开始的,协议栈收到数据之后执行发送操作。

协议栈不会关心应用程序传输过来的是什么数据,因为这些数据最终都会转换为二进制序列,协议栈在收到数据之后并不会马上把数据发送出去,而是会将数据放在发送缓冲区,再等待应用程序发送下一条数据。

为什么收到数据包不会直接发送出去,而是放在缓冲区中呢?

因为只要一旦收到数据就会发送,就有可能发送大量的小数据包,导致网络效率下降(所以协议栈需要将数据积攒到一定数量才能将其发送出去)。

至于协议栈会向缓冲区放多少数据,这个不同版本和种类的操作系统有不同的说法。

不过,所有的操作系统都会遵循下面这几个标准:

1)第一个判断要素:是每个网络包能够容纳的数据长度,判断的标准是 MTU,它表示的是一个网络包的最大长度。最大长度包含头部,所以如果单论数据区的话,就会用 MTU - 包头长度,由此的出来的最大数据长度被称为 MSS。

 

2)另一个判断标准:是时间,当应用程序产生的数据比较少,协议栈向缓冲区放置数据效率不高时,如果每次都等到 MSS 再发送的话,可能因为等待时间太长造成延迟。在这种情况下,即使数据长度没有到达 MSS,也应该把数据发送出去。

但协议栈并没有告诉我们怎样平衡这两个因素,如果数据长度优先,那么效率有可能比较低;如果时间优先,那又会降低网络的效率。

经过了一段时间。。。。。。

假设我们使用的是长度有限法则:此时缓冲区已满,协议栈要发送数据了,协议栈刚要把数据发送出去,却发现无法一次性传输这么大数据量(相对的)的数据,那怎么办呢?

在这种情况下,发送缓冲区中的数据就会超过 MSS 的长度,发送缓冲区中的数据会以 MSS 大小为一个数据包进行拆分,拆分出来的每块数据都会加上 TCP,IP,以太网头部,然后被放进单独的网络包中。

到现在,网络包已经准备好发往服务器了,但是数据发送操作还没有结束,因为服务器还未确认是否已经收到网络包。因此在客户端发送数据包之后,还需要服务器进行确认。

TCP 模块在拆分数据时,会计算出网络包偏移量,这个偏移量就是相对于数据从头开始计算的第几个字节,并将算好的字节数写在 TCP 头部,TCP 模块还会生成一个网络包的序号(SYN),这个序号是唯一的,这个序号就是用来让服务器进行确认的。

服务器会对客户端发送过来的数据包进行确认,确认无误之后,服务器会生成一个序号和确认号(ACK)并一起发送给客户端,客户端确认之后再发送确认号给服务器。

我们来看一下实际的工作过程:

首先:客户端在连接时需要计算出序号初始值,并将这个值发送给服务器。

接下来:服务器通过这个初始值计算出确认号并返回给客户端(初始值在通信过程中有可能会丢弃,因此当服务器收到初始值后需要返回确认号用于确认)。

同时:服务器也需要计算出从服务器到客户端方向的序号初始值,并将这个值发送给客户端。然后,客户端也需要根据服务器发来的初始值计算出确认号发送给服务器。

至此:连接建立完成,接下来就可以进入数据收发阶段了。

数据收发阶段中,通信双方可以同时发送请求和响应,双方也可以同时对请求进行确认。

请求 - 确认机制非常强大:通过这一机制,我们可以确认接收方有没有收到某个包,如果没有收到则重新发送,这样一来,但凡网络中出现的任何错误,我们都可以即使发现并补救。

上面的文字不够生动,动画可以更好的理解请求 - 确认机制:

网卡、集线器、路由器(见《史上最通俗的集线器、交换机、路由器功能原理入门)都没有错误补救机制,一旦检测到错误就会直接丢弃数据包,应用程序也没有这种机制,起作用的只是 TCP/IP 模块。

由于网络环境复杂多变,所以数据包会存在丢失情况,因此发送序号和确认号也存在一定规则,TCP 会通过窗口管理确认号,我们这篇文章不再赘述,大家可以阅读《通俗易懂-深入理解TCP协议(下):RTT、滑动窗口、拥塞处理》来寻找答案。

PS:另一篇《我们在读写Socket时,究竟在读写什么?》中用动画详细说明了这个过程,有兴趣可以读一读。

6、Socket 是如何断开连接的

当通信双方不再需要收发数据时,需要断开连接。不同的应用程序断开连接的时机不同。

以 Web 为例:浏览器向 Web 服务器发送请求消息,Web 服务器再返回响应消息,这时收发数据就全部结束了,服务器可能会首先发起断开响应,当然客户端也有可能会首先发起(谁先断开连接是应用程序做出的判断),与协议栈无关。

无论哪一方发起断开连接的请求,都会调用 Socket 库的 close 程序。

我们以服务器断开连接为例:服务器发起断开连接请求,协议栈会生成断开连接的 TCP 头部,其实就是设置 FIN 位,然后委托 IP 模块向客户端发送数据,与此同时,服务器的Socket会记录下断开连接的相关信息。

收到服务器发来 FIN 请求后:客户端协议栈会将Socket标记为断开连接状态,然后,客户端会向服务器返回一个确认号,这是断开连接的第一步,在这一步之后,应用程序还会调用 read 来读取数据。等到服务器数据发送完成后,协议栈会通知客户端应用程序数据已经接收完毕。

只要收到服务器返回的所有数据,客户端就会调用 close 程序来结束收发操作,这时客户端会生成一个 FIN 发送给服务器,一段时间后服务器返回 ACK 号。至此,客户端和服务器的通信就结束了。

上面的文字不够生动,动画可以更好的说明这个过程:

PS:断开连接的详细理论知识,可以阅读《理论经典:TCP协议的3次握手与4次挥手过程详解》、《跟着动画来学TCP三次握手和四次挥手》,这里不再赘述。

7、Socket的删除

上述通信过程完成后,用来通信的Socket就不再会使用了,此时我们就可以删除这个Socket了。

不过,这时候Socket不会马上删除,而是等过一段时间再删除。

等待这段时间是为了防止误操作,最常见的误操作就是客户端返回的确认号丢失,至于等待多长时间,和数据包重传的方式有关,这里我们就深入展开讨论了。

关于Socket操作的全过程,如果从系统的角度来看,可能会更深入一些,建议可以深入阅读张彦飞的《深入操作系统,从内核理解网络包的接收过程(Linux篇)》一文。

8、系列文章

本文是系列文章中的第14篇,本系列文章的大纲如下:

[1] 网络编程懒人入门(一):快速理解网络通信协议(上篇)

[2] 网络编程懒人入门(二):快速理解网络通信协议(下篇)

[3] 网络编程懒人入门(三):快速理解TCP协议一篇就够

[4] 网络编程懒人入门(四):快速理解TCP和UDP的差异

[5] 网络编程懒人入门(五):快速理解为什么说UDP有时比TCP更有优势

[6] 网络编程懒人入门(六):史上最通俗的集线器、交换机、路由器功能原理入门

[7] 网络编程懒人入门(七):深入浅出,全面理解HTTP协议

[8] 网络编程懒人入门(八):手把手教你写基于TCP的Socket长连接

[9] 网络编程懒人入门(九):通俗讲解,有了IP地址,为何还要用MAC地址?

[10] 网络编程懒人入门(十):一泡尿的时间,快速读懂QUIC协议

[11] 网络编程懒人入门(十一):一文读懂什么是IPv6

[12] 网络编程懒人入门(十二):快速读懂Http/3协议,一篇就够!

[13] 网络编程懒人入门(十三):一泡尿的时间,快速搞懂TCP和UDP的区别

[14] 网络编程懒人入门(十四):到底什么是Socket?一文即懂!* 本文)

9、参考资料

[1] TCP/IP详解 - 第17章·TCP:传输控制协议

[2] TCP/IP详解 - 第18章·TCP连接的建立与终止

[3] TCP/IP详解 - 第21章·TCP的超时与重传

[4] 快速理解网络通信协议(上篇)

[5] 快速理解网络通信协议(下篇)

[6] 面视必备,史上最通俗计算机网络分层详解

[7] 假如你来设计网络,会怎么做?

[8] 假如你来设计TCP协议,会怎么做?

[10] 浅析TCP协议中的疑难杂症(下篇)

[11] 关闭TCP连接时为什么会TIME_WAIT、CLOSE_WAIT

[12] 从底层入手,深度分析TCP连接耗时的秘密

本文已同步发布于:http://www.52im.net/thread-3821-1-1.html

posted @ 2022-02-16 12:17 Jack Jiang 阅读(252) | 评论 (0)编辑 收藏

本文原题“搭建高性能的IM系统”,作者“刘莅”,内容有修订和改动。为了尊重原创,如需转载,请联系作者获得授权。

1、引言

相信很多朋友对微信、QQ等聊天软件的实现原理都非常感兴趣,笔者同样对这些软件有着深厚的兴趣。而且笔者在公司也是做IM的,公司的IM每天承载着上亿条消息的发送!

正好有这样的技术资源和条件,所以前段时间,笔者利用业余时间,基于Netty开发了一套基本功能比较完善的IM系统。该系统支持私聊、群聊、会话管理、心跳检测,支持服务注册、负载均衡,支持任意节点水平扩容。

这段时间,网上的一些读者,也希望笔者分享一些Netty或者IM相关的知识,所以今天笔者把开发的这套IM系统分享给大家。

本文将根据笔者这次的业余技术实践,为你讲述如何基于Netty+Zk+Redis来搭建一套高性能IM集群,包括本次实现IM集群的技术原理和实例代码,希望能带给你启发。

学习交流:

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK 

本文已同步发布于:http://www.52im.net/thread-3816-1-1.html )

2、本文源码

主地址:https://github.com/nicoliuli/chat

备地址:https://github.com/52im/chat

源码的目录结构,如下图所示:

3、知识准备

* 重要提示:本文不是一篇即时通讯理论文章,文章内容来自代码实战,如果你对即时通讯(IM)技术理论了解的太少,建议先详细阅读:《新手入门一篇就够:从零开发移动端IM》。

可能有人不知道 Netty 是什么,这里简单介绍下:

Netty 是一个 Java 开源框架。Netty 提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。

也就是说,Netty 是一个基于 NIO 的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户,服务端应用。

Netty 相当简化和流线化了网络应用的编程开发过程,例如,TCP 和 UDP 的 Socket 服务开发。

以下是有关Netty的入门文章:

如果你连Java的NIO都不知道是什么,下面的文章建议优先读:

Netty源码和API的在线查阅地址:

4、系统架构

系统的架构如上图所示:整个系统是一个C/S系统,客户端没有做复杂的图形化界面而是用Java终端开发的(黑窗口),服务端IM实例是Netty写的socket服务。

ZK作为服务注册中心,Redis用来做分布式会话的缓存,并保存用户信息和轻量级的消息队列。

对于整个系统架构中各部分的工作原理,我们将在接下来的各章节中一一介绍。

5、服务端的工作原理

在上述架构中:NettyServer启动,每启动一台Server节点,都会把自身的节点信息,如:ip、port等信息注册到ZK上(临时节点)。

正如上节架构图上启动了两台NettyServer,所以ZK上会保存两个Server的信息。

同时ZK将监听每台Server节点,如果Server宕机ZK就会删除当前机器所注册的信息(把临时节点删除),这样就完成了简单的服务注册的功能。

6、客户端的工作原理

Client启动时,会先从ZK上随机选择一个可用的NettyServer(随机表示可以实现负载均衡),拿到NettyServer的信息(IP和port)后与NettyServer建立链接。

链接建立起来后,NettyServer端会生成一个Session(即会话),用来把当前客户端的Channel等信息组装成一个Session对象,保存在一个SessionMap里,同时也会把这个Session保存在Redis中。

这个会话特别重要,通过会话,我们能获取当前Client和NettyServer的Channel等信息。

7、Session的作用

我们启动多个Client,由于每个Client启动,都会先从ZK上随机获取NettyServer的的信息,所以如果启动多个Client,就会连接到不同的NettyServer上。

熟悉Netty的朋友都知道,Client与Server建立接连后会产生一个Channel,通过Channel,Client和Server才能进行正常的网络数据传输。

如果Client1和Client2连接在同一个Server上:那么Server通过SessionMap分别拿到Client1和Client2的会话,会话中包含Channel信息,有了两个Client的Channel,Client1和Client2便可完成消息通信。

如果Client1和Client2连接到不同的NettyServer上:Client1和Client2要进行通信,该怎么办?这个问题放在后面解答。

8、高效的数据传输

无论是IM系统,还是分布式的RPC框架,高效的网络数据传输,无疑会极大的提升系统的性能。

数据通过网络传输时,一般把对象通序列化成二进制字节流数组,然后将数据通过socket传给对方服务器,对方服务器拿到二进制字节流后再反序列化成对象,达到远程通信的目的。

在Java领域,Java序列化对象的方式有严重的性能问题,业界常用谷歌的protobuf来实现序列化反序列化(见《Protobuf通信协议详解:代码演示、详细原理介绍等)。

protobuf支持不同的编程语言,可以实现跨语言的系统调用,并且有着极高的序列化反序列化性能,本系统也采用protobuf来做数据的序列化。

关于Protobuf的基本认之,下面这几篇可以深入读一读:

  1. 强列建议将Protobuf作为你的即时通讯应用数据传输格式
  2. 全方位评测:Protobuf性能到底有没有比JSON快5倍?
  3. 金蝶随手记团队分享:还在用JSON? Protobuf让数据传输更省更快(原理篇)

另外:一套海量在线用户的移动端IM架构设计实践分享(含详细图文)》一文中,“3、协议设计”这一节有关于protobuf在IM中的实战设计和使用,可以一并学习一下。

9、聊天协议定义

我们在使用各种聊天APP时,会发各种各样的消息,每种消息都会对应不同的消息格式(即“聊天协议”)。

聊天协议中主要包含几种重要的信息:

  • 1)消息类型;
  • 2)发送时间;
  • 3)消息的收发人;
  • 4)聊天类型(群聊或私聊)。

我的这套IM系统中,聊天协议定义如下:

syntax = "proto3";

option java_package = "model.chat";

option java_outer_classname = "RpcMsg";

message Msg{

    string msg_id = 1;

    int64 from_uid = 2;

    int64 to_uid = 3;

    int32 format = 4;

    int32 msg_type = 5;

    int32 chat_type = 6;

    int64 timestamp = 7;

    string body = 8;

    repeated int64 to_uid_list = 9;

}

如上面的protobuf代码,字段的具体含义如下:

  • 1)msg_id:表示消息的唯一id,可以用UUID表示;
  • 2)from_uid:消息发送者的uid;
  • 3)to_uid:消息接收者的uid;
  • 4)format:消息格式,我们使用各种聊天软件时,会发送文字消息,语音消息,图片消息等等等等,每种消息有不同的消息格式,我们用format来表示(由于本系统是java终端,format字段没有太大含义,可有可无);
  • 5)msg_type:消息类型,比如登录消息、聊天消息、ack消息、ping、pong消息;
  • 6)chat_type:聊天类型,如群聊、私聊;
  • 7)timestamp:发送消息的时间戳;
  • 8)body:消息的具体内容,载体;
  • 9)to_uid_list:这个字段用户群聊消息提高群聊消息的性能,具体作用会在群聊原理部分详细解释。

10、私聊消息发送原理

Client1给Client2发消息时,我们需要构建上节中的消息体。

具体就是:from_uid是Client1的uid、to_uid是Client2的uid。

NettyServer收到消息后的处理逻辑是:

  • 1)解析到to_uid字段;
  • 2)从SessionMap或者Redis中保存的Session集合中获取to_uid即Client2的Session;
  • 3)从Session中取出Client2的Channel;
  • 4)然后将消息通过Client2的Channel发给Client2。

11、群聊消息发送原理

群聊消息的分发通常有两种技术实现方式,我们一一来看看。

方式一:假设一个群有100人,如果Client1给一个群的所有人发消息,其实相当于Client1分别给其余99人分别发一条消息。我们可以直接在Client端,通过循环,分别给群里的99人发消息即可,相当于Client发送给NettyServer发送了99次相同的消息(除了to_uid不同)。

上述方案有很严重的性能问题:Client1通过循环99次,分别把消息发给NettyServer,NettyServer收到这99条消息后,分别将消息发给群内其余的用户。先抛开移动端的特殊性(比如循环还没完成手机就有可能退到后台被系统挂起),显然Client1到NettyServer的99次循环存在明显不合理地方。

方式二:上节的消息体中to_uid_list字段就是为了解决这个方式一的性能问题的。Client1把群内其余99个Client的uid保存在to_uid_list中,然后NettyServer只发一条消息,NettyServer收到这一条消息后,通过to_uid_list字段解析群内其余99的Client的uid,再通过循环把消息分别发送给群内其余的Client。

可以看到:方式二的群聊时,Client1与NettyServer只进行1次消息传输,相比于方式一,效率提高了50%。

11、技术关键点1:客户端分别连接在不同IM实例时如何通信?

针对本文中的架构,如果多个Client分别连接在不同的Server上,Client之间应该如何通信呢?

为了回答这个问题,我们首先要明白Session的作用。

我们做过JavaWeb开发的朋友都知道,Session用来保存用户的登录信息。

在IM系统中也是如此:Session中保存用户的Channel信息。当Client与Server建立链接成功后,会产生一个Channel,Client和Server是通过Channel,实现数据传输。当两端链接建立起来后,Server会构建出一个Session对象,保存uid和Channel等信息,并把这个Session保存在一个SessionMap里(NettyServer的内存里),uid为key,我们可以通过uid就可以找到这个uid对应的Session。

但只有SessionMap还不够:我们需要利用Redis,它的作用是保存整个NettyServer集群全部链接成功的用户,这也是一种Session,但这种Session没有保存uid和Channel的对应关系,而是保存Client链接到NettyServer的信息,如Client链接到的这个NettyServer的ip、port等。通过uid,我们同样可以从Redis中拿到当前Client链接到的NettyServer的信息。正是有了这个信息,我们才能做到,NettyServer集群任意节点水平扩容。

当用户量少的时候:我们只需要一台NettyServer节点便可以扛住流量,所有的Client链接到同一个NettyServer上,并在NettyServer的SessionMap中保存每个Client的会话。Client1与Client2通信时,Client1把消息发给NettyServer,NettyServer从SessionMap中取出Client2的Session和Channel,将消息发给Client2。

随着用户量不断增多:一台NettyServer不够,我们增加了几台NettyServer,这时Client1链接到NettyServer1上并在SessionMap和Redis中保存了会话和Client1的链接信息,Client2链接到NettyServer2上并在SessionMap和Redis中保存了会话和Client2的链接信息。Client1给Client2发消息时,通过NettyServer1的SessionMap找不到Client2的会话,消息无法发送,于是便从Redis中获取Client2链接在哪台NettyServer上。获取到Client2所链接的NettyServer信息后,我们可以把消息转发给NettyServer2,NettyServer2收到消息后,从NettyServer2的SessionMap中获取Client2的Session和Channel,然后将消息发送给Client2。

那么:NettyServer1的消息如何转发给NettyServer2呢?答案是通过消息队列,如Redis中的list数据结构。每台NettyServer启动后都需要监听一个自己的Redis中的消息队列,这个队列用户接收其他NettyServer转发给当前NettyServer的消息。

* Jack Jiang点评:上述集群方案中,Redis既作为在线用户列表存储中心,又作为集群中不同IM长连接实例的消息中转服务(此时的Redis作用相当于MQ),那Redis不就成为了整个分布式集群的单点瓶颈了吗?

12、技术关键点2:链接断开,如何处理?

如果Client与NettyServer,由于某种原因(客户端退出、服务端重启、网络因素等)断开链接,我们必须要从SessionMap删除会话和Redis中保留的数据。

如果不清除这两类数据的话,很有可能Client1发送给Client2的消息,可能会发给其他用户,或者就算Client2处于登录状态,Client2也收到不到消息。

我们可以在Netty框架中的channelInactive方法里,处理链接断开后的会话清除操作。

13、技术关键点3:ping、pong的作用

当Client与NettyServer建立链接后,由于双端网络较差,Client与NettyServer断开链接后,如果NettyServer没有感知到,也就没有清除SessionMap和Redis中的数据,这将会造成严重的问题(对于服务端来说,这个Client的会话实际处于“假死”状态,消息是无法实时发送过去的)。

此时就需要一种ping/pong机制(也就是心跳机制啦)。

实现原理就是:通过定时任务,Client每隔一段时间给NettyServer发一个ping消息,NettyServer收到ping消息后给客户端回复一个pong消息,确保客户端和服务端能一直保持链接状态。如果Client与NettyServer断连了,NettyServer可以立即发现并清空会话数据。Netty中的我们可以在Pipeline中添加IdleStateHandler,可达到这样的目的。

如果你不明白心跳的作用,务必读以下文章:

  1. 为何基于TCP协议的移动端IM仍然需要心跳保活机制?
  2. 一文读懂即时通讯应用中的网络心跳包机制:作用、原理、实现思路等

也可以学习一下主流IM的心跳逻辑:

  1. 微信团队原创分享:Android版微信后台保活实战分享(进程保活篇)
  2. 微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)
  3. 移动端IM实践:实现Android版微信的智能心跳机制
  4. 移动端IM实践:WhatsApp、Line、微信的心跳策略分析

如果觉得理论不够直观,下面的代码实例可以直观地进行学习:

  1. 正确理解IM长连接的心跳及重连机制,并动手实现(有完整IM源码)
  2. 一种Android端IM智能心跳算法的设计与实现探讨(含样例代码)
  3. 自已开发IM有那么难吗?手把手教你自撸一个Andriod版简易IM (有源码)
  4. 手把手教你用Netty实现网络通信程序的心跳机制、断线重连机制

其实,心跳算法的实际效果,还是有一些逻辑技巧的,以下两篇建议必读:

  1. Web端即时通讯实践干货:如何让你的WebSocket断网重连更快速?
  2. 融云技术分享:融云安卓端IM产品的网络链路保活技术实践

14、技术关键点4:为Server和Client添加Hook

如果NettyServer重启了或者进程被kill掉,我们需要清除当前节点的SessionMap(其实不用清理SessionMap,数据在内存里重启会自动删除的)和Redis保存的Client的链接信息。

我们需要遍历SessionMap找出所有的uid,然后一一清除Redis的数据,然后优雅退出。此时,我们就需要为我们的NettyServer添加一个Hook,来做数据清理。

15、技术关键点5:对方不在线该如何处理消息?

Client1给对方发消息,我们通过SessionMap或Redis拿不到对方的会话数据,这就表明对方不在线。

此时:我们需要把消息存储在离线消息表中,当对方下次登录时,NettyServer查离线消息表,把消息发给登录用户(最好是批量发送,提高性能)。

IM中的离线消息处理,也不是个简单的技术点,有兴趣可以深入学习一下:

  1. IM消息送达保证机制实现(二):保证离线消息的可靠投递
  2. 阿里IM技术分享(六):闲鱼亿级IM消息系统的离线推送到达率优化
  3. IM开发干货分享:我是如何解决大量离线消息导致客户端卡顿的
  4. IM开发干货分享:如何优雅的实现大量离线消息的可靠投递
  5. 喜马拉雅亿级用户量的离线消息推送系统架构设计实践

16、写在最后

代码写成这样,也算是了确了自已手撸IM的心愿。唯一遗憾的是,时间比较紧张,还没来得及实现消息ack机制,保证消息一定会送达,这个笔者以后会补充上去的。

好了,这就是我开发的这个简易的聊天系统,麻雀虽小,五脏俱全,大家有什么不明白的地方,可以直接在下方留言,笔者会一一回复的,谢谢大家。

17、系列文章

18、参考资料

[1] 新手入门:目前为止最透彻的的Netty高性能原理和框架架构解析

[2] 写给初学者:Java高性能NIO框架Netty的学习方法和进阶策略

[3] 史上最强Java NIO入门:担心从入门到放弃的,请读这篇!

[4] Java的BIO和NIO很难懂?用代码实践给你看,再不懂我转行!

[5] 史上最通俗Netty框架入门长文:基本介绍、环境搭建、动手实战

[6] 理论联系实际:一套典型的IM通信协议设计详解

[7] 浅谈IM系统的架构设计

[8] 简述移动端IM开发的那些坑:架构设计、通信协议和客户端

[9] 一套海量在线用户的移动端IM架构设计实践分享(含详细图文)

[10] 一套原创分布式即时通讯(IM)系统理论架构方案

[11]  一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践

[12] 一套亿级用户的IM架构技术干货(上篇):整体架构、服务拆分等

[13] 一套亿级用户的IM架构技术干货(下篇):可靠性、有序性、弱网优化等

[14] 从新手到专家:如何设计一套亿级消息量的分布式IM系统

[15] 基于实践:一套百万消息量小规模IM系统技术要点总结

本文已同步发布于:http://www.52im.net/thread-3816-1-1.html )

posted @ 2022-01-18 16:45 Jack Jiang 阅读(244) | 评论 (0)编辑 收藏

关于RainbowChat-Web

► RainbowChat-Web详细介绍:http://www.52im.net/thread-2483-1-1.html
► 历史版本更新记录:http://www.52im.net/thread-2480-1-1.html

RainbowChat-Web是一套基于MobileIMSDK-Web的网页端IM系统。

▲ 主界面更多截图点此进入更多演示视频点此进入

▲ 主界面(聊天窗全屏时)更多截图点此进入更多演示视频点此进入

v4.0 版更新内容

此版更新内容:

  • 1)[前端] [新增增加了消息“撤回”功能,体验与微信保持一致(支持3种聊天模式,包含完整的前后端处理逻辑);
  • 2)[前端] [新增增加了删除聊天消息功能
  • 3)[前端] [新增增加了设置好友备注(及附属字段)的功能;
  • 4)[前端/服务端] [优化] 升级MobileIMSDK-Web库至v5.0版(支持完整消息送达保证机制);
  • 5)[前端] [优化] 将原UI各模块代码按js文件分拆,使得代码更易理解和阅读;
  • 6)[前端] [优化] 增强了表情功能,且可与APP产品互通;

增强版的表情功能效果截图:


消息“撤回”功能效果截图:

 

消息气泡右键功能效果截图:

设置好友备注功能效果截图:

posted @ 2022-01-15 22:54 Jack Jiang 阅读(79) | 评论 (0)编辑 收藏

     摘要: 本文由ELab技术团队分享,原题“浅谈WebRTC技术原理与应用”,有修订和改动。1、基本介绍WebRTC(全称 Web Real-Time Communication),即网页即时通信。 是一个支持网页浏览器进行实时语音对话或视频对话的技术方案。从前端技术开发的视角来看,是一组可调用的API标准。 在WebRTC发布之前,开发实时音视频交互应用的成本是非常昂贵,...  阅读全文

posted @ 2022-01-10 16:19 Jack Jiang 阅读(214) | 评论 (0)编辑 收藏

本文由融云技术团队原创分享,原题“聊天室海量消息分发之消息丢弃策略”,内容有修订。

1、引言

随着直播类应用的普及,尤其直播带货概念的风靡,大用户量的直播间场景已然常态化。

大用户量直播间中的实时互动是非常频繁的,具体体现在技术上就是各种用户聊天、弹幕、礼物、点赞、禁言、系统通知等实时消息(就像下图这样)。

▲ 某电商APP的卖货直播间

如此大量的实时消息,在分发时如何处理才能不至于把服务端搞垮,而到了客户端时也不至于让APP出现疯狂刷屏和卡顿(不至于影响用户体验),这显然需要特殊的技术手段和实现策略才能应对。

其实,直播间中的实时消息分发,在技术上是跟传统的在线聊天室这种概念是一样的,只是传统互联网时代,聊天室同时在线的用户量不会这么大而已,虽然量级不同,但技术模型是完全可以套用的。

本文将基于直播技术实践的背景,分享了单直播间百万用户在线量的实时消息分发的技术经验总结,希望带给你启发。

学习交流:

- 移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM

- 开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK 

本文已同步发布于:http://www.52im.net/thread-3799-1-1.html

2、系列文章

本文是系列文章中的第6篇:

3、技术挑战

我们以一个百万人观看的直播间为例进行分析,看看需要面临哪些技术挑战。

1)在直播中会有一波一波的消息高峰,比如直播中的“刷屏”消息,即大量用户在同一时段发送的海量实时消息,一般情况下此类“刷屏”消息的消息内容基本相同。如果将所有消息全部展示在客户端,则客户端很可能出现卡顿、消息延迟等问题,严重影响用户体验。

2)海量消息的情况下,如果服务端每条消息都长期存储将导致服务缓存使用量激增,使得内存、存储成为性能瓶颈。

3)在另外一些场景下,比如直播间的房间管理员进行操作后的通知消息或者系统通知,一般情况下这类消息是较为重要的,如何优先保障它的到达率。

基于这些挑战,我们的服务需要做一个基于业务场景的优化来应对。

4、架构模型

我们的架构模型图如下:

 

如上图所示,下面将针对主要服务进行简要说明。

1)直播间服务:

主要作用是:缓存直播间的基本信息。包括用户列表、禁言/封禁关系、白名单用户等。

2)消息服务:

主要作用是:缓存本节点需要处理的用户关系信息、消息队列信息等。

具体说是以下两个主要事情。

直播间用户关系同步:

  • a)成员主动加入退出时:直播间服务同步至==> 消息服务;
  • b)分发消息发现用户已离线时:消息服务同步至==> 直播间服务。

发送消息:   

  • a)直播间服务经过必要校验通过后将消息广播至消息服务;
  • b)直播间服务不缓存消息内容。

3)Zk(就是 Zookeeper 啦):

主要作用就是:将各服务实例均注册到 Zk,数据用于服务间流转时的落点计算。

具体就是:

  • a)直播间服务:按照直播间 ID 落点;
  • b)消息服务:按照用户 ID 落点。

4)Redis:

主要作为二级缓存,以及服务更新(重启)时内存数据的备份。

5、消息分发总体方案

直播间服务的消息分发完整逻辑主要包括:消息分发流程和消息拉取流程。

5.1 消息分发流程

如上图所示,我们的消息分发流程主要是以下几步:

  • 1)用户 A 在直播间中发送一条消息,首先由直播间服务处理;
  • 2)直播间服务将消息同步到各消息服务节点;
  • 3)消息服务向本节点缓存的所有成员下发通知拉取;
  • 4)如上图中的“消息服务-1”,将向用户 B 下发通知。

另外,因为消息量过大,我们在在分发的过程中,是具有通知合并机制的,通知合并机制主要提现在上述步骤 3 中。

上述步骤3的通知合并机制原理如下:

  • a)将所有成员加入到待通知队列中(如已存在则更新通知消息时间);
  • b)下发线程,轮训获取待通知队列;
  • c)向队列中用户下发通知拉取。

通过通知合并机制,我们可以可保障下发线程一轮只会向同一用户发送一个通知拉取,即多个消息会合并为一个通知拉取,从面有效提升了服务端性能且降低了客户端与服务端的网络消耗。

PS:以上通知合并机制,在大消息量的情况下,非常适合使用Actor分布式算法来实现,有兴趣的同学可以进一步学习《分布式高并发下Actor模型如此优秀》、《分布式计算技术之Actor计算模式》。

5.2 消息拉取流程

 

如上图所示,我们的消息拉取流程主要是以下几步:

  • 1)用户 B 收到通知后将向服务端发送拉取消息请求;
  • 2)该请求将由“消息服务-1”节点处理;
  • 3)“消息服务-1”将根据客户端传递的最后一条消息时间戳,从消息队列中返回消息列表(原理详见下图 ▼);
  • 4)用户 B 获取到新的消息。

上述步骤 3 中拉取消息的具体逻辑如下图所示:

6、消息分发的丢弃策略

对于直播间中的用户来说,很多消息其实并没有太多实际意义,比如大量重复的刷屏消息和动态通知等等,为了提升用户体验,这类消息是可以有策略地进行丢弃的(这是跟IM中的实时聊天消息最大的不同,IM中是不允许丢消息的)。

PS:直播间中消息分发的丢弃策略,跟上节中的通知合并机制一起,使得直接间海量消息的稳定、流畅分发得以成为可能。

我们的丢弃策略主要由以下3部分组成:

  • 1)上行限速控制(丢弃)策略;
  • 2)下行限速控制(丢弃)策略;
  • 3)重要消息防丢弃策略。

如下图所示:

我们来逐个解释一下。

1)上行限速控制(丢弃)策略:

针对上行的限速控制,我们默认是 200 条/秒,根据业务需要可调整。达到限速后发送的消息将在直播间服务丢弃,不再向各消息服务节点同步。

2)下行限速控制(丢弃)策略:

针对下行的限速控制,即对消息环形队列(见“5.2 消息拉取流程”中的拉取消息详细逻辑图)长度的控制,达到最大值后最“老”的消息将被淘汰丢弃。

每次下发通知拉取后服务端将该用户标记为“拉取中”,用户实际拉取消息后移除该标记。

拉取中标记的作用:例如产生新消息时用户具有拉取中标记,如果距设置标记时间在 2 秒内则不会下发通知(降低客户端压力,丢弃通知未丢弃消息),超过 2 秒则继续下发通知(连续多次通知未拉取则触发用户踢出策略,不在此赘述)。

因此消息是否被丢弃取决于客户端拉取速度(受客户端性能、网络影响),客户端及时拉取消息则没有被丢弃的消息。

3)重要消息防丢弃策略:

如前文所述:在直播间场景下对某些消息应具有较高优先级,不应丢弃。

例如:直播间的房间管理员进行操作后的通知消息或者系统通知。

针对此场景:我们设置了消息白名单、消息优先级的概念,保障不丢弃。如本节开始的图所示,消息环形队列可以为多个,与普通直播间消息分开则保障了重要消息不丢弃。

通过上述“1)上行限速控制(丢弃)策略”和“下行限速控制(丢弃)策略”保障了:

  • 1)客户端不会因为海量消息出现卡顿、延迟等问题;
  • 2)避免出现消息刷屏,肉眼无法查看的情况;
  • 3)同时降低了服务端存储压力,不会因为海量消息出现内存瓶颈从而影响服务。

7、写在最后

随着移动互联网的发展,直播间的实时消息业务模型和压力也在不停地扩展变化,后续可能还会遇到更多的挑战,我们的服务会与时俱进、跟进更优的方案策略进行应对。

附录:多人群聊天技术文章

[1]《IM单聊和群聊中的在线状态同步应该用“推”还是“拉”?

[2]《IM群聊消息如此复杂,如何保证不丢不重?

[3]《移动端IM中大规模群消息的推送如何保证效率、实时性?

[4]《现代IM系统中聊天消息的同步和存储方案探讨

[5]《关于IM即时通讯群聊消息的乱序问题讨论

[6]《IM群聊消息的已读回执功能该怎么实现?

[7]《IM群聊消息究竟是存1份(即扩散读)还是存多份(即扩散写)?

[8]《一套高可用、易伸缩、高并发的IM群聊、单聊架构方案设计实践

[9]《IM群聊机制,除了循环去发消息还有什么方式?如何优化?

[10]《网易云信技术分享:IM中的万人群聊技术方案实践总结

[11]《阿里钉钉技术分享:企业级IM王者——钉钉在后端架构上的过人之处

[12]《IM群聊消息的已读未读功能在存储空间方面的实现思路探讨

[13]《企业微信的IM架构设计揭秘:消息模型、万人群、已读回执、消息撤回等

[14]《融云IM技术分享:万人群聊消息投递方案的思考和实践

本文已同步发布于:http://www.52im.net/thread-3799-1-1.html

posted @ 2022-01-05 12:04 Jack Jiang 阅读(157) | 评论 (0)编辑 收藏

     摘要: 本文引用了作者Fundebug的“一文搞懂TCP与UDP的区别”一文的内容,感谢无私分享。1、引言网络协议是每个搞网络通信应用开发(比如IM、推送、网关等等)的程序员都必须要掌握的基础知识,TCP/IP协议簇中有两个最具有代表性的传输层协议——分别是 TCP 和 UDP。有过网络通信开发经验的同学们都知道,TCP和UDP协议是平时用的最多的两种协议,...  阅读全文

posted @ 2021-12-29 15:01 Jack Jiang 阅读(152) | 评论 (0)编辑 收藏

     摘要: 本文作者小傅哥,原题“使用DDD+Netty,开发一个分布式IM(即时通信)系统”。为了提升阅读体验,有大量修订和改动,感谢原作者。0、系列文章《跟着源码学IM(一):手把手教你用Netty实现心跳机制、断线重连机制》《跟着源码学IM(二):自已开发IM很难?手把手教你撸一个Andriod版IM》《跟着源码学IM(三):基于Netty,从零开发一个IM服务端》《跟着源码学I...  阅读全文

posted @ 2021-12-20 18:21 Jack Jiang 阅读(184) | 评论 (0)编辑 收藏

一、更新内容简介

本次更新为次要版本更新,进行了若干优化(更新历史详见:码云 Release Nodes)。可能是市面上唯一同时支持 UDP+TCP+WebSocket 三种协议的同类开源IM框架。

二、MobileIMSDK简介

MobileIMSDK 是一套专为移动端开发的原创IM通信层框架:

  • 历经8年、久经考验;
  • 超轻量级、高度提炼,lib包50KB以内;
  • 精心封装,一套API同时支持UDP、TCP、WebSocket三种协议(可能是全网唯一开源的);
  • 客户端支持 iOSAndroid标准JavaH5小程序(开发中..)、Uniapp(开发中..);
  • 服务端基于Netty,性能卓越、易于扩展;👈
  • 可与姊妹工程 MobileIMSDK-Web 无缝互通实现网页端聊天或推送等;👈
  • 可应用于跨设备、跨网络的聊天APP、企业OA、消息推送等各种场景。

MobileIMSDK工程始于2013年10月,起初用作某产品的即时通讯底层实现,完全从零开发,技术自主可控!

您可能需要:查看关于MobileIMSDK的详细介绍

三、代码托管同步更新

OsChina.net

GitHub.com

四、MobileIMSDK设计目标

让开发者专注于应用逻辑的开发,底层复杂的即时通讯算法交由SDK开发人员,从而解偶即时通讯应用开发的复杂性。

五、MobileIMSDK框架组成

整套MobileIMSDK框架由以下5部分组成:

  1. Android客户端SDK:用于Android版即时通讯客户端,支持Android 2.3及以上,查看API文档
  2. iOS客户端SDK:用于开发iOS版即时通讯客户端,支持iOS 8.0及以上,查看API文档
  3. Java客户端SDK:用于开发跨平台的PC端即时通讯客户端,支持Java 1.6及以上,查看API文档
  4. H5客户端SDK:暂无开源版,查看精编注释版
  5. 服务端SDK:用于开发即时通讯服务端,支持Java 1.7及以上版本,查看API文档

整套MobileIMSDK框架的架构组成:

 另外:MobileIMSDK可与姊妹工程 MobileIMSDK-Web 无缝互通,从而实现Web网页端聊天或推送等。

六、MobileIMSDK v6.1.2更新内容 

【重要说明】:

MobileIMSDK v6.1.2 为次要版本,进行了若干优化! 查看详情

【解决的Bug】:

  1. [Andior/iOS]解决了当网络断线后,重传队列中的包不增加重次数从而一直重传的问题;
  2. [iOS] 解决了RMMapper库中,因重写父类copyWithZone方法而导致某些工程里的动画效果不生效的问题!

【其它优化和提升】:

  1. [Andiord]Andriod端Demo中补全了完整的proguard混淆配置,否则真有人对Demo进行“realease”时,会运行报错哦;
  2. [iOS] 上一个版本中的Protocal类中忘记补上“sm”字段,现在补上了;
  3. [服务端] 服务端Demo同步为最新工程,之前提交的版本未正确合并最新lib等;
  4. [服务端] 升级log4j2至2.15.0,解决Log4j2远程代码执行高危漏洞;
  5. [Andiord]Andriod端SDK和Demo工程的targetSdkVersion提升为30;
  6. [Andriod]Andriod端TCP版协议Netty库加载方式改为gradle加载;

【版本地址】:

https://gitee.com/jackjiang/MobileIMSDK/releases/6.1.2

posted @ 2021-12-16 22:07 Jack Jiang 阅读(152) | 评论 (0)编辑 收藏

仅列出标题
共49页: First 上一页 19 20 21 22 23 24 25 26 27 下一页 Last 
Jack Jiang的 Mail: jb2011@163.com, 联系QQ: 413980957, 微信: hellojackjiang