# 连接管理基础 (/zh-CN/realtime-media/rtm/build/authentication-and-connection/link-basic)

> For AI agents: see the complete documentation index at [llms.txt](/llms.txt).

<_PlatformTabsGroup>
<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="android" />
客户端需要首先与 RTM 服务器建立连接，才能在后续访问 RTM 网络资源，SDK 的 API 调用以及频道中的消息和事件都通过此网络连接进行传输。虽然 RTM SDK 会帮助你建立和维护网络连接，从而极大地简化监控网络状态的业务逻辑，但充分理解 RTM 的网络连接管理机制，仍有助于你在 App 中处理可能出现的网络错误，提升用户体验。

:::info[信息]
本文仅适用于 2.2.0 及之后版本。
:::

## 创建连接 [#创建连接]

当客户端创建一个 RTM 实例或 Stream Channel 实例时，实际上客户端还没有真正地与服务器建立网络连接。你只是拥有了一个对象，具备在此对象中调用 API 的能力。只有当你主动调用 `login` 方法或 `join` 方法时，客户端才开始尝试与服务器建立网络连接。

RTM SDK 在很大程度上屏蔽了直接处理网络连接的种种细节，提供 `onLinkStateEvent` 事件通知向用户透传各种连接状态的迁移，你只需监听此事件即可从各种连接错误中恢复。

## 连接类型 [#连接类型]

在进一步探索之前，你需要了解 RTM 客户端的两种服务类型：`MESSAGE` 服务和 `STREAM` 服务。一个客户端中可能会用到一种或两种服务，这两种服务对应的连接类型有所不同。

### MESSAGE 类型 [#message-类型]

`MESSAGE` 服务为用户提供 Message Channel、User Channel、Presence、Storage、Lock、Token 等服务，这些服务的消息、数据、事件通知等共享此连接。这条连接在客户端生命周期内唯一存在，在你主动调用 `login` 方法登录服务器时建立，在你主动调用 `logout` 方法登出服务器时结束。

你可以通过以下方式建立 `MESSAGE` 连接：

```java
rtmClient.login(token, new ResultCallback<Void>() {
    @Override
    public void onSuccess(Void responseInfo) {
    }
    @Override
    public void onFailure(ErrorInfo errorInfo) {
    }
});
```

:::info[信息]
建立 `MESSAGE` 连接会增加峰值连接数计量。
:::

`MESSAGE` 连接是长连接，客户端会定时向服务器发送心跳包以保持活跃状态。当你订阅多个 Message Channel 或 User Channel 时，你将通过此连接接受所有频道消息或事件通知。

### STREAM 类型 [#stream-类型]

`STREAM` 服务为用户提供 Stream Channel 的所有能力。与 `MESSAGE` 连接不同，`STREAM` 连接并不具备全局唯一性，而是每个 Stream Channel 实例都会单独创建一个连接。例如，当你创建 3 个 Stream Channel 实例并调用 `join` 方法加入后，客户端会与服务器建立 3 条网络连接。

在建立 `STREAM` 连接前，你需要先建立 `MESSAGE` 连接，即调用 `login` 方法并成功登录服务器。`STREAM` 连接的生命周期从你主动调用 `join` 方法加入频道时开始，到你主动调用 `leave` 方法离开频道时结束。你可以通过以下方式建立 `STREAM` 连接：

```java
JoinChannelOptions options = new JoinChannelOptions();
options.setToken("yourToken")
options.setWithPresence(true);
options.setWithMetadata(false);
options.setWithLock(false);

streamChannel.join(options, new ResultCallback<Void>() {
    @Override
    public void onSuccess(Void responseInfo) {
        log(CALLBACK, "join stream channel success");
    }

    @Override
    public void onFailure(ErrorInfo errorInfo) {
        log(ERROR, errorInfo.toString());
    }
});
```

:::info[信息]
建立 `STREAM` 连接不会增加峰值连接数计量。
:::

`STREAM` 连接也是长连接，客户端会定时向服务器发送心跳包以保持活跃状态，发送心跳包的时间间隔是 0.5 秒且不可更改。对于需要优化电池寿命的应用场景，不建议使用 `STREAM` 服务。

## 监控连接 [#监控连接]

客户端通过 `onLinkStateEvent` 事件通知监控和管理网络连接状态的迁移。通过监听此事件，你可以随时知道以下信息：

- 当前网络连接处于何种状态。
- 何种原因或操作导致状态迁移。
- 迁移之前的网络状态是什么。
- 网络连接状态何时发生迁移。
- 什么服务类型的连接状态发生了迁移。
- 网络状态迁移影响了哪些已加入的频道。
- 哪些之前加入的频道没有成功恢复网络连接。

以上信息对于客户端从意外断开状态恢复到连接状态后的业务处理将非常有帮助，用户能更快速地定位和排除由于网络连接变化产生的故障及恢复业务现场。

你可以通过以下方式监听 `onLinkStateEvent` 事件通知：

```java
RtmEventListener listener = new RtmEventListener() {
    @Override
    public void onLinkStateEvent(LinkStateEvent event) {
    }
};

rtmClient.addEventListener(listener);
```

其中各字段的含义如下：

| 属性        |   描述  |
|---|-------- |
| `currentState`            |  当前的连接状态。   |
| `previousState`            |  之前的连接状态。   |
| `serviceType`    |  服务类型。可能是 `MESSAGE` 或 `STREAM`。 |
| `operation`    | 触发本次状态迁移的操作。 |
| `reasonCode`    | 触发本次状态迁移的原因。 |
| `affectedChannels`    |  本次状态迁移影响的频道。 |
| `unrestoredChannels`    |  未恢复订阅或加入的频道信息，包含频道名、频道类型和频道中的临时状态数据。一般情况下为空。 |
| `isResumed`    |  在连接断开的 2 分钟内，是否从 `DISCONNECTED` 状态恢复成 `CONNECTED` 状态。`true` 表示已恢复。 |
| `timestamp`    |  本次事件通知发送的时间戳。 |

## 心跳探测 [#心跳探测]

心跳探测能帮助 RTM 客户端和服务器及时发现突然的网络断连，例如客户端网络掉线、网络切换等。

### MESSAGE 类型 [#message-类型-1]

默认情况下，RTM 客户端会每隔 5 秒向服务器发送一次心跳探测包（Ping 包）。如果在 10 秒内没有收到服务器的回复（Pong 包），客户端则会认为网络连接出现问题。此时，客户端的网络连接状态（`RtmLinkState`）会由 `CONNECTED` 状态切换到 `DISCONNECTED` 状态，并开始尝试重新连接。

同理，服务器如果在 15 秒内未能收到来自客户端的 Ping 包，则会认为客户端出现了网络连接问题。此时，服务器会等待一段时间（`presenceTimeout`），如果这段时间内客户端未能重新与服务器建立连接，则 RTM 服务器会向此客户端所在频道的其他用户发送 `REMOTE_TIMEOUT` 事件通知。

需要注意的是，以上机制只适用于客户端与服务器意外断开连接的情况，如果你是主动断开连接，例如通过调用 `logout` 方法主动登出时，则不会遵循上述机制。

RTM SDK 默认的心跳探测时间间隔为 5 秒，这是平衡了设备续航能力和意外断连识别及时性的结果。如果你的 App 需要增加电池续航或者需要更及时地识别网络断连情况，那么你可以通过 `RtmConfig` 中的 `heartbeatInterval` 参数自定义心跳探测时间间隔，支持的取值为 [5,1800] 之间的任意整数值。

设置更高的 `heartbeatInterval` 值可能会降低客户端和服务器对于意外断连识别的及时性，峰值连接数（PCU）也可能显得更高。虽然 `heartbeatInterval` 最高可以设置成 1800 秒，但是我们不建议这么做。

你可以通过以下方式自定义心跳探测时间间隔，例如修改为 30 秒：

```java
RtmConfig rtmConfig = new RtmConfig.Builder("appid", "userId")
    .heartbeatInterval(30)
    .build();
```

### STREAM 类型 [#stream-类型-1]

当你创建一个 Stream Channel 实例并调用 `join` 方法成功加入频道时，客户端会按 0.5 秒的间隔向服务器发送心跳包以保持活跃状态。`STREAM` 连接的心跳时间间隔不可更改，对于需要增加电池续航的应用场景，不建议使用 `STREAM` 服务。

<a id="heartbeatinterval-and-presencetimeout"></a>
## Heartbeat Interval 与 Presence Timeout [#heartbeat-interval-与-presence-timeout]

`RtmConfig` 中包含两个与时间间隔相关的参数，分别为 `heartbeatInterval` 和 `presenceTimeout`。这两个参数常常会让人感到困惑，本节主要介绍它们的含义，以便你更有效地配置 App。

- `heartbeatInterval`：客户端会定期向服务器发送证明自身处于活跃状态的 Ping-Pong 数据包，以确保服务器能够及时清除超时计数并维持连接状态。该设置会影响 PCU 计数，是 SDK 全局连接管理功能中的一个重要特性。
- `presenceTimeout`：如果在设置的 `heartbeatInterval` 时间加 10 秒冗余时间内未收到客户端的 Ping 包，服务器则会认为该客户端可能遇到了网络连接故障。考虑到此时客户端可能在尝试修复问题，为避免重复通知事件，服务器会延迟一段时间（即 `presenceTimeout` 时间）再向频道中的其他用户广播事件通知。
  - 如果客户端在 `presenceTimeout` 设置的时间内与服务器重新建立连接，则服务器不会向频道中的其他用户发送事件通知，并且用户设置的 Presence 临时状态也会被保留。
  - 如果客户端在 `presenceTimeout` 设置的时间内未能重新连接，则服务器会向频道中的其他用户广播 `REMOTE_TIMEOUT` 类型的 `onPresenceEvent` 事件通知，该客户端的临时用户状态也会被清除。当客户端重新建立连接并恢复对频道的订阅关系时，服务器也会向频道中的其他用户广播 `REMOTE_JOIN` 事件通知。如果你之前设置过临时用户状态，则客户端会重新应用本地会话缓存的临时用户状态，在频道中的其他用户也会收到对应的 `REMOTE_STATE_CHANGED` 事件通知。

对于本地用户而言，由 Presence Timeout 触发的离开与调用 `unsubscribe` 方法造成的离开都被视为频道离开行为，不同之处在于 Presence Timeout 的离开通常是由网络连接故障引起的。本地用户可能处于 `DISCONNECTED` 或 `SUSPENDED` 状态，并尝试重新连接。如果重新连接成功，频道订阅关系和临时用户状态都会被自动恢复，对于频道中的其他用户来说，该用户为重新加入的用户。调用 `unsubscribe` 被视为用户主动离开频道，客户端会清除此频道的订阅关系和临时用户状态。

例如，客户端 A 在初始化实例时设置 `heartbeatInterval` 为 10 秒、`presenceTimeout` 为 5 秒。如果 A 发出了 Ping 包，但因为网络等原因在 10 秒内没有收到 Pong 包，则 A 的连接状态会变为 `DISCONNECTED` 类型。同时，A 会尝试重新连接，服务器最多等待 25 秒。25 秒后，如果 A 未能重新连接，那么服务器会代替 A 向频道中其他用户广播 `REMOTE_TIMEOUT` 类型的 `onPresenceEvent` 事件通知；如果 A 在 25 秒内成功恢复连接，那么服务器不会广播事件通知，频道中其他用户会认为 A 一直都在频道中未曾离开。在这个例子中，从 A 掉线到其他用户被通知先后经历了 10 秒的 `heartbeatInterval`、10 秒的误差范围（以允许网络延迟）、5 秒的 `presenceTimeout`，即最长的时间间隔为 25 秒。

设定 `presenceTimeout` 时间的一个重要理由是，客户端经常遇到断开网络连接又快速恢复的情况，例如网络切换、行驶经过隧道等。如果不允许客户端尝试重新连接就立即在频道内广播用户离开或加入的情况，则会造成过多的 Presence 事件通知，极大地降低用户体验。

## 关闭连接 [#关闭连接]

当客户端和服务器之间不再需要连接时，你可以主动调用方法关闭连接。

### MESSAGE 类型 [#message-类型-2]

对于 `MESSAGE` 类型服务，连接状态会影响峰值连接数的计量，推荐你调用 `logout` 方法显式地登出服务器，从而主动断开连接。

```java
rtmClient.logout(new ResultCallback<Void>() {
    @Override
    public void onSuccess(Void responseInfo) {
    }

    @Override
    public void onFailure(ErrorInfo errorInfo) {
    }
});
```

:::info[信息]
一旦你主动调用 `logout` 方法断开 `MESSAGE` 连接，`STREAM` 连接也会同时断开，你将离开所有频道，客户端中所有缓存数据都会被清除。
:::

### STREAM 类型 [#stream-类型-2]

当你不再需要 `STREAM` 类型服务时，可以调用 `leave` 方法退出频道，从而断开 `STREAM` 连接。

```java
streamChannel.leave(new ResultCallback<Void>() {
    @Override
    public void onSuccess(Void responseInfo) {
        log(CALLBACK, "leave stream channel success");
    }

    @Override
    public void onFailure(ErrorInfo errorInfo) {
        log(ERROR, errorInfo.toString());
    }
});
```
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="ios" />
客户端需要首先与 RTM 服务器建立连接，才能在后续访问 RTM 网络资源，SDK 的 API 调用以及频道中的消息和事件都通过此网络连接进行传输。虽然 RTM SDK 会帮助你建立和维护网络连接，从而极大地简化监控网络状态的业务逻辑，但充分理解 RTM 的网络连接管理机制，仍有助于你在 App 中处理可能出现的网络错误，提升用户体验。

