融云 SDK 5.0.0 功能迭代

融云 SDK 发布了 5.0 Dev 版本,在持续迭代新功能的同时,此版本对 IMKit SDK 进⾏了重构优化并且对外开源,SDK 初始化速度更快,同时减少了不必要的内存占⽤,针对 Android 库⽂件引⼊了加固技术,提升 App 安全性,详细如下:新增功...
继续阅读 »

融云 SDK 发布了 5.0 Dev 版本,在持续迭代新功能的同时,此版本对 IMKit SDK 进⾏了重构优化并且对外开源,SDK 初始化速度更快,同时减少了不必要的内存占⽤,针对 Android 库⽂件引⼊了加固技术,提升 App 安全性,详细如下:

新增功能

1.⾃定义多语⾔推送⽂案

功能描述:
根据⽬标⽤户设置的语⾔环境,从推送模板中选择对应语⾔的内容进⾏远程推送。

应用场景:
应⽤中存在多个国家的终端⽤户,在发送系统升级、运营活动类的通知时,需要给不同语⾔环境的⽤户推送不同的运营内容,可使⽤此功能设置多语⾔推送⽂案,增强运营效果,提升⽤户使⽤体验。

使用方式:
通过融云开发者后台,设置⾃定义推送模板,创建模板 ID。
终端⽤户通过 SDK 中 setPushLanguageCode 接⼝上报⽤户的语⾔环境。
SDK 或 Server API 发送消息时,携带后台创建的模板 ID(templateId),如⽬标⽤户未在线时,融云服务端会⾃动根据⽬标⽤户的语⾔环境选择对应的语⾔内容进⾏推送。


1.png

2.静默撤回消息功能

需要撤回⼀条消息,⼜不希望通知对⽅时,可以在撤回消息时设置 isDisableNotification 属性,设置后⽬标⽤户不会收到撤回消息时的通知栏提醒,⽀持通过 SDK 或 Server API 撤回消息时进⾏设置。

功能优化


  • 对 IMKit SDK 进⾏了代码重构并且开源,提升了 UI 品质及⽤户体验,解决了开发者在⽼版 IMKit 中某些 UI ⾃定义需求不能满⾜的问题。
  • 从 5.0 版本开始 Android IMKit SDK 使⽤ AndroidX 库实现,Google ⾃ support v7:28 开始,⼤部分 support 包都迁移到 AndroidX 下,建议开发者们尽早将项⽬转移到 AndroidX 下。
  • iOS IMLib 通讯能⼒库使⽤动态库技术,减少了不必要的内存占⽤,使 SDK 内存占⽤更⼩。
  • 对 SDK 整体性能进⾏优化,SDK 初始化速度更快,耗电更少。
  • Android SDK 引⼊了 Android 库⽂件加固技术,提升 App 安全性。
  • Android SDK 对 Android 11 系统进⾏了兼容适配。
  • 修复了 RTC SDK 的部分 BUG。

详细内容请查看官⽹版本更新描述:
IM 版本更新描述
音视频版本更新描述
收起阅读 »

IM 消息数据存储结构设计

1背景在移动互联网高速发展的时代,生活中 IM 类产品已经是我们离不开的应用了,像微信、钉钉等都是以 IM 为核心功能的社交产品。另外也有一些应用不是以 IM 为核心,但是也是其重要功能,比如在线游戏、电商直播等应用。在 IM 庞大的体系中,消息系统无疑是最核...
继续阅读 »

1背景

在移动互联网高速发展的时代,生活中 IM 类产品已经是我们离不开的应用了,像微信、钉钉等都是以 IM 为核心功能的社交产品。另外也有一些应用不是以 IM 为核心,但是也是其重要功能,比如在线游戏、电商直播等应用。

IM 庞大的体系中,消息系统无疑是最核心的,而消息系统中,最关键的部分是消息的分发和存储。

在以往传统消息系统中,对于在线的用户,消息会直接实时发送到在线的接收方,消息发送完成后,服务器端并不会对消息进行落地存储。对于离线的用户,服务器端会将消息存入到离线库,当用户登录后,从离线库中将离线消息拉走,然后服务器端将离线消息删除,这样的缺点是消息不持久化,导致消息无法支持消息漫游,降低了消息的可靠性。而在我们的消息系统中,服务器只要接收到了发送方发上来的消息,在转发给接收方的同时也会在离线数据库以及历史消息库中进行消息的落地存储,消息的落地也就支持了整体的消息漫游等相关功能。

 

2离线消息和历史消息的区别

离线消息,就是用户在离线过程中收到的消息,这些消息大多是用户比较关心的消息,具有一定的时效性。我们的系统设计,离线消息默认只保存最近七天的消息。离线消息在用户登录后会全量的获取,然后客户端根据会话进行整体离线消息的展示。

历史消息,存储了用户所有的消息,包括发的消息以及接收的消息。在客户端获取历史消息时,是按照会话进行分页获取的。历史消息的存储时间我们系统设计默认为半年,当然这个是可配置的。

3消息的发送以及存储的流程

融云整体的消息发送以及存储的流程如下图所示:

1.png

用户发送消息到服务器端后,首先会进入到消息系统中,消息系统会对消息进行分发以及存储。对于在线的接收方,会选择直接推送消息,但是遇到接收方不在线或者是消息推送失败的情况下,也会有另外的消息获取方式,接收方会主动向服务器拉取未收到的消息,但是接收方何时来服务器拉取消息以及从哪里拉取是未知的,所以消息存入到离线库的意义也就在这里。

   消息系统存储离线的过程中,为了不影响整个系统的更为平稳,融云使用了消息队列,消息是异步存入到离线库中的。

   在分发完消息后,消息服务会同步一份消息数据到历史消息服务中,历史消息服务会对消息进行落地存储。对于新的同步设备,会有消息漫游的需求,这也是历史消息的主要作用。在历史消息库中,客户端可以拉取任意会话的全量历史消息。

4 离线消息以及历史消息存储区别

   上述的图中我们能清晰的看到,离线消息我们存储介质选用的是 Redis,历史消息我们选用的是 HBase。为何选用不同的存储介质针对的是不同的业务场景和读写模式。下面我们重点介绍一下离线消息和历史消息存储的区别。

