融云 IM 即时聊天系统 SDK 架构研读笔记

RongCloud IM Live Chat System SDK architecture study notes

nojsja 2022-01-21
字数:21k丨 阅读时间:82 分钟

目录:

IM 系统 SDK 研读文档 (Web)

文档描述了 IM Web SDK 设计时参考产品的解决方案、IM SDK 自身的架构设计和业务流程设计、IM SDK 和后端的交互模式以及通信字段定义、IM SDK 的接口详细设计以及示例代码等。

一、IM 系统的概念

即时通信是一个终端连往即时通信网络的服务。即时通信不同于电子邮件在于它的交谈是即时(实时)的。大部分的即时通信服务提供了状态信息的特性──显示联系人名单,联系人是否在在线与能否与联系人交谈。 – 《维基百科》

一个典型的 IM 系统整体架构:

im_structure

  • 客户端:作为与服务端进行消息收发通信的终端。
  • 接入层:也叫网关层,为客户端收发消息提供入口。
  • 逻辑层:负责 IM 系统各功能的核心逻辑实现。
  • 存储层:负责 IM 系统相关数据的持久化存储,包括消息内容、账号信息、社交关系链等。
  • 第三方服务:保证 APP 在未打开或后台运行时也能收到消息通知(这主要是第第 3 方消息推送服务)。

二、设计方案参考

➣ 网易云信

1. 官方文档

链接

2. 整体架构

netease_im_structure

服务端分为 应用服务器云信服务器。云信服务器用于客户端的收发消息功能,应用服务器用于好友关系、用户信息等业务数据获取以及登录功能。同时云信服务器和应用服务器之间也会进行接口调用以同步用户数据和状态。

3. 消息通信机制和兼容性处理

网易云信 SDK 兼容到 IE8, IE8/IE9 使用 xhr-polling 来模拟长连接,也就是 http 长轮询, 其它高级浏览器使用 WebSocket 建立长连接。

➣ 腾讯 IM

1. 官方文档

链接

2. 整体架构

tencent_im_structure

腾讯 IM 架构和网易云信类似,服务端分为 应用服务器IM 服务器。IM 服务器用于客户端的消息管理、用户管理、群组管理、好友关系功能,应用服务器用于用户信息数据获取。同时 IM 服务器和应用服务器之间也会进行接口调用以同步用户数据和状态。

从整体架构上来看,两者不同之处体现在:对于好友关系和用户登录方面的处理,网易云信放在应用服务器,而腾讯 IM 放在 IM 服务器。网易的应用服务器含有通信业务方面的处理,而腾讯的应用服务器更偏向于纯数据存储和读取。

3. 消息通信机制和兼容性处理

腾讯 IM 从 v2.11.2 起,SDK 使用 WebSocket 方案替代之前的 HTTP 长轮询方案作为底层传输技术。不过接口都未更改,应该是默认会降级兼容。

三、IM Web SDK 功能设计

➣ 关于 HTTP 长连接和长轮询的说明

1. HTTP 长连接 / 短连接

在 HTTP/1.0 中默认使用 http 短连接。也就是说,客户端和服务器每进行一次 HTTP 操作,就建立一次连接,任务结束就中断连接。当客户端浏览器访问的某个 HTML 或其他类型的 Web 页中包含有其他的 Web 资源(如 JavaScript 文件、图像文件、CSS 文件等),每遇到这样一个 Web 资源,浏览器就会重新建立一个 HTTP 会话。

而从 HTTP/1.1 起,默认使用长连接,用以保持连接特性,提高传输的效率和减低服务器的开销。使用长连接的 HTTP 协议,会在响应头加入这行代码:

1
Connection:keep-alive

在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输 HTTP 数据的 TCP 连接不会关闭,客户端再次访问这个服务器时,会继续使用这一条已经建立的连接。Keep-Alive 不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如 Apache)中设定这个时间。实现长连接需要客户端和服务端都支持长连接。

HTTP 协议的长连接和短连接,实质上是 TCP 协议的长连接和短连接。

2. HTTP 长轮询

HTTP 长轮询是人为编程实现的一种通信方式,它在短轮询的基础上加入服务端的连接保持、连接超时和数据变化监听等功能。而 HTTP 长连接本质上是 http 协议的特性,主要用于优化客户端的连接重用,减少资源占用,不可人为编程。

➣ 通信机制

推荐使用 WebSocket + HTTP 长轮询兼容的方案。

1. 短轮询 (不使用,仅了解)

short_loop

在短轮询模式中,服务器接到请求后,如果有新消息就会将新消息返回给客户端,如果没有新消息就返回空列表,并关闭连接。

这种短轮询的方式就好像一位焦急等待重要信件的人,每天骑车跑到家门口的邮局去问是否有自己的信件,有就拿回家,没有第二天接着去邮局问。

作为从一问一答的请求响应模式孵化出来的短轮询模式,具有较低的迁移升级成本,比较容易落地。但劣势也很明显:

  • 为了提升实时性,短轮询的频率一般较高,但大部分轮询请求实际上是无用的,客户端既费电也费流量;
  • 高频请求对服务端资源的压力也较大,一是大量服务器用于抗高频轮询的 QPS(每秒查询率),二是对后端存储资源也有较大压力。

因此,“短轮询” 这种方式,一般多用在用户规模比较小,且不愿花费太多服务改造成本的小型应用上。

2. 长轮询 (作为兼容方案)

long_loop

正是由于 “短轮询” 存在着高频无用功的问题,为了避免这个问题,IM 逐步进化出 “长轮询” 的消息获取模式。

长轮询和短轮询相比,一个最大的改进之处在于:短轮询模式下,服务端不管本轮有没有新消息产生,都会马上响应并返回。而长轮询模式当本次请求没有获取到新消息时,并不会马上结束返回,而是会在服务端 “悬挂(hang)”,等待一段时间;如果在等待的这段时间内有新消息产生,就能马上响应返回。

这种方式就像等待收信的人每天跑到邮局去问是否有自己的信件,如果没有,他不是马上回家,而是在邮局待上一天,如果还是没有就先回家,然后第二天再来。

长轮询能大幅降低短轮询模式中客户端高频无用的轮询导致的网络开销和功耗开销,也降低了服务端处理请求的 QPS,相比短轮询模式而言,显得更加先进。

但是长轮询并没有完全解决服务端资源高负载的问题,仍然存在以下问题:

  • 服务端保持(hang)住请求,只是降低了入口请求的 QPS,并没有减少对后端资源轮询的压力。假如有 1000 个请求在等待消息,可能意味着有 1000 个线程在不断轮询消息存储资源。

  • 长轮询在超时时间内没有获取到消息时,会结束返回,因此仍然没有完全解决客户端 “无效” 请求的问题。

长轮询的使用场景多见于: 对实时性要求比较高,但是整体用户量不太大。它在不支持 WebSocket 的浏览器端的场景下还是有比较多的使用。

3. WebSocket (首选协议)

websocket

WebSocket 正是一种服务端推送的技术代表。

随着 HTML5 的出现,基于单个 TCP 连接的全双工通信的协议 WebSocket 在 2011 年成为 RFC 标准协议,逐渐代替了短轮询和长轮询的方式,而且由于 WebSocket 协议获得了 Web 原生支持,被广泛应用于 IM 服务中,特别是在 Web 端基本属于 IM 的标配通信协议。

和短轮询、长轮询相比,基于 WebSocket 实现的 IM 服务,客户端和服务端只需要完成一次握手,就可以创建持久的长连接,并进行随时的双向数据传输。当服务端接收到新消息时,可以通过建立的 WebSocket 连接,直接进行推送,真正做到 “边缘触发”,也保证了消息到达的实时性。

WebSocket 的优点是:

  • 支持服务端推送的双向通信,大幅降低服务端轮询压力;
  • 数据交互的控制开销低,降低双方通信的网络开销;
  • Web 原生支持,实现相对简单。

4. TCP 长连接衍生的 IM 协议 (其它参考)

除了 WebSocket 协议,在 IM 领域,还有其他一些常用的基于 TCP 长连接衍生的通信协议,如 XMPP 协议、MQTT 协议以及各种私有协议。

这些基于 TCP 长连接的通信协议,在用户上线连接时,在服务端维护好连接到服务器的用户设备和具体 TCP 连接的映射关系,通过这种方式客户端能够随时找到服务端,服务端也能通过这个映射关系随时找到对应在线的用户的客户端。

而且这个长连接一旦建立,就一直存在,除非网络被中断。这样当有消息需要实时推送给某个用户时,就能简单地通过这个长连接实现 “服务端实时推送” 了。

但是上面提到的这些私有协议都各有优缺点,如:XMPP 协议虽然比较成熟、扩展性也不错,但基于 XML 格式的协议传输上冗余比较多,在流量方面不太友好,而且整体实现上比较复杂,在如今移动网络场景下用得并不多。

而轻量级的 MQTT 基于代理的 “发布 / 订阅” 模式,在省流量和扩展性方面都比较突出,在很多消息推送场景下被广泛使用,但这个协议并不是 IM 领域的专有协议,因此对于很多 IM 下的个性化业务场景仍然需要大量复杂的扩展和开发,比如不支持群组功能、不支持离线消息。

因此,对于开发人力相对充足的大厂,目前很多是基于 TCP(或者 UDP)来实现自己的私有协议,一方面私有协议能够贴合业务需要,做到真正的高效和省流;另一方面私有协议相对安全性更高一些,被破解的可能性小。目前主流的大厂很多都是采用私有协议为主的方式来实现。

➣ 异常和容错处理

建立一个完备的 IM 系统需要考虑的问题:

  • 如何保证长连接通道能随时处于良好的连接状态(随时接收对方 write 的消息)?
  • 服务端如何实现心跳机制?
  • 当连接不可用时,用户此时发送的消息该怎么处理?
  • 连接断开时,对方发送消息该怎么实现?
  • 当恢复连接时,怎么恢复之前的聊天现场?
  • 如何保证发送的消息的实时性?
  • 当我收到对方的消息时,对方怎么知道我已经收到了?
  • 当重复收到对方的消息时,该怎么处理?
  • 当收到的消息时序有错乱,该怎么处理?
  • 离线消息该保存多久?
  • 图片、短视频、大文件这类的离线消息,多媒体文件该怎么存(有可能量会很大)?
  • 当本地的消息积累太多时,怎么能保证本地存储的性能?
  • 当应用更新、升级或异常时,怎么能保证本地存储的完整性(不被破坏)?

1. 网络异常和重连处理

SDK 设计了重连机制,当网络异常时,可以使用 SDK 接口传入设置的重连频率自动重连。

2. 发送接收消息乱序问题处理

每个会话和每条消息除了有自己的 ID 外还有时间戳 timestamp 属性,开发者收到 SDK 推送的消息时,需要根据时间戳来进行排序。由于 SDK 没有本地存储机制,因此不具备二次处理消息的能力,需要开发者自己处理这部分逻辑。

3. 对低版本浏览器的兼容处理

  • 本地数据存储方面:采用 LocalStorage,浏览器不支持时降级为 MemoryStorage。对于 IE 浏览器还使用了 ElementUserData 的兼容方式。
  • 长连接通信方面:优先使用 Websocket,浏览器不支持时降级为 XHR 长轮询。

4. 历史消息和未读消息的处理

首先应该明确,每个会话的每条消息都带有时间戳,并且 SDK 会记录每个会话的最新一条消息的时间戳。

调用 SDK 接口查询历史消息需要提供这些参数:

  • conversationType:会话类型
  • targetId:会话 ID
  • timestamp:时间戳
  • count:查询条数