:::info[信息]
本文仅适用于 2.2.0 及之后版本。
:::

## 创建连接 [#创建连接-1]

当客户端创建一个 RTM 实例或 Stream Channel 实例时，实际上客户端还没有真正地与服务器建立网络连接。你只是拥有了一个对象，具备在此对象中调用 API 的能力。只有当你主动调用 `loginByToken` 方法或 `joinWithOption` 方法时，客户端才开始尝试与服务器建立网络连接。

RTM SDK 在很大程度上屏蔽了直接处理网络连接的种种细节，提供 `didReceiveLinkStateEvent` 事件通知向用户透传各种连接状态的迁移，你只需监听此事件即可从各种连接错误中恢复。

## 连接类型 [#连接类型-1]

在进一步探索之前，你需要了解 RTM 客户端的两种服务类型：`AgoraRtmServiceTypeMessage` 服务和 `AgoraRtmServiceTypeStream` 服务。一个客户端中可能会用到一种或两种服务，这两种服务对应的连接类型有所不同。

### MESSAGE 类型 [#message-类型-3]

`MESSAGE` 服务为用户提供 Message Channel、User Channel、Presence、Storage、Lock、Token 等服务，这些服务的消息、数据、事件通知等共享此连接。这条连接在客户端生命周期内唯一存在，在你主动调用 `loginByToken` 方法登录服务器时建立，在你主动调用 `logout` 方法登出服务器时结束。

你可以通过以下方式建立 `MESSAGE` 连接：

```objc
[rtm loginByToken:@"token" completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
    if (errorInfo == nil) {
        NSLog(@"login success!!");
    } else {
        NSLog(@"login failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
    }
}];
```

:::info[信息]
建立 `MESSAGE` 连接会增加峰值连接数计量。
:::

`MESSAGE` 连接是长连接，客户端会定时向服务器发送心跳包以保持活跃状态。当你订阅多个 Message Channel 或 User Channel 时，你将通过此连接接受所有频道消息或事件通知。

### STREAM 类型 [#stream-类型-3]

`AgoraRtmServiceTypeStream` 服务为用户提供 Stream Channel 的所有能力。与 `MESSAGE` 连接不同，`STREAM` 连接并不具备全局唯一性，而是每个 Stream Channel 实例都会单独创建一个连接。例如，当你创建 3 个 Stream Channel 实例并调用 `joinWithOption` 方法加入后，客户端会与服务器建立 3 条网络连接。

在建立 `STREAM` 连接前，你需要先建立 `MESSAGE` 连接，即调用 `loginByToken` 方法并成功登录服务器。`STREAM` 连接的生命周期从你主动调用 `joinWithOption` 方法加入频道时开始，到你主动调用 `leave` 方法离开频道时结束。你可以通过以下方式建立 `STREAM` 连接：

```objc
AgoraRtmJoinChannelOption* join_option =  [[AgoraRtmJoinChannelOption alloc] init];
join_option.token =  @"yourToken";
join_option.features = AgoraRtmSubscribeChannelFeaturePresence | AgoraRtmSubscribeChannelFeatureMetadata;
[stream_channel joinWithOption:join_option completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
    if (errorInfo == nil) {
        NSLog(@"join channel success!!");
    } else {
        NSLog(@"join channel failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
    }
}];
```

:::info[信息]
建立 `STREAM` 连接不会增加峰值连接数计量。
:::

`STREAM` 连接也是长连接，客户端会定时向服务器发送心跳包以保持活跃状态，发送心跳包的时间间隔是 0.5 秒且不可更改。对于需要优化电池寿命的应用场景，不建议使用 `AgoraRtmServiceTypeStream` 服务。

## 监控连接 [#监控连接-1]

客户端通过 `didReceiveLinkStateEvent` 事件通知监控和管理网络连接状态的迁移。通过监听此事件，你可以随时知道以下信息：

- 当前网络连接处于何种状态。
- 何种原因或操作导致状态迁移。
- 迁移之前的网络状态是什么。
- 网络连接状态何时发生迁移。
- 什么服务类型的连接状态发生了迁移。
- 网络状态迁移影响了哪些已加入的频道。
- 哪些之前加入的频道没有成功恢复网络连接。

以上信息对于客户端从意外断开状态恢复到连接状态后的业务处理将非常有帮助，用户能更快速地定位和排除由于网络连接变化产生的故障及恢复业务现场。

你可以通过以下方式监听 `didReceiveLinkStateEvent` 事件通知：

```objc
@interface RtmListenerEx : NSObject <AgoraRtmClientDelegate>
@end

@implementation RtmListenerEx
- (void)rtmKit:(AgoraRtmClientKit *)rtmKit didReceiveLinkStateEvent:(AgoraRtmLinkStateEvent *)event {}
@end

RtmListenerEx* handlerEx =  [[RtmListenerEx alloc] init];

[rtm addDelegate:handlerEx];
```

其中各字段的含义如下：

| 属性        |   描述  |
|---|-------- |
| `currentState`            |  当前的连接状态。   |
| `previousState`            |  之前的连接状态。   |
| `serviceType`    |  服务类型。可能是 `AgoraRtmServiceTypeMessage` 或 `AgoraRtmServiceTypeStream`。 |
| `operation`    | 触发本次状态迁移的操作。 |
| `reasonCode`    | 触发本次状态迁移的原因。 |
| `affectedChannels`    |  本次状态迁移影响的频道。 |
| `unrestoredChannels`    |  未恢复订阅或加入的频道信息，包含频道名、频道类型和频道中的临时状态数据。一般情况下为空。 |
| `isResumed`    |  在连接断开的 2 分钟内，是否从 `AgoraRtmLinkStateDisconnected` 状态恢复成 `AgoraRtmLinkStateConnected` 状态。`true` 表示已恢复。 |
| `timestamp`    |  本次事件通知发送的时间戳。 |

## 心跳探测 [#心跳探测-1]

心跳探测能帮助 RTM 客户端和服务器及时发现突然的网络断连，例如客户端网络掉线、网络切换等。

### MESSAGE 类型 [#message-类型-4]

默认情况下，RTM 客户端会每隔 5 秒向服务器发送一次心跳探测包（Ping 包）。如果在 10 秒内没有收到服务器的回复（Pong 包），客户端则会认为网络连接出现问题。此时，客户端的网络连接状态（`AgoraRtmLinkState`）会由 `AgoraRtmLinkStateConnected` 状态切换到 `AgoraRtmLinkStateDisconnected` 状态，并开始尝试重新连接。

同理，服务器如果在 15 秒内未能收到来自客户端的 Ping 包，则会认为客户端出现了网络连接问题。此时，服务器会等待一段时间（`presenceTimeout`），如果这段时间内客户端未能重新与服务器建立连接，则 RTM 服务器会向此客户端所在频道的其他用户发送 `AgoraRtmPresenceEventTypeRemoteConnectionTimeout` 事件通知。

需要注意的是，以上机制只适用于客户端与服务器意外断开连接的情况，如果你是主动断开连接，例如通过调用 `logout` 方法主动登出时，则不会遵循上述机制。

RTM SDK 默认的心跳探测时间间隔为 5 秒，这是平衡了设备续航能力和意外断连识别及时性的结果。如果你的 App 需要增加电池续航或者需要更及时地识别网络断连情况，那么你可以通过 `AgoraRtmClientConfig` 中的 `heartbeatInterval` 参数自定义心跳探测时间间隔，支持的取值为 [5,1800] 之间的任意整数值。

设置更高的 `heartbeatInterval` 值可能会降低客户端和服务器对于意外断连识别的及时性，峰值连接数（PCU）也可能显得更高。虽然 `heartbeatInterval` 最高可以设置成 1800 秒，但是我们不建议这么做。

你可以通过以下方式自定义心跳探测时间间隔，例如修改为 30 秒：

```objc
AgoraRtmClientConfig* rtm_config = [[AgoraRtmClientConfig alloc] initWithAppId:@"your_appid" userId:@"your_userid"];
rtm_config.heartbeatInterval = 30;
```

### STREAM 类型 [#stream-类型-4]

当你创建一个 Stream Channel 实例并调用 `joinWithOption` 方法成功加入频道时，客户端会按 0.5 秒的间隔向服务器发送心跳包以保持活跃状态。`STREAM` 连接的心跳时间间隔不可更改，对于需要增加电池续航的应用场景，不建议使用 `AgoraRtmServiceTypeStream` 服务。

<a id="heartbeatinterval-and-presencetimeout"></a>
## Heartbeat Interval 与 Presence Timeout [#heartbeat-interval-与-presence-timeout-1]

`AgoraRtmClientConfig` 中包含两个与时间间隔相关的参数，分别为 `heartbeatInterval` 和 `presenceTimeout`。这两个参数常常会让人感到困惑，本节主要介绍它们的含义，以便你更有效地配置 App。

- `heartbeatInterval`：客户端会定期向服务器发送证明自身处于活跃状态的 Ping-Pong 数据包，以确保服务器能够及时清除超时计数并维持连接状态。该设置会影响 PCU 计数，是 SDK 全局连接管理功能中的一个重要特性。
- `presenceTimeout`：如果在设置的 `heartbeatInterval` 时间加 10 秒冗余时间内未收到客户端的 Ping 包，服务器则会认为该客户端可能遇到了网络连接故障。考虑到此时客户端可能在尝试修复问题，为避免重复通知事件，服务器会延迟一段时间（即 `presenceTimeout` 时间）再向频道中的其他用户广播事件通知。
  - 如果客户端在 `presenceTimeout` 设置的时间内与服务器重新建立连接，则服务器不会向频道中的其他用户发送事件通知，并且用户设置的 Presence 临时状态也会被保留。
  - 如果客户端在 `presenceTimeout` 设置的时间内未能重新连接，则服务器会向频道中的其他用户广播 `AgoraRtmPresenceEventTypeRemoteConnectionTimeout` 类型的 `didReceivePresenceEvent` 事件通知，该客户端的临时用户状态也会被清除。当客户端重新建立连接并恢复对频道的订阅关系时，服务器也会向频道中的其他用户广播 `AgoraRtmPresenceEventTypeRemoteJoinChannel` 事件通知。如果你之前设置过临时用户状态，则客户端会重新应用本地会话缓存的临时用户状态，在频道中的其他用户也会收到对应的 `AgoraRtmPresenceEventTypeRemoteStateChanged` 事件通知。

对于本地用户而言，由 Presence Timeout 触发的离开与调用 `unsubscribeWithChannel` 方法造成的离开都被视为频道离开行为，不同之处在于 Presence Timeout 的离开通常是由网络连接故障引起的。本地用户可能处于 `AgoraRtmLinkStateDisconnected` 或 `AgoraRtmLinkStateSuspended` 状态，并尝试重新连接。如果重新连接成功，频道订阅关系和临时用户状态都会被自动恢复，对于频道中的其他用户来说，该用户为重新加入的用户。调用 `unsubscribeWithChannel` 被视为用户主动离开频道，客户端会清除此频道的订阅关系和临时用户状态。

例如，客户端 A 在初始化实例时设置 `heartbeatInterval` 为 10 秒、`presenceTimeout` 为 5 秒。如果 A 发出了 Ping 包，但因为网络等原因在 10 秒内没有收到 Pong 包，则 A 的连接状态会变为 `AgoraRtmLinkStateDisconnected` 类型。同时，A 会尝试重新连接，服务器最多等待 25 秒。25 秒后，如果 A 未能重新连接，那么服务器会代替 A 向频道中其他用户广播 `AgoraRtmPresenceEventTypeRemoteConnectionTimeout` 类型的 `didReceivePresenceEvent` 事件通知；如果 A 在 25 秒内成功恢复连接，那么服务器不会广播事件通知，频道中其他用户会认为 A 一直都在频道中未曾离开。在这个例子中，从 A 掉线到其他用户被通知先后经历了 10 秒的 `heartbeatInterval`、10 秒的误差范围（以允许网络延迟）、5 秒的 `presenceTimeout`，即最长的时间间隔为 25 秒。

设定 `presenceTimeout` 时间的一个重要理由是，客户端经常遇到断开网络连接又快速恢复的情况，例如网络切换、行驶经过隧道等。如果不允许客户端尝试重新连接就立即在频道内广播用户离开或加入的情况，则会造成过多的 Presence 事件通知，极大地降低用户体验。

## 关闭连接 [#关闭连接-1]

当客户端和服务器之间不再需要连接时，你可以主动调用方法关闭连接。

### MESSAGE 类型 [#message-类型-5]

对于 `AgoraRtmServiceTypeMessage` 类型服务，连接状态会影响峰值连接数的计量，推荐你调用 `logout` 方法显式地登出服务器，从而主动断开连接。

```objc
[rtm logout:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
    if (errorInfo == nil) {
        NSLog(@"logout success!!");
    } else {
        NSLog(@"logout failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
    }
}];
```

:::info[信息]
一旦你主动调用 `logout` 方法断开 `MESSAGE` 连接，`STREAM` 连接也会同时断开，你将离开所有频道，客户端中所有缓存数据都会被清除。
:::

### STREAM 类型 [#stream-类型-5]

当你不再需要 `AgoraRtmServiceTypeStream` 类型服务时，可以调用 `leave` 方法退出频道，从而断开 `STREAM` 连接。