离线消息的存储模式是放大写,如下图所示,每个用户都有自己单独的收件箱和发件箱,收件箱存放需要向这个接收端同步的所有消息,发件箱里存的是发送端发送的所有消息。二人会话中的消息会产生两次写,发送者的发件箱以及接收端的收件箱。而在群的场景下,写入会被更加的放大,如果群里有 N 个人,那一条群消息就会被放大写 N 次。

放大写的优点是,接收端的逻辑会非常清晰简单,只需要从收件箱里读取一次即可,大大降低了同步消息所需的读的压力,但是缺点就是写入会被放大,特别是针对群这种场景。

2.png

历史消息的存储模式是放大读,因为历史消息中,每个会话都保存了整个会话的全量消息。在放大读这种模式下,每个会话的消息只保存一次。相比放大写的那种模式,写入次数大大降低,特别是针对群消息,只需要存一次即可。但是缺点是接收端接收消息非常的复杂和低效,因为这种模式客户端想拉取到所有消息就只能每个会话同步一次,读就会被放大,而且可能会产生很多次无效的读,因为有些会话可能根本没有新消息。

3.png

  IM 这种应用场景下,通常会用到写扩散这种消息同步模型,一条消息产生一条,但是可能会被读多次,是典型的读多写少的场景。一个优化好的系统,必须从设计上平衡读写压力,避免读或者写任意一个维度达到天花板。当然写扩展这种模式也有其弊端,比如万人群,会导致一条消息,写入了一万次。综合来讲,我们需要根据自己的业务场景做相应设计选择,我们的系统是根据了离线和历史消息的不同场景选择了写扩散和读扩散的组合模式。

5 客户端拉取消息

    离线消息的获取针对的是自己的整个离线消息,包括所有的会话。离线消息的获取是自上而下的方式,一次获取 200 条。在客户端拉取离线消息的信令中,需要带上当前客户端缓存的消息的最大时间戳,上面的图我们应该知道,离线消息我们存储的是一个线性结构,Server 会根据这个时间戳向下查找离线消息,重装或者新安装 App 时,客户端可以传 0 上来,Server 也会缓存客户端拉取到的最后一条消息的时间戳,然后根据业务场景,客户端类型等因素来决定从哪里开始拉取,如果没有拉取完 Server 会在拉取消息的应答中带相应的标记位,告诉客户端继续拉取,客户端循环拉取,直到所有离线消息拉完。