SDK 接口返回时除了携带所有消息,还会有一个附带参数表明是否还有更多历史消息,如果还有更多历史消息,则需要再次调用 SDK 接口获取其余历史消息。

每个会话会有一个未读消息属性,查询会话信息时可以拿到,SDK 内部会自动更新这个属性值,不需要开发者多次发送查询请求进行实时更新。

5. 前端对大量消息展示的性能优化处理

有时候项目中要求我们在不使用分页的情况下渲染一个超长的列表组件,比如一个文件上传列表里面的每个文件上传任务,我们同时添加成千上万个上传任务,然后并行上传几个,操作者同时也能通过列表的上下滚动来查看每个上传任务的状态。这种数量级的界面元素展示可能导致我们的界面会有一定程度的卡顿。

一个解决方案就是可以采用懒加载技术来实现当滚动到任务列表底部时加载其余的一小部分任务列表元素,这样虽然解决了初次渲染时耗费时间过长的问题,不过随着滚动到底部加载的任务条目越来越多,界面的渲染负载也会越来越大。这种情况下采用虚拟化滚动技术来进行优化就显得很有必要了。

虚拟列表是一种根据滚动容器元素的可视区域高度来渲染长列表数据中某一个部分数据的技术,如果要直接使用的话可以考虑这两个热门的虚拟滚动库 react-windowreact-virtualized

6. 前端负载均衡的处理

在 SDK 设计时考虑了后端多组服务器的情况,前端首先会通过固定的 Navigation Server (导航服务器) 获取所有可用的服务器配置信息等,然后在 SDK 初始化时同时向这些服务器发送连接请求,首先连接上的会被作为当前默认的服务器节点。

此机制设计的参考依据:

  • 首先连接上的服务器在当前用户的网络环境下响应速度最快。
  • 无后端介入机制,减少实现复杂度,提高系统可靠性。

四、IM Web SDK 架构和业务流程

参考融云 IM 开源 SDK 设计

➣ Web SDK 架构图

web_sdk_structure

1. Storage 存储

主要分为临时存储和持久化存储:

  • 临时数据:SDK 云信过程中产生的临时数据,比如会话列表、各种时间戳、token 等。
  • 持久化数据:比如群消息已读状态回执、历史连接过的 appId、sessionId、导航数据等等。下文有详细说明。

持久化存储方面也对低版本浏览器做了兼容:

  • 优先使用 LocalStorage

  • IE 早期版本采用 Element#UserData 方式兼容,原理就是浏览器将 DOM Element 作为存储容器,不过只是作为备用兼容方案,并不支持持久化。

  • 兜底方案降级为 MemoryStorage,直接使用内存存储。这种方案下用户多次登录都需要重新获取所有数据,包括导航数据、自定义数据等,对用户的友好度降低。

2. Service 业务和服务

  • [01] IM Client:作为一个装饰器对象对外提供 SDK 接口,大部分接口功能会实际调用 Data Access ProviderBridge ,相当于对内部整体逻辑做了一层装饰器处理。
  • [02] Data Access Provider:负责大部分业务逻辑,会直接调用 Bridge 和 Storage。
  • [03] Bridge:将 IM Client 和 Msg Client 连接起来,设置监听者,内部 Client 的调用 Handler。
    • ├─ Navigation:导航数据处理,获取用于建立服务器连接前的一些配置信息和定制化信息。
    • └─ Client:消息客户端,整个消息通信的中心,外部响应消息监听者,内部调用 Channel 通道进行消息通信。
      • ├─ MessageHandler:消息接收和处理
      • └─ Channel:消息通道逻辑,会注册 连接状态监听者消息监听者
        • ├─ Socket:通信载体对象,底层会调用 WebSocketLong Polling 传输对象进行数据发送和接收。
          • ├─ MessageOutputStream:在数据发送前会对明文请求消息数据进行 Header 定制和 Stream 流化编码处理。
          • └─ MessageInputStream:在数据接收前,会对 Stream 流化响应数据进行解码生成 SDK 直接使用的明文对象。
        • ├─ Listeners:连接状态监听器和消息监听器
          • ├─ ConnectionStatusListener: 连接状态监听者
          • └─ ReceiveMessageListener: 消息接收监听者

3. Network 网络

以 WebSocket 作为主要通信协议,HTTP 长轮询作为兼容方案,上传文件、下载文件、获取导航信息等会用到 HTTP 短连接用于即时数据的获取。

需要注意的是整个通信过程中,Socket 传输的数据都是二进制数据,不会传明文,客户端发送消息时进行二进制编码,服务端接收消息时进行二进制解码。这样设计的目的一个是为了信息传送安全,另一个就是二进制数据有更好的传输性能。

➣ 整体业务流程

基础概念说明:

  • appKey:应用的唯一标识 (可能不需要实现)
  • userId :用户的唯一标识
  • token :用户的通信凭证

1. 获取应用的 appKey / appSecret

如果要作为一个云平台是公开对外公开提供服务的,类似于微信小程序的概念的话。appKey 可以用于区分云平台不同的开发者的应用,以提供每个开发者独立的服务。

早期简单实现可以只考虑一个开发者,也就是个 appKey / appSecret 固定,不用需要动态分发给不同的开发者。

2. 前端通过 App 应用服务器接口获取 token

开发环境下,为了方便开发者调试,可以使用可用的测试 token。SDK 调用者固定传入测试 token,以测试正常流程。

token 作为用户使用 IM 系统时通信的凭证,用于验证用户身份,以及用户的消息接收和发送。

流程图:

get_token

(1)客户端向 应用服务器 发送请求,请求携带 userId、appKey 等参数。

(2)应用服务器拿到请求后,使用步骤一拿到的 appSecret,根据签名算法使用 “appSecret + 随机数 (从前端请求拿到) + timestrap(从前端请求拿到)” 生成签名 signature,然后使用 appKey + 随机数 + timestrap + signature 作为参数向 API 服务器 发送请求,获取用户 token。拿到 token 后应用服务器会将其存入数据库,最后 token 作为 http 请求结果并返回给客户端。(这里有个概念就是区分了 API 服务器和应用服务器,如果简化架构的话应用服务器和 API 服务器可以为同一个服务器)。

(3)客户端拿到的 token 后存储 token 数据,之后客户端直接使用 token 跟 IM 消息服务器通信,API 服务器只会用于用户的某次会话中最初的 token 获取。

(4)对于后续客户端获取 token 的请求,App 应用服务器直接从数据库中返回之前从 API 服务器拿到的 token。进而流程简化为:

get_token_cache

3. 客户端对 token 的本地化处理

客户端获取到 token 后,可以直接使用 token 跟应用服务器、消息服务器通信。对 token 做本地持久化存储支持后,客户端也可自行实现自动登录和登录过期逻辑。

client_token

4. 客户端和 IM 消息服务端进行业务通信

(1)客户端初始化 sdk
1
RongIMLib.RongIMClient.init(appkey, [options]);
(2)客户端设置状态监听器和消息监听器

必须设置监听器后,再连接 IM 消息服务器,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/* 连接状态监听器 */
RongIMClient.setConnectionStatusListener({
onChanged: function (status) {
// status 标识当前连接状态
switch (status) {
case RongIMLib.ConnectionStatus.CONNECTED:
console.log('链接成功');
break;
case RongIMLib.ConnectionStatus.CONNECTING:
console.log('正在链接');
break;
case RongIMLib.ConnectionStatus.DISCONNECTED:
console.log('断开连接');
break;
case RongIMLib.ConnectionStatus.KICKED_OFFLINE_BY_OTHER_CLIENT:
console.log('其他设备登录');
break;
case RongIMLib.ConnectionStatus.DOMAIN_INCORRECT:
console.log('域名不正确');
break;
case RongIMLib.ConnectionStatus.NETWORK_UNAVAILABLE:
console.log('网络不可用');
break;
}
}
});

/* 消息监听器 */
RongIMClient.setOnReceiveMessageListener({
// 接收到的消息
onReceived: function (message) {
// 判断消息类型
switch(message.messageType){
case RongIMClient.MessageType.TextMessage:
// message.content.content => 文字内容
break;
case RongIMClient.MessageType.VoiceMessage:
// message.content.content => 格式为 AMR 的音频 base64
break;
case RongIMClient.MessageType.ImageMessage:
// message.content.content => 图片缩略图 base64
// message.content.imageUri => 原图 URL
break;
case RongIMClient.MessageType.LocationMessage:
// message.content.latiude => 纬度
// message.content.longitude => 经度
// message.content.content => 位置图片 base64
break;
case RongIMClient.MessageType.RichContentMessage:
// message.content.content => 文本消息内容
// message.content.imageUri => 图片 base64
// message.content.url => 原图 URL
break;
case RongIMClient.MessageType.InformationNotificationMessage:
// do something
break;
case RongIMClient.MessageType.ContactNotificationMessage:
// do something
break;
case RongIMClient.MessageType.ProfileNotificationMessage:
// do something
break;
case RongIMClient.MessageType.CommandNotificationMessage:
// do something
break;
case RongIMClient.MessageType.CommandMessage:
// do something
break;
case RongIMClient.MessageType.UnknownMessage:
// do something
break;
default:
// do something
}
}
});
(3)客户端连接服务器

连接消息服务器必须在执行 RongIMLib.RongIMClient.init(appkey); 之后调用,除监听以外所有方法都必须在调用 connect 连接服务器成功之后再调用。

  • dev 开发环境下客户端可以直接写死 token 测试
  • prod 生产环境下需要通过 应用服务器 (App Server) 获取 token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 本地调试写死或者通过应用服务器获取,dev/prod 的两种处理方式
var token = "mKmyKqTSf7aNDinwAFMnz7NXKILeV3X0+CAKgQ==";

RongIMClient.connect(token, {
onSuccess: function(userId) {
console.log('Connect successfully.' + userId);
},
onTokenIncorrect: function() {
console.log('token 无效');
},
onError: function(errorCode){
var info = '';
switch (errorCode) {
case RongIMLib.ErrorCode.TIMEOUT:
info = '超时';
break;
case RongIMLib.ConnectionState.UNACCEPTABLE_PAROTOCOL_VERSION:
info = '不可接受的协议版本';
break;
case RongIMLib.ConnectionState.IDENTIFIER_REJECTED:
info = 'appkey 不正确';
break;
case RongIMLib.ConnectionState.SERVER_UNAVAILABLE:
info = '服务器不可用';
break;
}
console.log(info);
}
});
(4)网络错误后客户端重新连接服务器

当网络不可用或网络断开导致客户端离线时,开发者可以手动通过 SDK 尝试重连。SDK 提供给开发者重连服务器方法 reconnect ,该功能内部采用定时器递归机制实现。

重连时提供两种方式,可以自行选择:

  • 重连单次,调用该方法后 SDK 会尝试重连一次,如果重连成功,会触发 onSuccess 回调,如果重连失败会触发 onError 回调。
  • 按照给定的重试频率数组进行自动重连,当给定的所有重试时间都用完后而服务器仍未连接上时,会携带 NETWORK_UNAVAILABLE 对应的错误码触发 onError 回调。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var callback = {
onSuccess: function(userId) {
console.log('Reconnect successfully.' + userId);
},
onTokenIncorrect: function() {
console.log('token 无效');
},
onError: function(errorCode){
console.log(errorcode);
}
};
var config = {
// 默认 false, true 启用自动重连,启用则为必选参数
auto: true,
// 网络嗅探地址 [http(s)://]cdn.ronghub.com/RongIMLib-2.2.6.min.js 可选
url: 'cdn.ronghub.com/RongIMLib-2.2.6.min.js',
// 重试频率 [100, 1000, 3000, 6000, 10000, 18000] 单位为毫秒,可选
rate: [100, 1000, 3000, 6000, 10000]
};