```objc
[stream_channel leave:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
    if (errorInfo == nil) {
        NSLog(@"leave channel success!!");
    } else {
        NSLog(@"leave channel failed, errorCode %d, reason %@", errorInfo.errorCode, errorInfo.reason);
    }
}];
```
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="web" />
客户端需要首先与 RTM 服务器建立连接，才能在后续访问 RTM 网络资源，SDK 的 API 调用以及频道中的消息和事件都通过此网络连接进行传输。虽然 RTM SDK 会帮助你建立和维护网络连接，从而极大地简化监控网络状态的业务逻辑，但充分理解 RTM 的网络连接管理机制，仍有助于你在 App 中处理可能出现的网络错误，提升用户体验。

:::info[信息]
本文仅适用于 2.2.0 及之后版本。
:::

## 创建连接 [#创建连接-2]

当客户端创建一个 RTM 实例或 Stream Channel 实例时，实际上客户端还没有真正地与服务器建立网络连接。你只是拥有了一个对象，具备在此对象中调用 API 的能力。只有当你主动调用 `login` 方法或 `join` 方法时，客户端才开始尝试与服务器建立网络连接。

RTM SDK 在很大程度上屏蔽了直接处理网络连接的种种细节，提供 `linkState` 事件通知向用户透传各种连接状态的迁移，你只需监听此事件即可从各种连接错误中恢复。

## 连接类型 [#连接类型-2]

在进一步探索之前，你需要了解 RTM 客户端的两种服务类型：`MESSAGE` 服务和 `STREAM` 服务。一个客户端中可能会用到一种或两种服务，这两种服务对应的连接类型有所不同。

### MESSAGE 类型 [#message-类型-6]

`MESSAGE` 服务为用户提供 Message Channel、User Channel、Presence、Storage、Lock、Token 等服务，这些服务的消息、数据、事件通知等共享此连接。这条连接在客户端生命周期内唯一存在，在你主动调用 `login` 方法登录服务器时建立，在你主动调用 `logout` 方法登出服务器时结束。

你可以通过以下方式建立 `MESSAGE` 连接：

```javascript
try{
    const result = await rtm.login({ token: "your_token" });
    console.log(result);
} catch (status){
    console.log(status);
}
```

:::info[信息]
建立 `MESSAGE` 连接会增加峰值连接数计量。
:::

`MESSAGE` 连接是长连接，客户端会定时向服务器发送心跳包以保持活跃状态。当你订阅多个 Message Channel 或 User Channel 时，你将通过此连接接受所有频道消息或事件通知。

### STREAM 类型 [#stream-类型-6]

`STREAM` 服务为用户提供 Stream Channel 的所有能力。与 `MESSAGE` 连接不同，`STREAM` 连接并不具备全局唯一性，而是每个 Stream Channel 实例都会单独创建一个连接。例如，当你创建 3 个 Stream Channel 实例并调用 `join` 方法加入后，客户端会与服务器建立 3 条网络连接。

在建立 `STREAM` 连接前，你需要先建立 `MESSAGE` 连接，即调用 `login` 方法并成功登录服务器。`STREAM` 连接的生命周期从你主动调用 `join` 方法加入频道时开始，到你主动调用 `leave` 方法离开频道时结束。你可以通过以下方式建立 `STREAM` 连接：

```javascript
const options ={
    token : "yourToken",
    withPresence : true,
    withMetadata : false,
    withLock : false
};
try {
    const result = await stChannel.join(options);
    console.log(result);
} catch (status) {
    console.log(status);
}
```

:::info[信息]
建立 `STREAM` 连接不会增加峰值连接数计量。
:::

`STREAM` 连接也是长连接，客户端会定时向服务器发送心跳包以保持活跃状态，发送心跳包的时间间隔是 0.5 秒且不可更改。对于需要优化电池寿命的应用场景，不建议使用 `STREAM` 服务。

## 监控连接 [#监控连接-2]

客户端通过 `linkState` 事件通知监控和管理网络连接状态的迁移。通过监听此事件，你可以随时知道以下信息：

- 当前网络连接处于何种状态。
- 何种原因或操作导致状态迁移。
- 迁移之前的网络状态是什么。
- 网络连接状态何时发生迁移。
- 什么服务类型的连接状态发生了迁移。
- 网络状态迁移影响了哪些已加入的频道。
- 哪些之前加入的频道没有成功恢复网络连接。

以上信息对于客户端从意外断开状态恢复到连接状态后的业务处理将非常有帮助，用户能更快速地定位和排除由于网络连接变化产生的故障及恢复业务现场。

你可以通过以下方式监听 `linkState` 事件通知：

```javascript
rtm.addEventListener('linkState', event => {
    const currentState = event.currentState;
    const previousState = event.previousState;
    const serviceType = event.serviceType;
    const operation = event.operation;
    const reason = event.reason;
    const reasonCode = event.reasonCode;
    const affectedChannels = event.affectedChannels;
    const timestamp = event.timestamp;
    const isResumed = event.isResumed;
});
```

其中各字段的含义如下：

| 属性        |   描述  |
|---|-------- |
| `currentState`            |  当前的连接状态。   |
| `previousState`            |  之前的连接状态。   |
| `serviceType`    |  服务类型。可能是 `MESSAGE` 或 `STREAM`。 |
| `operation`    | 触发本次状态迁移的操作。 |
| `reasonCode`    | 触发本次状态迁移的原因。 |
| `affectedChannels`    |  本次状态迁移影响的频道。 |
| `unrestoredChannels`    |  未恢复订阅或加入的频道信息，包含频道名、频道类型和频道中的临时状态数据。一般情况下为空。 |
| `isResumed`    |  在连接断开的 2 分钟内，是否从 `DISCONNECTED` 状态恢复成 `CONNECTED` 状态。`true` 表示已恢复。 |
| `timestamp`    |  本次事件通知发送的时间戳。 |

## 心跳探测 [#心跳探测-2]

心跳探测能帮助 RTM 客户端和服务器及时发现突然的网络断连，例如客户端网络掉线、网络切换等。

### MESSAGE 类型 [#message-类型-7]

默认情况下，RTM 客户端会每隔 5 秒向服务器发送一次心跳探测包（Ping 包）。如果在 10 秒内没有收到服务器的回复（Pong 包），客户端则会认为网络连接出现问题。此时，客户端的网络连接状态（`LinkState`）会由 `CONNECTED` 状态切换到 `DISCONNECTED` 状态，并开始尝试重新连接。

同理，服务器如果在 15 秒内未能收到来自客户端的 Ping 包，则会认为客户端出现了网络连接问题。此时，服务器会等待一段时间（`presenceTimeout`），如果这段时间内客户端未能重新与服务器建立连接，则 RTM 服务器会向此客户端所在频道的其他用户发送 `REMOTE_TIMEOUT` 事件通知。

需要注意的是，以上机制只适用于客户端与服务器意外断开连接的情况，如果你是主动断开连接，例如通过调用 `logout` 方法主动登出时，则不会遵循上述机制。

RTM SDK 默认的心跳探测时间间隔为 5 秒，这是平衡了设备续航能力和意外断连识别及时性的结果。如果你的 App 需要增加电池续航或者需要更及时地识别网络断连情况，那么你可以通过 `RTMConfig` 中的 `heartbeatInterval` 参数自定义心跳探测时间间隔，支持的取值为 [5,1800] 之间的任意整数值。

设置更高的 `heartbeatInterval` 值可能会降低客户端和服务器对于意外断连识别的及时性，峰值连接数（PCU）也可能显得更高。虽然 `heartbeatInterval` 最高可以设置成 1800 秒，但是我们不建议这么做。

你可以通过以下方式自定义心跳探测时间间隔，例如修改为 30 秒：

```javascript
const { RTM, EncryptionMode } = AgoraRTM;
const rtmConfig = { heartbeatInterval : 30 };
const rtm = new RTM("yourAppId", "Tony", rtmConfig);
```

### STREAM 类型 [#stream-类型-7]

当你创建一个 Stream Channel 实例并调用 `join` 方法成功加入频道时，客户端会按 0.5 秒的间隔向服务器发送心跳包以保持活跃状态。`STREAM` 连接的心跳时间间隔不可更改，对于需要增加电池续航的应用场景，不建议使用 `STREAM` 服务。

<a id="heartbeatinterval-and-presencetimeout"></a>
## Heartbeat Interval 与 Presence Timeout [#heartbeat-interval-与-presence-timeout-2]

`RTMConfig` 中包含两个与时间间隔相关的参数，分别为 `heartbeatInterval` 和 `presenceTimeout`。这两个参数常常会让人感到困惑，本节主要介绍它们的含义，以便你更有效地配置 App。

- `heartbeatInterval`：客户端会定期向服务器发送证明自身处于活跃状态的 Ping-Pong 数据包，以确保服务器能够及时清除超时计数并维持连接状态。该设置会影响 PCU 计数，是 SDK 全局连接管理功能中的一个重要特性。
- `presenceTimeout`：如果在设置的 `heartbeatInterval` 时间加 10 秒冗余时间内未收到客户端的 Ping 包，服务器则会认为该客户端可能遇到了网络连接故障。考虑到此时客户端可能在尝试修复问题，为避免重复通知事件，服务器会延迟一段时间（即 `presenceTimeout` 时间）再向频道中的其他用户广播事件通知。
  - 如果客户端在 `presenceTimeout` 设置的时间内与服务器重新建立连接，则服务器不会向频道中的其他用户发送事件通知，并且用户设置的 Presence 临时状态也会被保留。
  - 如果客户端在 `presenceTimeout` 设置的时间内未能重新连接，则服务器会向频道中的其他用户广播 `REMOTE_TIMEOUT` 类型的 `presence` 事件通知，该客户端的临时用户状态也会被清除。当客户端重新建立连接并恢复对频道的订阅关系时，服务器也会向频道中的其他用户广播 `REMOTE_JOIN` 事件通知。如果你之前设置过临时用户状态，则客户端会重新应用本地会话缓存的临时用户状态，在频道中的其他用户也会收到对应的 `REMOTE_STATE_CHANGED` 事件通知。

对于本地用户而言，由 Presence Timeout 触发的离开与调用 `unsubscribe` 方法造成的离开都被视为频道离开行为，不同之处在于 Presence Timeout 的离开通常是由网络连接故障引起的。本地用户可能处于 `DISCONNECTED` 或 `SUSPENDED` 状态，并尝试重新连接。如果重新连接成功，频道订阅关系和临时用户状态都会被自动恢复，对于频道中的其他用户来说，该用户为重新加入的用户。调用 `unsubscribe` 被视为用户主动离开频道，客户端会清除此频道的订阅关系和临时用户状态。

例如，客户端 A 在初始化实例时设置 `heartbeatInterval` 为 10 秒、`presenceTimeout` 为 5 秒。如果 A 发出了 Ping 包，但因为网络等原因在 10 秒内没有收到 Pong 包，则 A 的连接状态会变为 `DISCONNECTED` 类型。同时，A 会尝试重新连接，服务器最多等待 25 秒。25 秒后，如果 A 未能重新连接，那么服务器会代替 A 向频道中其他用户广播 `REMOTE_TIMEOUT` 类型的 `presence` 事件通知；如果 A 在 25 秒内成功恢复连接，那么服务器不会广播事件通知，频道中其他用户会认为 A 一直都在频道中未曾离开。在这个例子中，从 A 掉线到其他用户被通知先后经历了 10 秒的 `heartbeatInterval`、10 秒的误差范围（以允许网络延迟）、5 秒的 `presenceTimeout`，即最长的时间间隔为 25 秒。

设定 `presenceTimeout` 时间的一个重要理由是，客户端经常遇到断开网络连接又快速恢复的情况，例如网络切换、行驶经过隧道等。如果不允许客户端尝试重新连接就立即在频道内广播用户离开或加入的情况，则会造成过多的 Presence 事件通知，极大地降低用户体验。

## 关闭连接 [#关闭连接-2]

当客户端和服务器之间不再需要连接时，你可以主动调用方法关闭连接。

### MESSAGE 类型 [#message-类型-8]

对于 `MESSAGE` 类型服务，连接状态会影响峰值连接数的计量，推荐你调用 `logout` 方法显式地登出服务器，从而主动断开连接。

```javascript
try{
    const result = await rtm.logout();
    console.log(result);
} catch (status){
    console.log(status);
}
```

:::info[信息]
一旦你主动调用 `logout` 方法断开 `MESSAGE` 连接，`STREAM` 连接也会同时断开，你将离开所有频道，客户端中所有缓存数据都会被清除。
:::

### STREAM 类型 [#stream-类型-8]

当你不再需要 `STREAM` 类型服务时，可以调用 `leave` 方法退出频道，从而断开 `STREAM` 连接。

```javascript
try{
    const result = await streamChannel.leave();
    console.log(result);
} catch (status){
    console.log(status);
}
```
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="harmonyos" />
客户端需要首先与 RTM 服务器建立连接，才能在后续访问 RTM 网络资源，SDK 的 API 调用以及频道中的消息和事件都通过此网络连接进行传输。虽然 RTM SDK 会帮助你建立和维护网络连接，从而极大地简化监控网络状态的业务逻辑，但充分理解 RTM 的网络连接管理机制，仍有助于你在 App 中处理可能出现的网络错误，提升用户体验。

:::info[信息]
本文仅适用于 2.2.0 及之后版本。
:::

## 创建连接 [#创建连接-3]

当客户端创建一个 RTM 实例或 Stream Channel 实例时，实际上客户端还没有真正地与服务器建立网络连接。你只是拥有了一个对象，具备在此对象中调用 API 的能力。只有当你主动调用 `login` 方法或 `join` 方法时，客户端才开始尝试与服务器建立网络连接。

