Jack Jiang

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

本文由小米技术团队分享,原题“小爱接入层单机百万长连接演进”,有修订。

1、引言

小爱接入层是小爱云端负责设备接入的第一个服务,也是最重要的服务之一,本篇文章介绍了小米技术团队2020至2021年在这个服务上所做的一些优化和尝试,最终将单机可承载长连接数从30w提升至120w+,节省了机器30+台

提示:什么是“小爱”?

小爱(全名“小爱同学”)是小米旗下的人工智能语音交互引擎,搭载在小米手机、小米AI音箱、小米电视等设备中,在个人移动、智能家庭、智能穿戴、智能办公、儿童娱乐、智能出行、智慧酒店、智慧学习共八大类场景中使用。

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

2、专题目录

本文是专题系列文章的第7篇,总目录如下:

  1. 长连接网关技术专题(一):京东京麦的生产级TCP网关技术实践总结
  2. 长连接网关技术专题(二):知乎千万级并发的高性能长连接网关技术实践
  3. 长连接网关技术专题(三):手淘亿级移动端接入层网关的技术演进之路
  4. 长连接网关技术专题(四):爱奇艺WebSocket实时推送网关技术实践
  5. 长连接网关技术专题(五):喜马拉雅自研亿级API网关技术实践
  6. 长连接网关技术专题(六):石墨文档单机50万WebSocket长连接架构实践
  7. 长连接网关技术专题(七):小米小爱单机120万长连接接入层的架构演进》(* 本文

3、什么是小爱接入层

整个小爱的架构分层如下:

接入层主要的工作在鉴权授权层和传输层,它是所有小爱设备和小爱大脑交互的第一个服务。

由上图我们知道小爱接入层的重要功能有如下几个:

  • 1)安全传输和鉴权:维护设备和大脑的安全通道,保障身份认证有效和传输数据安全;
  • 2)维护长连接:维持设备和大脑的长连接(Websocket等),做好连接状态存储,心跳维护等工作;
  • 3)请求转发:针对每一次小爱设备的请求做好转发,保障每一次请求的稳定。

4、早期接入层的技术实现

小爱接入层最早的实现是基于AkkaPlay,我们使用它们搭建了第一个版本,该版本特点如下:

  • 1)基于Akka我们基本做到了初步的异步化,保障核心线程不被阻塞,性能尚可。
  • 2)Play框架天然支持Websocket,因此我们在有限的人力下能够快速搭建和实现,且能够保障协议实现的标准性。

5、早期接入层的技术问题

随着小爱长连接的数量突破千万大关,针对早期的接入层方案,我们发现了一些问题。

主要的问题如下:

1)长连接数量上来后,需要维护的内存数据越来越多,JVM的GC成为不可忽略的性能瓶颈,且一旦代码写的不好有GC风险。经过之前事故分析,Akka+Play版的接入层其单实例长连接数量的上限在28w左右。

2)老版本的接入层实现比较随意,其Akka Actor之间存在非常多的状态依赖而不是基于不可变的消息传递这样使得Actor之间的通信变成了函数调用,导致代码可读性差且维护很困难,没有发挥出Akka Actor在构建并发程序的优势。

3)作为接入层服务,老版本对协议的解析是有很强的依赖的,这导致它要随着版本变动而频繁上线,其上线会引起长连接重连,随时有雪崩的风险。

4)由于依赖Play框架,我们发现其长连接打点有不准确的问题(因为拿不到底层TCP连接的数据),这个会影响我们每日巡检对服务容量的评估,且依赖其他框架在长连接数量上来后我们没有办法做更细致的优化。

6、新版接入层的设计目标

基于早期接入层技术方案的种种问题,我们打算重构接入层。

对于新版接入层我们制定的目标是:

  • 1)足够稳定:上线尽可能不断连接且服务稳定;
  • 2)极致性能:目标单机至少100w长连接,最好不要受GC影响;
  • 3)最大限度可控:除了底层网络I/O的系统调用,其他所有代码都要是自己实现/或者内部实现的组件,这样我们有足够的自主权。

于是,我们开始了单机百万长连接的漫漫实践之路。。。

7、新版接入层的优化思路

7.1 接入层的依赖关系

接入层与外部服务的关系理清如下:

7.2 接入层的功能划分

接入层的主要功能划分如下:

  • 1)WebSocket解析:收到的客户端字节流,要按照WebSocket协议要求解析出数据;
  • 2)Socket状态保持:存储连接的基本状态信息;
  • 3)加密解密:与客户端通讯的所有数据都是加密过的,而与后端模块之间传输是json明文的;
  • 4)顺序化:同一个物理连接上,先后两个请求A、B到达服务器,后端服务中B可能先于A得到了应答,但是我们收到B不能立刻发送给客户端,必须等待A完成后,再按照A,B的顺序发给客户端;
  • 5)后端消息分发:接入层后面不止对接单个服务,可能根据不同的消息转发给不同的服务;
  • 6)鉴权:安全相关验证,身份验证等。