RongIMClient.reconnect(callback, config);

五、SDK 源码文件说明

模块文件内容和职责索引:

1. 内部业务流程处理类定义

  • [file] internal/navi.ts

    • [class] Navigation:导航信息,存储所有可能用到的服务器和其它配置等
      • [caller] Client: 创建和调用消息连接端实例
      • [function] connect:开始连接服务器
      • [function] requestNavi:获取导航信息
      • [function] getNaviSuccess:设置本地持久化数据
  • [file] internal/connect_client.ts

    • [class] Bridge:连接类,实现 imclient 与 connect_client 的连接
      • [instance] Navigation:导航信息实例
      • [instance] Client:消息连接端实例
      • [function] setListener:设置监听器
      • [function] connect:连接服务器
        • [function] Client.connect:调用实例连接
      • [function] queryMsg:查询消息
      • [function] pubMsg:发布消息
    • [class] Client: 消息连接客户端类
      • [instance] MessageHandler:消息处理实例
      • [instance] Channel:消息通道实例
      • [attr] token:用户 token
      • [attr] userId: 用户 ID
      • [attr] heartbeat:心跳间隔
      • [attr] chatroomId:聊天室 ID
      • [function] connect:初始化消息处理、创建 Channel、设置监听器
      • [function] syncTime:同步消息
      • [function] invoke:syncTime同步消息时,会调用 invoke,分为在线和离线消息两种情况
    • [class] Channel: 消息通道类
      • [instance] Socket: Socket通信实例
      • [super] token:用户 token
      • [listener] _ConnectionStatusListener: 连接状态监听者
      • [listener] _ReceiveMessageListener: 消息接收监听者
      • [attr] url:服务器地址
      • [function] startConnect:获取服务器配置后开始调用 Transportation 实例连接
    • [class] MessageHandler:消息处理类
      • [super] Client: 消息连接端实例
      • [function] handleMessage:消息处理功能
        • [01] 处理顶层消息:
          • [enum] ConnAckMessage:连接应答消息类型,服务器连接成功后收到(发向客户端)
          • [enum] QueryAckMessage:请求查询消息应答(发向客户端,携带查询结果)
          • [enum] QueryConMessage:请求查询消息回执(发向服务器,表明已收到查询结果)
          • [enum] PingReqMessage:心跳请求消息信令(双向,用于心跳检测和 Socket 验活)
          • [enum] PingRespMessage:心跳响应消息信令(发往客户端)
          • [enum] DisconnectMessage:断开连接消息(发往客户端)
        • [02] 调用 onReceived 处理聊天消息和聊天回执消息:
          • [enum] PublishMessage:聊天消息,客户端发布或收到此类消息(双向)
          • [enum] PubAckMessage: 发送聊天消息应答。某些消息发送之后要求服务端接收时回发回执消息,同样的收到某些新消息时,服务端可能要求客户端发送回执消息(双向)
      • [function] onReceived:接收消息处理
    • [class] Socket: Socket通信类
      • [listener] _events:事件监听者容器
      • [instance] PollingTransportation:长轮询传输实例
      • [instance] SocketTransportation:Websocket 传输实例
      • [function] connect:连接远程服务器
      • [function] send:发送消息
      • [function] disconnect:端开连接
      • [function] fire:触发监听器
      • [function] on:注册监听器
      • [function] removeEvent:移除监听器

2. 基础单元类定义

  • [file] internal/transportation.ts

    • [interface] Transportation:数据传输接口定义
  • [file] internal/transportation/websocket.ts

    • [class] SocketTransportation:Websocket 数据传输类
      • [instance] MessageOutputStream:消息数据输出流实例(发送)
      • [instance] MessageInputStream:消息数据输入流实例(接收)
  • [file] internal/transportation/xhrpolling.ts

    • [class] PollingTransportation:XHR 长轮询数据传输类
      • [instance] MessageInputStream:消息数据输入流实例(接收)
      • [attr] MessageOutputStream:消息数据输出流实例(发送,由上层进行编码处理)
  • [file] internal/internal_callback.ts

    • [class] MessageCallback:基础消息回调基类
    • [class] CallbackMapping: 聊天室消息映射处理类
    • [class extends MessageCallback] PublishCallback: 发送消息回调类,内部包含定时器超时处理
    • [class extends MessageCallback] QueryCallback:拉取新消息回调类,内部包含定时器超时处理
    • [class extends MessageCallback] ConnectAck:服务器连接回调处理类,调用 checkSocket,并触发 StatusChanged 事件
  • [file] internal/rmtp/signalling.ts

    • [class] BaseMessage:基础消息类
    • [class extends BaseMessage] ConnectMessage: 连接消息类型
    • [class extends BaseMessage] ConnAckMessage: 连接应答消息类型,服务器连接成功后收到(发向客户端)
    • [class extends BaseMessage] DisconnectMessage:断开连接消息(发往客户端)
    • [class extends BaseMessage] PingReqMessage:心跳请求消息信令(双向,用于心跳检测和 Socket 验活)
      • [01] 用于 keepLive 心跳
      • [02] 第一次连接服务器时,用于 checkSocket 验证服务器是否已经正常连接
    • [class extends BaseMessage] PingRespMessage:心跳响应消息信令(发往客户端)
    • [class extends BaseMessage] RetryableMessage:用于 MesssageId 封装
    • [class extends RetryableMessage] PubAckMessage:发送聊天消息应答。某些消息发送之后要求服务端接收时回发回执消息,同样的收到某些新消息时,服务端可能要求客户端发送回执消息(双向)
    • [class extends RetryableMessage] PublishMessage:聊天消息,客户端发布或收到此类消息(双向)
    • [class extends RetryableMessage] QueryMessage:请求查询消息类型
    • [class extends RetryableMessage] QueryConMessage:请求查询消息回执(发向服务器,表明已收到查询结果)
    • [class extends RetryableMessage] QueryAckMessage:请求查询消息应答(发向客户端,携带查询结果)
  • [file] internal/rmtp/stream.ts

    • [class] MessageOutputStream:消息对象流对象(消息对象写入流,用于发送请求)
      • [attr] out:消息对象流,使用 binaryHelper.convertStream 生成
    • [class] MessageInputStream:流转换为消息对象(消息对象写入流,用于接收响应)
      • [instance] msg:消息类型
        • [enum] ConnectMessage:连接消息
        • [enum] ConnAckMessage:连接应答消息
        • [enum] PublishMessage:聊天消息,客户端发布或收到此类消息(双向)
        • [enum] PubAckMessage:发送聊天消息应答。某些消息发送之后要求服务端接收时回发回执消息,同样的收到某些新消息时,服务端可能要求客户端发送回执消息(双向)
        • [enum] QueryMessage:查询聊天记录消息(双向)
        • [enum] QueryAckMessage:请求查询消息应答(发向客户端,携带查询结果)
        • [enum] QueryConMessage:请求查询消息回执(发向服务器,表明已收到查询结果)
        • [enum] PingReqMessage:心跳请求消息信令(双向,用于心跳检测和 Socket 验活)
        • [enum] PingRespMessage:心跳响应消息信令(发往客户端)
        • [enum] DisconnectMessage:断开连接消息(发往客户端)
    • [class] Header:消息头部
    • [class] BinaryHelper:二进制帮助对象
    • [class] RongIMStream:流对象
  • [file] internal/utils.ts

    • [class] MessageUtil
      • [function] messageParser:消息解析器
      • [function] detectCMP:XHR 对象创建

3. 整体业务流程定义

  • [file] providers/data_access/server.ts

    • [class extends DataAccessProvider] ServerDataProvider:服务器数据访问提供者
      • [function] connect:连接服务器
      • [caller] RongIMClient.bridge: 连接桥
        • [function] RongIMClient.bridge.setListener:设置监听器
        • [function] RongIMClient.bridge.connect:执行连接
        • [function] RongIMClient.bridge.pushMsg:发送消息
  • [file] imclient.ts

    • [class] RongIMClient:SDK 客户端类
      • [function] init:SDK 初始化
      • [attr] RTCListener: RTC消息监听器
      • [attr] Conversation:会话对象
      • [attr] Protobuf:公众服务订阅相关
      • [attr] MessageParams:所有消息参数定义
      • [attr] MessageType:所有消息内容类型定义,比如文本类型、图片类型
      • [attr] LogFactory:存储长连接通信消息码和消息含义映射关系,比如通信超时、聊天室异常等
      • [instance] _dataAccessProvider:imclient 相关逻辑实际执行者,imclient 作为一个装饰器包装了它,并且它也有多种实现
      • [instance] _storageProvider:本地存储区域,内部可能由 LocalStorage/Memory/UseData实现
      • [instance] _memoryStore:内存临时存储区域,用于存储会话列表和其它临时数据
      • [function] initApp:执行 init,并调用外部回调函数
      • [function] connect:连接
      • [function] reconnect:重新连接
      • [function] registerMessageType:注册自定义消息类型
      • [function] setConnectionStatusListener:设置连接状态变化的监听器
      • [function] setOnReceiveMessageListener:设置接收消息的监听器
      • [function] logout:清理所有连接相关的变量
      • [function] disconnect:断开连接
      • [function] startCustomService:开启客服
      • [function] stopCustomeService:关闭客服
      • [function] switchToHumanMode:切换到人工模式
      • [function] evaluateRebotCustomService:评价机器人客服
      • [function] evaluateHumanCustomService:评价人工客服
      • [function] getCurrentConnectionStatus:获取当前连接状态
      • [function] getConnectionChannel:获取当前连接通道
      • [function] getStorageProvider:获取本地存储提供者
      • [function] setFilterMessages:过滤聊天室消息
      • [function] getAgoraDynamicKey:获取 Agora 动态密钥
      • [function] getCurrentUserId:获取当前用户 ID
      • [function] getDeltaTime:获取服务器时间和本地时间的差值
      • [function] getMessage:获取消息
      • [function] deleteLocalMessages:删除本地消息
      • [function] updateMessage:更新消息
      • [function] clearData:清除本地存储数据
      • [function] clearMessages:清除消息
      • [function] clearMessagesUnreadStatus:清除本地存储的未读消息,目前清空内存中的未读消息
      • [function] deleteMessages:删除消息记录
      • [function] sendLocalMessage:发送本地消息
      • [function] getPullSetting:获取拉取设置
      • [function] setOfflineMessageDuration:设置离线消息持续时间
      • [function] sendMessage:发送消息
      • [function] sendReceiptResponse:发送消息已读回执
      • [function] sendTypingStatusMessage:更新“正在输入”状态
      • [function] sendTextMessage:发送文本消息
      • [function] sendRecallMessage:发送撤回消息
      • [function] insertMessage:向本地插入一条消息,不发送到服务器
      • [function] setMessageContent:设置消息内容
      • [function] setMessageSearchField:设置消息搜索字段
      • [function] getHistoryMessages:获取历史消息记录
      • [function] getRemoteHistoryMessages:拉取远程某个时间戳之前的消息
      • [function] clearHistoryMessages:清除历史消息记录
      • [function] clearRemoteHistoryMessages:清除某个时间戳之前的历史消息
      • [function] deleteRemoteMessages:删除远程历史消息
      • [function] hasRemoteUnreadMessages:是否有未接收的消息,jsonp方法
      • [function] getTotalUnreadCount:获取未读消息总数
      • [function] getConversationUnreadCount:指定多种会话类型获取未读消息数
      • [function] getUnreadCount:获取用户、会话类型的未读消息总数。
      • [function] setUnreadCount:指定用户、会话类型的未读消息总数。
      • [function] clearUnreadCountByTimestamp:清除某个时间戳之前的未读消息数。
      • [function] clearUnreadCount:清除会话未读消息数
      • [function] clearTotalUnreadCount:清除未读消息总数
      • [function] clearLocalStorage:清除本地存储
      • [function] setMessageExtra:设置消息额外信息
      • [function] setMessageReceivedStatus:设置消息接收状态
      • [function] setMessageStatus:设置消息状态
      • [function] setMessageSentStatus:设置消息发送状态
      • [function] clearTextMessageDraft:清除文本消息草稿
      • [function] getTextMessageDraft:获取指定消息和会话的草稿
      • [function] saveTextMessageDraft:保存文本消息草稿
      • [function] searchConversationByContent:搜索会话
      • [function] searchMessageByContent:搜索消息内容
      • [function] clearCache:清除缓存
      • [function] clearConversations:清除会话
      • [function] getConversation:获取指定会话,此方法需在getConversationList之后执行
      • [function] pottingConversation:组装会话列表
      • [function] addConversation:添加会话
      • [function] getConversationList:获取会话列表
      • [function] getRemoteConversationList:拉取远程会话列表
      • [function] updateConversation:更新会话
      • [function] createConversation:创建会话
      • [function] removeConversation:删除会话
      • [function] setConversationHidden:设置会话隐藏状态
      • [function] setConversationToTop:设置会话置顶状态
      • [function] getConversationNotificationStatus:获取指定用户和会话类型免提醒
      • [function] setConversationNotificationStatus:设置指定用户和会话类型免提醒
      • [function] getNotificationQuietHours:获取免打扰时间
      • [function] addMemberToDiscussion:加入讨论组
      • [function] createDiscussion:创建讨论组
      • [function] getDiscussion:获取讨论组
      • [function] quitDiscussion:退出讨论组
      • [function] removeMemberFromDiscussion:移除讨论组成员
      • [function] setDiscussionInviteStatus:设置讨论组邀请状态
      • [function] setDiscussionName:设置讨论组名称
      • [function] joinChatRoom:加入聊天室
      • [function] setDeviceInfo:设置设备信息
      • [function] setChatroomHisMessageTimestamp:设置聊天室历史消息时间戳
      • [function] getChatRoomHistoryMessages:获取聊天室历史消息
      • [function] getChatRoomInfo:获取聊天室信息
      • [function] quitChatRoom:退出聊天室
      • [function] setChatroomEntry:设置聊天室入口
      • [function] forceSetChatroomEntry:强制设置聊天室入口
      • [function] getChatroomEntry:获取聊天室入口
      • [function] getAllChatroomEntries:获取所有聊天室入口
      • [function] removeChatroomEntry:移除聊天室入口
      • [function] forceRemoveChatroomEntry:强制移除聊天室入口
      • [function] addToBlacklist:加入黑名单
      • [function] getBlacklist:获取黑名单
      • [function] getBlacklistStatus:得到指定人员再黑名单中的状态
      • [function] removeFromBlacklist:将指定用户移除黑名单
      • [function] getFileToken:获取文件上传的token
      • [function] getFileUrl:获取文件下载的url
      • [function] getUserStatus:获取用户在线状态
      • [function] setUserStatus:设置用户在线状态
      • [function] setUserStatusListener:设置用户在线状态监听者
      • [function] getAppInfo:获取应用信息
      • [function] getSDKInfo:获取 SDK 信息