RTM SDK 在很大程度上屏蔽了直接处理网络连接的种种细节，提供 `linkState` 事件通知向用户透传各种连接状态的迁移，你只需监听此事件即可从各种连接错误中恢复。

## 连接类型 [#连接类型-3]

在进一步探索之前，你需要了解 RTM 客户端的两种服务类型：`MESSAGE` 服务和 `STREAM` 服务。一个客户端中可能会用到一种或两种服务，这两种服务对应的连接类型有所不同。

### MESSAGE 类型 [#message-类型-9]

`MESSAGE` 服务为用户提供 Message Channel、User Channel、Presence、Storage、Lock、Token 等服务，这些服务的消息、数据、事件通知等共享此连接。这条连接在客户端生命周期内唯一存在，在你主动调用 `login` 方法登录服务器时建立，在你主动调用 `logout` 方法登出服务器时结束。

你可以通过以下方式建立 `MESSAGE` 连接：

```typescript
import { RtmError } from '@shengwang/rtm-full';

try {
  const response = await rtmClient.login("your_token");
  console.log("Login successful:", response);
} catch (error) {
  console.error("Login failed:", error);
  if (error instanceof RtmError) {
    console.error(`Login failed: ${error.reason} (code: ${error.errorCode})`);
  }
}
```

:::info[信息]
建立 `MESSAGE` 连接会增加峰值连接数计量。
:::

`MESSAGE` 连接是长连接，客户端会定时向服务器发送心跳包以保持活跃状态。当你订阅多个 Message Channel 或 User Channel 时，你将通过此连接接受所有频道消息或事件通知。

### STREAM 类型 [#stream-类型-9]

`STREAM` 服务为用户提供 Stream Channel 的所有能力。与 `MESSAGE` 连接不同，`STREAM` 连接并不具备全局唯一性，而是每个 Stream Channel 实例都会单独创建一个连接。例如，当你创建 3 个 Stream Channel 实例并调用 `join` 方法加入后，客户端会与服务器建立 3 条网络连接。

在建立 `STREAM` 连接前，你需要先建立 `MESSAGE` 连接，即调用 `login` 方法并成功登录服务器。`STREAM` 连接的生命周期从你主动调用 `join` 方法加入频道时开始，到你主动调用 `leave` 方法离开频道时结束。你可以通过以下方式建立 `STREAM` 连接：

```typescript
import { JoinOptions } from '@shengwang/rtm-full';
import { RtmError } from '@shengwang/rtm-full';

const options: JoinOptions = {
  token: "yourToken",
  withPresence: true,
  withMetadata: false,
  withLock: false
};

try {
  const streamChannel = rtmClient.createStreamChannel("test");
  const response = await streamChannel.join(options);
  console.log("Join channel successful:", response);
} catch (error) {
  console.error("Join channel failed:", error);
  if (error instanceof RtmError) {
    console.error(`Join channel failed: ${error.reason} (code: ${error.errorCode})`);
  }
}
```

:::info[信息]
建立 `STREAM` 连接不会增加峰值连接数计量。
:::

`STREAM` 连接也是长连接，客户端会定时向服务器发送心跳包以保持活跃状态，发送心跳包的时间间隔是 0.5 秒且不可更改。对于需要优化电池寿命的应用场景，不建议使用 `STREAM` 服务。

## 监控连接 [#监控连接-3]

客户端通过 `linkState` 事件通知监控和管理网络连接状态的迁移。通过监听此事件，你可以随时知道以下信息：

- 当前网络连接处于何种状态。
- 何种原因或操作导致状态迁移。
- 迁移之前的网络状态是什么。
- 网络连接状态何时发生迁移。
- 什么服务类型的连接状态发生了迁移。
- 网络状态迁移影响了哪些已加入的频道。
- 哪些之前加入的频道没有成功恢复网络连接。

以上信息对于客户端从意外断开状态恢复到连接状态后的业务处理将非常有帮助，用户能更快速地定位和排除由于网络连接变化产生的故障及恢复业务现场。

你可以通过以下方式监听 `linkState` 事件通知：

```typescript
import type { LinkStateEvent } from '@shengwang/rtm-full';

rtmClient.addEventListener('linkState', (event: object) => {
  const linkEvent = event as LinkStateEvent;
  const currentState = linkEvent.currentState;
  const previousState = linkEvent.previousState;
  const serviceType = linkEvent.serviceType;
  const operation = linkEvent.operation;
  const reason = linkEvent.reason;
  const reasonCode = linkEvent.reasonCode;
  const affectedChannels = linkEvent.affectedChannels;
  const timestamp = linkEvent.timestamp;
  const isResumed = linkEvent.isResumed;
  
  console.log(`Link state changed: ${previousState} -> ${currentState}`);
  console.log(`Reason: ${reason} (code: ${reasonCode})`);
});
```

其中各字段的含义如下：

| 属性        |   描述  |
|---|-------- |
| `currentState`            |  当前的连接状态。   |
| `previousState`            |  之前的连接状态。   |
| `serviceType`    |  服务类型。可能是 `MESSAGE` 或 `STREAM`。 |
| `operation`    | 触发本次状态迁移的操作。 |
| `reasonCode`    | 触发本次状态迁移的原因。 |
| `affectedChannels`    |  本次状态迁移影响的频道。 |
| `unrestoredChannels`    |  未恢复订阅或加入的频道信息，包含频道名、频道类型和频道中的临时状态数据。一般情况下为空。 |
| `isResumed`    |  在连接断开的 2 分钟内，是否从 `DISCONNECTED` 状态恢复成 `CONNECTED` 状态。`true` 表示已恢复。 |
| `timestamp`    |  本次事件通知发送的时间戳。 |

## 心跳探测 [#心跳探测-3]

心跳探测能帮助 RTM 客户端和服务器及时发现突然的网络断连，例如客户端网络掉线、网络切换等。

### MESSAGE 类型 [#message-类型-10]

默认情况下，RTM 客户端会每隔 5 秒向服务器发送一次心跳探测包（Ping 包）。如果在 10 秒内没有收到服务器的回复（Pong 包），客户端则会认为网络连接出现问题。此时，客户端的网络连接状态（`RtmLinkState`）会由 `CONNECTED` 状态切换到 `DISCONNECTED` 状态，并开始尝试重新连接。

同理，服务器如果在 15 秒内未能收到来自客户端的 Ping 包，则会认为客户端出现了网络连接问题。此时，服务器会等待一段时间（`presenceTimeout`），如果这段时间内客户端未能重新与服务器建立连接，则 RTM 服务器会向此客户端所在频道的其他用户发送 `REMOTE_TIMEOUT` 事件通知。

需要注意的是，以上机制只适用于客户端与服务器意外断开连接的情况，如果你是主动断开连接，例如通过调用 `logout` 方法主动登出时，则不会遵循上述机制。

RTM SDK 默认的心跳探测时间间隔为 5 秒，这是平衡了设备续航能力和意外断连识别及时性的结果。如果你的 App 需要增加电池续航或者需要更及时地识别网络断连情况，那么你可以通过 `RtmConfig` 中的 `heartbeatInterval` 参数自定义心跳探测时间间隔，支持的取值为 [5,1800] 之间的任意整数值。

设置更高的 `heartbeatInterval` 值可能会降低客户端和服务器对于意外断连识别的及时性，峰值连接数（PCU）也可能显得更高。虽然 `heartbeatInterval` 最高可以设置成 1800 秒，但是我们不建议这么做。

你可以通过以下方式自定义心跳探测时间间隔，例如修改为 30 秒：

```typescript
import { RtmClient, RtmConfig } from '@shengwang/rtm-full';
import { RtmError } from '@shengwang/rtm-full';

const rtmConfig: RtmConfig = {
  appId: "your_appId",
  userId: "your_userId",
  heartbeatInterval: 30
};

try {
  const rtmClient = new RtmClient(rtmConfig);
  console.log("RTM client initialized with custom heartbeat interval");
} catch (error) {
  if (error instanceof RtmError) {
    console.error(`Initialize failed: ${error.reason} (code: ${error.errorCode})`);
  } else {
    console.error("Failed to initialize RTM client:", error);
  }
}
```

### STREAM 类型 [#stream-类型-10]

当你创建一个 Stream Channel 实例并调用 `join` 方法成功加入频道时，客户端会按 0.5 秒的间隔向服务器发送心跳包以保持活跃状态。`STREAM` 连接的心跳时间间隔不可更改，对于需要增加电池续航的应用场景，不建议使用 `STREAM` 服务。

<a id="heartbeatinterval-and-presencetimeout"></a>
## Heartbeat Interval 与 Presence Timeout [#heartbeat-interval-与-presence-timeout-3]

`RtmConfig` 中包含两个与时间间隔相关的参数，分别为 `heartbeatInterval` 和 `presenceTimeout`。这两个参数常常会让人感到困惑，本节主要介绍它们的含义，以便你更有效地配置 App。

- `heartbeatInterval`：客户端会定期向服务器发送证明自身处于活跃状态的 Ping-Pong 数据包，以确保服务器能够及时清除超时计数并维持连接状态。该设置会影响 PCU 计数，是 SDK 全局连接管理功能中的一个重要特性。
- `presenceTimeout`：如果在设置的 `heartbeatInterval` 时间加 10 秒冗余时间内未收到客户端的 Ping 包，服务器则会认为该客户端可能遇到了网络连接故障。考虑到此时客户端可能在尝试修复问题，为避免重复通知事件，服务器会延迟一段时间（即 `presenceTimeout` 时间）再向频道中的其他用户广播事件通知。
  - 如果客户端在 `presenceTimeout` 设置的时间内与服务器重新建立连接，则服务器不会向频道中的其他用户发送事件通知，并且用户设置的 Presence 临时状态也会被保留。
  - 如果客户端在 `presenceTimeout` 设置的时间内未能重新连接，则服务器会向频道中的其他用户广播 `REMOTE_TIMEOUT` 类型的 `presence` 事件通知，该客户端的临时用户状态也会被清除。当客户端重新建立连接并恢复对频道的订阅关系时，服务器也会向频道中的其他用户广播 `REMOTE_JOIN_CHANNEL` 事件通知。如果你之前设置过临时用户状态，则客户端会重新应用本地会话缓存的临时用户状态，在频道中的其他用户也会收到对应的 `REMOTE_STATE_CHANGED` 事件通知。

对于本地用户而言，由 Presence Timeout 触发的离开与调用 `unsubscribe` 方法造成的离开都被视为频道离开行为，不同之处在于 Presence Timeout 的离开通常是由网络连接故障引起的。本地用户可能处于 `DISCONNECTED` 或 `SUSPENDED` 状态，并尝试重新连接。如果重新连接成功，频道订阅关系和临时用户状态都会被自动恢复，对于频道中的其他用户来说，该用户为重新加入的用户。调用 `unsubscribe` 被视为用户主动离开频道，客户端会清除此频道的订阅关系和临时用户状态。

例如，客户端 A 在初始化实例时设置 `heartbeatInterval` 为 10 秒、`presenceTimeout` 为 5 秒。如果 A 发出了 Ping 包，但因为网络等原因在 10 秒内没有收到 Pong 包，则 A 的连接状态会变为 `DISCONNECTED` 类型。同时，A 会尝试重新连接，服务器最多等待 25 秒。25 秒后，如果 A 未能重新连接，那么服务器会代替 A 向频道中其他用户广播 `REMOTE_TIMEOUT` 类型的 `presence` 事件通知；如果 A 在 25 秒内成功恢复连接，那么服务器不会广播事件通知，频道中其他用户会认为 A 一直都在频道中未曾离开。在这个例子中，从 A 掉线到其他用户被通知先后经历了 10 秒的 `heartbeatInterval`、10 秒的误差范围（以允许网络延迟）、5 秒的 `presenceTimeout`，即最长的时间间隔为 25 秒。

设定 `presenceTimeout` 时间的一个重要理由是，客户端经常遇到断开网络连接又快速恢复的情况，例如网络切换、行驶经过隧道等。如果不允许客户端尝试重新连接就立即在频道内广播用户离开或加入的情况，则会造成过多的 Presence 事件通知，极大地降低用户体验。

## 关闭连接 [#关闭连接-3]

当客户端和服务器之间不再需要连接时，你可以主动调用方法关闭连接。

### MESSAGE 类型 [#message-类型-11]

对于 `MESSAGE` 类型服务，连接状态会影响峰值连接数的计量，推荐你调用 `logout` 方法显式地登出服务器，从而主动断开连接。

```typescript
import { RtmError } from '@shengwang/rtm-full';

try {
  const response = await rtmClient.logout();
  console.log("Logout successful:", response);
} catch (error) {
  console.error("Logout failed:", error);
  if (error instanceof RtmError) {
    console.error(`Logout failed: ${error.reason} (code: ${error.errorCode})`);
  }
}
```

:::info[信息]
一旦你主动调用 `logout` 方法断开 `MESSAGE` 连接，`STREAM` 连接也会同时断开，你将离开所有频道，客户端中所有缓存数据都会被清除。
:::

### STREAM 类型 [#stream-类型-11]

当你不再需要 `STREAM` 类型服务时，可以调用 `leave` 方法退出频道，从而断开 `STREAM` 连接。