7.3 接入层的拆分思路

把之前的单一模块按照是否有状态,拆分为两个子模块。

具体如下:

  • 1)前端:有状态,功能最小化,尽量少上线;
  • 2)后端:无状态,功能最大化,上线可做到用户无感知。

所以,按照上面的原则,理论上我们会做出这样的功能划分,即前端很小、后端很大。示意图如下图所示。

8、新版接入层的技术实现

8.1 总览

模块拆分为前后端:

  • 1)前端有状态,后端无状态;
  • 2)前后端是独立进程,同机部署。

补充:前端负责建立与维护设备长连接的状态,为有状态服务;后端负责具体业务请求,为无状态服务。后端服务上线不会导致设备连接断开重连及鉴权调用,避免了长连接状态因版本升级或逻辑调整而引起的不必要抖动;

前端使用CPP实现:

  • 1)Websocket协议完全自己解析:可以从Socket层面获取所有信息,任何Bug都可以处理;
  • 2)更高的CPU利用率:没有任何额外JVM代价,无GC拖累性能;
  • 3)更高的内存利用率:连接数量变大后与连接相关的内存开销变大,自己管理可以极端优化。

后端暂时使用Scala实现:

  • 1)已实现的功能直接迁移,比重写代价要低得多;
  • 2)依赖的部分外部服务(比如鉴权)有可直接利用的Scala(Java)SDK库,而没有C++版本,若用C++重写代价非常大;
  • 3)全部功能无状态化改造,可以做到随时重启而用户无感知。

通讯使用ZeroMQ

进程间通讯最高效的方式是共享内存,ZeroMQ基于共享内存实现,速度没问题。

8.2 前端实现

整体架构:

 

如上图所示,由四个子模块组成:

  • 1)传输层:Websocket协议解析,XMD协议解析;
  • 2)分发层:屏蔽传输层的差异,不管传输层使用的什么接口,在分发层转化成统一的事件投递到状态机;
  • 3)状态机层:为了实现纯异步服务,使用自研的基于Actor模型的类Akka状态机框架XMFSM,这里面实现了单线程的Actor抽象;
  • 4)ZeroMQ通讯层:由于ZeroMQ接口是阻塞实现,这一层通过两个线程分别负责发送和接收。

8.2.1)传输层:

WebSocket 部分使用 C++ 和 ASIO 实现 websocket-lib。小爱长连接基于WebSocket协议,因此我们自己实现了一个WebSocket长连接库。

这个长连接库的特点是:

  • a. 无锁化设计,保障性能优异;
  • b. 基于BOOST ASIO 开发,保障底层网络性能。

压测显示该库的性能十分优异的:

这一层同时也承担了除原始WebSocket外,其他两种通道的的收发任务。

目前传输层一共支持以下3种不同的客户端接口:

  • a. websocket(tcp):简称ws;
  • b. 基于ssl的加密websocket(tcp):简称wss;
  • c. xmd(udp):简称xmd。

8.2.2)分发层:

把不同的传输层事件转化成统一事件投递到状态机,这一层起到适配器的作用,确保无论前面的传输层使用哪种类型,到达分发层变都变成一致的事件向状态机投递。

8.2.3)状态机处理层:

主要的处理逻辑都位于这一层中,这里非常重要的一个部分是对于发送通道的封装。

对于小爱应用层协议,不同的通道处理逻辑是完全一致的,但是在处理和安全相关逻辑上每个通道又有细节差异。

比如:

  • a. wss 收发不需要加解密,加解密由更前端的Nginx做了,而ws需要使用AES加密发送;
  • b. wss 在鉴权成功后不需要向客户端下发challenge文本,因为wss不需要做加解密;
  • c. xmd 发送的内容与其他两个不同,是基于protobuf封装的私有协议,且xmd需要处理发送失败后的逻辑,而ws/wss不用考虑发送失败的问题,由底层Tcp协议保证。

针对这种情况:我们使用C++的多态特性来处理,专门抽象了一个Channel接口,这个接口中提供的方法包含了一个请求处理的一些关键差异步骤,比如如何发送消息到客户端,如何stop连接,如何处理发送失败等等。对于3种(ws/wss/xmd)不同的发送通道,每个通道有自己的Channel实现。

客户端连接对象一创建,对应类型的具体Channel对象就立刻被实例化。这样状态机主逻辑中只实现业务层的公共逻辑即可,当在有差异逻辑调用时,直接调用Channel接口完成,这样一个简单的多态特性帮助我们分割了差异,确保代码整洁。

8.2.4)ZeroMQ 通讯层:

通过两个线程将ZeroMQ的读写操作异步化,同时负责若干私有指令的封装和解析。

8.3 后端实现

8.3.1)无状态化改造:

后端做的最重要改造之一就是将所有与连接状态相关的信息进行剔除。