六、概念和模块说明

➣ 用户、群组、聊天室相关的逻辑

group

  • 应用服务器维护用户所有信息,请求注册用户(提供 id、name、portrait),id 用于消息收发,name 用于 push,此接口同时提供了刷新方法。

  • 应用服务器维护群组及成员信息,也就是说,群的创建、群成员的维护都需要在应用服务器完成,后同步到消息服务器,但只同步群组(群 id)及群成员信息(成员 id),两种 id 同样用于消息的收发,消息发送给群组 id,群成员可以获取对应消息。

  • 用户创建和加入聊天室是在端上调用 SDK 相应方法完成,具体参考 Web SDK Demo 代码示例。

  • 通过连接服务器、消息收发等携带相关 id (用户 Id、群组 Id)到各端,端上通过 id 请求应用服务器获取详细信息(用户名、头像、群组名称、群头像、群成员信息以及其他业务需要的职位、等级等业务相关数据)。

  • 用户使用 token 链接消息服务器,成功后返回当前用户的 id,可以获取登录用户的详细信息。

  • 接收到消息时,消息体里有 conversationType、targetId 和 senderUserId,单聊(conversationType = 1)可以通过 userId = senderUserId 获取发送者详细信息;群聊(conversationType = 3)可以通过 groupId = targetId 获取群组信息,可以直接返回群组及成员信息,也可以分开请求获取,具体取决于接口设计。

  • 收到聊天室消息(conversationType = 4),可以通过 senderUserId 得知发送者 id,通过 targetId 得知聊天室 id,然后可以获取聊天室成员列表,进而处理相应的用户信息逻辑。

➣ 会话和消息

会话与消息的关系

Web SDK 不包含消息数据的本地存储机制,开发者需要自己进行消息的存储。

会话实体类和消息实体类是用来存储本地会话和消息的容器类,除了包含会话内容和消息内容外,还包括了保存在本地的各种状态。

用来存储消息的实体类主要有 Conversation(会话) 和 Message(消息) 两个实体类,在客户端读取消息时,获取的对象都和这两个类相关。会话有多种类型,可以是私聊会话,也可以是群组会话等,每一个 Conversation(会话)包含多条 Message(消息),关系如下图所示:

conversation_msg

如何标识一个会话

通过 conversationType 和 targetId,可以唯一确定一个会话。ConversationType 枚举值意义和对应的 targetId 意义为:

会话名称会话类型枚举 ConversationType对应的 targetId状态值
单聊PRIVATE用户的 Id(userId)1
群组GROUP群组的 Id(groupId)3
聊天室CHATROOM聊天室的 Id(chatroomId)4
客服CUSTOMER_SERVICE客服的 Id(customerServiceId)5
系统会话SYSTEM系统账户 Id6
应用公众服务APP_PUBLIC_SERVICE应用公众服务的 Id (publicServiceId)7
公众服务PUBLIC_SERVICE公众服务的 Id (publicServiceId)8

消息的定义

MessageContent 是消息基类,所有消息类都继承于 MessageContent 类。需要注意的是,MessageContent 类和 Message 类之间的关系:Message 中包含了一个具体的继承自 MessageContent 的消息,就是 Content 属性,可以通过 setContent() 和 getContent() 进行存取。

约定:如果您要定义一个内容类消息(需要显示在聊天会话界面中,且不是通知类消息),请从 MessageContent 类继承,命名为 XxxxxMessage。

内容消息表示一个用户间发送的包含具体内容的消息,需要展现在聊天界面上,如文字消息、语音消息等。

消息的分类

消息分类消息行为状态标识
内容类消息表示一个用户发送的包含具体内容的消息,需要展现在聊天界面上,如文字消息、语音消息等。
通知类消息表示一个通知信息,可能展现在聊天界面上,如提示条通知。
状态类消息表示一个状态,用来实现如 “对方正在输入” 的功能。
信令类消息在实现 SDK 自身业务功能时使用的,开发者不需要对其做任何处理。

(1)内容类消息

优先实现 TextMessage,其它类型 Message 需要再进行规划。

消息类型ObjectName类名是否计数是否存储
文字消息RC:TxtMsgTextMessage
语音消息RC:VcMsgVoiceMessage
高质量语音消息RC:HQVCMsgHQVoiceMessage
图片消息RC:ImgMsgImageMessage
GIF 图片消息RC:GIFMsgGIFMessage
图文消息RC:ImgTextMsgRichContentMessage
文件消息RC:FileMsgFileMessage
位置消息RC:LBSMsgLocationMessage
小视频消息RC:SightMsgSightMessage
公众服务单图文消息RC:PSImgTxtMsgPublicServiceRichContentMessage
公众服务多图文消息RC:PSMultiImgTxtMsgPublicServiceMultiRichContentMessage

(2)通知类消息

好友通知、提示条通知、已读通知消息可以考虑优先实现,其它通知类型再进行规划。

消息类型ObjectName类名是否计数是否存储
好友通知消息RC:ContactNtfContactNotificationMessage
资料通知消息RC:ProfileNtfProfileNotificationMessage
通用命令通知消息RC:CmdNtfCommandNotificationMessage
提示条通知消息RC:InfoNtfInformationNotificationMessage
群组通知消息RC:GrpNtfGroupNotificationMessage
已读通知消息RC:ReadNtfReadReceiptMessage
公众服务命令消息RC:PSCmdPublicServiceCommandMessage
命令消息RC:CmdMsgCommandMessage

(3)状态类消息

可以优实现正在输入状态处理,其它状态类型再进行规划。

消息类型ObjectName类名是否计数是否存储
对方正在输入状态消息RC:TypStsTypingStatusMessage
群消息已读状态回执RC:RRRspMsgReadReceiptResponseMessage

(4)信令类消息

内部用于和后端通信,比如服务器连接成功后信令、心跳检测信令、接收消息回执信令等。

待完善

消息体解析和交互流程说明

1. 消息类型
  • [class] ConnectMessage:连接消息 (发向客户端) / 消息类型值:1
  • [class] ConnAckMessage:连接应答消息类型,服务器连接成功后收到 (发向客户端) / 消息类型值:2
  • [class] QueryMessage:请求查询消息 (发向服务器,用于查询) / 消息类型值:5
  • [class] QueryAckMessage:请求查询消息应答 (发向客户端,携带查询结果) / 消息类型值:6
  • [class] QueryConMessage:请求查询消息回执 (发向服务器,表明已收到查询结果) / 消息类型值:7
  • [class] PingReqMessage:心跳请求消息信令 (双向,用于心跳检测和 Socket 验活) / 消息类型值:12
  • [class] PingRespMessage:心跳响应消息信令 (发往客户端) / 消息类型值:13
  • [class] DisconnectMessage:断开连接消息 (发往客户端) / 消息类型值:14
  • [class] PublishMessage:聊天消息,客户端发布或收到此类消息 (双向) / 消息类型值:3
    • s_ntf:系统通知消息
    • s_msg:聊天消息
    • s_stat:用户状态更新消息
    • s_cmd:聊天室消息
    • pp/pd/ch/pc:群组消息
  • [class] PubAckMessage: 发送聊天消息应答。某些消息发送之后要求服务端接收时回发回执消息,同样的收到某些消息后,客户端会往服务端发送回执消息。(双向) 码值:4
2. 消息体

消息头各字段数据意义 (flags)

以下参数由 HEADER 运算规则和枚举 处的解码算法计算得出。

1
2
3
4
5
6
7
{
"retain": true, // 是否保留
"qos": 0, // 是否需要回执,0:不需要,1:需要至少一次回执,其它枚举值暂时未用
"dup": false, // 是否为重发消息
"syncMsg": false, // 是否是同步消息 (一般为 false,true 情况暂时未用到)
"type": 2 // 消息类型值 - ConnAckMessage
}

消息体数据格式 (body)

1
2
3
4
{
"_header": [number], // 消息头码值,见 `HEADER 运算规则和枚举 ` 说明,需要使用算法进行编码和解码
"data": [{...}], 消息体
}
3. 消息头部码值编解码规则

消息头部有很多标志位,通过编解码算法简化标志位的表示,服务器和客户端拿到消息后需要进行解码,发送消息时需要编码。