```typescript
import { RtmError } from '@shengwang/rtm-full';

try {
  const response = await streamChannel.leave();
  console.log("Leave channel successful:", response);
} catch (error) {
  console.error("Leave channel failed:", error);
  if (error instanceof RtmError) {
    console.error(`Leave channel: ${error.reason} (code: ${error.errorCode})`);
  }
}
```
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="flutter" />
客户端需要首先与 RTM 服务器建立连接，才能在后续访问 RTM 网络资源，SDK 的 API 调用以及频道中的消息和事件都通过此网络连接进行传输。虽然 RTM SDK 会帮助你建立和维护网络连接，从而极大地简化监控网络状态的业务逻辑，但充分理解 RTM 的网络连接管理机制，仍有助于你在 App 中处理可能出现的网络错误，提升用户体验。

:::info[信息]
本文仅适用于 2.2.0 及之后版本。
:::

## 创建连接 [#创建连接-4]

当客户端创建一个 RTM 实例或 Stream Channel 实例时，实际上客户端还没有真正地与服务器建立网络连接。你只是拥有了一个对象，具备在此对象中调用 API 的能力。只有当你主动调用 `login` 方法或 `join` 方法时，客户端才开始尝试与服务器建立网络连接。

RTM SDK 在很大程度上屏蔽了直接处理网络连接的种种细节，提供 `linkState` 事件通知向用户透传各种连接状态的迁移，你只需监听此事件即可从各种连接错误中恢复。

## 连接类型 [#连接类型-4]

在进一步探索之前，你需要了解 RTM 客户端的两种服务类型：`message` 服务和 `stream` 服务。一个客户端中可能会用到一种或两种服务，这两种服务对应的连接类型有所不同。

### MESSAGE 类型 [#message-类型-12]

`MESSAGE` 服务为用户提供 Message Channel、User Channel、Presence、Storage、Lock、Token 等服务，这些服务的消息、数据、事件通知等共享此连接。这条连接在客户端生命周期内唯一存在，在你主动调用 `login` 方法登录服务器时建立，在你主动调用 `logout` 方法登出服务器时结束。

你可以通过以下方式建立 `MESSAGE` 连接：

```dart
try {
    var (status, response) = await rtmClient.login('your_Token');
    if (status.error == true ){
       print('${status.operation} failed, errorCode: ${status.errorCode}, due to ${status.reason}');
    } else {
        print('${status.operation} success!');
    }
} catch (e) {
    print('something went wrong: $e');
}
```

:::info[信息]
建立 `MESSAGE` 连接会增加峰值连接数计量。
:::

`MESSAGE` 连接是长连接，客户端会定时向服务器发送心跳包以保持活跃状态。当你订阅多个 Message Channel 或 User Channel 时，你将通过此连接接受所有频道消息或事件通知。

### STREAM 类型 [#stream-类型-12]

`stream` 服务为用户提供 Stream Channel 的所有能力。与 `MESSAGE` 连接不同，`STREAM` 连接并不具备全局唯一性，而是每个 Stream Channel 实例都会单独创建一个连接。例如，当你创建 3 个 Stream Channel 实例并调用 `join` 方法加入后，客户端会与服务器建立 3 条网络连接。

在建立 `STREAM` 连接前，你需要先建立 `MESSAGE` 连接，即调用 `login` 方法并成功登录服务器。`STREAM` 连接的生命周期从你主动调用 `join` 方法加入频道时开始，到你主动调用 `leave` 方法离开频道时结束。你可以通过以下方式建立 `STREAM` 连接：

```dart
try {
    var (status,response) = await stChannel.join(token:'your_token',withPresence:true);
    if (status.error == true ){
       print('${status.operation} failed, errorCode: ${status.errorCode}, due to ${status.reason}');
    } else {
        print('${status.operation} success!');
    }
} catch (e) {
    print('something went wrong: $e');
}
```

:::info[信息]
建立 `STREAM` 连接不会增加峰值连接数计量。
:::

`STREAM` 连接也是长连接，客户端会定时向服务器发送心跳包以保持活跃状态，发送心跳包的时间间隔是 0.5 秒且不可更改。对于需要优化电池寿命的应用场景，不建议使用 `stream` 服务。

## 监控连接 [#监控连接-4]

客户端通过 `linkState` 事件通知监控和管理网络连接状态的迁移。通过监听此事件，你可以随时知道以下信息：

- 当前网络连接处于何种状态。
- 何种原因或操作导致状态迁移。
- 迁移之前的网络状态是什么。
- 网络连接状态何时发生迁移。
- 什么服务类型的连接状态发生了迁移。
- 网络状态迁移影响了哪些已加入的频道。
- 哪些之前加入的频道没有成功恢复网络连接。

以上信息对于客户端从意外断开状态恢复到连接状态后的业务处理将非常有帮助，用户能更快速地定位和排除由于网络连接变化产生的故障及恢复业务现场。

你可以通过以下方式监听 `linkState` 事件通知：

```dart
rtmClient.addListener(
    linkState: (event) {
        var currentState = event.currentState;
        var previousState = event.previousState;
        var serviceType = event.serviceType;
        var operation = event.operation;
        var reason = event.reason;
        var affectedChannels = event.affectedChannels;
        var unrestoredChannels = event.unrestoredChannels;
        var isResumed = event.isResumed;
        var timestamp = event.timestamp;
    }
);
```

其中各字段的含义如下：

| 属性        |   描述  |
|---|-------- |
| `currentState`            |  当前的连接状态。   |
| `previousState`            |  之前的连接状态。   |
| `serviceType`    |  服务类型。可能是 `message` 或 `stream`。 |
| `operation`    | 触发本次状态迁移的操作。 |
| `reasonCode`    | 触发本次状态迁移的原因。 |
| `affectedChannels`    |  本次状态迁移影响的频道。 |
| `unrestoredChannels`    |  未恢复订阅或加入的频道信息，包含频道名、频道类型和频道中的临时状态数据。一般情况下为空。 |
| `isResumed`    |  在连接断开的 2 分钟内，是否从 `disconnected` 状态恢复成 `connected` 状态。`true` 表示已恢复。 |
| `timestamp`    |  本次事件通知发送的时间戳。 |

## 心跳探测 [#心跳探测-4]

心跳探测能帮助 RTM 客户端和服务器及时发现突然的网络断连，例如客户端网络掉线、网络切换等。

### MESSAGE 类型 [#message-类型-13]

默认情况下，RTM 客户端会每隔 5 秒向服务器发送一次心跳探测包（Ping 包）。如果在 10 秒内没有收到服务器的回复（Pong 包），客户端则会认为网络连接出现问题。此时，客户端的网络连接状态（`RtmLinkState`）会由 `connected` 状态切换到 `disconnected` 状态，并开始尝试重新连接。

同理，服务器如果在 15 秒内未能收到来自客户端的 Ping 包，则会认为客户端出现了网络连接问题。此时，服务器会等待一段时间（`presenceTimeout`），如果这段时间内客户端未能重新与服务器建立连接，则 RTM 服务器会向此客户端所在频道的其他用户发送 `remoteTimeout` 事件通知。

需要注意的是，以上机制只适用于客户端与服务器意外断开连接的情况，如果你是主动断开连接，例如通过调用 `logout` 方法主动登出时，则不会遵循上述机制。

RTM SDK 默认的心跳探测时间间隔为 5 秒，这是平衡了设备续航能力和意外断连识别及时性的结果。如果你的 App 需要增加电池续航或者需要更及时地识别网络断连情况，那么你可以通过 `RtmConfig` 中的 `heartbeatInterval` 参数自定义心跳探测时间间隔，支持的取值为 [5,1800] 之间的任意整数值。

设置更高的 `heartbeatInterval` 值可能会降低客户端和服务器对于意外断连识别的及时性，峰值连接数（PCU）也可能显得更高。虽然 `heartbeatInterval` 最高可以设置成 1800 秒，但是我们不建议这么做。

你可以通过以下方式自定义心跳探测时间间隔，例如修改为 30 秒：

```dart
final userId = 'your_userId';
final appId = 'your_appId';
late RtmClient rtmClient;
var privateConfig = RtmPrivateConfig(heartbeatInterval:30);
var rtmConfig = RtmConfig( privateConfig:privateConfig );

try {
    var (status,client) = await RTM(appId, userId, rtmConfig:rtmConfig);
    if (status.error == true) {
        print(status);
    } else {
        rtmClient = client;
        print('Initialize success!');
    }
} catch (e) {
    print('Failed to create RTM client: $e');
}
```

### STREAM 类型 [#stream-类型-13]

当你创建一个 Stream Channel 实例并调用 `join` 方法成功加入频道时，客户端会按 0.5 秒的间隔向服务器发送心跳包以保持活跃状态。`STREAM` 连接的心跳时间间隔不可更改，对于需要增加电池续航的应用场景，不建议使用 `stream` 服务。

<a id="heartbeatinterval-and-presencetimeout"></a>
## Heartbeat Interval 与 Presence Timeout [#heartbeat-interval-与-presence-timeout-4]

`RtmConfig` 中包含两个与时间间隔相关的参数，分别为 `heartbeatInterval` 和 `presenceTimeout`。这两个参数常常会让人感到困惑，本节主要介绍它们的含义，以便你更有效地配置 App。

- `heartbeatInterval`：客户端会定期向服务器发送证明自身处于活跃状态的 Ping-Pong 数据包，以确保服务器能够及时清除超时计数并维持连接状态。该设置会影响 PCU 计数，是 SDK 全局连接管理功能中的一个重要特性。
- `presenceTimeout`：如果在设置的 `heartbeatInterval` 时间加 10 秒冗余时间内未收到客户端的 Ping 包，服务器则会认为该客户端可能遇到了网络连接故障。考虑到此时客户端可能在尝试修复问题，为避免重复通知事件，服务器会延迟一段时间（即 `presenceTimeout` 时间）再向频道中的其他用户广播事件通知。
  - 如果客户端在 `presenceTimeout` 设置的时间内与服务器重新建立连接，则服务器不会向频道中的其他用户发送事件通知，并且用户设置的 Presence 临时状态也会被保留。
  - 如果客户端在 `presenceTimeout` 设置的时间内未能重新连接，则服务器会向频道中的其他用户广播 `remoteTimeout` 类型的 `presence` 事件通知，该客户端的临时用户状态也会被清除。当客户端重新建立连接并恢复对频道的订阅关系时，服务器也会向频道中的其他用户广播 `remoteJoinChannel` 事件通知。如果你之前设置过临时用户状态，则客户端会重新应用本地会话缓存的临时用户状态，在频道中的其他用户也会收到对应的 `remoteStateChanged` 事件通知。

对于本地用户而言，由 Presence Timeout 触发的离开与调用 `unsubscribe` 方法造成的离开都被视为频道离开行为，不同之处在于 Presence Timeout 的离开通常是由网络连接故障引起的。本地用户可能处于 `disconnected` 或 `suspended` 状态，并尝试重新连接。如果重新连接成功，频道订阅关系和临时用户状态都会被自动恢复，对于频道中的其他用户来说，该用户为重新加入的用户。调用 `unsubscribe` 被视为用户主动离开频道，客户端会清除此频道的订阅关系和临时用户状态。

例如，客户端 A 在初始化实例时设置 `heartbeatInterval` 为 10 秒、`presenceTimeout` 为 5 秒。如果 A 发出了 Ping 包，但因为网络等原因在 10 秒内没有收到 Pong 包，则 A 的连接状态会变为 `disconnected` 类型。同时，A 会尝试重新连接，服务器最多等待 25 秒。25 秒后，如果 A 未能重新连接，那么服务器会代替 A 向频道中其他用户广播 `remoteTimeout` 类型的 `presence` 事件通知；如果 A 在 25 秒内成功恢复连接，那么服务器不会广播事件通知，频道中其他用户会认为 A 一直都在频道中未曾离开。在这个例子中，从 A 掉线到其他用户被通知先后经历了 10 秒的 `heartbeatInterval`、10 秒的误差范围（以允许网络延迟）、5 秒的 `presenceTimeout`，即最长的时间间隔为 25 秒。

设定 `presenceTimeout` 时间的一个重要理由是，客户端经常遇到断开网络连接又快速恢复的情况，例如网络切换、行驶经过隧道等。如果不允许客户端尝试重新连接就立即在频道内广播用户离开或加入的情况，则会造成过多的 Presence 事件通知，极大地降低用户体验。

## 关闭连接 [#关闭连接-4]

当客户端和服务器之间不再需要连接时，你可以主动调用方法关闭连接。

### MESSAGE 类型 [#message-类型-14]

对于 `message` 类型服务，连接状态会影响峰值连接数的计量，推荐你调用 `logout` 方法显式地登出服务器，从而主动断开连接。

```dart
try {
    var (status, response) = await rtmClient.logout();
    if (status.error == true ){
       print('${status.operation} failed, errorCode: ${status.errorCode}, due to ${status.reason}');
    } else {
        print('${status.operation} success!');
    }
} catch (e) {
    print('something went wrong: $e');
}
```

:::info[信息]
一旦你主动调用 `logout` 方法断开 `MESSAGE` 连接，`STREAM` 连接也会同时断开，你将离开所有频道，客户端中所有缓存数据都会被清除。
:::

### STREAM 类型 [#stream-类型-14]

当你不再需要 `stream` 类型服务时，可以调用 `leave` 方法退出频道，从而断开 `STREAM` 连接。