整个服务以 Request(一次连接上可以传输N个Request)为核心进行各种转发和处理,每次请求与上一次请求没有任何关联。一个连接上的多次请求在后端模块被当做独立请求处理。

8.3.2)架构:

Scala 服务采用 Akka-Actor 架构实现了业务逻辑。

服务从 ZeroMQ 收到消息后,直接投递到 Dispatcher 中进行数据解析与请求处理,在 Dispatcher 中不同的请求会发送给对应的 RequestActor进行 Event 协议解析并分发给该 event 对应的业务 Actor 进行处理。最后将处理后的请求数据通过XmqActor 发送给后端 AIMS&XMQ 服务。

一个请求在后端多个 Actor 中的处理流程:

8.3.3)Dispatcher 请求分发:

前端与后端之间通过 Protobuf 进行交互,避免了Json 解析的性能消耗,同时使得协议更加规范化。

后端服务从 ZeroMQ 收到消息后,会在 DispatcherActor 中进行PB协议解析并根据不同的分类(简称CMD)进行数据处理,分类包括如下几种。

* BIND 命令:

鉴权功能,由于鉴权功能逻辑复杂,使用C++语言实现起来较为困难,目前依然放在 scala 业务层进行鉴权。该部分对设备端请求的 HTTP Headers 进行解析,提取其中的 token 进行鉴权,并将结果返回前端。

* LOGIN 命令:

设备登入,设备鉴权通过后当前连接已成功建立,此时会进行 Login 命令的执行,用于将该长连接信息发送至AIMS并记录于Varys服务中,方便后续的主动下推等功能。在 Login 过程中,服务首先将请求 Account 服务获取长连接的 uuid(用于连接过程中的路由寻址),然后将设备信息+uuid 发送至AIMS进行设备登入操作。

* LOGOUT 命令:

设备登出,设备在与服务端断开连接时需要进行 Logout 操作,用于从 Varys 服务中删除该长连接记录。

* UPDATE 与 PING 命令:

a. Update 命令,设备状态信息更新,用于更新该设备在数据库中保存的相关信息;

b. Ping 命令,连接保活,用于确认该设备处于在线连接状态。

* TEXT_MESSAGE 与 BINARY_MESSAGE:

文本消息与二进制消息,在收到文本消息或二进制消息时将根据 requestid 发送给该请求对应的RequestActor进行处理。

8.3.4)Request 请求解析:

针对收到的文本和二进制消息,DispatcherActor 会根据 requestId 将其发送给对应的RequestActor进行处理。

其中:文本消息将会被解析为Event请求,并根据其中的 namespace 和 name 将其分发给指定的业务Actor。二进制消息则会根据当前请求的业务场景被分发给对应的业务Actor。

8.4 其他优化

在完成新架构 1.0 调整过程中,我们也在不断压测长连接容量,总结几点对容量影响较大的点。

8.4.1)协议优化:

a. JSON替换为Protobuf: 早期的前后端通信使用的是 json 文本协议,后来发现 json 序列化、反序列化这部分对CPU的占用较大,改为了 protobuf 协议后,CPU占用率明显下降。

b. JSON支持部分解析:业务层的协议是基于json的,没有办法直接替换,我们通过"部分解析json"的方式,只解析很小的 header 部分拿到 namespace 和 name,然后将大部分直接转发的消息转发出去,只将少量 json 消息进行完整反序列化成对象。此种优化后CPU占用下降10%。

8.4.2)延长心跳时间:

在第一次测试20w连接时,我们发现在前后端收发的消息中,一种用来保持用户在线状态的心跳PING消息占了总消息量的75%,收发这个消息耗费了大量CPU。因此我们延长心跳时间也起到了降低CPU消耗的目的。

8.4.3)自研内网通讯库:

为了提高与后端服务通信的性能,我们使用自研的TCP通讯库,该库是基于Boost ASIO开发的一个纯异步的多线程TCP网络库,其卓越的性能帮助我们将连接数提升到120w+。

9、未来规划

经过新版架构1.0版的优化,验证了我们的拆分方向是正确的,因为预设的目标已经达到:

  • 1)单机承载的连接数 28w => 120w+(普通服务端机器 16G内存 40核 峰值请求QPS过万),接入层下线节省了50%+的机器成本;
  • 2)后端可以做到无损上线。

再重新审视下我们的理想目标,以这个为方向,我们就有了2.0版的雏形:

具体就是:

  • 1)后端模块使用C++重写,进一步提高性能和稳定性。同时将后端模块中无法使用C++重写的部分,作为独立服务模块运维,后端模块通过网络库调用;
  • 2)前端模块中非必要功能尝试迁移到后端,让前端功能更少,更稳定;
  • 3)如果改造后,前端与后端处理能力差异较大,考虑到ZeroMQ实际是性能过剩的,可以考虑使用网络库替换掉ZeroMQ,这样前后端可以从1:1单机部署变为1:N多机部署,更好的利用机器资源。