运算规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
const Qos = {

AT_MOST_ONCE: 0, // 最多一次

AT_LEAST_ONCE: 1, // 至少一次

EXACTLY_ONCE: 2, // 必须一次

DEFAULT: 3 // 默认
};

class Header {
type = ''
retain = ''
qos = Qos.AT_LEAST_ONCE
dup = ''
syncMsg = ''
constructor(_type, _retain, _qos, _dup) {
if (_type && +_type == _type && arguments.length == 1) {
this.decode(_type);
} else {
this.type = _type;
this.retain = _retain;
this.qos = _qos;
this.dup = _dup;
}
}
getSyncMsg() {
return this.syncMsg;
}
getType() {
return this.type;
}
decode(_type) {
this.retain = (_type & 1) > 0; // 奇数
this.qos = (_type & 6) >> 1; //
this.dup = (_type & 8) > 0;
this.type = (_type>> 4) & 15;
this.syncMsg = (_type & 8) == 8;
}
encode() {
var me = this;
switch (this.qos) {
case 'AT_MOST_ONCE':
me.qos = Qos.AT_MOST_ONCE; // 0
break;
case 'AT_LEAST_ONCE':
me.qos = Qos.AT_LEAST_ONCE; // 1
break;
case 'EXACTLY_ONCE':
me.qos = Qos.EXACTLY_ONCE; // 2
break;
case 'DEFAULT':
me.qos = Qos.DEFAULT; // 3
break;
}
var _byte = (this.type << 4);
_byte |= this.retain ? 1 : 0;
_byte |= this.qos << 1;
_byte |= this.dup ? 8 : 0;
return _byte;
}
toString() {
return "Header [type=" + this.type + ",retain=" + this.retain + ",qos=" + this.qos + ",dup=" + this.dup + "]";
}
}
4. 消息交互逻辑描述
  • 历史消息 (会话)

    • 历史消息是针对某一个会话的,根据会话中的某条消息时间戳,可以向上或向下查询历史消息。
  • 离线消息 (全局)

    • 离线消息就是用户不在线时,服务端存储的消息,需要在用户上线的时候由手动调用 SDK 拉取,SDK 会每隔 180s 向服务端发送消息拉取离线消息。
    • SDK 连接服务器成功后,会自动拉取一次离线消息 (QueryMessage-pullMsg),客户端断开连接后重连时也会调用。
    • 注意离线消息和历史消息不一样的是,历史消息是针对用户的,SDK 调用者在会话中会主动拉取历史消息。
    • 而离线消息是 SDK 自发的一种消息机制,一方面防止网络波动和其它情况下的消息丢失,另一方面可以在用户上线时自动获取服务器的离线消息,它不属于用户 (SDK 调用者) 行为。
  • 心跳消息

    • SDK 会每隔 30s 向服务端发送心跳包 PingReqMessage,用于保持连接。服务端需要回传 PingRespMessage 用于响应。同时服务器端也是通过这个过程来确定客户端是否在线,因为活跃的客户端一定会每隔 30s 向服务器发送心跳包,断线的用户收到的消息会被离线处理。
  • 用户初次加载界面后

    • 需要使用 QueryMessage-qryCon 拉取所有会话,每个会话都有未读消息数量和最后一条未读消息内容。
    • 首次拉取离线消息和首次列举会话并不冲突,如果首次拉取的离线消息先到达,会先根据某些离线消息来创建某几个本地会话。而列举会话请求返回后也会直接利用前面创建的会话,属于 SDK 自发的逻辑。
    • 之后 SDK 主动拉取离线消息 (消息心跳,需要区别于 ping 心跳) 或是收到消息通知后拉取在线消息时,SDK 调用者需要根据每条消息所属的会话对会话列表做更新。
  • 聊天界面

    • 进入聊天界面:将最新的消息设置为已读 (通过 PublishMessage 的方式发送,同时会收到服务器端的发送回执 PubAckMessage)。
    • 输入信息:聚焦时发送 sendTypingStatusMessage 表明正在输入。
    • 点击发送文本:向后端发送 PublishMessage-ppMsgP 信息。
  • 发送消息整体流程

    • 客户端向服务器发送 PublishMessage-ppMsgP 消息。
    • 服务端收到后向消息发送端发一条 PubAckMessage 消息表明消息发送成功。
    • 服务器开始进行消息转发,如果接收方不在线,则将消息存储为离线消息。如果接收方在线,则服务器先向接收端发送 PublishMessage(s_ntf) 用于通知有消息到达。另一种情况时恰好此时接收端正在查询消息列表,那么此消息数据也可能由一条 QueryAckMessage 响应带回。
    • 消息接收端成功接收新消息通知 PublishMessage-s_ntf 后,再向服务端发送 QueryMessage-pullMsg 用于拉取消息,服务器端接收到拉取消息请求,将消息数据通过 QueryAckMessage 消息发送给消息接收端,然后消息接收端收到后向服务端发送 QueryConMessage 消息表明成功接收。
    • 另外如果此时接收端正在在聊天界面则会继续向服务器发送已读回执,回执通过 PublishMessage 格式发出 (参数见下方描述)。
    • 服务器端拿到已读回执做数据处理后,向消息接收端发出 PubAckMessage 消息表明已读回执已经成功处理,最后向消息发送端发送 PublishMessage 用于表示消息已经被接收端读取。
    • 消息发送端拿到最后的 PublishMessage 信息将本地的消息状态更新为对方已读,并向服务器发送 PubAckMessage-s_msg 表明已处理。
  • 发送消息整体流程 (补充)

    • 融云在处理消息转发的时候,会有一定的动态机制,有些情况下会通过服务端下发通知让接收端拉取消息,有时候会直接向接收端发送 PublishMessag 传输消息数据,暂时未知为何这样处理。
  • 查询消息流程

    • 客户端向服务器发送 QueryMessage 消息 (离线消息 / 历史消息)。
    • 服务器统计消息数量,向消息查询客户端发送 QueryAckMessage 携带所有消息。
    • 某些消息客户端收到后,需要向服务端回发 QueryConMessage 表明所查询的消息已经成功接收。
  • 发送方发消息流程

    • 发送 PublishMessage-ppMsgP (私聊个人消息),然后收到服务器 PubAckMessage 表明发送成功。
  • 接收方收消息流程

    • 收到 PublishMessage-s_ntf 消息通知,接收方发出 QueryMessage-pullMsg 请求拉取消息。
    • 服务器向客户端发送 QueryAckMessage 用于传输消息数据
    • 客户端接收消息后向服务器发送 QueryConMessage 表示接收成功。
    • 如果用户在聊天界面,还会发出已读信息 PublishMessag-s_msg 将消息标注为已读。

➣ 网络通信

Socket 长连接和 XHR 长轮询的方式,消息体都是以二进制 Buffer 流的形式传输的。在一次传输中前端会将各种类型的消息内容加上个本次通信的消息头部转换成对应的 Buffer 流,然后发送给服务器。

通信方式

状态标识状态值状态中文
XHR_POLLING0长轮询
WEBSOCKET1长连接
1. Socket 长连接

作为优先的通信方式

WebSocket 是一种全双工的通信协议,支持服务器推消息给客户端以及客户端主动推送消息到服务端,通常 WebSocket 只需要在开始通信时建立连接,然后通信过程中连接会一直存在。

2. XHR 长轮询

作为兼容的通信方式

HTTP 长轮询是人为编程实现的一种通信方式,它在短轮询的基础上加入服务端的连接保持、连接超时和数据变化监听等功能。

前端发送请求后设置定时器,服务端需要在定时器时间内 hold 住前端请求,在定时器到期前需要返回新消息或是返回空以表明没有新消息。反之定时器超时后后端还没返回,则前端视为长连接通信错误,Socket 连接进入 error 状态。

每次前端拿到新消息并处理后,需要立即发送下一个长连接请求给服务器,循环这个流程。它不同于平常使用的短连接,短连接只在需要的时候发送请求到服务端获取接口数据。

通信状态枚举字段

SDK 内部使用

socket 连接会伴随各种状态变化,比如连接成功、收到消息、连接断开等等。

内部逻辑代码:

1
2
3
socket.on('StatusChanged|message|disconnect', function() {
/* do somethings */
});

三种 Socket 信号:

1)StatusChanged:网络连接状态变化

状态标识状态值状态中文
CONNECTED0连接成功
CONNECTING1连接中
DISCONNECTED2断开连接
KICKED_OFFLINE_BY_OTHER_CLIENT6用户账户在其他设备登录,本机会被踢掉线
WEBSOCKET_UNAVAILABLE7websocket 连接失败
WEBSOCKET_ERROR8websocket 报错
NETWORK_UNAVAILABLE3网络不可用
DOMAIN_INCORRECT12域名错误
APPKEY_IS_FAKE20appkey 不正确
CONNECTION_CLOSED4连接关闭
ULTRALIMIT1101互踢次数过多(count > 5)
REQUEST_NAVI201开始请求导航
RESPONSE_NAVI202请求导航结束
RESPONSE_NAVI_ERROR203请求导航失败
RESPONSE_NAVI_TIMEOUT204请求导航超时

2)message:消息通信

  • [class] ConnAckMessage:连接应答消息类型,服务器连接成功后收到(发向客户端)
  • [class] QueryAckMessage:请求查询消息应答(发向客户端,携带查询结果)
  • [class] QueryConMessage:请求查询消息回执(发向服务器,表明已收到查询结果)
  • [class] PingReqMessage:心跳请求消息信令(双向,用于心跳检测和 Socket 验活)
  • [class] PingRespMessage:心跳响应消息信令(发往客户端)
  • [class] DisconnectMessage:断开连接消息(发往客户端)
  • [class] PublishMessage:聊天消息,客户端发布或收到此类消息(双向)
    • s_ntf:系统通知消息
    • s_msg:聊天消息
    • s_stat:用户状态更新消息
    • s_cmd:聊天室消息
    • pp/pd/ch/pc:群组消息
  • [class] PubAckMessage: 发送聊天消息应答。某些消息发送之后要求服务端接收时回发回执消息,同样的收到某些
状态标识状态值状态中文
CONNECT1请求连接
CONNACK2请求连接返回
PUBLISH3发布消息
PUBACK4发布消息回执
QUERY5查询消息
QUERYACK6查询消息返回
QUERYCON7请求查询消息回执,表明已收到查询结果
SUBSCRIBE8订阅公众号
SUBACK9订阅公众号回执
UNSUBSCRIBE10取消订阅公众号
UNSUBACK11取消订阅公众号回执
PINGREQ12心跳请求消息
PINGRESP13心跳响应消息
DISCONNECT14断开连接

3)disconnect:连接断开

通信过程中产生的各种错误码定义