```dart
try {
    var (status, response) = await stChannel.leave();
    if (status.error == true ){
       print('${status.operation} failed, errorCode: ${status.errorCode}, due to ${status.reason}');
    } else {
        print('${status.operation} success!');
    }
} catch (e) {
    print('something went wrong: $e');
}
```
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="cpp" />
客户端需要首先与 RTM 服务器建立连接，才能在后续访问 RTM 网络资源，SDK 的 API 调用以及频道中的消息和事件都通过此网络连接进行传输。虽然 RTM SDK 会帮助你建立和维护网络连接，从而极大地简化监控网络状态的业务逻辑，但充分理解 RTM 的网络连接管理机制，仍有助于你在 App 中处理可能出现的网络错误，提升用户体验。

:::info[信息]
本文仅适用于 2.2.0 及之后版本。
:::

## 创建连接 [#创建连接-5]

当客户端创建一个 RTM 实例或 Stream Channel 实例时，实际上客户端还没有真正地与服务器建立网络连接。你只是拥有了一个对象，具备在此对象中调用 API 的能力。只有当你主动调用 `login` 方法或 `join` 方法时，客户端才开始尝试与服务器建立网络连接。

RTM SDK 在很大程度上屏蔽了直接处理网络连接的种种细节，提供 `onLinkStateEvent` 事件通知向用户透传各种连接状态的迁移，你只需监听此事件即可从各种连接错误中恢复。

## 连接类型 [#连接类型-5]

在进一步探索之前，你需要了解 RTM 客户端的两种服务类型：`RTM_SERVICE_TYPE_MESSAGE` 服务和 `RTM_SERVICE_TYPE_STREAM` 服务。一个客户端中可能会用到一种或两种服务，这两种服务对应的连接类型有所不同。

### MESSAGE 类型 [#message-类型-15]

`MESSAGE` 服务为用户提供 Message Channel、User Channel、Presence、Storage、Lock、Token 等服务，这些服务的消息、数据、事件通知等共享此连接。这条连接在客户端生命周期内唯一存在，在你主动调用 `login` 方法登录服务器时建立，在你主动调用 `logout` 方法登出服务器时结束。

你可以通过以下方式建立 `MESSAGE` 连接：

```cpp
uint64_t requestId;
rtmClient->login("token", requestId);
```

调用该方法后，SDK 会触发 `onLoginResult` 回调并返回 API 调用结果。

```cpp
// 异步回调
class RtmEventHandler : public IRtmEventHandler {
    void onLoginResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) {
        if (errorCode != RTM_ERROR_OK) {
            printf("login rtm failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode));
        } else {
            printf("login rtm success\n");
        }
    }
};
```

:::info[信息]
建立 `MESSAGE` 连接会增加峰值连接数计量。
:::

`MESSAGE` 连接是长连接，客户端会定时向服务器发送心跳包以保持活跃状态。当你订阅多个 Message Channel 或 User Channel 时，你将通过此连接接受所有频道消息或事件通知。

### STREAM 类型 [#stream-类型-15]

`RTM_SERVICE_TYPE_STREAM` 服务为用户提供 Stream Channel 的所有能力。与 `MESSAGE` 连接不同，`STREAM` 连接并不具备全局唯一性，而是每个 Stream Channel 实例都会单独创建一个连接。例如，当你创建 3 个 Stream Channel 实例并调用 `join` 方法加入后，客户端会与服务器建立 3 条网络连接。

在建立 `STREAM` 连接前，你需要先建立 `MESSAGE` 连接，即调用 `login` 方法并成功登录服务器。`STREAM` 连接的生命周期从你主动调用 `join` 方法加入频道时开始，到你主动调用 `leave` 方法离开频道时结束。你可以通过以下方式建立 `STREAM` 连接：

```cpp
JoinChannelOptions options;
options.token = "yourToken";
options.withPresence = true;
options.withMetadata = false;
options.withLock = false;

uint64_t requestId;
streamChannel.join(options, requestId);
```

调用该方法后，SDK 会触发 `onJoinResult` 回调并返回 API 调用结果。

```cpp
// 异步回调
class RtmEventHandler : public IRtmEventHandler {
    void onJoinResult(const uint64_t requestId, const char *channelName, const char *userId, RTM_ERROR_CODE errorCode) {
        if (errorCode != RTM_ERROR_OK) {
            printf("join rtm channel failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode));
        } else {
            printf("join rtm channel %s success\n", channelName);
        }
    }
};
```

:::info[信息]
建立 `STREAM` 连接不会增加峰值连接数计量。
:::

`STREAM` 连接也是长连接，客户端会定时向服务器发送心跳包以保持活跃状态，发送心跳包的时间间隔是 0.5 秒且不可更改。对于需要优化电池寿命的应用场景，不建议使用 `RTM_SERVICE_TYPE_STREAM` 服务。

## 监控连接 [#监控连接-5]

客户端通过 `onLinkStateEvent` 事件通知监控和管理网络连接状态的迁移。通过监听此事件，你可以随时知道以下信息：

- 当前网络连接处于何种状态。
- 何种原因或操作导致状态迁移。
- 迁移之前的网络状态是什么。
- 网络连接状态何时发生迁移。
- 什么服务类型的连接状态发生了迁移。
- 网络状态迁移影响了哪些已加入的频道。
- 哪些之前加入的频道没有成功恢复网络连接。

以上信息对于客户端从意外断开状态恢复到连接状态后的业务处理将非常有帮助，用户能更快速地定位和排除由于网络连接变化产生的故障及恢复业务现场。

你可以通过以下方式监听 `onLinkStateEvent` 事件通知：

```cpp
class RtmEventHandler : public IRtmEventHandler {
    void onLinkStateEvent(const LinkStateEvent& event) override {}

    // ...
};

RtmConfig config;
config.eventHandler = new RtmEventHandler();
```

其中各字段的含义如下：

| 属性        |   描述  |
|---|-------- |
| `currentState`            |  当前的连接状态。   |
| `previousState`            |  之前的连接状态。   |
| `serviceType`    |  服务类型。可能是 `RTM_SERVICE_TYPE_MESSAGE` 或 `RTM_SERVICE_TYPE_STREAM`。 |
| `operation`    | 触发本次状态迁移的操作。 |
| `reasonCode`    | 触发本次状态迁移的原因。 |
| `affectedChannels`    |  本次状态迁移影响的频道。 |
| `unrestoredChannels`    |  未恢复订阅或加入的频道信息，包含频道名、频道类型和频道中的临时状态数据。一般情况下为空。 |
| `isResumed`    |  在连接断开的 2 分钟内，是否从 `RTM_LINK_STATE_DISCONNECTED` 状态恢复成 `RTM_LINK_STATE_CONNECTED` 状态。`true` 表示已恢复。 |
| `timestamp`    |  本次事件通知发送的时间戳。 |

## 心跳探测 [#心跳探测-5]

心跳探测能帮助 RTM 客户端和服务器及时发现突然的网络断连，例如客户端网络掉线、网络切换等。

### MESSAGE 类型 [#message-类型-16]

默认情况下，RTM 客户端会每隔 5 秒向服务器发送一次心跳探测包（Ping 包）。如果在 10 秒内没有收到服务器的回复（Pong 包），客户端则会认为网络连接出现问题。此时，客户端的网络连接状态（`RTM_LINK_STATE`）会由 `RTM_LINK_STATE_CONNECTED` 状态切换到 `RTM_LINK_STATE_DISCONNECTED` 状态，并开始尝试重新连接。

同理，服务器如果在 15 秒内未能收到来自客户端的 Ping 包，则会认为客户端出现了网络连接问题。此时，服务器会等待一段时间（`presenceTimeout`），如果这段时间内客户端未能重新与服务器建立连接，则 RTM 服务器会向此客户端所在频道的其他用户发送 `RTM_PRESENCE_EVENT_TYPE_REMOTE_TIMEOUT` 事件通知。

需要注意的是，以上机制只适用于客户端与服务器意外断开连接的情况，如果你是主动断开连接，例如通过调用 `logout` 方法主动登出时，则不会遵循上述机制。

RTM SDK 默认的心跳探测时间间隔为 5 秒，这是平衡了设备续航能力和意外断连识别及时性的结果。如果你的 App 需要增加电池续航或者需要更及时地识别网络断连情况，那么你可以通过 `RtmConfig` 中的 `heartbeatInterval` 参数自定义心跳探测时间间隔，支持的取值为 [5,1800] 之间的任意整数值。

设置更高的 `heartbeatInterval` 值可能会降低客户端和服务器对于意外断连识别的及时性，峰值连接数（PCU）也可能显得更高。虽然 `heartbeatInterval` 最高可以设置成 1800 秒，但是我们不建议这么做。

你可以通过以下方式自定义心跳探测时间间隔，例如修改为 30 秒：

```cpp
RtmConfig config;
config.appId = "appid";
config.userId = "userId";
config.heartbeatInterval = 30;
config.eventHandler = new RtmEventHandler();

int errorCode = 0;
IRtmClient* rtmClient = createAgoraRtmClient(config, errorCode);
```

### STREAM 类型 [#stream-类型-16]

当你创建一个 Stream Channel 实例并调用 `join` 方法成功加入频道时，客户端会按 0.5 秒的间隔向服务器发送心跳包以保持活跃状态。`STREAM` 连接的心跳时间间隔不可更改，对于需要增加电池续航的应用场景，不建议使用 `RTM_SERVICE_TYPE_STREAM` 服务。

<a id="heartbeatinterval-and-presencetimeout"></a>
## Heartbeat Interval 与 Presence Timeout [#heartbeat-interval-与-presence-timeout-5]

`RtmConfig` 中包含两个与时间间隔相关的参数，分别为 `heartbeatInterval` 和 `presenceTimeout`。这两个参数常常会让人感到困惑，本节主要介绍它们的含义，以便你更有效地配置 App。

- `heartbeatInterval`：客户端会定期向服务器发送证明自身处于活跃状态的 Ping-Pong 数据包，以确保服务器能够及时清除超时计数并维持连接状态。该设置会影响 PCU 计数，是 SDK 全局连接管理功能中的一个重要特性。
- `presenceTimeout`：如果在设置的 `heartbeatInterval` 时间加 10 秒冗余时间内未收到客户端的 Ping 包，服务器则会认为该客户端可能遇到了网络连接故障。考虑到此时客户端可能在尝试修复问题，为避免重复通知事件，服务器会延迟一段时间（即 `presenceTimeout` 时间）再向频道中的其他用户广播事件通知。
  - 如果客户端在 `presenceTimeout` 设置的时间内与服务器重新建立连接，则服务器不会向频道中的其他用户发送事件通知，并且用户设置的 Presence 临时状态也会被保留。
  - 如果客户端在 `presenceTimeout` 设置的时间内未能重新连接，则服务器会向频道中的其他用户广播 `RTM_PRESENCE_EVENT_TYPE_REMOTE_TIMEOUT` 类型的 `onPresenceEvent` 事件通知，该客户端的临时用户状态也会被清除。当客户端重新建立连接并恢复对频道的订阅关系时，服务器也会向频道中的其他用户广播 `RTM_PRESENCE_EVENT_TYPE_REMOTE_JOIN_CHANNEL` 事件通知。如果你之前设置过临时用户状态，则客户端会重新应用本地会话缓存的临时用户状态，在频道中的其他用户也会收到对应的 `RTM_PRESENCE_EVENT_TYPE_REMOTE_STATE_CHANGED` 事件通知。

对于本地用户而言，由 Presence Timeout 触发的离开与调用 `unsubscribe` 方法造成的离开都被视为频道离开行为，不同之处在于 Presence Timeout 的离开通常是由网络连接故障引起的。本地用户可能处于 `RTM_LINK_STATE_DISCONNECTED` 或 `RTM_LINK_STATE_SUSPENDED` 状态，并尝试重新连接。如果重新连接成功，频道订阅关系和临时用户状态都会被自动恢复，对于频道中的其他用户来说，该用户为重新加入的用户。调用 `unsubscribe` 被视为用户主动离开频道，客户端会清除此频道的订阅关系和临时用户状态。

例如，客户端 A 在初始化实例时设置 `heartbeatInterval` 为 10 秒、`presenceTimeout` 为 5 秒。如果 A 发出了 Ping 包，但因为网络等原因在 10 秒内没有收到 Pong 包，则 A 的连接状态会变为 `RTM_LINK_STATE_DISCONNECTED` 类型。同时，A 会尝试重新连接，服务器最多等待 25 秒。25 秒后，如果 A 未能重新连接，那么服务器会代替 A 向频道中其他用户广播 `RTM_PRESENCE_EVENT_TYPE_REMOTE_TIMEOUT` 类型的 `onPresenceEvent` 事件通知；如果 A 在 25 秒内成功恢复连接，那么服务器不会广播事件通知，频道中其他用户会认为 A 一直都在频道中未曾离开。在这个例子中，从 A 掉线到其他用户被通知先后经历了 10 秒的 `heartbeatInterval`、10 秒的误差范围（以允许网络延迟）、5 秒的 `presenceTimeout`，即最长的时间间隔为 25 秒。

设定 `presenceTimeout` 时间的一个重要理由是，客户端经常遇到断开网络连接又快速恢复的情况，例如网络切换、行驶经过隧道等。如果不允许客户端尝试重新连接就立即在频道内广播用户离开或加入的情况，则会造成过多的 Presence 事件通知，极大地降低用户体验。

## 关闭连接 [#关闭连接-5]

当客户端和服务器之间不再需要连接时，你可以主动调用方法关闭连接。

### MESSAGE 类型 [#message-类型-17]

对于 `RTM_SERVICE_TYPE_MESSAGE` 类型服务，连接状态会影响峰值连接数的计量，推荐你调用 `logout` 方法显式地登出服务器，从而主动断开连接。