2.0版目标是:经过以上改造后,期望单前端模块可以达到200w+的连接处理能力。

10、参考资料

[1] 上一个10年,著名的C10K并发连接问题

[2] 下一个10年,是时候考虑C10M并发问题了

[3] 一文读懂高性能网络编程中的线程模型

[4] 深入操作系统,一文读懂进程、线程、协程

[5] Protobuf通信协议详解:代码演示、详细原理介绍等

[6] WebSocket从入门到精通,半小时就够!

[7] 如何让你的WebSocket断网重连更快速?

[8] 从100到1000万高并发的架构演进之路

学习交流:

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

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

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

posted @ 2022-03-22 17:00 Jack Jiang 阅读(618) | 评论 (0)编辑 收藏

本文由阿里闲鱼技术团队书闲分享,原题“如何有效缩短闲鱼消息处理时长”,有修订和改动。

1、引言

闲鱼技术团队围绕IM这个技术范畴,已经分享了好几篇实践性总结文章,本篇将要分享的是闲鱼IM系统中在线和离线聊天消息数据的同步机制上所遇到的一些问题,以及实践性的解决方案。

学习交流:

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

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

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

2、系列文章

本文是系列文章的第7篇,总目录如下:

  1. 阿里IM技术分享(一):企业级IM王者——钉钉在后端架构上的过人之处
  2. 阿里IM技术分享(二):闲鱼IM基于Flutter的移动端跨端改造实践
  3. 阿里IM技术分享(三):闲鱼亿级IM消息系统的架构演进之路
  4. 阿里IM技术分享(四):闲鱼亿级IM消息系统的可靠投递优化实践
  5. 阿里IM技术分享(五):闲鱼亿级IM消息系统的及时性优化实践
  6. 阿里IM技术分享(六):闲鱼亿级IM消息系统的离线推送到达率优化
  7. 阿里IM技术分享(七):闲鱼IM的在线、离线聊天数据同步机制优化实践》(* 本文

3、问题背景

随着用户数的快速增长,闲鱼IM系统也迎来了前所未有的挑战。

历经多年的业务迭代,客户端侧IM的代码已经因为多年的迭代层次结构不足够清晰,之前一些隐藏起来的聊天数据同步问题,也随着用户数的增大而被放大。

这里面的具体流程在于:后台需要同步到用户端侧的数据包,后台会根据数据包的业务类型划分成不同的数据域,数据包在对应域里面存在唯一且连续的编号,每一个数据包发送到端侧并且被成功消费后,端侧会记录当前每一个数据域已经同步过的版本编号,下一次数据同步就以本地数据域的编号开始,不断的同步到客户端。

当然用户不会一直在线等待消息,所以之前端侧采用了推拉结合的方式保证数据的同步。

具体就是:

  • 1)客户端在线时:使用ACCS实时的将最新的数据内容推送到客户端(ACCS是淘宝无线向开发者提供全双工、低延时、高安全的通道服务);
  • 2)客户端从离线状态启动后:根据本地的数据域编号,拉取不在线时候的数据差;
  • 3)当数据获取出现黑洞时:触发数据同步拉取(“黑洞”即指数据包Version不连续的状态)。

4、问题分析

当前的聊天数据同步策略确实是可以基本保障IM的数据同步的,但是也伴随着一些隐含的问题。

这些隐含的问题主要有:

  • 1)短时间密集数据推送时,会快速的触发多次数据域同步。域同步回来的数据如果存在问题,又会触发新一轮的同步,造成网络资源的浪费。冗余数据包/无效的数据内容会占用有效内容的处理资源,又对CPU和内存资源造成浪费;
  • 2)数据域中的数据包客户端是否正常消费,服务端侧无感知,只能被动地根据当前数据域信息返回数据;
  • 3)数据收取/消息数据体解析/存储落库逻辑拆分不够清晰,无法针对性的对某一层的代码拆分替换进行ABTest。

针对上述问题,我们对闲鱼IM进行了分层改造——即抽离数据同步层。这样优化,除了希望以后这个数据的同步内容可以用在IM之外,也希望随着稳定性的增加,赋能其他的业务场景。

接下来的内容,我们重点来看下解决客户端侧闲鱼IM聊天数据同步问题的一些实践思路。

5、优化思路

5.1 分层拆分

对于服务端来说:业务侧产出数据包后,会拼接上当前的数据域信息,然后通过数据同步层将数据推送到端侧。

对于客户端来说:接收到数据包后,会根据当前的数据域信息,来确定需要消费数据包的业务方,确保数据包在数据域内完整连续后,将数据体脱壳后交于业务侧消费,并且应答消费的状况。

数据同步层的抽取:把数据同步中的加壳、脱壳、校验、重试流程封装到一起,可以让上层业务只需要关心自己需要监听的数据域信息,然后当这些数据域更新数据的时候,可以获取到这些数据进行消费,而不再需要关心数据包是否完整。