历史消息的获取针对的是单一会话,在拉取过程中需要带上来对方的 ID(如果是单聊的话就是对方的 UserID,如果是群,则是群组 ID 以及当前会话的最前面消息的时间戳,Server 会定位到这个人的这个会话然后一次获取 20 条,采用的是自下而上的方式,即从最后面往前翻。只要有消息,客户端可以一直向前翻,手动触发获取会话的历史消息。

 

6总结

   本篇文章主要讲了 IM 中消息系统的消息分发、存储等,重点介绍了离线消息和历史消息的区别以及两者存储中所选用的不同存储方式以及其优缺点。关于文中内容,也欢迎大家随时留言与我讨论。


收起阅读 »

Flutter Platform Channel深度解析

一、简介Platform Channel 是 Flutter 端与 Platform 端制定的通信机制,由官方提供用于 Dart 和平台之间的相互通信。分为以下 3 种(1)BaseMessageChannel :用于传递字符串和半结构化的信息(在大内存数据块...
继续阅读 »

一、简介

Platform Channel 是 Flutter 端与 Platform 端制定的通信机制,由官方提供用于 Dart 和平台之间的相互通信。

分为下 3 种

(1)BaseMessageChannel :用于传递字符串和半结构化的信息(在大内存数据块传递的情况下使用)

(2)MethodChannel:用于传递方法调用(Method Invocation

(3)EventChannel: 用于数据流(Event Streams)的通信

 

二、消息传递与编码器

Flutter 的消息传递工具是 BinaryMessager ,通过它与 Platform 建立起通信关系,消息以二进制的格式进行传递。

1.png

如图所示 BinaryMessager 的传递需要经过 BinaryMessageHandler,BinaryMessagerHandler 是以 Channel Name 作为键值生成出来再被注册到 BinaryMessager 上的,BinaryMessageHandler 和 BinaryMessager 是一一对应的,二进制格式的消息通过消息编码器(Codec)解码为识别的信息,并传递给 Handler 进行处理。Handler 处理完后,会把结果编码为二进制格式,再通过回调函数返回结果并发送回 Flutter 端

1.编码器分类

(1)MessageCodec:BinaryCodec、StringCodec、JSONMessageCodec、StandardMessageCodec

(2)MethodCodec:JSONMethodCodec、StandardMessageCodec

2.png

经过消息编码器处理后,消息就可以被 Handler 进行处理了

 

2.消息编码过程

Android 端的返回值是 java.lang.Integer 类型的,而 iOS 端返回值则是一个 NSNumber 类型的(通过NSNumber numberWithInt:获取)。而到了 Flutter 端时,这个返回值自动变成Dart 语言的 Int 类型。

standard platform channels 使用 standard messsage codec message response 进行序列化和反序列化,message response 可以是 booleans, numbers, Strings, byte buffers,List, Maps 等等,而序列化后得到的则是二进制格式的数据

Flutter 默认的消息编码器是 StandardMessageCodec ,支持的数据类型如下:3.png

三、MethodChannel

MethodChannel 是 Flutter 与 Platform之间传递信息的一种,其传递过程是:BinaryMessager > BinaryMessagerHandler > MethodChannel。

4.png

如上图:Native 端(iOS 和 Android)为宿主端(host)Flutter 则是客户端(client),Flutter 调用 Native 方法时,需要传递的信息是通过平台通道传递到宿主端的,Native 收到调用的信息后方可执行指定的操作。如有返回的数据,则 Native 会将数据再通过平台通道一并传递给 Flutter,其中数据传递是异步的,这样就能确保消息传递时用户界面不会被阻塞。

 

1.Flutter 层(Dart 层)

Flutter 端使用 MethodChannel 的 invokeMethod 方法发起一次方法调用时,开始了消息传递流程。 invokeMethod 方法会将其入参 message 和 arguments 封装成一个 MethodCall 对象,并使用 MethodCodec 将其编码为二进制格式数据,再通过 BinaryMessages 将消息发出。(注意,此处提到的类名与方法名均为 Dart 层的实现)

 上述过程最终会调用到 ui.Window 的 _sendPlatformMessage 方法,该方法是一个 Native 方法,其实现在 Native 层,这与 Java 的 JNI 技术非常类似。我们向 Native 层发送了三个参数:

            name,String 类型,代表 Channel 名称

            dataByteData 类型,即之前封装的二进制数据

            callback,Function 类型,用于结果回调

2.Native

Native 层后,window.cc 的 SendPlatformMessage 方法接受了来自 Dart 层的三个参数,并对它们做了一定的处理:Dart 层的回调 callback 封装为 Native 层的 PlatformMessageResponseDart 类型的 response;dart 层的二进制数据 data 转化为 std::vector<uint8t> 类型数据 data;根据 response, data 以及 Channel 名称 name 创建一个 PlatformMessage 对象,并通过 dartstate->window()->client()->HandlePlatformMessage 方法处理 PlatformMessage 对象。

dart_state->window()->client() 是一个 WindowClient,而其具体的实现为 RuntimeController,RuntimeController 会将消息交给其代理 RuntimeDelegate 处理。

RuntimeDelegate 的实现为 Engine,Engine 在处理 Message 时,会判断该消息是否是为了获取资源(channel 等于"flutter/assets"),如果是,则走获取资源逻辑,否则调用 Engine::Delegate 的 OnEngineHandlePlatformMessage 方法。

Engine::Delegate 的具体实现为 Shell,其 OnEngineHandlePlatformMessage 接收到消息后,会向 PlatformTaskRunner 添加一个 Task,该 Task 会调用 PlatformView 的 HandlePlatformMessage 方法。值得注意的是,Task 中的代码执行在 Platform Task Runner 中,而之前的代码均执行在 UI Task Runner 中。

四、消息处理

PlatformView 的 HandlePlatformMessage 方法在不同平台有不同的实现,但是其基本原理是相同的

5.png

1.PlatformView

AndroidPlatformViewAndroid 是 Platformview 的子类,也是其在 Android 端的具体实现。当 PlatformViewAndroid 接收到 PlatformMessage 类型的消息时,如果消息中有 response(类型为 PlatformMessageResponseDart),则生成一个自增长的 responseid,并以 responseid 为 key,response 为 value 存入字典 pendingresponses 中。接着,将 channel 和 data 均转化为 Java 可识别的数据,通过 JNI Java 层发起调用,将 response_id、channel 和 data 传递过去。

 Java 层中,被调用的代码为 FlutterNativeView (BinaryMessager 的具体实现)的 handlePlatformMessage ,该方法会根据 channel 找到对应的 BinaryMessageHandler 并将消息传递给它处理。

 BinaryMessageHandler 处理完成后,FlutterNativeView 会通过 JNI 调用 native 的方法,将 responsedata 和 responseid 传递到 native 层。

 Native 层,PlatformViewAndroid 的 InvokePlatformMessageResponseCallback 接收到了respondid 和 responsedata。其先将 responsedata 转化为二进制结果,并根据 responseid,从 pandingresponses 中找到对应的 PlatformMessageResponseDart 对象,调用其 Complete 方法将二进制结果返回。

2.PlatformViewIOS

PlatformViewIOS 是 PlatformView 的子类,也是其在 iOS 端的具体实现,当 PlatformViewIOS 接收到 message 时会交给 PlatformMessageRouter 处理。

PlatformMessageRouter 通过 PlatformMessage 中的 channel 找到对应的 FlutterBinaryMessageHandler,并将二进制消息其处理,消息处理完成后,直接调用 PlatformMessage 对象中的 PlatformMessageResponseDart 对象的 Complete 方法将二进制结果返回

3.结果回传

PlatformMessageResponseDart 的 Complete 方法向 UI Task Runner 添加了一个新的 Task,这个 Task 的作用是将二进制结果从 native 的二进制数据类型转化为 Dart 的二进制数据类型 response,并调用 Dart 的 callback 将 response 传递到 Dart 层。

Dart 层接收到二进制数据后,使用 MethodCodec 将数据解码,并返回给业务层。至此,一次从 Flutter 发起的方法调用就完整结束了

五、具体使用

6.png

1.Flutter 端调用 Android 方法

2.Android 端代码

1)继承 MethodCallHandler 并设置 Handler MethodChannel 需要保存在对象一会调用回调时需要使用,onMethodCall Flutter 层回调的方法这边用 RCIMFlutterWrapper 承接处理

7.png2RCIMFlutterWrapper 类中处理, MethodCall Method,对应 Flutter 层调用 invokeMethod 方法的传入的第一个参数,两端需完全对应一致

8.png

9.png

(3)直接通过 result 对象回调回去这样就能将结果回调

3.关于 Android 回调 Flutter 的使用

10.png

(1)Flutter 端回调监听,设置监听 Key 两端对应

 11.png

(2)Android 端代码回调, mChannel.invokeMethod 方法将数据回调给 Flutter 层



收起阅读 »

技术实践丨IM 消息同步机制全面解析

综述即时通讯系统最基础、最重要的是消息的及时性与准确性,及时体现在延迟,准确则具体表现为不丢、不重、不乱序。综合考虑业务场景、系统复杂度、网络流量、终端能耗等,融云精心设计了消息收发机制,并不断打磨优化,形成了现在的消息同步机制。整体思路:1.客户端、服务端共...
继续阅读 »
综述


即时通讯系统最基础、最重要的是消息的及时性与准确性,及时体现在延迟,准确则具体表现为不丢、不重、不乱序。

综合考虑业务场景、系统复杂度、网络流量、终端能耗等,融云精心设计了消息收发机制,并不断打磨优化,形成了现在的消息同步机制。

整体思路:
1.客户端、服务端共同配合,互相补充。
2.采用多重机制,从不同层面保障。
3.拆分上下行,分别处理。

协议层

首先,从协议层保证,协议栈需要提供可靠、有序的双向字节流传输,融云自研通信协议 RMTP(RongCloud Message Transfer Protocol)。

1.jpg

协议交互示意图


协议层通过 qos、 ack 等机制,保证数据传输的可靠性。

业务层

在关键业务,采用 ack 确认机制,配合状态机,服务感知当前业务传输状态,保障业务按照预期执行。

2.jpg

业务层确认机制示意图


消息 ID

采用全局唯一的消息 ID 生成策略。保证消息可通过 ID 进行识别,排重。

3.jpg

如何实现分布式场景下唯一 ID 生成,请点击融云过往技术文章了解。


客户端服务端交互

客户端与服务端之间使用长连接,基于 RMTP 协议传输数据。
经过总结,主要用三种行为:

1.客户端主动拉取消息,主动拉取有两个触发方式:
①与 IM 服务新建立连接成功,用于获取不在线的这段时间未收到的消息。(此处叫做获取离线消息)
②定时器触发。在客户端最后收到消息后启动定时器,比如 3-5 分钟执行一次, 主要有两个目的,一个是用于防止因网络,中间设备等不确定因素引起的通知送达失败,服务端客户端状态不一致,一个是可通过本次请求,对业务层做状态机保活。

2.服务端主动-发送消息(直发消息) 在线消息发送机制之一,简单理解为服务端将消息内容直接发送给客户端,适用于消息频率较低,并且持续交互,比如二人或者群内的正常交流讨论。

3、服务端主动-发送通知(通知拉取) 在线消息发送机制之一,简单理解为服务端给客户端发送一个通知,通知包含时间戳等可作为排序索引的内容,客户端收到通知后,依据自身数据,对比通知内时间戳,发起拉取消息的流程。适用于较多消息传递。比如某人有很多大规模的群,每个群内都有很多成员正在激烈讨论。通过通知拉取机制,可以有效的减少客户端服务端网络交互次数,并且对多条消息进行打包,提升有效数据载荷。既能保证时效,又能保证性能。

4.jpg

客户端服务端交互示意图


业务拆分

在有了多层机制保证后,将业务进行拆分,首先将业务拆分出上下行,在上行过程保证发送消息顺序,为了保证消息有序, 最好的方式是按照 userId 区分,然后使用时间戳排序。那么分布式部署情况下,将用户归属到固定的业务服务器上,会使得上行排序变得更容易。同时归属到同一个服务器,在多端维护时也更容易。

客户端连接过程
1.客户端通过 APP server ,获取到连接使用的 token。
2.客户端使用 token 通过导航服务,获取具体连接的 IM 接入服务器(CMP),导航服务通过 userId 计算接入服务器,然后下发,使得某一客户端可以连接在同一台接入服务器(CMP)。

5.jpg

示意图


上行

客户端发出消息后,通过接入服务,按照 userId 投递到指定消息服务器,生成消息 Id, 依据最后一条消息时间,确认更新当前消息的时间戳(如果存在相同时间戳则后延),然后将时间戳,以及消息 Id,通过 Ack 返回给客户端 ; 然后对上行消息使用 userId + 时间戳进行缓存以及持久化存储,后续业务操作均使用此时间戳。(此业务流程我们成为上行流程,上行过程存储的消息为发件箱消息)

下行

消息节点在处理完上行流程后,消息按照目标用户投递到所在消息节点,进入下行流程。下行过程,按照目标 userId 以及本消息在上行过程中生成的时间戳,计算是否需要更新时间戳(正向)。如果需要更新则对时间戳进行加法操作,直到当前用户时间戳不重复。如此处理后,目标用户的存储以及客户端接收到消息后的排重可以做到一致,并且可以做到同一个会话内的时间戳是有序的。从而保证同一个接收用户的消息不会出现乱序。

至此,我们已经完成了发送过程,接收过程的消息顺序保障,那么消息流程还剩下直发与通知拉取的处理流程,以及服务端如何选择是直发还是通知拉取。

直发消息

1、客户端 SDK 依据本地存储的最新消息时间戳判断,用来做排序等逻辑。
2、对同一个用户直发消息1条,其他转通知。通知拉取时候客户端选择本地最新一条消息时间戳作为开始拉取时间。
3、在消息发送过程中,如果上一条消息发送流程未结束,下一条消息则不用直发(s_msg),而是用(s_ntf)

6.jpg

直发逻辑示意图


通知拉取

1.服务端在通知体中携带当前消息时间戳。投递给客户端。
2.客户端收到通知后,比对本地消息时间戳,选择是否发拉取消息信令。
3.服务端收到拉取消息信令后,以信令携带的时间戳为开始,查询出消息列表(200 条或者 5M),并给客户端应答。
4.客户端收到后,给服务端 ack,服务端维护状态。
5.客户端拉取消息时使用的时间戳,是客户端本地最新一条消息的时间戳。

7.jpg

上图中,3-7 步可能需要循环多次,有以下考虑: 1.客户端一次收到的消息过多,应答体积过于庞大,传输过程对网络质量要求更高, 因此按照数量以及消息体积分批次进行。2. 一次拉取到的消息过多,客户端处理会占用大量资源,可能会有卡顿等,体验较差。


服务端直发消息与通知拉取切换逻辑

主要涉及到的是状态机的更新。下面示意图集成直发消息与通知拉取过程针对状态机的更新:

8.jpg

至此,消息收发核心流程介绍完毕,只剩下多端在线的处理。


多端在线同步

多端按照上下行,同样区分为发送方多端同步以及接收方多端同步。

发送方多端同步

在前面客户端连接 IM 服务过程中,我们已经将同一个用户的客户端汇聚在了同一台服务,那么维护一个 userId 的多端就会变得很简单。

1.用户多个终端链接成功后,发送一条消息,这个消息到达 CMP(IM 接入服务) 后,CMP 做基础检查,然后获此用户的其他终端连接。

2.服务把客户端上行的消息,封装为服务端下行消息,直接投递给用户的其他客户端。这样完成了发送方的多端抄送,然后将这条消息投递到 IM 服务。进入正常发送投递流程。发送方的多端同步没有经过 IM Server,这么做的好处是:1.比较快速;2.经过越少的服务节点,出问题的几率越小。

接收方多端同步

1.IM 服务收到消息后,先判断接收方的投递范围,这个范围指的是接收方用户的哪些的终端要接收消息。

2.IM 服务将范围以及当前消息,发送到 CMP,CMP 依据范围,匹配接收方的终端,然后投递消息。

区分接收方多端范围应用场景: 消息一般是所有终端。

有一些特殊业务,比如我在 A 客户端上,控制另外某个端的状态,可能需要一些命令消息, 这时候需要这个作用范围,针对性的投递消息。

9.jpg到此,我们分析完了有关 IM 消息核心处理流程,通过层层拆解逻辑,提供可靠的消息投递机制。

收起阅读 »

【融云分析】WebRTC如何通过STUN、ICE协议实现P2P连接

WebRTC中两个或多个主机进行P2P连接是通过STUN、TURN、ICE等技术实现的。主机往往都是在NAT之后,且不同的NAT导致外部主机向内网主机发送数据的可见性不同。 内网主机通过STUN协议可以获得NAT分配的外部地址。ICE是主机之间发现P2P传输路...
继续阅读 »

WebRTC中两个或多个主机进行P2P连接是通过STUN、TURN、ICE等技术实现的。主机往往都是在NAT之后,且不同的NAT导致外部主机向内网主机发送数据的可见性不同。 内网主机通过STUN协议可以获得NAT分配的外部地址。ICE是主机之间发现P2P传输路径机制,ICE中使用了STUN协议进行连通检测、传输路径的指定和保活。 本文将对STUN和ICE协议进行分析和解读,希望能为开发者们带来一些启发和帮助

1. NAT类型

网络地址转换, 简称NAT,节省了IPv4地址空间的使用并且隔离了内网和外网。NAT对待UDP的实现方式有4种,分别如下:

1.1完全圆锥型

一个内网地址(iAddr:iPort)被映射到一个外网地址 (eAddr:ePort)。这个内网(iAddr:iPort)地址发送的数据包都会通过这个外网地址(eAddr:ePort)。外部主机可以通过这个外网地址(eAddr:ePort)向这个内网地址(iAddr:iPort)发送数据包

1.png

1.2地址受限锥型

一个内网地址(iAddr:iPort)被映射到一个外网地址 (eAddr:ePort)。这个内网(iAddr:iPort)地址发送的数据包都会通过这个外网地址(eAddr:ePort)。外部主机 (hAddr:any) 只有接收过从内网(iAddr:iPort)发送来的数据包后,才能通过外部地址 (eAddr:ePort)发送数据包给内网地址(iAddr:iPort)。其中外部主机的端口可以是任意的。

2.png

1.3端口受限锥型

一个内网地址(iAddr:iPort)被映射到一个外网地址 (eAddr:ePort)。这个内网(iAddr:iPort)地址发送的数据包都会通过这个外网地址(eAddr:ePort)。外部主机 (hAddr:hPort) 只有接收过从内网(iAddr:iPort)发送来的数据包后,才能通过外部地址 (eAddr:ePort)发送数据包给内网地址(iAddr:iPort)。其中外部主机的端口hPort是受限的。

3.png

1.4对称型

一个内网地址(iAddr:iPort)向外网地址 (sAddr1:sPort1)发送的多次请求时,NAT会分配同一个外网地址(eAddr1:ePort1)。若向不同的外网地址如(sAddr2:sPort2)发送数据包时,NAT分配另外一个外网地址(eAddr2:ePort2)。外网地址 (sAddr1:sPort1)只有接收过从内网(iAddr:iPort)发送来的数据后,才能通过已经在NAT上开辟的(eAddr1:ePort1)发送数据包给内网.

4.png

2. STUN简介

STUN是Session Traversal Utilities for NAT简写,RFC5389规定了具体内容。STUN协议是用来获取内网地址对应在NAT上的外网地址,NAT穿越。 STUN是C/S模式的协议,由客户端发送STUN请求;STUN服务响应,告知由NAT分配给主机的IP地址和端口号。

2.1 STUN消息结构

STUN消息头为20字节,后面紧跟0或多个属性。STUN头部包含一STUN消息类型、magic cookie、事务ID和消息长度。 

5.png

(图)STUN消息结构

每个STUN消息的最高位前2位必须为0。当多个协议复用同一个端口的时候,这个可以用于与其他协议区分STUN数据包。 消息类型确定消息的类别(如请求、成功回应、失败回应、指示indication)。虽然这里有四种消息类型,但可以分为2类事务:请求/响应事务、指示事务。

magic cookie为固定值0x2112A442。

Transaction ID标识同一个事务的请求和响应。当客户端发送多个STUN请求,通过Transaction ID识别对应的STUN响应。

2.2 STUN消息类型

6.png

(图)STUN消息类型

C0和C1位置的bit指明了消息的分类。其余的12位置标示不同的请求类型,比如绑定请求。

2.3 STUN属性

STUN头之后是0或多个属性。每个属性都采用TLV编码,type为16位的类型、lenght为16位的长度、value为属性值。每个STUN属性必须是4字节对齐。

7.png

(图)STUN属性

STUN属性格式

STUN服务器请求和响应都包含消息属性。一些属性不是强制性的,其中一些只能出现在绑定请求中,而其他一些只能出现在绑定响应中。  属性空间被划分为2个范围。强制理解属性STUN代理必须处理,否则STUN代理将无法正常处理该属性的消息;STUN代理不能理解可选理解属性的话,这些属性可以被忽略。

强制理解属性 (0x0000-0x7FFF):

8.png

 可选理解属性 (0x8000-0xFFFF)

9.png

具体说明如下: MAPPED-ADDRESS属性标识了NAT映射后的地址。

XOR-MAPPED-ADDRESS属性与MAPPED-ADDRESS属性一致,映射后的地址要做异或处理。

USERNAME属性用于消息完整性。用户名和密码包含在消息完整性中。

MESSAGE-INTEGRITY属性是STUN消息的HMAC-SHA1值,长度20字节。MESSAGE-INTEGRITY属性可以出现在任何类型的STUN消息中。用作HMAC输入的文本是STUN消息,包括头部,直到且包括MESSAGE-INTEGRITY属性前面的属性。FINGERPRINT属性出现MESSAGE-INTEGRITY后。所以FINGERPRINT属性外,STUN代理忽略其他出现在MESSAGE-INTEGRITY属性后的任何属性。

FINGERPRINT属性可以出现在所有的STUN消息中,该属性用于区分STUN数据包与其他协议的包。属性的值为采用CRC32方式计算STUN消息直到但不包括FINGERPRINT属性的的结果,并与32位的值0x5354554e异或。

ERROR-CODE属性被用于错误响应消息中。它包含一个在300至699范围内的错误响应号。

REALM属性出现在请求中,表示认证时要用长期资格。出现在响应中,表示服务器希望客户端使用长期资格进行认证。

NONCE属性是出现在请求和响应消息中的一段字符串。

UNKNOWN-ATTRIBUTES属性出现在错误代码为420的的错误响应,表示服务器端无法理解的属性。

SOFTWARE属性用于代理发送消息时所使用的软件的描述。

ALTERNATE-SERVER属性表示STUN客户可以尝试的不同的STUN服务器地址。属性格式与MAPPED-ADDRESS相同。

2.4 STUN示例

下面是Wireshark抓取的一对STUN绑定请求和响应。STUN绑定请求,源地址192.168.2.36:47798,目标地址180.76.137.157:30001。

10.png

STUN绑定响应,源地址180.76.137.157:30001,目标地址192.168.2.36:47798

11.png

其中ICE-CONTROLLING、PRIORITY属性是下面提到的ICE中扩充的属性。

3. ICE简介

ICE两端并不知道所处的网络的位置和NAT类型,通过ICE能够动态的发现最优的传输路径。如下图L和R是ICE代理,下面简称L和R。L和R有各自的传输地址,包括主机的网卡地址、NAT上的外网地址、 TURN服务地址。ICE就是要从这些地址中,找到L和R的候选地址对,实现两端高效连通。

12.png

(图)ICE部署图举例

ICE两端可以通过信令服务器交换SDP信息。ICE使用STUN,TURN等协议来建立会话。

3.1收集候选地址

ICE端收集本地地址。通过STUN服务收集NAT外网地址;通过TURN收集中继地址。

所以有四种候选地址:

13.png

如下图: 主机候选X:x 服务器反射候选X1':x1' 中继候选Y:y 这里称主机候选地址是服务器候选地址的BASE。

14.png

(图)ICE端口

3.2连通检测

L收集了所有的候选地址后,按优先级从高到低排序,通过信令服务器发送SDP offer给R。R收到offer后,收集获选地址,并将自己候选地址放入SDP answer发送给L。此时两端都有了对端的和本端的候选地址。然后配对,生成candidate pair。为了确保candidate pair的有效性,两端都要做连通检测。根据candidate pair,从本地candidate发送STUN请求到远端candidate;接收端返回STUN响应给发送端。如下图。

15.png

(图)ICE基本连通检测

两端都按照各自checklist分别进行检查。

当R收到L的检测时,R发送向L的检测被称为Triggered检测。

3.3 Candidates pair排序

将连通性检查成功的candidate pair按优先级排序加入check list。两端定期遍历这个check list, 发送STUN请求给对端称为Ordinary检测。优先级的计算根据以下原则: 每端给自己的candidate一个优先级数值。本端优先级和远端优先级结合,得到candidate pair的优先级优先级。

公式 priority = (2^24)*(type preference) + (2^8)*(local preference) + (2^0)*(256 - component ID)

16.png

再根据candidate的优先级计算candidate pair的优先级。

priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0)

G:controlling candidate 优先级 D:controlled candidate 优先级

3.4提名Candidates

ICE中有两种角色, controlling角色可以选取最终的candidate pair;controlled角色会等待controlling角色选取的candidate pair。 ICE指定一个ICE代理为controlling角色,其他ICE代理为controlled角色。ICE优先检测优先级高的candidate pair。

Controlling角色有两中提名方案:REGULAR提名:当得到至少一对有效的pair的时候,Controlling角色就会选择其中的一个pair作为候选,此次连通检测发送一个带flag的请求,告诉对端这个就是被选中的pair。

17.png

(图)REGULAR提名

AGGRESSIVE提名:Controlling角色会在每个STUN请求之中添加flag标志,最先成功的那个被选为媒体传输通道。

18.png

(图)AGGRESSIVE提名

3.5 ICE示例

下面是例子中,L和R都是full模式ICE代理,采用aggressive提名,传输媒体为RTP。full模式为双方都要进行连通性检查,都要的走一遍流程;lite模式为,full模式ICE一方进行连通性检查,lite一方只需回应response消息。

19.png

(图)ICE举例

便于理解,采用"主机类型-网络类型-序号"的格式表示传输的地址。地址有两个分量,分别是IP和PORT。L,R,STUN,NAT代表不同的主机类型;PUB代表外网,PRV代表内网; L处在内网中,内网地址是10.0.1.1,R处在外网,外网地址是192.0.2.1。L和R都配置了STUN服务,地址是192.0.2.2,端口是3478。L在NAT后面,NAT外网地址是192.0.2.3。序号表示不同的媒体类型,这里只有RTP所以序号为1。 "S="表示STUN消息的发送地址、"D=" 表示STUN消息的接收地址。 "MA=" 表示STUN绑定响应的中mapped address。"USE-CAND" 表示带有"USE-CANDIDATE" STUN消息。

L收集本地候选地址,并发起STUN绑定请求给STUN服务器,L得到 NAT-PUB-1作为服务器反射候选地址。 L计算候选的优先级,主机候选地址type preference为126;服务器反射候选地址type preference为100。local preference为65535。component ID为1 套用公式priority = (2^24)*(type preference) + (2^8)*(local preference) + (2^0)*(256 - component ID) 得主机候选地址的优先级为2130706431,服务器反射候选地址的优先级为1694498815。 L设置主机候选地址的foundation为1,服务器反射候选地址foundation为2。  L将服务器反射候选地址作为default候选地址。对应的offer sdp为

20.png

替换地址后

21.png

因为L和R都采用的是full-mode,这种情况下ICE协议规定发送offer为controlling端,L为controlling端。 L和R生成candidate pair,两端都有2个candidate pair。L会裁减掉包含服务映射候选地址,保留candidate pair为本端$L_PRIV_1、远端$R_PUB_1

22.png

消息9表示R做连通检测,因为R是controlled角色,所以无需设置USE-CANDIDATE。L处于NAT后面,且没有向R发送过请求,所以此次连通检测会失败。

当L收到answer sdp后,开始连通检测(消息10-13)。L采用的是aggressive提名,所以每个请求都会有USE-CANDIDATE。L使用candidate pair为$L_PRIV_1/$R_PUB_1发起的连通检测成功后,L创建一个新的candidate pair,本端为NAT-PUB-1(消息13中得到) 、远端为R-PUB-1(消息10中得到),加入valid list中。这个连通检测中设置了USE-CANDIDA属性,该candidate pair为选中的候选。L的RTP流在valid list中有选中的candidate pair,所以L进入完成状态。

R收到L的STUN绑定请求(消息11)后,R发起消息11对应的Triggered检测,其candidate pair的本端为R-PUB-1、远端为NAT-PUB-1。检测成功后,R创建本端为R-PUB-1、远端为NAT-PUB-1的candidate pair,加入valid list。因为消息11中包含了USE-CANDIDATE,所以这个candidate pair就被选中为这个RTP流的传输通道。R进入完成状态。

4. 总结

本文介绍了NAT、STUN、ICE等基本概念。STUN部分介绍了STUN的消息结构、消息类型和消息属性ICE协议中STUN消息要遵循STUN协议。 ICE部分介绍了ICE代理之间是如何根据各自的网络地址建立连接的步骤有收集候选地址、连通检测、Candidates pair生成与排序、提名Candidates。 详细内容还需查看ICE协议rfc5245以及webrtc的p2p部分的具体实现。


收起阅读 »

万人群聊的消息分发控速方案

当前阶段,群聊已经成为主流IM软件的基本功能,不管是亲属群,朋友群亦或是工作群,都是非常常见的场景。随着移动互联网的发展,即时通讯服务被广泛应用到各个行业,客户业务快速发展,传统百人甚至千人上限的群聊已经无法满足很多业务发展需求,所以超大群的业务应运而生。&n...
继续阅读 »

当前阶段,群聊已经成为主流IM软件的基本功能,不管是亲属群,朋友群亦或是工作群,都是非常常见的场景。随着移动互联网的发展,即时通讯服务被广泛应用到各个行业,客户业务快速发展,传统百人甚至千人上限的群聊已经无法满足很多业务发展需求,所以超大群的业务应运而生。

 

1超大群面临的挑战

我们以一个万人群的模型进行举例:

1、如果群中有人发了消息,那么这条消息需要按照1:9999的比例进行分发投递,如果我们按照常规消息的处理流程,那么消息处理服务压力巨大。

2、消息量大的情况下,服务端向客户端直推消息的处理速度将会成为系统瓶颈,而一旦用户的消息下发队列造成了挤压,会影响到正常的消息分发,也会导致服务缓存使用量激增。

3、在微服务架构中,服务以及存储(DB,缓存)之间的QPS和网络流量也会急剧增高。

4、以群为单位的消息缓存,内存和存储开销较大(消息体的存储被放大了万倍)。

基于这些挑战,我们的服务势必要做一定的优化来应对。

 

2群消息分发模型

整体的群聊服务架构如下图所示:

1.png

   用户在群里发了一条群消息后,消息先到群组服务,然后通过群组服务缓存的群关系,锁定这条消息最终需要分发的目标用户,然后根据一定的策略分发到消息服务上,消息服务再根据用户的在线状态和消息状态来判断这条消息是直推、通知拉取还是转Push,最终投递给目标用户。

 

3超大群消息分发解决方案

3.1分发控速:

第一,首先我们会根据服务器的核数来建立多个群消息分发队列,这些队列我们设置了不同的休眠时间以及不同的消费线程数,这里可以理解为快、中、慢等队列。如下图所示:

2.png

第二,我们根据群成员数量的大小来将所有群映射到相应的队列中,规则是小群映射到快队列中,大群映射到相应的慢队列中。

第三,小群由于人数少,对服务的影响很小,所以服务利用快队列快速的将群消息分发出去,而大群群消息则利用慢队列的相对高延时来起到控速的作用。

3.2 合并分发:

一条群消息发送到IM服务器后,需要从群组服务投递给消息服务,如果每一个群成员都投递一次,并且投递的群消息内容是一致的话,那肯定会造成相应的资源浪费和服务压力。

服务落点计算中我们使用的是一致性哈希,群成员落点相对固定,所以落点一致的群成员我们可以合并成一次请求进行投递,这样就大幅提高了投递效率同时减少了服务的压力。

3.3 超大规模群的处理方案

在实际群聊业务中,还有一种业务场景是超大规模群,这种群的群人数达到了数十万甚至上百万,这种群如果按照上述的分发方案,势必也会造成消息节点的巨大压力。比如我们有一个十万人的群,消息节点五台,消息服务处理消息的上限是一秒钟4000条,那每台消息节点大约会分到2万条群消息,这超出了消息节点的处理能力。

所以为了避免上述问题,我们的超大群(群成员上线超过3000,可以根据服务器数量和服务器配置相应做调整)会用特殊的队列来处理群消息的分发,这个特殊的队列一秒钟往后端消息服务投递的消息数是消息服务处理上限的一半(留相应的能力处理其他消息),如果单台消息服务处理的QPS上限是4000,那群组服务一秒往单台消息服务最多投递2000条。

 

结束语

我们后续也会针对群消息进行引用分发,对于大群里发的消息体比较大的消息,我们给群成员只分发和缓存消息的索引,比如MessageID,等群成员真正拉取群消息时再从将消息组装好给客户端分发下去。这样做会节省分发的流量以及存储的空间。

随着互联网的发展,群组业务的模型和压力也在不停地扩展,后续可能还会遇到更多的挑战,届时我们服务器也会通过更优的处理方式来应对。

 

感兴趣的开发者可以扫码下载融云的 IM 即时通讯 Demo 产品:SealTalk,体验融云的群聊、聊天室等通信能力。

3.png

收起阅读 »

感谢融云的新春礼包 -- 暖暖冬日,雪中送炭

昨天收到一个高大上的礼盒,以为里面装着一件古董打开一看,惊呆了。。。正好美国最近大动乱,啥都不缺,就缺这件防弹背心,顺便还可以防寒保暖。以后深夜撸代码,就可以穿着这件背心了。哇咔咔。。。谢谢融云的惊喜

昨天收到一个高大上的礼盒,以为里面装着一件古董

打开一看,惊呆了。。。


2.jpg


正好美国最近大动乱,啥都不缺,就缺这件防弹背心,顺便还可以防寒保暖。

3.jpg

以后深夜撸代码,就可以穿着这件背心了。哇咔咔。。。谢谢融云的惊喜

融云会话页面刷新不及时问题

项目用的融云 IMKit SDK,调试中发现收到消息的时候,不刷新,上拉一下才会显示。排查方法是直接使用 SDK 的会话页面,排除是子类代码的问题,替换后发现还是有此问题。后来和技术人员沟通发现是使用了 RCIMClient 中的初始化接口,这样会影响 UI ...
继续阅读 »

项目用的融云 IMKit SDK,调试中发现收到消息的时候,不刷新,上拉一下才会显示。排查方法是直接使用 SDK 的会话页面,排除是子类代码的问题,替换后发现还是有此问题。后来和技术人员沟通发现是使用了 RCIMClient 中的初始化接口,这样会影响 UI 刷新的。替换为 RCIM 的初始化方法,问题解决!希望此文字可以帮助到后续开发者!

/*!
 初始化融云SDK

 @param appKey  从融云开发者平台创建应用后获取到的App Key

 @discussion 您在使用融云SDK所有功能(包括显示SDK中或者继承于SDK的View)之前,您必须先调用此方法初始化SDK。
 在App整个生命周期中,您只需要执行一次初始化。

 @warning 如果您使用IMKit,请使用此方法初始化SDK;
 如果您使用IMLib,请使用RCIMClient中的同名方法初始化,而不要使用此方法。
 */
- (void)initWithAppKey:(NSString *)appKey;

友情提示融云官网:(www.rongcloud.cn)

收起阅读 »

融云 IMKit 音频录制参数

场景:使用融云自带的界面进行语音消息的播放。自己进行音频录制。使用的融云的 RCHQMessage问题:语音消息 iOS 和 Android 不互通,接收到消息之后无法播放。解决方案:经过与融云开发者的确认,使用时必须保证如下录制参数:iOS AVA...
继续阅读 »

场景:

  1. 使用融云自带的界面进行语音消息的播放。

  2. 自己进行音频录制。

  3. 使用的融云的 RCHQMessage

问题:

  1. 语音消息 iOS 和 Android 不互通,接收到消息之后无法播放。

解决方案:

经过与融云开发者的确认,使用时必须保证如下录制参数:

iOS AVAudioRecorder 录制参数如下设置:

AVFormatIDKey : @(kAudioFormatMPEG4AAC_HE),
AVSampleRateKey : @(44100.0),
AVNumberOfChannelsKey : @1,
AVEncoderBitRateKey : @(16000)

Android MediaRecorder 录制参数如下:

setAudioSamplingRate(44100);
setAudioEncodingBitRate(16000);
setAudioChannels(1);
setAudioSource(MediaRecorder.AudioSource.MIC);
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
setAudioEncoder(MediaRecorder.AudioEncoder.HE_AAC);

其他一些内容的使用可以自己去官网文档搜索:

融云文档:https://docs.rongcloud.cn/v4/


收起阅读 »

关于融云 SDK 在使用 p8 证书的坎坷~

新上的项目使用了融云的 IM SDK,但在项目集成 APNs 推送的时候,尝鲜使用了一下开发者后台的 p8 证书,此文记录使用 p8 的辛酸史~P8 简介苹果文档传送门官网给出了这种更 "快" 的推送通道: Establishing a T...
继续阅读 »

新上的项目使用了融云的 IM SDK,但在项目集成 APNs 推送的时候,尝鲜使用了一下开发者后台的 p8 证书,此文记录使用 p8 的辛酸史~

P8 简介

苹果文档传送门

官网给出了这种更 "快" 的推送通道: Establishing a Token-Based Connection to APNs,并且这个生成的这个 key 可以适用于当前账户的所有 APP,为开发人员省了不少力气。福音啊~

想想那一堆证书...... 脑阔痛!

辛酸史

起因是这样的,在融云开发者后台上传了 p8 之后,发现 debug 环境,一直无法收到推送,在经过和融云提供的推送文档进行严格的比对之后,发现没毛病啊~

最后终于在融云开发人员的帮助下找到了问题~,融云后台目前阶段只支持生产环境~ OMG,我打你信不~

区别

p8 是可以同时支持生产和测试环境的,那么为什么融云收不到呢~

让我们大胆猜测一下:

之前基于证书进行校验的时候,一套证书是基于开发者后台一个 AppKey 绑定的,那么我用了哪个 AppKey,后端就基于 AppKey 解析对应的证书,这样就可以发送到对应的 push 环境去了,那么问题来了?使用了 p8 之后,他怎么区分呢?

我也不知道~ 哈哈哈,但我猜测应该是没有解析都去走了生产环境,因为提示我环境不匹配~

苹果 APNs 服务

传送门

Development server: api.sandbox.push.apple.com:443

Production server: api.push.apple.com:443

融云文档传送门


收起阅读 »