状态标识状态值状态中文
TIMEOUT-1超时
UNKNOWN-2未知
PARAMETER_ERROR-3参数错误
RECALL_MESSAGE25101消息撤回
SEND_FREQUENCY_TOO_FAST20604发送频率过快
RC_MSG_UNAUTHORIZED20406未经授权
RC_DISCUSSION_GROUP_ID_INVALID20407群组 Id 无效
FORBIDDEN_IN_GROUP22408群组被禁言
NOT_IN_DISCUSSION21406不在讨论组
NOT_IN_GROUP22406不在群组
NOT_IN_CHATROOM23406不在聊天室
FORBIDDEN_IN_CHATROOM23408聊天室被禁言
RC_CHATROOM_USER_KICKED23409聊天室中成员被踢出
RC_CHATROOM_NOT_EXIST23410聊天室不存在
RC_CHATROOM_IS_FULL23411聊天室人数已满
RC_CHATROOM_PATAMETER_INVALID23412聊天室参数无效
CHATROOM_GET_HISTORYMSG_ERROR23413聊天室异常
CHATROOM_NOT_OPEN_HISTORYMSG_STORE23414聊天室未开启历史消息存储
CHATROOM_KV_EXCEED23423聊天室 KV 设置超限
CHATROOM_KV_OVERWRITE_INVALID23424聊天室 KV 设置失败 (kv 已存在, 需覆盖设置)
CHATROOM_KV_STORE_NOT_OPEN23426聊天室 KV 存储未开通
CHATROOM_KEY_NOT_EXIST23427聊天室 Key 不存在
SENSITIVE_SHIELD21501敏感词屏蔽
SENSITIVE_REPLACE21502敏感词替换
JOIN_IN_DISCUSSION21407加入讨论组失败
CREATE_DISCUSSION21408创建讨论组失败
INVITE_DICUSSION21409设置讨论组邀请状态失败
GET_USERINFO_ERROR23407获取用户信息失败
REJECTED_BY_BLACKLIST405在黑名单中
RC_NET_CHANNEL_INVALID30001通信过程中,当前 Socket 不存在
RC_NET_UNAVAILABLE30002Socket 连接不可用
RC_MSG_RESP_TIMEOUT30003通信超时
RC_HTTP_SEND_FAIL30004导航操作时,HTTP 请求失败
RC_HTTP_REQ_TIMEOUT30005HTTP 请求失败
RC_HTTP_RECV_FAIL30006HTTP 接收失败
RC_NAVI_RESOURCE_ERROR30007导航操作的 HTTP 请求,返回不是 200
RC_NODE_NOT_FOUND30008导航数据解析后,其中不存在有效数据
RC_DOMAIN_NOT_RESOLVE30009导航数据解析后,其中不存在有效 IP 地址
RC_SOCKET_NOT_CREATED30010创建 Socket 失败
RC_SOCKET_DISCONNECTED30011Socket 被断开
RC_PING_SEND_FAIL30012PING 失败
RC_PONG_RECV_FAIL30013PONG 超时
RC_MSG_SEND_FAIL30014消息发送失败
RC_CONN_ACK_TIMEOUT31000做 connect 连接时,收到的 ACK 超时
RC_CONN_PROTO_VERSION_ERROR31001参数错误
RC_CONN_ID_REJECT31002参数错误,App Id 错误
RC_CONN_SERVER_UNAVAILABLE31003服务器不可用
RC_CONN_USER_OR_PASSWD_ERROR31004token 错误
RC_CONN_NOT_AUTHRORIZED31005appId 与 token 不匹配
RC_CONN_REDIRECTED31006重定向,地址错误
RC_CONN_PACKAGE_NAME_INVALID31007NAME 与后台注册信息不一致
RC_CONN_APP_BLOCKED_OR_DELETED31008应用被删除或不存在
RC_CONN_USER_BLOCKED31009用户被封禁
RC_DISCONN_KICK31010异常断开,由服务器返回,比如用户互踢
RC_DISCONN_EXCEPTION31011异常断开,服务器内部异常
RC_QUERY_ACK_NO_DATA32001协议层内部错误,query,上传下载过程中数据错误
RC_MSG_DATA_INCOMPLETE32002协议层内部错误
BIZ_ERROR_CLIENT_NOT_INIT33001客户端未初始化
BIZ_ERROR_DATABASE_ERROR33002数据库初始化失败
BIZ_ERROR_INVALID_PARAMETER33003参数无效
BIZ_ERROR_NO_CHANNEL33004网络通道无效
BIZ_ERROR_RECONNECT_SUCCESS33005重新连接成功
BIZ_ERROR_CONNECTING33006连接中,再调用 connect 被拒绝
MSG_INSERT_ERROR33008消息插入失败
MSG_DEL_ERROR33009消息删除失败
CONVER_REMOVE_ERROR34001删除会话失败
CONVER_GETLIST_ERROR34002获取历史消息失败
CONVER_SETOP_ERROR34003会话置顶异常
CONVER_TOTAL_UNREAD_ERROR34004获取会话未读消息总数失败
CONVER_TYPE_UNREAD_ERROR34005获取指定会话类型未读消息数异常
CONVER_ID_TYPE_UNREAD_ERROR34006获取指定用户 ID & 会话类型未读消息数异常
CONVER_CLEAR_ERROR34007清除会话失败
CLEAR_HIS_ERROR34010清除历史消息失败
CLEAR_HIS_TYPE_ERROR34008清除指定会话类型历史消息失败
CLEAR_HIS_TIME_ERROR34011清除指定时间段历史消息失败
CONVER_GET_ERROR34009获取会话失败
GROUP_SYNC_ERROR35001群组异常
GROUP_MATCH_ERROR35002匹配群信息异常
CHATROOM_ID_ISNULL36001聊天室 ID 为空
CHARTOOM_JOIN_ERROR36002聊天室加入失败
CHATROOM_HISMESSAGE_ERROR36003获取聊天室历史消息失败
CHATROOM_KV_NOT_FOUND36004聊天室 kv 没有找到
BLACK_ADD_ERROR37001添加黑名单失败
BLACK_GETSTATUS_ERROR37002获得指定人员在黑名单中的状态异常
BLACK_REMOVE_ERROR37003删除黑名单失败
DRAF_GET_ERROR38001获取草稿失败
DRAF_SAVE_ERROR38002保存草稿失败
DRAF_REMOVE_ERROR38003删除草稿失败
SUBSCRIBE_ERROR39001关注公众号失败
COOKIE_ENABLE51001cookie 被禁用
GET_MESSAGE_BY_ID_ERROR61001根据消息 ID 获取消息失败
HAVNODEVICEID24001用户没有登陆
DEVICEIDISHAVE24002用户已经存在
SUCCESS0成功
FEILD24009没有对应的用户或 token
NULLCHANNELNAME24011通道名称为空
INTERNALERRROR24015服务器内部错误
CLOSE_BEFORE_OPEN51001在服务开启之前调用关闭操作
ALREADY_IN_USE51002已经被使用
INVALID_CHANNEL_NAME51003通道名称无效
DELETE_MESSAGE_ID_IS_NULL61001删除消息数组长度为 0
REMOTE_ENGINE_UN_SUPPORTED16对方不支持当前引擎
REMOTE_NETWORK_ERROR17对方网络错误

➣ 本地数据存储

SDK 主要使用两种数据存储方式:

  • 一种用于存取 SDK 初始化后在本次会话中需要的即时数据;
  • 另一种就是持久化本地数据,不过如果 SDK 运行平台不支持持久化存储的话,会降级为 memory 存储;

临时存储区:memoryStore

一些必要的临时数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
token: "", // 用户 token
callback: null, // 用于连接的回调
lastReadTime: new LimitableMap(), // 存储各种会话的上次读取时间
historyMessageLimit: new MemoryCache(), // 存储历史消息的获取时间和是否有更多历史消息
conversationList: [], // 存储会话列表
appKey: appKey, // appKey
publicServiceMap: new PublicServiceMap(), // 公共订阅服务相关存储
providerType: 1, // 本地存储提供者标识
deltaTime: 0, // 服务器时间与本地时间的差值,单位为毫秒
filterMessages: [], // 用于聊天室过滤消息
isSyncRemoteConverList: true, // 是否同步远程会话列表到本地
otherDevice: false, // 其它设备 (暂未)
custStore: {}, // 用于客服相关的数据存储
converStore: {latestMessage: {} }, // 用于会话
connectAckTime: 0, // 用于 queryMessage 中判断是否为离线消息
voipStategy: 0, // Voip 策略相关
isFirstPingMsg: true, // 用于 socket 验活
depend: options, //
notification: {} // 用于获取指定用户和会话类型免提醒
}

本地存储区:storageProvider

1. 存储内容

存储一些必要的持久化数据:比如群消息已读状态回执、历史连接过的 appId、sessionId、导航数据等等。

示例:

全部持久化数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
RC_TMP_KEY: "RC_TMP_VAL",
rong_HptcMH1K7: "1642057220386",
rong_appId: "n19jmcy59f1q9",
rong_current_user: "HptcMH1K7",
rong_fullnavi: [持久化的导航数据],
rong_last_sentTime_HptcMH1K7: "1642057479727",
rong_n19jmcy59f1q9_HptcMH1K7_receivebox: "1642057220386",
rong_n19jmcy59f1q9_HptcMH1K7_sendbox: "1642057479727",
rong_openMp1a85c192: "1",
rong_openMp99b44d42: "1",
rong_rc_uid: "99b44d42",
rong_rongSDK: "websocket",
rong_servers: "[\"ws-uc.ronghub.com:443\",\"wsap-cn.ronghub.com:443\"]",
rong_voipStrategy: "1",
}

持久化的导航数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
activeServer: "stats.cn.ronghub.com",
alone: false,
backupServer: "wsap-cn.ronghub.com:443",
bosAddr: "gz.bcebos.com",
chatroomMsg: true,
code: 200,
compDays: 6,
extkitSwitch: 0,
gifSize: 3072,
grpMsgLimit: 0,
grpRRVer: 0,
historyMsg: true,
isFormatted: 0,
joinMChrm: true,
kvStorage: 1,
location: "{\"configure\":true,\"conversationTypes\":[1],\"maxParticipant\":5,\"refreshInterval\":5}",
logPolicy: "{\"url\":\"logcollection.ronghub.com\",\"level\":4,\"itv\":6,\"times\":5}",
logSwitch: 1,
monitor: 1,
msgAck: null,
offlinelogserver: "http://feedback.cn.ronghub.com",
onlinelogserver: "",
openAnti: 0,
openHttpDNS: 1,
openMp: 1,
openUS: 1,
ossConfig: "[{\"qiniu\":\"upload.qiniup.com\",\"p\":\"1\"}]",
qnAddr: "upload.qiniup.com",
server: "ws-uc.ronghub.com:443",
ugMsg: 0,
uploadServer: "upload.qiniup.com",
userId: "HptcMH1K7",
videoTimes: 120,
voipCallInfo: "{\"strategy\":1,\"callEngine\":[{\"engineType\":4,\"mediaServer\":\"https://rtc-info.ronghub.com\",\"maxStreamCount\":30,\"wwise\":1}]}",
voipServer: "",
}
2. 存储方式
  • LocalStorage 存储容器 (优先):现代浏览器支持的本地数据化持久方式,能够方便的对浏览器同一个域 (IP / 域名) 下的本地数据进行操作。对比 Cookie 其提供的存储空间更大,可以达到数 MB 的大小,而且不会自动过期。

  • UserData 存储容器 (兼容):主要用于 IE 浏览器的兼容,它将某个 dom 节点作为存储容器,支持类似 localStorage 数据存取方式,但是跟 localStorage 不同的是它不是持久化存储。

  • Memory 存储容器 (兜底):作为一种兜底的存储方式,主要用于内存中直接存储数据,不具有持久化特性,页面刷新后所有数据都会丢失。

七、SDK 接口详细设计

➣ SDK 引入

1
2
3
4
var RongIMLib = require('../../static/js/RongIMLib-2.5.6.js')  // RongIMLib 相对路径
var Protobuf = require('../../static/js/protobuf-2.3.7.min.js') // protobuf 相对路径 [外部库,用于处理 Message]
var RongIMClient = RongIMLib.RongIMClient

➣ 初始化

1
2
3
4
var appkey = 'kj29chm026yyn'
RongIMClient.init('appkey', null, {
protobuf: Protobuf
})

➣ 连接和监听