这样做的话:

  • 1)业务侧只需要关心业务侧对接的协议;
  • 2)数据侧只需要关心数据侧包装的协议;
  • 3)网络层负责真实的数据传输。

整体的架构原理如下:

总结一下就是:

  • 1)对齐数据层数据传输协议、描述当前数据包体数据域信息;
  • 2)将消息的处理/合并/落库抽离成数据消费者;
  • 3)上下楼依赖抽象化,去除对于具体实现的依赖。

5.2 数据层结构模型

基于对于数据模型剥离和对当下遇见问题的解决方案规整,将数据同步层拆分为下图这样的架构。

具体的实施思路就是:

  • 1)App启动时建立ACCS长链接服务,保证推推送信道链接,并且根据当前本地数据域信息触发一次数据拉取;
  • 2)数据消费者注册消费者信息和需要监听的数据域信息,这里是一对多的关系;
  • 3)新的数据抵达端侧后,将数据包放到指定的数据域的缓冲池,批量数据归纳结束后,重新出发数据的读取;
  • 4)根据当前数据域优先级弹出最高优的数据包,判断数据域版本是否符合消费者要求,符合则将数据包脱壳后丢给消费者消费,不符合则根据上一次正确的数据包的域信息触发增量的数据域同步拉取;
  • 5)触发数据域同步拉取时,block数据读取,此时通过ACCS触达的数据依旧会在继续归纳到指定的数据域队列中,等待数据域同步拉取结果,将数据包进行排序、去重,合并到对应的数据域队列中。然后重新激活数据读取;
  • 6)数据包体被消费者正确消费后,更新域信息并且通过上行信道告知服务端已经正确处理的数据域信息。

* 数据域同步协议:

Region中携带的数据不必过多,但需将数据包的内容描述清楚,具体是:

  • 1)目标用户的ID,用以确定目标数据包是否正确;
  • 2)数据域ID和优先级信息;
  • 3)当前数据包的域优先级版本。

* 排序策略:

针对于域数据归纳,无论是在写入数据的时候进行排序还是在读取的时候进行查找都需要进行一次排序的操作,时间复杂度最优也是O(logn)级别的。

在实际coding中发现由于在一个数据域里面,数据包的Version信息是连续唯一并且不存在断层的,上一个稳定消费的数据体的Version信息自增就是下一个数据包的Version,所以这里采用了以Versio为主键的Map存储,既降低了时间复杂度,也使得唯一标识的数据包后抵达端侧的包内容可以覆盖之前的包内容。

6、新的问题及解决策略

6.1 多数据来源和唯一数据消费的平衡

每当产生一条针对于当前用户的数据包:

  • 1)如果当前ACCS长链接存在,就会通过ACCS将数据包推送到客户端;
  • 2)如果App切换到后台一段时间,或者直接被杀死,ACCS链接断开,那么只能通过离线推送到用户的通知面板。

所以:每当App切换到活跃状态,都需要根据当前本地存储的数据域信息从后台触发一次数据同步。

数据包触达到客户端侧的来源主要是ACCS长链接的推送和域同步时的拉取,但是数据包的消费是根据数据域的监听划分的唯一消费者,也就是同一时间内只能消费一个数据包。

在压力测试中:当后台短时间内密集的将数据包通过ACCS推送到端侧时,端侧接收到的数据包并不有序,不连续的数据包域版本又会触发新的数据域同步,导致同样的一份数据包会通过两个不同的渠道多次的触达到端侧,浪费了不必要的流量。

当数据域同步时:这个时间节点产生的新数据包也会推送到端侧,数据体有效,并且需要被正确的消费。

针对上述这些问题的解决策略:

即在数据消费和数据获取中间装载一个数据中间层,当触发数据域同步的时候block数据的读取并且ACCS推送下来的数据包会被存放在一个数据的中转站里面,当数据域同步拉取的数据回来后,对数据进行合并后再重启数据读取流程。

6.2 数据域优先级

需要推送到客户端侧的数据包,根据业务的不同优先级也有不同的划分。

用户和用户的聊天产生的数据包会比运营类的消息的数据包优先级要高一些,所以要当多优先级的数据包快速的抵达端侧时,高优先级数据域的数据包需要被优先消费,而数据域的优先级也是需要动态调整,需要不断变换的优先级策略。

针对这个问题的解决策略:

不同的数据域,产生不同的数据队列,高优队列里面的数据包会被优先读取消费。

每一个数据包体中带回的数据域信息,都可以标注当前的数据域优先级,当数据域优先级发生变化的时候,调整数据包消费优先级策略。

7、优化后的效果

除去结构上分层梳理,使得数据同步层和依赖的服务内容可便捷解耦/每一个环节可插拔之外,数据同步中对于消息消费时长/流量节省,压力测试场景下优化效果更加明显。

在“500ms内100条全乱序数据包推送”压力测试场景下:

  • 1)消息处理时长(接收-上屏)缩短 31%;
  • 2)流量损耗(最终拉取到端侧数据包累积大小)降低35%。