```cpp
uint64_t requestId = 0;
rtmClient->logout(requestId);
```

调用该方法后，SDK 会触发 `onLogoutResult` 回调并返回 API 调用结果。

```cpp
// 异步回调
class RtmEventHandler : public IRtmEventHandler {
    void onLogoutResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) {
        if (errorCode != RTM_ERROR_OK) {
          // logout failed
        } else {
          // logout success
        }
    }

    // ...
};
```

:::info[信息]
一旦你主动调用 `logout` 方法断开 `MESSAGE` 连接，`STREAM` 连接也会同时断开，你将离开所有频道，客户端中所有缓存数据都会被清除。
:::

### STREAM 类型 [#stream-类型-17]

当你不再需要 `RTM_SERVICE_TYPE_STREAM` 类型服务时，可以调用 `leave` 方法退出频道，从而断开 `STREAM` 连接。

```cpp
uint64_t requestId;
streamChannel.leave(requestId);
```

调用该方法后，SDK 会触发 `onLeaveResult` 回调并返回 API 调用结果。

```cpp
// 异步回调
class RtmEventHandler : public IRtmEventHandler {
    void onLeaveResult(const uint64_t requestId, const char *channelName, const char *userId, RTM_ERROR_CODE errorCode) {
        if (errorCode != RTM_ERROR_OK) {
            printf("leave rtm channel failed error is %d reason is %s\n", errorCode, getErrorReason(errorCode));
        } else {
            printf("leave rtm channel %s success\n", channelName);
        }
    }
};
```
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="swift" />
客户端需要首先与 RTM 服务器建立连接，才能在后续访问 RTM 网络资源，SDK 的 API 调用以及频道中的消息和事件都通过此网络连接进行传输。虽然 RTM SDK 会帮助你建立和维护网络连接，从而极大地简化监控网络状态的业务逻辑，但充分理解 RTM 的网络连接管理机制，仍有助于你在 App 中处理可能出现的网络错误，提升用户体验。

:::info[信息]
本文仅适用于 2.2.0 及之后版本。
:::

## 创建连接 [#创建连接-6]

当客户端创建一个 RTM 实例或 Stream Channel 实例时，实际上客户端还没有真正地与服务器建立网络连接。你只是拥有了一个对象，具备在此对象中调用 API 的能力。只有当你主动调用 `login` 方法或 `join` 方法时，客户端才开始尝试与服务器建立网络连接。

RTM SDK 在很大程度上屏蔽了直接处理网络连接的种种细节，提供 `didReceiveLinkStateEvent` 事件通知向用户透传各种连接状态的迁移，你只需监听此事件即可从各种连接错误中恢复。

## 连接类型 [#连接类型-6]

在进一步探索之前，你需要了解 RTM 客户端的两种服务类型：`message` 服务和 `stream` 服务。一个客户端中可能会用到一种或两种服务，这两种服务对应的连接类型有所不同。

### MESSAGE 类型 [#message-类型-18]

`MESSAGE` 服务为用户提供 Message Channel、User Channel、Presence、Storage、Lock、Token 等服务，这些服务的消息、数据、事件通知等共享此连接。这条连接在客户端生命周期内唯一存在，在你主动调用 `login` 方法登录服务器时建立，在你主动调用 `logout` 方法登出服务器时结束。

你可以通过以下方式建立 `MESSAGE` 连接：

```swift
rtm.login(token) {res, error in
    if error != nil {
        print("\(error?.operation) failed! error reason is \(error?.reason)")
    } else {
        print("success")
    }
}
```

:::info[信息]
建立 `MESSAGE` 连接会增加峰值连接数计量。
:::

`MESSAGE` 连接是长连接，客户端会定时向服务器发送心跳包以保持活跃状态。当你订阅多个 Message Channel 或 User Channel 时，你将通过此连接接受所有频道消息或事件通知。

### STREAM 类型 [#stream-类型-18]

`stream` 服务为用户提供 Stream Channel 的所有能力。与 `MESSAGE` 连接不同，`STREAM` 连接并不具备全局唯一性，而是每个 Stream Channel 实例都会单独创建一个连接。例如，当你创建 3 个 Stream Channel 实例并调用 `join` 方法加入后，客户端会与服务器建立 3 条网络连接。

在建立 `STREAM` 连接前，你需要先建立 `MESSAGE` 连接，即调用 `login` 方法并成功登录服务器。`STREAM` 连接的生命周期从你主动调用 `join` 方法加入频道时开始，到你主动调用 `leave` 方法离开频道时结束。你可以通过以下方式建立 `STREAM` 连接：

```swift
let join_option = AgoraRtmJoinChannelOption();
join_option.token = "your_token"
join_option.features = [.presence, .storage]
stream_channel.join(join_option, completion: { (res, error) in
    if error != nil {
        print("\(error?.operation) failed! error reason is \(error?.reason)")
    } else {
        print("success")
    }
})
```

:::info[信息]
建立 `STREAM` 连接不会增加峰值连接数计量。
:::

`STREAM` 连接也是长连接，客户端会定时向服务器发送心跳包以保持活跃状态，发送心跳包的时间间隔是 0.5 秒且不可更改。对于需要优化电池寿命的应用场景，不建议使用 `stream` 服务。

## 监控连接 [#监控连接-6]

客户端通过 `didReceiveLinkStateEvent` 事件通知监控和管理网络连接状态的迁移。通过监听此事件，你可以随时知道以下信息：

- 当前网络连接处于何种状态。
- 何种原因或操作导致状态迁移。
- 迁移之前的网络状态是什么。
- 网络连接状态何时发生迁移。
- 什么服务类型的连接状态发生了迁移。
- 网络状态迁移影响了哪些已加入的频道。
- 哪些之前加入的频道没有成功恢复网络连接。

以上信息对于客户端从意外断开状态恢复到连接状态后的业务处理将非常有帮助，用户能更快速地定位和排除由于网络连接变化产生的故障及恢复业务现场。

你可以通过以下方式监听 `didReceiveLinkStateEvent` 事件通知：

```swift
class RtmListenerEx: NSObject, AgoraRtmClientDelegate {
    func rtmKit(_ rtmKit: AgoraRtmClientKit, didReceiveLinkStateEvent event: AgoraRtmLinkStateEvent) {
        // code for handle link state change
    }
}

let listenerEx = RtmListenerEx()
rtm.addDelegate(listenerEx)
```

其中各字段的含义如下：

| 属性        |   描述  |
|---|-------- |
| `currentState`            |  当前的连接状态。   |
| `previousState`            |  之前的连接状态。   |
| `serviceType`    |  服务类型。可能是 `message` 或 `stream`。 |
| `operation`    | 触发本次状态迁移的操作。 |
| `reasonCode`    | 触发本次状态迁移的原因。 |
| `affectedChannels`    |  本次状态迁移影响的频道。 |
| `unrestoredChannels`    |  未恢复订阅或加入的频道信息，包含频道名、频道类型和频道中的临时状态数据。一般情况下为空。 |
| `isResumed`    |  在连接断开的 2 分钟内，是否从 `disconnected` 状态恢复成 `connected` 状态。`true` 表示已恢复。 |
| `timestamp`    |  本次事件通知发送的时间戳。 |

## 心跳探测 [#心跳探测-6]

心跳探测能帮助 RTM 客户端和服务器及时发现突然的网络断连，例如客户端网络掉线、网络切换等。

### MESSAGE 类型 [#message-类型-19]

默认情况下，RTM 客户端会每隔 5 秒向服务器发送一次心跳探测包（Ping 包）。如果在 10 秒内没有收到服务器的回复（Pong 包），客户端则会认为网络连接出现问题。此时，客户端的网络连接状态（`AgoraRtmLinkState`）会由 `connected` 状态切换到 `disconnected` 状态，并开始尝试重新连接。

同理，服务器如果在 15 秒内未能收到来自客户端的 Ping 包，则会认为客户端出现了网络连接问题。此时，服务器会等待一段时间（`presenceTimeout`），如果这段时间内客户端未能重新与服务器建立连接，则 RTM 服务器会向此客户端所在频道的其他用户发送 `remoteConnectionTimeout` 事件通知。

需要注意的是，以上机制只适用于客户端与服务器意外断开连接的情况，如果你是主动断开连接，例如通过调用 `logout` 方法主动登出时，则不会遵循上述机制。

RTM SDK 默认的心跳探测时间间隔为 5 秒，这是平衡了设备续航能力和意外断连识别及时性的结果。如果你的 App 需要增加电池续航或者需要更及时地识别网络断连情况，那么你可以通过 `AgoraRtmClientConfig` 中的 `heartbeatInterval` 参数自定义心跳探测时间间隔，支持的取值为 [5,1800] 之间的任意整数值。

设置更高的 `heartbeatInterval` 值可能会降低客户端和服务器对于意外断连识别的及时性，峰值连接数（PCU）也可能显得更高。虽然 `heartbeatInterval` 最高可以设置成 1800 秒，但是我们不建议这么做。

你可以通过以下方式自定义心跳探测时间间隔，例如修改为 30 秒：

```swift
let rtm_config = AgoraRtmClientConfig(appId: "yourAppId", userId: "uniqueUserId")
rtm_config.heartbeatInterval = 30
```

### STREAM 类型 [#stream-类型-19]

当你创建一个 Stream Channel 实例并调用 `join` 方法成功加入频道时，客户端会按 0.5 秒的间隔向服务器发送心跳包以保持活跃状态。`STREAM` 连接的心跳时间间隔不可更改，对于需要增加电池续航的应用场景，不建议使用 `stream` 服务。

<a id="heartbeatinterval-and-presencetimeout"></a>
## Heartbeat Interval 与 Presence Timeout [#heartbeat-interval-与-presence-timeout-6]

`AgoraRtmClientConfig` 中包含两个与时间间隔相关的参数，分别为 `heartbeatInterval` 和 `presenceTimeout`。这两个参数常常会让人感到困惑，本节主要介绍它们的含义，以便你更有效地配置 App。

- `heartbeatInterval`：客户端会定期向服务器发送证明自身处于活跃状态的 Ping-Pong 数据包，以确保服务器能够及时清除超时计数并维持连接状态。该设置会影响 PCU 计数，是 SDK 全局连接管理功能中的一个重要特性。
- `presenceTimeout`：如果在设置的 `heartbeatInterval` 时间加 10 秒冗余时间内未收到客户端的 Ping 包，服务器则会认为该客户端可能遇到了网络连接故障。考虑到此时客户端可能在尝试修复问题，为避免重复通知事件，服务器会延迟一段时间（即 `presenceTimeout` 时间）再向频道中的其他用户广播事件通知。
  - 如果客户端在 `presenceTimeout` 设置的时间内与服务器重新建立连接，则服务器不会向频道中的其他用户发送事件通知，并且用户设置的 Presence 临时状态也会被保留。
  - 如果客户端在 `presenceTimeout` 设置的时间内未能重新连接，则服务器会向频道中的其他用户广播 `remoteConnectionTimeout` 类型的 `didReceivePresenceEvent` 事件通知，该客户端的临时用户状态也会被清除。当客户端重新建立连接并恢复对频道的订阅关系时，服务器也会向频道中的其他用户广播 `remoteJoinChannel` 事件通知。如果你之前设置过临时用户状态，则客户端会重新应用本地会话缓存的临时用户状态，在频道中的其他用户也会收到对应的 `remoteStateChanged` 事件通知。

对于本地用户而言，由 Presence Timeout 触发的离开与调用 `unsubscribe` 方法造成的离开都被视为频道离开行为，不同之处在于 Presence Timeout 的离开通常是由网络连接故障引起的。本地用户可能处于 `disconnected` 或 `suspended` 状态，并尝试重新连接。如果重新连接成功，频道订阅关系和临时用户状态都会被自动恢复，对于频道中的其他用户来说，该用户为重新加入的用户。调用 `unsubscribe` 被视为用户主动离开频道，客户端会清除此频道的订阅关系和临时用户状态。

例如，客户端 A 在初始化实例时设置 `heartbeatInterval` 为 10 秒、`presenceTimeout` 为 5 秒。如果 A 发出了 Ping 包，但因为网络等原因在 10 秒内没有收到 Pong 包，则 A 的连接状态会变为 `disconnected` 类型。同时，A 会尝试重新连接，服务器最多等待 25 秒。25 秒后，如果 A 未能重新连接，那么服务器会代替 A 向频道中其他用户广播 `remoteConnectionTimeout` 类型的 `didReceivePresenceEvent` 事件通知；如果 A 在 25 秒内成功恢复连接，那么服务器不会广播事件通知，频道中其他用户会认为 A 一直都在频道中未曾离开。在这个例子中，从 A 掉线到其他用户被通知先后经历了 10 秒的 `heartbeatInterval`、10 秒的误差范围（以允许网络延迟）、5 秒的 `presenceTimeout`，即最长的时间间隔为 25 秒。

设定 `presenceTimeout` 时间的一个重要理由是，客户端经常遇到断开网络连接又快速恢复的情况，例如网络切换、行驶经过隧道等。如果不允许客户端尝试重新连接就立即在频道内广播用户离开或加入的情况，则会造成过多的 Presence 事件通知，极大地降低用户体验。

## 关闭连接 [#关闭连接-6]

当客户端和服务器之间不再需要连接时，你可以主动调用方法关闭连接。

### MESSAGE 类型 [#message-类型-20]