1. 状态监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
RongIMClient.setConnectionStatusListener({
onChanged: function (status) {
// status 标识当前连接状态
switch (status) {
case RongIMLib.ConnectionStatus.CONNECTED:
console.log('链接成功');
break;
case RongIMLib.ConnectionStatus.CONNECTING:
console.log('正在链接');
break;
case RongIMLib.ConnectionStatus.DISCONNECTED:
console.log('断开连接');
break;
case RongIMLib.ConnectionStatus.KICKED_OFFLINE_BY_OTHER_CLIENT:
console.log('其他设备登录, 本端被踢');
break;
case RongIMLib.ConnectionStatus.DOMAIN_INCORRECT:
console.log('域名不正确, 请至开发者后台查看安全域名配置');
break;
case RongIMLib.ConnectionStatus.NETWORK_UNAVAILABLE:
console.log('网络不可用, 此时可调用 reconnect 进行重连');
break;
default:
console.log('链接状态为', status);
break;
}
}
});
状态说明枚举值
RongIMLib.ConnectionStatus.CONNECTED连接成功0
RongIMLib.ConnectionStatus.CONNECTING连接中1
RongIMLib.ConnectionStatus.DISCONNECTED链接已断开2
RongIMLib.ConnectionStatus.NETWORK_UNAVAILABLE网络错误3
RongIMLib.ConnectionStatus.CONNECTION_CLOSED链接已关闭4
RongIMLib.ConnectionStatus.KICKED_OFFLINE_BY_OTHER_CLIENT其他设备登录 (被踢)6
RongIMLib.ConnectionStatus.WEBSOCKET_UNAVAILABLE WebSocket不可用7
RongIMLib.ConnectionStatus.DOMAIN_INCORRECT域名错误 (需通过开发者后台检查安全域名设置)12
RongIMLib.ConnectionStatus.REQUEST_NAVI正在请求导航201
RongIMLib.ConnectionStatus.RESPONSE_NAVI请求导航成功202
RongIMLib.ConnectionStatus.RESPONSE_NAVI_ERROR请求导航失败203
RongIMLib.ConnectionStatus.RESPONSE_NAVI_TIMEOUT请求导航超时204

2. 消息监听器

收到消息时, 将触发消息监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
RongIMClient.setOnReceiveMessageListener({
// 接收到的消息
onReceived: function (message) {
var messageContent = message.content;
// 判断消息类型
switch(message.messageType) {
case RongIMClient.MessageType.TextMessage: // 文字消息
console.log('文字内容', messageContent.content);
break;
case RongIMClient.MessageType.ImageMessage: // 图片消息
console.log('图片缩略图 base64', messageContent.content);
console.log('原图 url', messageContent.imageUri);
break;
case RongIMClient.MessageType.HQVoiceMessage: // 音频消息
console.log('音频 type', messageContent.type); // 编解码类型,默认为 aac 音频
console.log('音频 url', messageContent.remoteUrl); // 播放:<audio src={remoteUrl} />
console.log('音频 时长', messageContent.duration);
break;
case RongIMClient.MessageType.RichContentMessage: // 富文本 (图文) 消息
console.log('文本内容', messageContent.content);
console.log('图片 base64', messageContent.imageUri);
console.log('原图 url', messageContent.url);
break;
case RongIMClient.MessageType.UnknownMessage: // 未知消息
console.log('未知消息, 请检查消息自定义格式是否正确', message);
break;
default:
console.log('收到消息', message);
break;
}
}
});
消息类型ObjectName类名是否计数是否存储
文字消息RC:TxtMsgTextMessage
语音消息RC:VcMsgVoiceMessage
高质量语音消息RC:HQVCMsgHQVoiceMessage
图片消息RC:ImgMsgImageMessage
GIF 图片消息RC:GIFMsgGIFMessage
图文消息RC:ImgTextMsgRichContentMessage
文件消息RC:FileMsgFileMessage
位置消息RC:LBSMsgLocationMessage
小视频消息RC:SightMsgSightMessage
公众服务单图文消息RC:PSImgTxtMsgPublicServiceRichContentMessage
公众服务多图文消息RC:PSMultiImgTxtMsgPublicServiceMultiRichContentMessage
消息状态ObjectName
已读READ0x1
已听LISTENED0x2
已下载DOWNLOADED0x2
未读UNREAD0
消息查询类型ObjectName
私聊qryPMsg1
讨论组qryDMsg2
群组qryGMsg3
聊天室qryCMsg4
查询离线消息pullMsg5
系统qrySMsg6
用户信息userInfo14
列举会话qryCon26
消息查询类型(专用于PublishMessage)
系统通知消息s_ntf
聊天消息s_msg
用户状态更新消息s_stat
聊天室消息s_cmd
群组消息pp
群组消息pd
群组消息ch
群组消息pc

3. 连接服务

注意:

  • 连接方法必须在执行 初始化 之后调用
  • 连接方法必须在设置 状态监听器、 消息监听器 之后调用
  • 除初始化、监听以外, 所有方法都必须在 connect 成功之后 再调用
  • 默认一个用户只支持一个页面连接, 开通 “多设备消息同步” 即可支持多页面连接
参数类型必填说明最低版本
tokenString用户的唯一标识2.0.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var token = "mKmyKqTSf7aNDinwAFMnz7NXKI3dV3X0+Cd1BOxmtO2pmvsjW2HViWrePIfq0GuTu9jELQqsckv4AhfjCAKgQ==";

RongIMClient.connect(token, {
onSuccess: function(userId) {
console.log('连接成功, 用户 id 为', userId);
// 连接已成功, 此时可通过 getConversationList 获取会话列表并展示
},
onTokenIncorrect: function() {
console.log('连接失败, 失败原因: token 无效');
},
onError: function(errorCode) {
var info = '';
switch (errorCode) {
case RongIMLib.ErrorCode.TIMEOUT:
info = '链接超时';
break;
case RongIMLib.ConnectionState.UNACCEPTABLE_PAROTOCOL_VERSION:
info = '不可接受的协议版本';
break;
case RongIMLib.ConnectionState.IDENTIFIER_REJECTED:
info = 'appkey 不正确';
break;
case RongIMLib.ConnectionState.SERVER_UNAVAILABLE:
info = '服务器不可用';
break;
default:
info = errorCode;
break;
}
console.log('连接失败, 失败原因:', info);
}
});

3. 断开链接

1
RongIMClient.getInstance().disconnect();

4. 重新连接

注意:

  • 不传 config 参数, 则为直接重连, 此时 reconnect 方法必须在网络正常的情况下调用
  • 传入 config 参数, SDK 内部自动做网络嗅探处理, 当检测到网络正常时, 进行重连

config 参数说明:

参数类型必填说明最低版本
autoBoolean是否自动重连, 默认 false2.3.3
urlString用于网络嗅探的地址, auto 为 true 时, 此参数必填2.3.3
rateArray网络嗅探频率, 单位为毫秒, auto 为 true 时, 此参数必填2.3.3

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var callback = {
onSuccess: function(userId) {
console.log('reconnect success.' + userId);
},
onTokenIncorrect: function() {
console.log('token 无效');
},
onError: function(errorCode) {
console.log(errorcode);
}
};
var config = {
auto: true,
url: 'cdn.ronghub.com/RongIMLib-2.2.6.min.js?d=' + Date.now(),
rate: [100, 1000, 3000, 6000, 10000]
};
RongIMClient.reconnect(callback, config);

5. 获取用户 id

1
RongIMClient.getInstance().getCurrentUserId();

6. 获取连接状态

1
RongIMClient.getInstance().getCurrentConnectionStatus();

➣ 会话

1. 会话数据结构

会话列表由多个会话组成, 单个会话的数据结构如下:

字段名类型说明
conversationTypeNumber会话类型
targetIdString目标
latestMessageIdString会话中最后一条消息 Id
objectNameString会话中最后一条消息的消息标识, SDK 内置消息以 “RC:” 开头
unreadMessageCountNumber当前会话的未读消息数
latestMessageObject会话中最后一条消息, 消息结构详见消息数据结构
sentStatusNumber会话中最后一条消息发送状态
sentTimeNumber会话中最后一条消息服务端的发送时间

会话类型说明:

会话类型说明枚举值
RongIMLib.ConversationType.PRIVATE单聊1
RongIMLib.ConversationType.DISCUSSION讨论组2
RongIMLib.ConversationType.GROUP群聊3
RongIMLib.ConversationType.CHATROOM聊天室4
RongIMLib.ConversationType.CUSTOMER_SERVICE客服5
RongIMLib.ConversationType.SYSTEM系统6
RongIMLib.ConversationType.APP_PUBLIC_SERVICE公众账号 (默认关注)7
RongIMLib.ConversationType.PUBLIC_SERVICE公众账号 (手动关注)7

2. 获取会话列表

参数说明:

参数类型必填说明最低版本
callbackObject回调对象2.2.0
callback.onSuccessFunction成功回调2.2.0
callback.onErrorFunction失败回调2.2.0
conversationTypesArray获取的会话类型, 获取所有会话类型传 null2.3.3
countNumber获取会话数量2.3.3

示例代码:

1
2
3
4
5
6
7
8
9
10
var conversationTypes = [RongIMLib.ConversationType.PRIVATE];
var count = 150;
RongIMClient.getInstance().getConversationList({
onSuccess: function(list) {
console.log('获取会话列表成功', list);
},
onError: function(error) {
console.log('获取会话列表失败', error);
}
}, conversationTypes, count);

3. 获取会话详情

参数说明:

参数类型必填说明最低版本
conversationTypeNumber会话类型2.2.0
targetIdString目标 id2.2.0
callbackObject回调对象2.2.0

示例代码:

1
2
3
4
5
6
7
8
9
var conversationType = RongIMLib.ConversationType.PRIVATE;
var targetId = 'user1';
RongIMClient.getInstance().getConversation(conversationType, targetId, {
onSuccess: function(conversation) {
if (conversation) {
console.log('获取指定会话成功', conversation);
}
}
});

4. 删除会话

参数说明:

参数类型必填说明最低版本
conversationTypeNumber会话类型2.2.0
targetIdString目标 id2.2.0
callbackObject回调对象2.2.0
1
2
3
4
5
6
7
8
9
10
var conversationType = RongIMLib.ConversationType.PRIVATE;
var targetId = 'user1';
RongIMClient.getInstance().removeConversation(conversationType, targetId, {
onSuccess: function(bool) {
console.log('删除指定会话成功');
},
onError: function(error) {
console.log('删除指定会话失败', error);
}
});

5. 清除会话列表

参数说明:

参数类型必填说明最低版本
callbackObject回调对象2.3.2
conversationTypesArray清除的会话类型, 不填则清除所有会话2.3.2
1
2
3
4
5
6
7
8
9
var conversationTypes = [RongIMLib.ConversationType.PRIVATE, RongIMLib.ConversationType.GROUP];
RongIMClient.getInstance().clearConversations({
onSuccess: function() {
console.log('清除会话成功');
},
onError: function(error) {
console.log('清除会话失败', error);
}
}, conversationTypes);

➣ 消息

1. 消息发送

注意事项:

  • 发送消息必须在成功连接服务器 connect 成功之后进行
  • 每秒最多发送 5 条消息
  • 群定向消息不存储在历史消息中

参数说明:

参数类型必填说明最低版本
conversationTypeNumber会话类型2.2.0
targetIdString目标 id2.2.0
msgObject消息2.2.0
callbackObject回调对象2.2.0
isMentionedBoolean是否为 @ 消息2.2.0
pushContentStringPush 显示内容2.2.0
pushDataStringPush 通知时附加信息2.2.0
methodTypeString已废弃2.2.0
configObject其他设置项2.5.3