8、后续的优化计划

8.1 数据同步层能力提升

数据同步侧的目标,既要保证数据包完整的到达端侧,又要在保证稳定性的前提下尽可能的减少数据的拉取,使得每一次数据的获取都有效。

后续数据同步层会着手于有效数据率和到达率进行更进一步的优化。

针对不同的场景,动态智能调整数据同步的优先级策略。

阻塞式长链接推送,保证同一时间只存在推模式或者拉模式,进一步减少冗余数据包的推送。

8.2 IM端侧整体架构升级

升级数据同步层策略主要还是要提升IM的能力,将数据同步分层后,接下来就是将消息的处理流程化,对每一个流程都可监控可回溯,提升IM数据包的正确解析存储和落库率。

细化一下就是:

  • 1)在数据来源侧剥离开后,后续对IM的整改也会逐步的将消息的处理分层剥离;
  • 2)消息处理关键节点的流程式上报、建立完整的监控体系,让问题发现先于用户舆情;
  • 3)消息完整性的动态自检,最小化数据补偿补全。

9、参考资料

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

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

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

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

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

[6] 融云技术分享:全面揭秘亿级IM消息的可靠投递机制

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

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

[9] 新手入门一篇就够:从零开发移动端IM

[10] IM消息送达保证机制实现(一):保证在线实时消息的可靠投递

[11] IM消息送达保证机制实现(二):保证离线消息的可靠投递

[12] 零基础IM开发入门(四):什么是IM系统的消息时序一致性?

[13] IM开发干货分享:我是如何解决大量离线消息导致客户端卡顿的

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

posted @ 2022-03-15 17:32 Jack Jiang 阅读(424) | 评论 (0)编辑 收藏

本文由作者小林coding分享,来自公号“小林coding”,有修订和改动。

1、引言

说到TCP协议,对于从事即时通讯/IM这方面应用的开发者们来说,再熟悉不过了。随着对TCP理解的越来越深入,很多曾今碰到过但没时间深入探究的TCP技术概念或疑问,现在是时候回头来恶补一下了。

本篇文章,我们就从系统层面深入地探讨一个有趣的TCP技术问题:拔掉网线后,再插上,原本的这条TCP连接还在吗?或者说它还“好”吗?

可能有的人会说:网线都被拔掉了,那说明物理层(也叫实体层)被断开了(关于网络协议分层模型请见《快速理解网络通信协议(上篇)》),那在物理层之上的传输层理应也会断开,所以原本的 TCP 连接就不会存在的了。就好像我们拨打有线电话的时候,如果某一方的电话线被拔了,那么本次通话就彻底断了。

答案真的是这样吗?可能并非你理解的这样哦,一起跟随笔者来深入探讨一下。

学习交流:

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

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

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

2、系列文章

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

3、比较笼统的答案

3.1 答案

引言里我们说到:有人认为,网线都被拔掉了,那说明物理层被断开,那么物理层之上的传输层肯定也会断开,所以原来的 TCP 连接自然也就不存在了。(PS:计算机网络分层详解请见《史上最通俗计算机网络分层详解

上面这个逻辑是有问题的。

问题在于:错误的认为拔掉网线这个动作会影响传输层,事实上并不会影响!

实际上:TCP 连接在 Linux 内核中是一个名为 struct socket 的结构体,该结构体的内容包含 TCP 连接的状态等信息。

所以:当拔掉网线的时候,操作系统并不会变更该结构体的任何内容,所以 TCP 连接的状态也不会发生改变。

3.2 实验验证一下

我做了个小实验:我用 ssh 终端连接了我的云服务器,然后我通过断开 wifi 的方式来模拟拔掉网线的场景,此时查看 TCP 连接的状态没有发生变化,还是处于 ESTABLISHED 状态(如下图所示)。

通过上面实验结果可以验证我的结论:拔掉网线这个动作并不会影响 TCP 连接的状态。

不过,这个答案还是有点笼统。实际上,我们应该在更具体的场景中来看待这个问题,答案才更准确一些。

这个具体场景就是:

  • 1)当拔掉网线后,有数据传输时;
  • 2)当拔掉网线后,没有数据传输时。

针对上面这两种具体的场景,我来更具体地来分析一下。我们继续往下阅读。

4、具体场景1:拔掉网线后,有数据传输时

4.1 数据传输过程中,恰好又把网线插回去了

如果是客户端被拔掉网线后,服务端向客户端发送的数据报文会得不到任何的响应,在等待一定时长后,服务端就会触发TCP协议的超时重传机制(详见:《TCP/IP详解 - 第21章·TCP的超时与重传),然而此时重传并不能得到响应的数据报文。

如果在服务端重传报文的过程中,客户端恰好把网线插回去了,由于拔掉网线并不会改变客户端的 TCP 连接状态,并且还是处于 ESTABLISHED 状态,所以这时客户端是可以正常接收服务端发来的数据报文的,然后客户端就会回 ACK 响应报文。