对于 `message` 类型服务，连接状态会影响峰值连接数的计量，推荐你调用 `logout` 方法显式地登出服务器，从而主动断开连接。

```swift
rtm.logout() {res, error in
    if error != nil {
        print("\(error?.operation) failed! error reason is \(error?.reason)")
    } else {
        print("success")
    }
}
```

:::info[信息]
一旦你主动调用 `logout` 方法断开 `MESSAGE` 连接，`STREAM` 连接也会同时断开，你将离开所有频道，客户端中所有缓存数据都会被清除。
:::

### STREAM 类型 [#stream-类型-20]

当你不再需要 `stream` 类型服务时，可以调用 `leave` 方法退出频道，从而断开 `STREAM` 连接。

```swift
stream_channel.leave({ res, error in
    if error != nil {
        print("\(error?.operation) failed! error reason is \(error?.reason)")
    } else {
        print("success")
    }
})
```
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="mini-program" />
客户端需要首先与 RTM 服务器建立连接，才能在后续访问 RTM 网络资源，SDK 的 API 调用以及频道中的消息和事件都通过此网络连接进行传输。虽然 RTM SDK 会帮助你建立和维护网络连接，从而极大地简化监控网络状态的业务逻辑，但充分理解 RTM 的网络连接管理机制，仍有助于你在 App 中处理可能出现的网络错误，提升用户体验。

:::info[信息]
本文仅适用于 2.2.0 及之后版本。
:::

## 创建连接 [#创建连接-7]

当客户端创建一个 RTM 实例或 Stream Channel 实例时，实际上客户端还没有真正地与服务器建立网络连接。你只是拥有了一个对象，具备在此对象中调用 API 的能力。只有当你主动调用 `login` 方法或 `join` 方法时，客户端才开始尝试与服务器建立网络连接。

RTM SDK 在很大程度上屏蔽了直接处理网络连接的种种细节，提供 `linkState` 事件通知向用户透传各种连接状态的迁移，你只需监听此事件即可从各种连接错误中恢复。

## 连接类型 [#连接类型-7]

在进一步探索之前，你需要了解 RTM 客户端的两种服务类型：`MESSAGE` 服务和 `STREAM` 服务。一个客户端中可能会用到一种或两种服务，这两种服务对应的连接类型有所不同。

### MESSAGE 类型 [#message-类型-21]

`MESSAGE` 服务为用户提供 Message Channel、User Channel、Presence、Storage、Lock、Token 等服务，这些服务的消息、数据、事件通知等共享此连接。这条连接在客户端生命周期内唯一存在，在你主动调用 `login` 方法登录服务器时建立，在你主动调用 `logout` 方法登出服务器时结束。

你可以通过以下方式建立 `MESSAGE` 连接：

```javascript
try {
  const result = await rtm.login({ token: "your_token" });
  console.log(result);
} catch (status) {
  console.log(status);
}
```

:::info[信息]
建立 `MESSAGE` 连接会增加峰值连接数计量。
:::

`MESSAGE` 连接是长连接，客户端会定时向服务器发送心跳包以保持活跃状态。当你订阅多个 Message Channel 或 User Channel 时，你将通过此连接接受所有频道消息或事件通知。

### STREAM 类型 [#stream-类型-21]

`STREAM` 服务为用户提供 Stream Channel 的所有能力。与 `MESSAGE` 连接不同，`STREAM` 连接并不具备全局唯一性，而是每个 Stream Channel 实例都会单独创建一个连接。例如，当你创建 3 个 Stream Channel 实例并调用 `join` 方法加入后，客户端会与服务器建立 3 条网络连接。

在建立 `STREAM` 连接前，你需要先建立 `MESSAGE` 连接，即调用 `login` 方法并成功登录服务器。`STREAM` 连接的生命周期从你主动调用 `join` 方法加入频道时开始，到你主动调用 `leave` 方法离开频道时结束。你可以通过以下方式建立 `STREAM` 连接：

```javascript
const options = {
  token: "yourToken",
  withPresence: true,
  withMetadata: false,
  withLock: false
};
try {
  const result = await streamChannel.join(options);
  console.log(result);
} catch (status) {
  console.log(status);
}
```

:::info[信息]
建立 `STREAM` 连接不会增加峰值连接数计量。
:::

`STREAM` 连接也是长连接，客户端会定时向服务器发送心跳包以保持活跃状态，发送心跳包的时间间隔是 0.5 秒且不可更改。对于需要优化电池寿命的应用场景，不建议使用 `STREAM` 服务。

## 监控连接 [#监控连接-7]

客户端通过 `linkState` 事件通知监控和管理网络连接状态的迁移。通过监听此事件，你可以随时知道以下信息：

- 当前网络连接处于何种状态。
- 何种原因或操作导致状态迁移。
- 迁移之前的网络状态是什么。
- 网络连接状态何时发生迁移。
- 什么服务类型的连接状态发生了迁移。
- 网络状态迁移影响了哪些已加入的频道。
- 哪些之前加入的频道没有成功恢复网络连接。

以上信息对于客户端从意外断开状态恢复到连接状态后的业务处理将非常有帮助，用户能更快速地定位和排除由于网络连接变化产生的故障及恢复业务现场。

你可以通过以下方式监听 `linkState` 事件通知：

```javascript
rtm.addEventListener('linkState', event => {
  const currentState = event.currentState;
  const previousState = event.previousState;
  const serviceType = event.serviceType;
  const operation = event.operation;
  const reason = event.reason;
  const reasonCode = event.reasonCode;
  const affectedChannels = event.affectedChannels;
  const timestamp = event.timestamp;
  const isResumed = event.isResumed;
});
```

其中各字段的含义如下：

| 属性        |   描述  |
|---|-------- |
| `currentState`            |  当前的连接状态。   |
| `previousState`            |  之前的连接状态。   |
| `serviceType`    |  服务类型。可能是 `MESSAGE` 或 `STREAM`。 |
| `operation`    | 触发本次状态迁移的操作。 |
| `reasonCode`    | 触发本次状态迁移的原因。 |
| `affectedChannels`    |  本次状态迁移影响的频道。 |
| `unrestoredChannels`    |  未恢复订阅或加入的频道信息，包含频道名、频道类型和频道中的临时状态数据。一般情况下为空。 |
| `isResumed`    |  在连接断开的 2 分钟内，是否从 `DISCONNECTED` 状态恢复成 `CONNECTED` 状态。`true` 表示已恢复。 |
| `timestamp`    |  本次事件通知发送的时间戳。 |

## 心跳探测 [#心跳探测-7]

心跳探测能帮助 RTM 客户端和服务器及时发现突然的网络断连，例如客户端网络掉线、网络切换等。

### MESSAGE 类型 [#message-类型-22]

默认情况下，RTM 客户端会每隔 5 秒向服务器发送一次心跳探测包（Ping 包）。如果在 10 秒内没有收到服务器的回复（Pong 包），客户端则会认为网络连接出现问题。此时，客户端的网络连接状态（`RtmLinkState`）会由 `CONNECTED` 状态切换到 `DISCONNECTED` 状态，并开始尝试重新连接。

同理，服务器如果在 15 秒内未能收到来自客户端的 Ping 包，则会认为客户端出现了网络连接问题。此时，服务器会等待一段时间（`presenceTimeout`），如果这段时间内客户端未能重新与服务器建立连接，则 RTM 服务器会向此客户端所在频道的其他用户发送 `REMOTE_TIMEOUT` 事件通知。

需要注意的是，以上机制只适用于客户端与服务器意外断开连接的情况，如果你是主动断开连接，例如通过调用 `logout` 方法主动登出时，则不会遵循上述机制。

RTM SDK 默认的心跳探测时间间隔为 5 秒，这是平衡了设备续航能力和意外断连识别及时性的结果。如果你的 App 需要增加电池续航或者需要更及时地识别网络断连情况，那么你可以通过 `RTMConfig` 中的 `heartbeatInterval` 参数自定义心跳探测时间间隔，支持的取值为 [5,1800] 之间的任意整数值。

设置更高的 `heartbeatInterval` 值可能会降低客户端和服务器对于意外断连识别的及时性，峰值连接数（PCU）也可能显得更高。虽然 `heartbeatInterval` 最高可以设置成 1800 秒，但是我们不建议这么做。

你可以通过以下方式自定义心跳探测时间间隔，例如修改为 30 秒：

```javascript
const rtmConfig = { heartbeatInterval: 30 };
const rtm = new RTM("yourAppId", "Tony", rtmConfig);
```

### STREAM 类型 [#stream-类型-22]

当你创建一个 Stream Channel 实例并调用 `join` 方法成功加入频道时，客户端会按 0.5 秒的间隔向服务器发送心跳包以保持活跃状态。`STREAM` 连接的心跳时间间隔不可更改，对于需要增加电池续航的应用场景，不建议使用 `STREAM` 服务。

<a id="heartbeatinterval-and-presencetimeout"></a>
## Heartbeat Interval 与 Presence Timeout [#heartbeat-interval-与-presence-timeout-7]

`RTMConfig` 中包含两个与时间间隔相关的参数，分别为 `heartbeatInterval` 和 `presenceTimeout`。这两个参数常常会让人感到困惑，本节主要介绍它们的含义，以便你更有效地配置 App。

- `heartbeatInterval`：客户端会定期向服务器发送证明自身处于活跃状态的 Ping-Pong 数据包，以确保服务器能够及时清除超时计数并维持连接状态。该设置会影响 PCU 计数，是 SDK 全局连接管理功能中的一个重要特性。
- `presenceTimeout`：如果在设置的 `heartbeatInterval` 时间加 10 秒冗余时间内未收到客户端的 Ping 包，服务器则会认为该客户端可能遇到了网络连接故障。考虑到此时客户端可能在尝试修复问题，为避免重复通知事件，服务器会延迟一段时间（即 `presenceTimeout` 时间）再向频道中的其他用户广播事件通知。
  - 如果客户端在 `presenceTimeout` 设置的时间内与服务器重新建立连接，则服务器不会向频道中的其他用户发送事件通知，并且用户设置的 Presence 临时状态也会被保留。
  - 如果客户端在 `presenceTimeout` 设置的时间内未能重新连接，则服务器会向频道中的其他用户广播 `REMOTE_TIMEOUT` 类型的 `presence` 事件通知，该客户端的临时用户状态也会被清除。当客户端重新建立连接并恢复对频道的订阅关系时，服务器也会向频道中的其他用户广播 `REMOTE_JOIN_CHANNEL` 事件通知。如果你之前设置过临时用户状态，则客户端会重新应用本地会话缓存的临时用户状态，在频道中的其他用户也会收到对应的 `REMOTE_STATE_CHANGED` 事件通知。

对于本地用户而言，由 Presence Timeout 触发的离开与调用 `unsubscribe` 方法造成的离开都被视为频道离开行为，不同之处在于 Presence Timeout 的离开通常是由网络连接故障引起的。本地用户可能处于 `DISCONNECTED` 或 `SUSPENDED` 状态，并尝试重新连接。如果重新连接成功，频道订阅关系和临时用户状态都会被自动恢复，对于频道中的其他用户来说，该用户为重新加入的用户。调用 `unsubscribe` 被视为用户主动离开频道，客户端会清除此频道的订阅关系和临时用户状态。

例如，客户端 A 在初始化实例时设置 `heartbeatInterval` 为 10 秒、`presenceTimeout` 为 5 秒。如果 A 发出了 Ping 包，但因为网络等原因在 10 秒内没有收到 Pong 包，则 A 的连接状态会变为 `DISCONNECTED` 类型。同时，A 会尝试重新连接，服务器最多等待 25 秒。25 秒后，如果 A 未能重新连接，那么服务器会代替 A 向频道中其他用户广播 `REMOTE_TIMEOUT` 类型的 `presence` 事件通知；如果 A 在 25 秒内成功恢复连接，那么服务器不会广播事件通知，频道中其他用户会认为 A 一直都在频道中未曾离开。在这个例子中，从 A 掉线到其他用户被通知先后经历了 10 秒的 `heartbeatInterval`、10 秒的误差范围（以允许网络延迟）、5 秒的 `presenceTimeout`，即最长的时间间隔为 25 秒。

设定 `presenceTimeout` 时间的一个重要理由是，客户端经常遇到断开网络连接又快速恢复的情况，例如网络切换、行驶经过隧道等。如果不允许客户端尝试重新连接就立即在频道内广播用户离开或加入的情况，则会造成过多的 Presence 事件通知，极大地降低用户体验。

## 关闭连接 [#关闭连接-7]

当客户端和服务器之间不再需要连接时，你可以主动调用方法关闭连接。

### MESSAGE 类型 [#message-类型-23]

对于 `MESSAGE` 类型服务，连接状态会影响峰值连接数的计量，推荐你调用 `logout` 方法显式地登出服务器，从而主动断开连接。

```javascript
try {
  const result = await rtm.logout();
  console.log(result);
} catch (status) {
  console.log(status);
}
```

:::info[信息]
一旦你主动调用 `logout` 方法断开 `MESSAGE` 连接，`STREAM` 连接也会同时断开，你将离开所有频道，客户端中所有缓存数据都会被清除。
:::

### STREAM 类型 [#stream-类型-23]

当你不再需要 `STREAM` 类型服务时，可以调用 `leave` 方法退出频道，从而断开 `STREAM` 连接。

```javascript
try {
  const result = await streamChannel.leave();
  console.log(result);
} catch (status) {
  console.log(status);
}
```
<_PlatformProcessedMarker close="true" />
</_PlatformTabsGroup>