config 说明:

参数类型必填说明最低版本
userIdsArray接收定向消息的用户 id2.2.0
isStatusBoolean是否发送状态消息2.5.6

发送消息示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
var isMentioned = true;  // @ 消息
var mentioneds = new RongIMLib.MentionedInfo(); // @ 消息对象
mentioneds.type = RongIMLib.MentionedType.PART;
mentioneds.userIdList = ['user1', 'user2']; // @ 人员列表

var msg = new RongIMLib.TextMessage({content: 'hello RongCloud!', extra: '附加信息', mentionedInfo: mentioneds});
var conversationType = RongIMLib.ConversationType.GROUP; // 群聊, 其他会话选择相应的消息类型即可
var targetId = 'group1'; // 目标 Id

var pushContent = 'user 发送了一条消息'; // Push 显示内容
var pushData = null; // Push 通知时附加信息, 可不填

var config = {
userIds: ['user1', 'user2'], // 群定向消息, 仅发消息给群中的 user1 和 user2
isVoipPush: true // 发送 voip push
};

RongIMClient.getInstance().sendMessage(conversationType, targetId, msg, {
onSuccess: function (message) {
// message 为发送的消息对象并且包含服务器返回的消息唯一 id 和发送消息时间戳
console.log('发送文本消息成功', message);
},
onError: function (errorCode) {
console.log('发送文本消息失败', errorCode);
}
}, isMentioned, pushContent, pushData, null, config);

1)文本消息

1
2
3
4
5
6
7
8
9
10
11
12
13
var msg = new RongIMLib.TextMessage({content: 'hello RongCloud!', extra: '附加信息'});
var conversationType = RongIMLib.ConversationType.PRIVATE;
var targetId = 'user1'; // 目标 Id

RongIMClient.getInstance().sendMessage(conversationType, targetId, msg, {
onSuccess: function (message) {
// message 为发送的消息对象并且包含服务器返回的消息唯一 id 和发送消息时间戳
console.log('发送文本消息成功', message);
},
onError: function (errorCode) {
console.log('发送文本消息失败', errorCode);
}
});

2)发送撤回消息

1
2
3
4
5
6
7
8
9
// recallMessage 为需要撤回的消息对象
RongIMClient.getInstance().sendRecallMessage(recallMessage, {
onSuccess: function (message) {
console.log('撤回成功', message);
},
onError: function (errorCode) {
console.log('撤回失败', errorCode);
}
});

3)发送已读通知消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 以下 3 个属性在会话的最后一条消息中可以获得 */
var messageUId = '1301-NBJQ-MK31-3417'; // 消息唯一 Id, message 中的 messageUid
var lastMessageSendTime = 1550719033312; // 最后一条消息的发送时间
var type = '1'; // 备用,默认赋值 1 即可

var msg = new RongIMLib.ReadReceiptMessage({messageUId: messageUId, lastMessageSendTime: lastMessageSendTime, type: type});
var conversationType = RongIMLib.ConversationType.PRIVATE;
var targetId = 'user1'; // 目标 Id

RongIMClient.getInstance().sendMessage(conversationType, targetId, msg, {
onSuccess: function (message) {
console.log('发送已读通知成功', message);
},
onError: function (errorCode) {
console.log('发送已读通知失败', errorCode);
}
});

2. 未读消息

注意事项:

  • 会话未读数指某一个会话中未读消息的数量
  • 未读消息数也可通过 conversation.unreadMessageCount 获取
  • 会话消息未读数存储在 WebStorage 中, 若浏览器不支持或禁用 WebStorage,未读消息数将不会保存,浏览器页面刷新未读消息数将不会存在

1)清除指定会话未读数:

参数说明

参数类型必填说明最低版本
conversationTypeNumber会话类型2.2.0
targetIdString目标 id2.2.0
callbackObject回调对象2.2.0

代码示例

1
2
3
4
5
6
7
8
9
10
var conversationType = RongIMLib.ConversationType.PRIVATE;
var targetId = 'user1';
RongIMClient.getInstance().clearUnreadCount(conversationType, targetId, {
onSuccess: function() {
console.log('清除指定会话未读消息数成功');
},
onError: function(error) {
console.log('清除指定会话未读消息数失败', error);
}
});

2)获取所有会话未读数

代码示例:

1
2
3
4
5
6
7
8
RongIMClient.getInstance().getTotalUnreadCount({
onSuccess: function(count) {
console.log('获取所有会话未读消息数成功', count);
},
onError: function(error) {
console.log('获取所有会话未读消息数失败', error);
}
});)

3)获取指定会话未读数

参数说明:

参数类型必填说明最低版本
conversationTypeNumber会话类型2.2.0
targetIdString目标 id2.2.0
callbackObject回调对象2.2.0

代码示例:

1
2
3
4
5
6
7
8
9
10
var conversationType = RongIMLib.ConversationType.PRIVATE;
var targetId = 'user1';
RongIMLib.RongIMClient.getInstance().getUnreadCount(conversationType, targetId, {
onSuccess: function(count) {
console.log('获取指定会话未读消息数成功', count);
},
onError: function(error) {
console.log('获取指定会话未读消息数失败', error);
}
});

4)获取指定会话类型总未读数

参数说明:

参数类型必填说明最低版本
conversationTypesArray指定会话类型2.2.0
callbackObject回调对象2.2.0

代码示例:

1
2
3
4
5
6
7
8
9
var conversationTypes = [RongIMLib.ConversationType.PRIVATE, RongIMLib.ConversationType.DISCUSSION];
RongIMClient.getInstance().getConversationUnreadCount(conversationTypes, {
onSuccess: function(count) {
console.log('获取指定会话类型总未读消息数成功', count);
},
onError: function(error) {
console.log('获取指定会话类型总未读消息数失败', error);
}
});

3. 历史消息

每个会话都存在历史消息,通常我们使用 SDK 连接服务器时,SDK 只会向用户推送最新的消息,历史消息需要开发者自己调用接口按照规则获取。

1)拉取历史消息

参数说明:

参数类型必填说明最低版本
conversationTypeConversationType指定会话类型2.2.0
targetIdstring目标ID2.2.0
dateTimeDate时间戳2.2.0
callbackObject回调对象2.2.0

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var params = {
conversationType: RongIMLib.ConversationType.PRIVATE, // 会话类型
targetId: 'dPd90Fkja', // 目标 Id
timestamp: 1513308018122 // 时间戳
};
RongIMLib.RongIMClient.getInstance().getRemoteHistoryMessages(params, {
onSuccess: function() {
// 拉取成功
},
onError: function(error) {
// 拉取失败
console.log(error);
}
});

2)清除历史消息

参数说明:

参数类型必填说明最低版本
conversationTypeConversationType指定会话类型2.2.0
targetIdstring目标ID2.2.0
dateTimeDate时间戳2.2.0
callbackObject回调对象2.2.0

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
conversationType: 会话类型
targetId: 目标 Id

timestamp: 清除时间点,message.sentTime <= timestamp 的消息将被清除 (message: 收发实时或者历史消息中有 sentTime 属性)
timestamp 取值范围: timestamp >=0 并且 timestamp <= 当前会话最后一条消息的 sentTime
*/
var params = {
conversationType: RongIMLib.ConversationType.PRIVATE, // 会话类型
targetId: 'dPd90Fkja', // 目标 Id
timestamp: 1513308018122 // 清除时间点
};
RongIMLib.RongIMClient.getInstance().clearRemoteHistoryMessages(params, {
onSuccess: function() {
// 清除成功
},
onError: function(error) {
// 请排查:单群聊消息云存储是否开通
console.log(error);
}
});

➣ 用户信息相关操作

说明:对于 interface over http 标识的接口,SDK 会向应用服务器发送请求,无任何标识的接口,会向消息服务器发送请求。

1. 用户登录

interface over http

开发者可以调用此方法登录一个账户,需要输入邮箱 / 手机号和密码进行登录验证。

SDK 会向应用服务器发送 HTTP 请求验证用户账户和密码,如果验证通过则会返回用户基本身份信息。

参数 - params:

参数类型必填说明最低版本
accountString用户邮箱或手机号2.5.6
passwordString密码2.5.6

响应数据 - user:

参数类型存在说明最低版本
accountString用户邮箱或手机号2.5.6
tokenString用户通信凭证2.5.6
userIdString用户 ID2.5.6

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
RongIMClient.getInstance().login(params, {
onSuccess: function (user) {
console.log('获取用户信息成功', user);
/* {
"id" : "user1",
"name" : "张三",
"portrait" : "https://cdn.ronghub.com/thinking-face.png"
} */
},
onError: function (errorCode) {
console.log('获取用户信息失败', errorCode);
}
});

2. 用户信息获取 - 通过 Socket 消息体

发消息时可以携带用户信息,以供接收方读取:

  • 1)获取当前用户 (也就是发送者) 的用户信息

  • 2)发消息时携带当前用户信息

  • 3)展示消息时, 通过消息体内的用户信息进行展示

以文本消息为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var msg = new RongIMLib.TextMessage({
content: 'hello RongCloud!',
user : {// 当前用户 (发送者) 信息
"id" : "user1",
"name" : "张三",
"portrait" : "https://cdn.ronghub.com/thinking-face.png"
},
});
var conversationType = RongIMLib.ConversationType.PRIVATE;
var targetId = 'user2'; // 目标 Id

RongIMClient.getInstance().sendMessage(conversationType, targetId, msg, {
onSuccess: function (message) {
console.log('发送消息成功, 用户信息为:', message.content.user);
/* {
"id" : "user1",
"name" : "张三",
"portrait" : "https://cdn.ronghub.com/thinking-face.png"
} */
},
onError: function (errorCode) {
console.log('发送消息失败', errorCode);
}
});

3. 用户信息获取 - 通过 HTTP 请求

interface over http

开发者可以调用此方法查询 userId 对应的用户信息,SDK 会向应用服务器发送 HTTP 请求获取用户信息:

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
RongIMClient.getInstance().getUserInfo(userId, {
onSuccess: function (user) {
console.log('获取用户信息成功', user);
/* {
"id" : "user1",
"name" : "张三",
"portrait" : "https://cdn.ronghub.com/thinking-face.png"
} */
},
onError: function (errorCode) {
console.log('获取用户信息失败', errorCode);
}
});

4. 用户修改资料

interface over http

开发者可以调用此方法修改账户信息,目前可修改性别、昵称、邮箱、手机号、密码。

SDK 会向应用服务器发送 HTTP 执行资料更改操作。

参数 - params:

参数类型必填说明最低版本
userIdString用户 Id2.5.6
accountString可选账户2.5.6
passwordString可选密码2.5.6
sexString可选性别2.5.6
nameString可选昵称2.5.6

响应数据 - user:

参数类型存在说明最低版本
userIdString用户 Id2.5.6
accountString账户2.5.6
passwordString密码2.5.6
sexString性别2.5.6
nameString昵称2.5.6

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
RongIMClient.getInstance().login(params, {
onSuccess: function (user) {
console.log('获取用户信息成功', user);
/* {
"id" : "user1",
"name" : "张三",
"portrait" : "https://cdn.ronghub.com/thinking-face.png"
} */
},
onError: function (errorCode) {
console.log('获取用户信息失败', errorCode);
}
});

5. 退出登录

interface over http

退出登录会清除 Session 和其它数据处理操作。

1
RongIMClient.getInstance().logout();

6. 注销账户

interface over http

注销账户会删除账户数据。

1
RongIMClient.getInstance().revoked();
[ loading ]⇷⇷