此时:客户端和服务端的 TCP 连接将依然存在且工作状态不会受到影响,给应用层的感觉就像什么事情都没有发生。。。

4.2 数据传输过程中,网线一直没有插回去

上面这种情况下,如果在服务端TCP协议重传报文的过程中,客户端一直没有将网线插回去,那么服务端超时重传报文的次数达到一定阈值后,内核就会判定出该 TCP 有问题。然后就会通过 Socket 接口告诉应用程序该 TCP 连接出问题了,于是服务端的 TCP 连接就会断开。

接下来,如果客户端再插回网线,如果客户端向服务端发送了数据,由于服务端已经没有与客户端匹配的 TCP 连接信息了,因此服务端内核就会回复 RST 报文,客户端收到后就会释放该 TCP 连接。

此时:客户端和服务端的 TCP 连接已经明确被断开,原本的这个连接也就不存在了。

4.3 刨根问底:TCP数据报文到底重传几次?

本着知其然更应知其所以然的精神,我们来刨根问底一下:TCP 的数据报文到底有重传几次呢?

在 Linux 系统中,提供了一个叫 tcp_retries2 配置项,默认值是 15(如下图所示)。

如上图所示:这个内核参数是控制 TCP 连接建立的情况下,超时重传的最大次数。

不过 tcp_retries2 设置了 15 次,并不代表 TCP 超时重传了 15 次才会通知应用程序终止该 TCP 连接,内核还会基于“最大超时时间”来判定。

每一轮的超时时间都是倍数增长的,比如第一次触发超时重传是在 2s 后,第二次则是在 4s 后,第三次则是 8s 后,以此类推。

内核会根据 tcp_retries2 设置的值,计算出一个最大超时时间。

在重传报文且一直没有收到对方响应的情况时,先达到“最大重传次数”或者“最大超时时间”这两个的其中一个条件后,就会停止重传,然后就会断开 TCP 连接。

PS:有关TCP超时重传机制的详细情况,可以阅读浅析TCP协议中的疑难杂症(下篇)》。

5、具体场景2:拔掉网线后,有数据传输时

5.1 场景分析

针对拔掉网线后,没有数据传输的场景,还得具体看看是否开启了 TCP KeepAlive 机制 (详见《彻底搞懂TCP协议层的KeepAlive保活机制》)。

1)如果没有开启 TCP KeepAlive 机制:

在客户端拔掉网线后,并且双方都没有进行数据传输,那么客户端和服务端的 TCP 连接将会一直保持存在。

2)如果开启了 TCP KeepAlive 机制:

在客户端拔掉网线后,即使双方都没有进行数据传输,在持续一段时间后,TCP 就会发送KeepAlive探测报文。

根据KeepAlive探测报文响应情况,会有以下两种可能:

  • 1)如果对端正常工作:当探测报文被对端收到并正常响应, TCP 保活时间将被重置,等待下一个 TCP 保活时间的到来;
  • 2)如果对端主机崩溃或对端由于其他原因导致报文不可达:当探测报文发送给对端后,石沉大海、没有响应,连续几次,达到保活探测次数后,TCP 会报告该连接已经死亡。

所以:TCP 保活机制可以在双方没有数据交互的情况,通过TCP KeepAlive 机制的探测报文,来确定对方的 TCP 连接是否存活。

5.2 刨根问底:TCP KeepAlive 机制具体是什么样的?

TCP KeepAlive 机制的原理是这样的:

定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文。该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。

在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔。

以下是 Linux 中的默认值:

net.ipv4.tcp_keepalive_time=7200

net.ipv4.tcp_keepalive_intvl=75 

net.ipv4.tcp_keepalive_probes=9

解释一下:

  • 1)tcp_keepalive_time=7200:表示保活时间是 7200 秒(2小时),也就 2 小时内如果没有任何连接相关的活动,则会启动保活机制;
  • 2)tcp_keepalive_intvl=75:表示每次检测间隔 75 秒;
  • 3)tcp_keepalive_probes=9:表示检测 9 次无响应,认为对方是不可达的,从而中断本次的连接。

也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个“死亡”连接。

计算公式是:

注意:应用程序若想使用 TCP 保活机制需要通过 socket 接口设置 SO_KEEPALIVE 选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制。

PS:关于TCP协议的KeepAlive 机制详见《彻底搞懂TCP协议层的KeepAlive保活机制》、《一文读懂即时通讯应用中的网络心跳包机制:作用、原理、实现思路等》。

5.3 刨根问底:TCP KeepAlive 机制的探测时间也太长了吧?

没错,确实有点长。

TCP KeepAlive  机制是 TCP 层(内核态) 实现的,它是给所有基于 TCP 传输协议的程序一个兜底的方案。

实际上:我们通常在应用层自己实现一套探测机制,可以在较短的时间内,探测到对方是否存活。

比如:一般Web 服务器都会提供 keepalive_timeout 参数,用来指定 HTTP 长连接的超时时间。如果设置了 HTTP 长连接的超时时间是 60 秒,Web 服务软件就会启动一个定时器,如果客户端在完后一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,定时器的时间一到,就会触发回调函数来释放该连接。

再比如:IM、消息推送系统里的心跳机制,通过应用层的心跳机制(由客户端发出,服务端回复响应包),来灵活控制和探测长连接的健康度。

为何基于TCP协议的移动端IM仍然需要心跳保活机制?》这篇文章解释了IM这类应用中应用层心跳保活的必要性,有兴趣可以读一读。

如果对应用层心跳的具体应用没什么概念,可以看看微信的这两篇文章:

  1. 微信团队原创分享:Android版微信后台保活实战分享(网络保活篇)
  2. 移动端IM实践:实现Android版微信的智能心跳机制

下面有几个针对im这类应用的心跳实现代码,可以具体感受学习一下:

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

6、本文小结

下面简单总结一下文中的内容,本文开头的问题并不是简单一句话能够准确说清楚的,需要分情况对待。

也就是:客户端拔掉网线后,并不会直接影响 TCP 的连接状态。所以拔掉网线后,TCP 连接是否还会存在,关键要看拔掉网线之后,有没有进行数据传输。

1)有数据传输的情况:

在客户端拔掉网线后:如果服务端发送了数据报文,那么在服务端重传次数没有达到最大值之前,客户端恰好插回网线的话,那么双方原本的 TCP 连接还是能存在并正常工作,就好像什么事情都没有发生。

在客户端拔掉网线后:如果服务端发送了数据报文,在客户端插回网线之前,服务端重传次数达到了最大值时,服务端就会断开 TCP 连接。等到客户端插回网线后,向服务端发送了数据,因为服务端已经断开了与客户端相同四元组的 TCP 连接,所以就会回 RST 报文,客户端收到后就会断开 TCP 连接。至此, 双方的 TCP 连接都断开了。

2)没有数据传输的情况:

  • a. 如果双方都没有开启 TCP keepalive 机制,那么在客户端拔掉网线后,如果客户端一直不插回网线,那么客户端和服务端的 TCP 连接状态将会一直保持存在;
  • b. 如果双方都开启了 TCP keepalive 机制,那么在客户端拔掉网线后,如果客户端一直不插回网线,TCP keepalive 机制会探测到对方的 TCP 连接没有存活,于是就会断开 TCP 连接。而如果在 TCP 探测期间,客户端插回了网线,那么双方原本的 TCP 连接还是能正常存在。

除了客户端拔掉网线的场景,还有客户端“宕机和杀死进程”的两种场景。

第一个场景:客户端宕机这件事跟拔掉网线是一样无法被服务端的感知的,所以如果在没有数据传输,并且没有开启 TCP keepalive 机制时,,服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态,直到服务端重启进程。

所以:我们可以得知一个点——在没有使用 TCP 保活机制,且双方不传输数据的情况下,一方的 TCP 连接处在 ESTABLISHED 状态时,并不代表另一方的 TCP 连接还一定是正常的。

第二个场景:杀死客户端的进程后,客户端的内核就会向服务端发送 FIN 报文,与客户端进行四次挥手(见《跟着动画来学TCP三次握手和四次挥手》)。

所以:即使没有开启 TCP KeepAlive,且双方也没有数据交互的情况下,如果其中一方的进程发生了崩溃,这个过程操作系统是可以感知的到的,于是就会发送 FIN 报文给对方,然后与对方进行 TCP 四次挥手。

7、参考资料

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

[2] 通俗易懂-深入理解TCP协议(上):理论基础

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

[4] 脑残式网络编程入门(一):跟着动画来学TCP三次握手和四次挥手

[5] 脑残式网络编程入门(七):面视必备,史上最通俗计算机网络分层详解

[6] 技术大牛陈硕的分享:由浅入深,网络编程学习经验干货总结

[7] 网络编程入门从未如此简单(二):假如你来设计TCP协议,会怎么做?

[8] 不为人知的网络编程(十):深入操作系统,从内核理解网络包的接收过程(Linux篇)

[9] 为何基于TCP协议的移动端IM仍然需要心跳保活机制?

[10] 一文读懂即时通讯应用中的网络心跳包机制:作用、原理、实现思路等

[11] Web端即时通讯实践干货:如何让你的WebSocket断网重连更快速?

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

posted @ 2022-03-07 18:15 Jack Jiang 阅读(258) | 评论 (0)编辑 收藏

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

posted @ 2022-02-28 17:48 Jack Jiang 阅读(141) | 评论 (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 阅读(237) | 评论 (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 阅读(247) | 评论 (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 阅读(239) | 评论 (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 阅读(76) | 评论 (0)编辑 收藏

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

posted @ 2022-01-10 16:19 Jack Jiang 阅读(198) | 评论 (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 阅读(149) | 评论 (0)编辑 收藏

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