# 实现收发消息 (/zh-CN/realtime-media/rtm/get-started/quick-start)

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

<_PlatformTabsGroup>
<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="android" />
本文将指导你如何利用 RTM Java SDK 构建简单的应用程序，内容涵盖了集成和开发的基础知识：开通声网账号、获取 SDK、发送消息、接收消息等。

同时，我们也提供了开源的示例项目供你参考，你可以前往下载或查看其中的源代码。

<Cards>
  <Card title="Android-Java" href="https://github.com/AgoraIO/RTM2/tree/main/Agora-RTM2-QuickStart-Android-Java" />
  <Card title="Linux-Java" href="https://github.com/AgoraIO/RTM2/tree/main/Agora-RTM2-QuickStart-Linux-Java" />
</Cards>

## 准备工作 [#准备工作]

开始构建项目前，你需要完成[开通 RTM 服务](/zh-CN/realtime-media/rtm/build/rtm-initialization/enable-service)，并检查你的开发环境是否满足[平台支持](/zh-CN/realtime-media/rtm/reference/capabilities-and-compatibility/platform-support)中的最低版本要求。

## 构建项目 [#构建项目]

### 1. 项目配置 [#1-项目配置]

根据以下步骤配置你的项目：

1. 在 Android Studio 里，创建一个 **Empty Activity** 类型的项目并打开。
2. 通过以下任意一种方式获取最新的 RTM Java SDK 并导入你的项目。

<Tabs defaultValue="cdn">
<TabsList>
  <TabsTrigger value="cdn">使用 CDN</TabsTrigger>
  <TabsTrigger value="maven">使用 Maven</TabsTrigger>
</TabsList>

<TabsContent value="cdn">

1. [点击此处](https://download.shengwang.cn/rtm2/release/RTM_JAVA_SDK_for_Android_v2.3.0.zip)下载最新版本的 Java SDK。
2. 将 SDK 包内 `sdk` 文件夹中的所有文件拷贝到项目的 `/app/libs` 路径下。
3. 添加 SDK 引用。在 **Project** 窗口找到 `/Gradle Scripts/build.gradle(Module: <projectname>.app)` 文件并添加如下代码：

   1. 在 `defaultConfig` 节点添加 `ndk` 节点，指定支持的架构。

      ```groovy
      Config {
          // ...

          ndk{
              abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
          }
      }
      ```

      :::info[信息]
      支持的架构越多，App 的体积会越大，声网建议你根据实际情况添加需要支持的架构。一般在发布 Android App 时只需保留 armeabi-v7a 和 arm64-v8a 架构。
      :::

   2. 在 `android` 节点添加 `sourceSets` 节点，指定 `libs` 所在目录。

      ```groovy
      android {
          // ...
          sourceSets {
              main {
                  jniLibs.srcDirs = ['libs']
              }
          }
      }
      ```

   3. 在 `dependencies` 节点引入 `libs` 路径下所有的 jar 库。

      ```groovy
      dependencies {
          implementation fileTree(dir: 'libs', include: ['*.jar'])
          // ...
      }
      ```

</TabsContent>

<TabsContent value="maven">

1. 在 `/Gradle Scripts/build.gradle(Project: <projectname>)` 文件中添加如下代码，添加 Maven Central 依赖：

   ```java
   buildscript {
       repositories {
           // ...
           mavenCentral()
       }
       ...
   }
   allprojects {
       repositories {
           // ...
           mavenCentral()
       }
   }
   ```

   :::info[信息]
   如果你的 Android 项目设置了 [dependencyResolutionManagement](https://docs.gradle.org/current/userguide/centralizing_repositories.html)，添加 Maven Central 依赖的方式可能存在差异。
   :::

2. 在 `/Gradle Scripts/build.gradle(Module: <projectname>.app)` 文件中添加如下代码，将 SDK 集成到你的 Android 项目中：

   ```groovy
   ...
   dependencies {
       // 将 x.y.z 替换为具体的 SDK 版本号，如 2.3.0
       // 对于 2.2.7 及之后版本:
       implementation 'cn.shengwang.rtm:android-java:x.y.z'
       // 对于 2.2.6 及之前版本:
       // implementation 'io.agora:agora-rtm:x.y.z'

   }
   ```

</TabsContent>
</Tabs>

   :::info[信息]
   如果你同时集成了 2.2.0 及以上版本的 RTM SDK 和 4.3.0 及以上版本的 RTC SDK，请参考 [FAQ](/zh-CN/reference/faq/integration/rtm2_rtc_integration_issue) 处理集成问题。
   :::

3. 添加网络权限。打开 `app/src/main/AndroidManifest.xml` 文件，在 `<manifest>` 的下一行添加如下代码：

   ```xml
   <uses-permission android:name="android.permission.INTERNET" />
   <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
   ```

4. 在 `/Gradle Scripts/proguard-rules.pro` 文件中添加如下代码，以防止声网 SDK 的代码被混淆：

   ```java
   -keep class io.agora.**{*;}
   ```

### 2. 初始化 RTM [#2-初始化-rtm]

调用 RTM SDK 的任何 API 之前，需要先初始化一个 `RtmClient` 对象实例。参考如下步骤创建用户界面、初始化一个 `RtmClient` 对象实例。

1. 创建用户界面。实时消息 App 一般会有如下输入框和按钮：
   - 用户名、频道名、消息输入框
   - 登录、登出按钮
   - 订阅、取消订阅频道按钮
   - 发送消息按钮

   本节提供简易的 UI 界面设计以便于验证功能特性，你可以根据实际项目需求进行修改。

   1. 打开 `/app/res/layout/activity_main.xml` 文件，将文件内容替换为如下代码：

      ```xml
      <?xml version="1.0" encoding="utf-8"?>
      <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:app="http://schemas.android.com/apk/res-auto"
          xmlns:tools="http://schemas.android.com/tools"
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:background="#FFFFFF"
          tools:context=".MainActivity">

          <EditText
              android:id="@+id/uid"
              android:layout_width="150dp"
              android:layout_height="40dp"
              android:layout_marginStart="36dp"
              android:layout_marginTop="48dp"
              android:autofillHints=""
              android:hint="@string/uid"
              android:inputType="text"
              android:lines="1"
              android:padding="5dp"
              app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintTop_toTopOf="parent" />

          <Button
              android:id="@+id/logout_button"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_marginStart="11dp"
              android:layout_marginTop="48dp"
              android:layout_marginEnd="28dp"
              android:onClick="onClickLogout"
              android:text="@string/logout_button"
              app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintHorizontal_bias="0.0"
              app:layout_constraintStart_toEndOf="@+id/login_button"
              app:layout_constraintTop_toTopOf="parent" />

          <Button
              android:id="@+id/login_button"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_marginStart="4dp"
              android:layout_marginTop="48dp"
              android:layout_marginEnd="132dp"
              android:onClick="onClickLogin"
              android:text="@string/login_button"
              app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintHorizontal_bias="0.0"
              app:layout_constraintStart_toEndOf="@+id/uid"
              app:layout_constraintTop_toTopOf="parent" />

          <EditText
              android:id="@+id/channel_name"
              android:layout_width="155dp"
              android:layout_height="41dp"
              android:layout_marginStart="36dp"
              android:layout_marginTop="124dp"
              android:layout_marginBottom="41dp"
              android:autofillHints=""
              android:ems="10"
              android:hint="@string/channel_name"
              android:inputType="text"
              app:layout_constraintTop_toBottomOf="@+id/uid"
              app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintTop_toTopOf="parent" />

          <Button
              android:id="@+id/subscribe_button"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_marginStart="4dp"
              android:layout_marginTop="28dp"
              android:layout_marginEnd="132dp"
              android:onClick="onClickSubscribe"
              android:text="@string/subscribe_button"
              app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintHorizontal_bias="0.0"
              app:layout_constraintStart_toEndOf="@+id/channel_name"
              app:layout_constraintTop_toBottomOf="@+id/login_button" />

          <Button
              android:id="@+id/unsubscribe_button"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_marginStart="11dp"
              android:layout_marginTop="27dp"
              android:layout_marginEnd="28dp"
              android:onClick="onClickUnsubscribe"
              android:text="@string/unsubscribe_button"
              app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintHorizontal_bias="0.0"
              app:layout_constraintStart_toEndOf="@+id/subscribe_button"
              app:layout_constraintTop_toBottomOf="@+id/logout_button" />

          <EditText
              android:id="@+id/msg_box"
              android:layout_width="190dp"
              android:layout_height="57dp"
              android:layout_marginStart="37dp"
              android:layout_marginTop="195dp"
              android:layout_marginBottom="26dp"
              android:autofillHints=""
              android:ems="10"
              android:hint="@string/msg"
              android:inputType="textPersonName"
              android:singleLine="false"
              app:layout_constraintTop_toTopOf="parent"
              app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintTop_toBottomOf="@+id/channel_name" />

          <Button
              android:id="@+id/send_channel_msg_button"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content"
              android:layout_marginTop="40dp"
              android:layout_marginEnd="40dp"
              android:onClick="onClickSendChannelMsg"
              android:text="@string/send_channel_msg_button"
              app:layout_constraintStart_toEndOf="@+id/msg_box"
              app:layout_constraintHorizontal_bias="0.0"
              app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintTop_toBottomOf="@+id/unsubscribe_button" />

          <TextView
              android:id="@+id/message_history"
              android:layout_width="412dp"
              android:layout_height="339dp"
              android:layout_marginTop="392dp"
              android:background="#AEA8A8"
              android:freezesText="false"
              android:isScrollContainer="false"
              android:scrollbars="vertical"
              android:textColor="#2196F3"
              app:layout_constraintBottom_toBottomOf="parent"
              app:layout_constraintEnd_toEndOf="parent"
              app:layout_constraintHorizontal_bias="0.491"
              app:layout_constraintStart_toStartOf="parent"
              app:layout_constraintTop_toTopOf="parent"
              app:layout_constraintVertical_bias="0.352" />

      </androidx.constraintlayout.widget.ConstraintLayout>
      ```

   2. 打开 `app/res/values/strings.xml` 文件，将文件内容替换为如下代码：

      ```xml
      <resources>
          <string name="app_name">RtmQuickstart</string>
          <string name="login_button">Login</string>
          <string name="logout_button">Logout</string>
          <string name="subscribe_button">Sub</string>
          <string name="unsubscribe_button">Unsub</string>
          <string name="send_channel_msg_button">Publish MSG</string>
          <string name="uid">User ID</string>
          <string name="msg">Message content</string>
          <string name="channel_name">Channel name</string>

          <string name="app_id">your_appid</string>
          <string name="token">your_token</string>
      </resources>
      ```

      :::info[信息]
      你需要将示例中的 `your_appid` 和 `your_token` 字段替换成你项目的 App ID 和 Token。在测试阶段，为快速验证功能，我们建议你在创建声网项目时将鉴权模式设置为调试模式，然后将示例中的 `your_appid` 和 `your_token` 字段都替换成你项目的 App ID。
      :::

2. 打开 `app/src/main/java/com/example/rtm/MainActivity.java` 文件，将文件内容替换为如下代码。你会发现此程序并不完整，不用紧张，我们在后续步骤中将一步步指导你补充完善代码。

   :::info[信息]
   `com.example.rtm` 为示例项目的路径，你需要根据实际项目路径查找文件并填写代码。
   :::

   ```java
   // Replace the package path with the path of your real project.
   package com.example.rtm;

   import android.content.pm.ActivityInfo;
   import android.os.Bundle;
   import android.view.View;
   import android.widget.EditText;
   import android.widget.TextView;
   import android.widget.Toast;

   import androidx.appcompat.app.AppCompatActivity;

   import io.agora.rtm.ErrorInfo;
   import io.agora.rtm.LinkStateEvent;
   import io.agora.rtm.LockEvent;
   import io.agora.rtm.MessageEvent;
   import io.agora.rtm.PresenceEvent;
   import io.agora.rtm.PublishOptions;
   import io.agora.rtm.ResultCallback;
   import io.agora.rtm.RtmClient;
   import io.agora.rtm.RtmConfig;
   import io.agora.rtm.RtmConstants;
   import io.agora.rtm.RtmEventListener;
   import io.agora.rtm.RtmMessage;
   import io.agora.rtm.StorageEvent;
   import io.agora.rtm.SubscribeOptions;
   import io.agora.rtm.TopicEvent;


   public class MainActivity extends AppCompatActivity {
       private EditText etUserId;
       private EditText etChannelName;
       private EditText etMessageContent;

       // The user ID of the publisher
       private String mUserId;
       // The channel name of the message channel
       private String mChannelName;

       // The RTM client instance
       private RtmClient mRtmClient;
       // The TextView to show the messages that you send
       private TextView mMessageHistory;

       // Add the event listener

       @Override
       protected void onCreate(Bundle savedInstanceState) {
           super.onCreate(savedInstanceState);
           setContentView(R.layout.activity_main);
           setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
       }

       @Override
       protected void onResume() {
           super.onResume();
       }

       @Override
       protected void onDestroy() {
           super.onDestroy();
       }

       public void onClickLogin(View v)
       {
           try {
               etUserId = (EditText) findViewById(R.id.uid);
               mUserId = etUserId.getText().toString();
               if (mUserId == null || mUserId.isEmpty()) {
                   showToast("invalid userId");
                   return;
               }
               RtmConfig config = new RtmConfig.Builder(getBaseContext().getString(R.string.app_id), mUserId)
                         .eventListener(eventListener)
                         .build();
               mRtmClient = RtmClient.create(config);
           } catch (Exception e) {
               showToast("create rtm client is null");
           }

           if (mRtmClient == null) {
               showToast("rtm client is null");
               return;
           }

           // Log in the RTM server
       }

       public void onClickSubscribe(View v)
       {
           if (mRtmClient == null) {
               showToast("rtm client is null");
               return;
           }

           // Subscribe to a channel
       }

       public void onClickLogout(View v)
       {
           if (mRtmClient == null) {
               showToast("rtm client is null");
           }
           // Log out from the RTM server
       }

       public void onClickUnsubscribe(View v)
       {
           if (mRtmClient == null) {
               showToast("rtm client is null");
               return;
           }
           // Unsubscribe from a channel
       }

       public void onClickSendChannelMsg(View v)
       {
           // Publish a message
       }

       // Write message history to TextView
       public void writeToMessageHistory(String record)
       {
           mMessageHistory = findViewById(R.id.message_history);
           mMessageHistory.append(record);
       }

       private void showToast(String text) {
           Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
       }
   }
   ```

如需了解更多初始化的信息，查看[初始配置](/zh-CN/api-reference/rtm/android/configuration)。

### 3. 添加事件监听 [#3-添加事件监听]

事件监听程序帮助你实现频道中消息、事件到达后的处理逻辑，添加以下代码到你的程序中以显示收到的消息或事件通知：

```java showLineNumbers
// Paste the following code snippet below "Add the event listener" comment
private RtmEventListener eventListener = new RtmEventListener() {
    @Override
    public void onMessageEvent(MessageEvent event) {
        String text = "Message received from " + event.getPublisherId() + " Message: " + event.getMessage().getData() + "\n";
        writeToMessageHistory(text);
    }

    @Override
    public void onPresenceEvent(PresenceEvent event) {
        String text = "receive presence event, user: " + event.getPublisherId() + " event: " + event.getEventType() + "\n";
        writeToMessageHistory(text);
    }

    @Override
    public void onLinkStateEvent(LinkStateEvent event) {
        String text = "Link state event: " + event + "\n";
        writeToMessageHistory(text);
    }
};
```

### 4. 登录服务 [#4-登录服务]

你需要执行登录操作才能建立与 RTM 服务器的连接，然后才能调用 SDK 的其他 API。将以下代码添加到程序中：

```java showLineNumbers
// Paste the following code snippet below "Log in the RTM server" comment
String token =getBaseContext().getString(R.string.token);
mRtmClient.login(token, new ResultCallback<Void>() {
    @Override
    public void onSuccess(Void responseInfo) {
        writeToMessageHistory("Successfully log in the RTM server!\n");
    }

    @Override
    public void onFailure(ErrorInfo errorInfo) {
        CharSequence text = "User: " + mUserId + " Fail to log in to the RTM system!" + errorInfo.toString();
        int duration = Toast.LENGTH_SHORT;
        runOnUiThread(new Runnable() {
            public void run() {
                Toast toast = Toast.makeText(getApplicationContext(), text, duration);
                toast.show();
            }
        });
    }
});
```

### 5. 收发消息 [#5-收发消息]

调用 `publish` 方法向 Message Channel 发送消息后，RTM 会把该消息分发给该频道的所有订阅者，以下代码演示如何发送字符串类型的消息，将此代码片段添加到程序中：

:::info[信息]
你需要先对消息负载进行字符串序列化，才能调用 `publish` 方法发送消息。
:::

```java showLineNumbers
// Paste the following code snippet below "Publish a message" comment
etMessageContent = findViewById(R.id.msg_box);
String message = etMessageContent.getText().toString();
etChannelName = (EditText) findViewById(R.id.channel_name);
mChannelName = etChannelName.getText().toString();
PublishOptions options = new PublishOptions();
options.setCustomType("");
mRtmClient.publish(mChannelName, message, options, new ResultCallback<Void>() {
    @Override
    public void onSuccess(Void responseInfo) {
        String text = "Message sent to channel " + mChannelName + " : " + message + "\n";
        writeToMessageHistory(text);
    }

    @Override
    public void onFailure(ErrorInfo errorInfo) {
        String text = "Message fails to send to channel " + mChannelName + " Error: " + errorInfo + "\n";
        writeToMessageHistory(text);
    }
});
```

调用 `subscribe` 方法订阅此频道以接收此频道中的消息。将以下代码添加到程序中：

```java showLineNumbers
// Paste the following code snippet below "Subscribe to a channel" comment
etChannelName = (EditText) findViewById(R.id.channel_name);
mChannelName = etChannelName.getText().toString();
SubscribeOptions options = new SubscribeOptions();
options.setWithMessage(true);
mRtmClient.subscribe(mChannelName, options, new ResultCallback<Void>() {
    @Override
    public void onSuccess(Void responseInfo) {
        writeToMessageHistory("Successfully subscribe to the channel!\n");
    }

    @Override
    public void onFailure(ErrorInfo errorInfo) {
        writeToMessageHistory("Fail to subscribe to the channel!\n");
    }
});
```

如果你不再需要在此频道收发消息，你可以调用 `unsubscribe` 方法取消订阅此频道。将以下代码添加到程序中：

``` java
// Paste the following code snippet below "Unsubscribe from a channel" comment
etChannelName = (EditText) findViewById(R.id.channel_name);
mChannelName = etChannelName.getText().toString();
mRtmClient.unsubscribe(mChannelName, new ResultCallback<Void>() {
    @Override
    public void onSuccess(Void responseInfo) {
        writeToMessageHistory("Successfully unsubscribe from the channel!\n");
    }

    @Override
    public void onFailure(ErrorInfo errorInfo) {
        writeToMessageHistory("Fail to unsubscribe from the channel!\n");
    }
});
```

如需了解更多收发消息的信息，查看[消息](/zh-CN/api-reference/rtm/android/message)。

### 6. 登出服务 [#6-登出服务]

当不再需要使用 RTM 服务时，你可以调用 `logout` 方法登出 RTM 系统。将以下代码添加到程序中：

```java
// Paste the following code snippet below "Log out from the RTM server" comment
mRtmClient.logout(new ResultCallback<Void>() {
    @Override
    public void onSuccess(Void responseInfo) {
        writeToMessageHistory("Successfully log out from the channel!\n");
    }

    @Override
    public void onFailure(ErrorInfo errorInfo) {
        writeToMessageHistory("Fail to log out from the channel!\n");
    }
});
```

:::info[信息]
本操作会影响你账单中的 PCU 计费项。
:::

### 7. 组合到一起 [#7-组合到一起]

经过上述步骤，你程序中的代码应该如下所示：

```java
  // Replace the package path with the path of your real project.
  package com.example.rtm;

  import android.content.pm.ActivityInfo;
  import android.os.Bundle;
  import android.view.View;
  import android.widget.EditText;
  import android.widget.TextView;
  import android.widget.Toast;

  import androidx.appcompat.app.AppCompatActivity;

  import io.agora.rtm.ErrorInfo;
  import io.agora.rtm.LinkStateEvent;
  import io.agora.rtm.LockEvent;
  import io.agora.rtm.MessageEvent;
  import io.agora.rtm.PresenceEvent;
  import io.agora.rtm.PublishOptions;
  import io.agora.rtm.ResultCallback;
  import io.agora.rtm.RtmClient;
  import io.agora.rtm.RtmConfig;
  import io.agora.rtm.RtmConstants;
  import io.agora.rtm.RtmEventListener;
  import io.agora.rtm.RtmMessage;
  import io.agora.rtm.StorageEvent;
  import io.agora.rtm.SubscribeOptions;
  import io.agora.rtm.TopicEvent;


  public class MainActivity extends AppCompatActivity {
      private EditText etUserId;
      private EditText etChannelName;
      private EditText etMessageContent;

      // The user ID of the publisher
      private String mUserId;
      // The channel name of the message channel
      private String mChannelName;

      // The RTM client instance
      private RtmClient mRtmClient;
      // The TextView to show the messages that you send
      private TextView mMessageHistory;

      // Add the event listener
      private RtmEventListener eventListener = new RtmEventListener() {
          @Override
          public void onMessageEvent(MessageEvent event) {
              String text = "Message received from " + event.getPublisherId() + " Message: " + event.getMessage().getData() + "\n";
              writeToMessageHistory(text);
          }

          @Override
          public void onPresenceEvent(PresenceEvent event) {
              String text = "receive presence event, user: " + event.getPublisherId() + " event: " + event.getEventType() + "\n";
              writeToMessageHistory(text);
          }

          @Override
          public void onLinkStateEvent(LinkStateEvent event) {
              String text = "Link state event: " + event + "\n";
              writeToMessageHistory(text);
          }
      };

      @Override
      protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_main);
          setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
      }

      @Override
      protected void onResume() {
          super.onResume();
      }

      @Override
      protected void onDestroy() {
          super.onDestroy();
      }

      public void onClickLogin(View v)
      {
          try {
              etUserId = (EditText) findViewById(R.id.uid);
              mUserId = etUserId.getText().toString();
              if (mUserId == null || mUserId.isEmpty()) {
                  showToast("invalid userId");
                  return;
              }
              RtmConfig config = new RtmConfig.Builder(getBaseContext().getString(R.string.app_id), mUserId)
                  .eventListener(eventListener)
                  .build();
              mRtmClient = RtmClient.create(config);
          } catch (Exception e) {
              showToast("create rtm client is null");
          }

          if (mRtmClient == null) {
              showToast("rtm client is null");
              return;
          }

          // Log in the RTM server
          String token =getBaseContext().getString(R.string.token);
          mRtmClient.login(token, new ResultCallback<Void>() {
              @Override
              public void onSuccess(Void responseInfo) {
                  writeToMessageHistory("Successfully log in the RTM server!\n");
              }

              @Override
              public void onFailure(ErrorInfo errorInfo) {
                  CharSequence text = "User: " + mUserId + " Fail to log in to the RTM system!" + errorInfo.toString();
                  int duration = Toast.LENGTH_SHORT;
                  runOnUiThread(new Runnable() {
                      public void run() {
                          Toast toast = Toast.makeText(getApplicationContext(), text, duration);
                          toast.show();
                      }
                  });
              }
          });
      }

      public void onClickSubscribe(View v)
      {
          if (mRtmClient == null) {
              showToast("rtm client is null");
              return;
          }

          // Subscribe to a channel
          etChannelName = (EditText) findViewById(R.id.channel_name);
          mChannelName = etChannelName.getText().toString();

          SubscribeOptions options = new SubscribeOptions();
          options.setWithMessage(true);
          mRtmClient.subscribe(mChannelName, options, new ResultCallback<Void>() {
              @Override
              public void onSuccess(Void responseInfo) {
                  writeToMessageHistory("Successfully subscribe to the channel!\n");
              }

              @Override
              public void onFailure(ErrorInfo errorInfo) {
                  writeToMessageHistory("Fail to subscribe to the channel!\n");
              }
          });
      }

      public void onClickLogout(View v)
      {
          if (mRtmClient == null) {
              showToast("rtm client is null");
          }
          // Log out from the RTM server
          mRtmClient.logout(new ResultCallback<Void>() {
              @Override
              public void onSuccess(Void responseInfo) {
                  writeToMessageHistory("Successfully log out from the channel!\n");
              }

              @Override
              public void onFailure(ErrorInfo errorInfo) {
                  writeToMessageHistory("Fail to log out from the channel!\n");
              }
          });
      }

      public void onClickUnsubscribe(View v)
      {
          if (mRtmClient == null) {
              showToast("rtm client is null");
              return;
          }
          // Unsubscribe from a channel
          etChannelName = (EditText) findViewById(R.id.channel_name);
          mChannelName = etChannelName.getText().toString();
          mRtmClient.unsubscribe(mChannelName, new ResultCallback<Void>() {
              @Override
              public void onSuccess(Void responseInfo) {
                  writeToMessageHistory("Successfully unsubscribe from the channel!\n");
              }

              @Override
              public void onFailure(ErrorInfo errorInfo) {
                  writeToMessageHistory("Fail to unsubscribe from the channel!\n");
              }
          });
      }

      public void onClickSendChannelMsg(View v)
      {
          etMessageContent = findViewById(R.id.msg_box);
          String message = etMessageContent.getText().toString();

          // Publish a message
          etChannelName = (EditText) findViewById(R.id.channel_name);
          mChannelName = etChannelName.getText().toString();
          PublishOptions options = new PublishOptions();
          options.setCustomType("");
          mRtmClient.publish(mChannelName, message, options, new ResultCallback<Void>() {
              @Override
              public void onSuccess(Void responseInfo) {
                  String text = "Message sent to channel " + mChannelName + " : " + message + "\n";
                  writeToMessageHistory(text);
              }

              @Override
              public void onFailure(ErrorInfo errorInfo) {
                  String text = "Message fails to send to channel " + mChannelName + " Error: " + errorInfo + "\n";
                  writeToMessageHistory(text);
              }
          });
      }

      // Write message history to TextView
      public void writeToMessageHistory(String record)
      {
          mMessageHistory = findViewById(R.id.message_history);
          mMessageHistory.append(record);
      }

      private void showToast(String text) {
          Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
      }
  }
```

现在，你可以开始运行你的程序：

1. 保存项目。
2. 在菜单栏中依次点击 **Run > Select Device > Select Multiple Devices** 并添加两个设备。例如，选择一个虚拟机和一个真机。
3. 运行项目。成功运行后，你可以在两个设备中看到 App 界面。
4. 将一个设备作为接收端，进行如下操作：
   1. 输入用户名，点击 **LOGIN**。
   2. 输入频道名，点击 **SUB**。
5. 将另一个设备作为发送端，进行如下操作：
   1. 输入不同的用户名，点击 **LOGIN**。
   2. 输入相同的频道名。
   3. 输入消息，点击 **PUBLISH MSG**。
6. 将上面的发送端和设备端互换，重复步骤 4 和 5。

如果两个设备均可收发消息，那么你已经成功集成并正确使用了 RTM 服务。

## 贯穿始终 [#贯穿始终]

相比于介绍如何写出代码，声网更愿意帮助你掌握上述程序编写的过程和逻辑。上述程序依次完成了以下操作，让你可以正确地收发消息：

1. 设置并初始化 RTM 对象。
2. 添加 `onMessageEvent`、`onPresenceEvent` 和 `onLinkStateEvent` 事件监听函数。
3. 调用 `login` 登录了 RTM 服务。
4. 调用 `subscribe` 订阅了一个 Message Channel。
5. 调用 `publish` 发送消息。
6. 调用 `unsubscribe` 取消订阅一个 Message Channel。
7. 调用 `logout` 登出 RTM 系统。

## 下一步 [#下一步]

现在，你已经学会了如何使用 RTM Java SDK 来实现 Message Channel 的收发消息功能。下一步，你可以通过 SDK 的 [API 参考](/zh-CN/api-reference/rtm/android/configuration)了解更多功能的使用方法。
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="ios" />
本文将指导你如何利用 RTM Objective-C SDK 构建简单的应用程序，内容涵盖了集成和开发的基础知识：开通声网账号、获取 SDK、发送消息、接收消息等。
## 准备工作 [#准备工作-1]

开始构建项目前，你需要完成[开通 RTM 服务](/zh-CN/realtime-media/rtm/build/rtm-initialization/enable-service)，并检查你的开发环境是否满足[平台支持](/zh-CN/realtime-media/rtm/reference/capabilities-and-compatibility/platform-support)中的最低版本要求。

## 构建项目 [#构建项目-1]

### 1. 项目配置 [#1-项目配置-1]

根据以下步骤配置你的项目：

1. 在 Xcode 中创建一个 iOS 平台下的 **Single View App**，项目设置如下：
   - **Product Name** 设为 `RtmQuickstart`。
   - **Organization Identifier** 设为 `agora`。
   - **User Interface** 选择 **Storyboard**。
   - **Language** 选择 **Objective-C**。
2. 通过以下任意一种方式获取最新的 RTM Objective-C SDK 并导入你的项目。

<Tabs defaultValue="cdn">
<TabsList>
  <TabsTrigger value="cdn">使用 CDN</TabsTrigger>
  <TabsTrigger value="cocoapods">使用 Cocoapods</TabsTrigger>
  <TabsTrigger value="spm">通过 SPM 集成 SDK</TabsTrigger>
</TabsList>

<TabsContent value="cdn">

1. [点击此处](https://download.shengwang.cn/rtm2/release/RTM_OC_SDK_v2.3.0.zip)下载最新版本的 Objective-C SDK。
2. 复制 SDK 包中 `/libs` 路径下所有的 `xcframework` 文件至项目路径下。
3. 打开 Xcode，进入 **TARGETS > Project Name > General > Frameworks, Libraries, and Embedded Content** 菜单。
4. 点击 **+ > Add Other… > Add Files** 添加所有的 `xcframework` 文件，并确保添加的动态库 **Embed** 属性设置为 **Embed & Sign**。

添加完成后，项目会自动链接所需系统库。

</TabsContent>

<TabsContent value="cocoapods">

1. 开始前请确保你已安装 Cocoapods，如尚未安装 Cocoapods，参考 [Getting Started with CocoaPods](https://guides.cocoapods.org/using/getting-started.html#getting-started) 安装说明。
2. 在终端里进入项目根目录，并运行 `pod init` 命令。项目文件夹下会生成一个 `Podfile` 文本文件。
3. 打开 `Podfile` 文件，修改文件为如下内容。注意将 `Your App` 替换为你的 Target 名称。

   ```ruby
   platform :ios, '11.0'
   target 'Your App' do
      # 对于 2.2.8 及之后版本:
      pod 'ShengWang-Rtm', '2.3.0', :subspecs => ['RtmKit']
      # 对于 2.2.7 版本:
      # pod 'ShengwangRtm', 'x.y.z'
      # 对于 2.2.6 及之前版本:
      # pod 'AgoraRtm', 'x.y.z'
      # 如需获取 2.2.0 之前的版本，请将包名修改为 AgoraRtm_iOS
   end
   ```

4. 在终端内运行 `pod install` 命令安装声网 SDK。成功安装后，Terminal 中会显示 `Pod installation complete!`。
5. 成功安装后，项目文件夹下会生成一个后缀为 `.xcworkspace` 的文件，通过 Xcode 打开该文件进行后续操作。

</TabsContent>

<TabsContent value="spm">

使用如下链接，通过 SPM (Swift Package Manager) 集成 SDK：

```text
https://github.com/AgoraIO/AgoraRtm_Apple.git
```

</TabsContent>
</Tabs>

   :::info[信息]
   如果你同时集成了 2.2.0 及以上版本的 RTM SDK 和 4.3.0 及以上版本的 RTC SDK，请参考 [FAQ](/zh-CN/reference/faq/integration/rtm2_rtc_integration_issue) 处理集成问题。
   :::

### 2. 初始化 RTM [#2-初始化-rtm-1]

调用 RTM SDK 的任何 API 之前，需要先初始化一个 `AgoraRtmClientKit` 对象实例。参考如下步骤创建用户界面、初始化一个 `AgoraRtmClientKit` 对象实例。

1. 创建用户界面。实时消息 App 一般会有如下输入框和按钮：
   - 用户名、频道名、消息输入框
   - 登录、登出按钮
   - 订阅、取消订阅频道按钮
   - 发送消息按钮

   本节提供简易的 UI 界面设计以便于验证功能特性，你可以根据实际项目需求进行修改。

   1. 使用 **Code View** 打开 `Main.storyboard` 并将文件内容替换为以下代码：

      ```xml
      <?xml version="1.0" encoding="UTF-8"?>
      <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
          <device id="retina6_1" orientation="portrait" appearance="light"/>
          <dependencies>
              <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
              <capability name="Safe area layout guides" minToolsVersion="9.0"/>
              <capability name="System colors in document resources" minToolsVersion="11.0"/>
              <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
          </dependencies>
          <scenes>
              <!--View Controller-->
              <scene sceneID="tne-QT-ifu">
                  <objects>
                      <viewController id="BYZ-38-t0r" customClass="ViewController" sceneMemberID="viewController">
                          <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
                              <rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
                              <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
                              <subviews>
                                  <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="M2K-rz-dlO">
                                      <rect key="frame" x="254" y="103" width="38" height="30"/>
                                      <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                      <state key="normal" title="Login"/>
                                      <connections>
                                          <action selector="Login:" destination="BYZ-38-t0r" eventType="touchUpInside" id="UEU-up-ksL"/>
                                      </connections>
                                  </button>
                                  <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CZ1-G5-AkG">
                                      <rect key="frame" x="326" y="103" width="48" height="30"/>
                                      <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                      <state key="normal" title="Logout"/>
                                      <connections>
                                          <action selector="Logout:" destination="BYZ-38-t0r" eventType="touchUpInside" id="a0d-8h-eyX"/>
                                      </connections>
                                  </button>
                                  <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="0px-1e-aMC">
                                      <rect key="frame" x="242" y="172" width="62" height="30"/>
                                      <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                      <state key="normal" title="Subscribe"/>
                                      <connections>
                                          <action selector="SubscribeChannel:" destination="BYZ-38-t0r" eventType="touchUpInside" id="12c-US-VQk"/>
                                      </connections>
                                  </button>
                                  <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="SDQ-Zt-rBD">
                                      <rect key="frame" x="307" y="172" width="87" height="30"/>
                                      <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                      <state key="normal" title="Unsubscribe"/>
                                      <connections>
                                          <action selector="UnsubscribeChannel:" destination="BYZ-38-t0r" eventType="touchUpInside" id="u4T-ze-wtj"/>
                                      </connections>
                                  </button>
                                  <textField opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="User ID" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="69v-UU-ObH">
                                      <rect key="frame" x="40" y="99" width="187" height="34"/>
                                      <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                      <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                      <textInputTraits key="textInputTraits"/>
                                  </textField>
                                  <textField opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="Channel name" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="dGk-g2-qHK">
                                      <rect key="frame" x="40" y="168" width="187" height="34"/>
                                      <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                      <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                      <textInputTraits key="textInputTraits"/>
                                  </textField>
                                  <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Qqk-6X-Tcb">
                                      <rect key="frame" x="304" y="247" width="88" height="30"/>
                                      <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                      <state key="normal" title="Publish MSG"/>
                                      <connections>
                                          <action selector="SendMessageToMessageChannel:" destination="BYZ-38-t0r" eventType="touchUpInside" id="XcX-nz-fPl"/>
                                      </connections>
                                  </button>
                                  <textField opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" text="Message content" borderStyle="roundedRect" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="lKg-ap-M6p">
                                      <rect key="frame" x="40" y="245" width="252" height="34"/>
                                      <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                      <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                      <textInputTraits key="textInputTraits"/>
                                  </textField>
                                  <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" fixedFrame="YES" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="e1v-i8-spC">
                                      <rect key="frame" x="40" y="416" width="330" height="428"/>
                                      <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
                                      <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                                      <color key="textColor" systemColor="labelColor"/>
                                      <fontDescription key="fontDescription" type="system" pointSize="14"/>
                                      <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
                                  </textView>
                              </subviews>
                              <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
                              <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                          </view>
                          <connections>
                              <outlet property="ChannelIDTextField" destination="dGk-g2-qHK" id="Iuh-ow-1MH"/>
                              <outlet property="GroupMsgButton" destination="Qqk-6X-Tcb" id="TV4-sg-bY7"/>
                              <outlet property="GroupMsgTextField" destination="lKg-ap-M6p" id="2Me-Z0-QIH"/>
                              <outlet property="LoginButton" destination="M2K-rz-dlO" id="ZxJ-oV-UKy"/>
                              <outlet property="LogoutButton" destination="CZ1-G5-AkG" id="ZQE-B7-yya"/>
                              <outlet property="MsgTextView" destination="e1v-i8-spC" id="76J-8Q-kKp"/>
                              <outlet property="SubsctibeButton" destination="0px-1e-aMC" id="9TL-8M-l6w"/>
                              <outlet property="UnSubscribeButton" destination="SDQ-Zt-rBD" id="tgd-fK-EfO"/>
                              <outlet property="UserIDTextField" destination="69v-UU-ObH" id="3BH-ci-t7A"/>
                          </connections>
                      </viewController>
                      <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
                  </objects>
                  <point key="canvasLocation" x="131.8840579710145" y="97.767857142857139"/>
              </scene>
          </scenes>
          <resources>
              <systemColor name="labelColor">
                  <color red="0.0" green="0.0" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
              </systemColor>
              <systemColor name="systemBackgroundColor">
                  <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
              </systemColor>
          </resources>
      </document>
      ```

   2. 打开 `ViewController.h` 文件，将文件内容替换为如下代码：

      ```objc
      #import <UIKit/UIKit.h>

      #import <AgoraRtmKit/AgoraRtmKit.h>

      @interface ViewController : UIViewController

      // Buttons
      @property (weak, nonatomic) IBOutlet UIButton *LoginButton;
      @property (weak, nonatomic) IBOutlet UIButton *LogoutButton;
      @property (weak, nonatomic) IBOutlet UIButton *SubsctibeButton;
      @property (weak, nonatomic) IBOutlet UIButton *UnSubscribeButton;
      @property (weak, nonatomic) IBOutlet UIButton *GroupMsgButton;

      // Textfields
      @property (weak, nonatomic) IBOutlet UITextField *UserIDTextField;
      @property (weak, nonatomic) IBOutlet UITextField *ChannelIDTextField;
      @property (weak, nonatomic) IBOutlet UITextField *GroupMsgTextField;

      @property (weak, nonatomic) IBOutlet UITextView *MsgTextView;

      @end
      ```

2. 打开 `ViewController.m` 文件，将文件内容替换为如下代码。你会发现此程序并不完整，不用紧张，我们在后续步骤中将一步步指导你补充完善代码。

   :::info[信息]
   你需要将示例中的 `your_appid` 和 `your_token` 字段替换成你项目的 App ID 和 Token。在测试阶段，为快速验证功能，我们建议你在创建声网项目时将鉴权模式设置为调试模式，然后将示例中的 `your_appid` 和 `your_token` 字段都替换成你项目的 App ID。
   :::

   ```objc
   #import "ViewController.h"

   @interface ViewController ()<AgoraRtmClientDelegate>

   @property(nonatomic, strong)AgoraRtmClientKit* kit;

   @property NSString* appID;
   @property NSString* token;

   @property NSString* uid;
   @property NSString* peerID;
   @property NSString* channelID;
   @property NSString* peerMsg;
   @property NSString* channelMsg;

   @property NSString* text;
   @property NSMutableArray* textArray;

   - (void)AddMsgToRecord:(NSString*)text;

   @end

   @implementation ViewController

   - (void)viewDidLoad {
   [super viewDidLoad];
   // Enter your App ID
   self.appID = @"your_appid";
   self.MsgTextView.textColor = UIColor.blueColor;
   self.textArray = [[NSMutableArray alloc]init];
   }

   // Add the event listener

   // Add message to the UI TextView
   - (void)AddMsgToRecord:(NSString*)text {


   [self.textArray addObject:(self.text)];
   self.MsgTextView.text = [self.textArray componentsJoinedByString:(@"\n")];

   }

   - (IBAction)Login:(id)sender {
       self.uid = self.UserIDTextField.text;
       // Enter your token
       self.token = @"your_token";

       AgoraRtmClientConfig*  rtm_config = [[AgoraRtmClientConfig alloc] initWithAppId:_appID userId:_uid];

       NSError* initError = nil;
       _kit = [[AgoraRtmClientKit alloc] initWithConfig:rtm_config delegate:self error:&initError];
       if (initError != nil) {
           self.text = [NSString stringWithFormat:@"init error %@",initError];
           NSLog(@"%@", self.text);
           [self AddMsgToRecord:(self.text)];
       }
       // Log in the RTM server
   }

   - (IBAction)Logout:(id)sender {
   if (_kit != nil) {
       // Log out from the RTM server
       }
   }

   - (IBAction)SubscribeChannel:(id)sender {
   self.channelID = self.ChannelIDTextField.text;
   if (_kit != nil) {
       // Subscribe to a channel
       }
   }

   - (IBAction)UnsubscribeChannel:(id)sender {
       if (_kit == nil)
           return;
       // Unsubscribe from a channel
   }

   - (IBAction)SendMessageToMessageChannel:(id)sender {
   self.channelID = self.ChannelIDTextField.text;
   self.channelMsg = self.GroupMsgTextField.text;
       // Publish a message
   }
   @end
   ```

如需了解更多初始化的信息，查看[初始配置](/zh-CN/api-reference/rtm/ios/configuration)。

### 3. 添加事件监听 [#3-添加事件监听-1]

事件监听程序帮助你实现频道中消息、事件到达后的处理逻辑，添加以下代码到你的程序中以显示收到的消息或事件通知：

```objc showLineNumbers
// Paste the following code snippet below "Add the event listener" comment
- (void)rtmKit:(AgoraRtmClientKit *)rtmKit didReceiveMessageEvent:(AgoraRtmMessageEvent *)event {
    NSLog(@"%@", self.text);
    self.text = [NSString stringWithFormat:@"receive message\nFrom channel: %@\npublisher:%@\nmessage:%@\n",event.channelName,event.publisher, event.message.stringData];
    [self AddMsgToRecord:(self.text)];
}
```

### 4. 登录服务 [#4-登录服务-1]

你需要执行登录操作才能建立与 RTM 服务器的连接，然后才能调用 SDK 的其他 API。将以下代码添加到程序中：

```objc showLineNumbers
// Paste the following code snippet below "Log in the RTM server" comment
[_kit loginByToken:self.token completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
    if (errorInfo.errorCode != AgoraRtmErrorOk){
        self.text = [NSString stringWithFormat:@"Login failed for user %@. Code: %ld",self.uid, (long)errorInfo.errorCode];
        NSLog(@"%@", self.text);
    } else {
        NSLog(@"%@", self.text);
        self.text = [NSString stringWithFormat:@"Login successful for user %@. Code: %ld",self.uid, (long)errorInfo.errorCode];
    }
    [self AddMsgToRecord:(self.text)];
}];
```

### 5. 收发消息 [#5-收发消息-1]

调用 `publish` 方法向 Message Channel 发送消息后，RTM 会把该消息分发给该频道的所有订阅者，以下代码演示如何发送字符串类型的消息，将此代码片段添加到程序中：

:::info[信息]
你需要先对消息负载进行字符串序列化，才能调用 `publish` 方法发送消息。
:::

```objc showLineNumbers
// Paste the following code snippet below "Publish a message" comment
[_kit publish:self.channelID message:self.channelMsg option:nil completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
    if (errorInfo == nil) {
        self.text = [NSString stringWithFormat:@"Message sent to channel %@ : %@", self.channelID, self.channelMsg];
    } else {
        self.text = [NSString stringWithFormat:@"Message failed to send to channel %@ : %@ ErrorCode: %ld", self.channelID, self.channelMsg, (long)errorInfo.errorCode];
    }

    [self AddMsgToRecord:(self.text)];
}];
```

调用 `subscribeWithChannel` 方法订阅此频道以接收此频道中的消息。将以下代码添加到程序中：

```objc showLineNumbers
// Paste the following code snippet below "Subscribe to a channel" comment
[_kit subscribeWithChannel:self.channelID option:nil completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
    if(errorInfo == nil) {
        self.text = [NSString stringWithFormat:@"Successfully subscribe channel %@",self.channelID];
        NSLog(@"%@", self.text);
    } else {
        self.text = [NSString stringWithFormat:@"Failed to subscribe channel %@ Code: %ld",self.channelID, (long)errorInfo.errorCode];
        NSLog(@"%@", self.text);
    }

    [self AddMsgToRecord:(self.text)];
}];
```

如果你不再需要在此频道收发消息，你可以调用 `unsubscribeWithChannel` 方法取消订阅此频道。将以下代码添加到程序中：

``` objc showLineNumbers
// Paste the following code snippet below "Unsubscribe from a channel" comment
[_kit unsubscribeWithChannel:self.channelID completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
    if (errorInfo == nil){
        self.text = [NSString stringWithFormat:@"Leave channel successful"];
    } else {
        self.text = [NSString stringWithFormat:@"Failed to leave channel Code: %ld", (long)errorInfo.errorCode];
    }

    [self AddMsgToRecord:(self.text)];
}];
```

如需了解更多收发消息的信息，查看[消息](/zh-CN/api-reference/rtm/ios/message)。

### 6. 登出服务 [#6-登出服务-1]

当不再需要使用 RTM 服务时，你可以调用 `logout` 方法登出 RTM 系统。将以下代码添加到程序中：

```objc showLineNumbers
// Paste the following code snippet below "Log out from the RTM server" comment
[_kit logout:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
    if (errorInfo == nil){
        self.text = [NSString stringWithFormat:@"Logout successful"];
        NSLog(@"%@", self.text);
        [_kit destroy];
        _kit = nil;
    } else {
        self.text = [NSString stringWithFormat:@"Logout failed. Code: %ld",(long)errorInfo.errorCode];
        NSLog(@"%@", self.text);
    }

    [self AddMsgToRecord:(self.text)];
}];
```

:::info[信息]
本操作会影响你账单中的 PCU 计费项。
:::

### 7. 组合到一起 [#7-组合到一起-1]

经过上述步骤，你程序中的代码应该如下所示：

```objc
#import "ViewController.h"

@interface ViewController ()<AgoraRtmClientDelegate>

@property(nonatomic, strong)AgoraRtmClientKit* kit;

@property NSString* appID;
@property NSString* token;

@property NSString* uid;
@property NSString* peerID;
@property NSString* channelID;
@property NSString* peerMsg;
@property NSString* channelMsg;

@property NSString* text;
@property NSMutableArray* textArray;

- (void)AddMsgToRecord:(NSString*)text;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
// Enter your App ID
self.appID = @"your_appid";
self.MsgTextView.textColor = UIColor.blueColor;
self.textArray = [[NSMutableArray alloc]init];
}

- (void)rtmKit:(AgoraRtmClientKit *)rtmKit didReceiveMessageEvent:(AgoraRtmMessageEvent *)event {
    NSLog(@"%@", self.text);
    self.text = [NSString stringWithFormat:@"receive message\nFrom channel: %@\npublisher:%@\nmessage:%@\n",event.channelName,event.publisher, event.message.stringData];
    [self AddMsgToRecord:(self.text)];
}

// Add message to the UI TextView
- (void)AddMsgToRecord:(NSString*)text {


[self.textArray addObject:(self.text)];
self.MsgTextView.text = [self.textArray componentsJoinedByString:(@"\n")];

}

- (IBAction)Login:(id)sender {
    self.uid = self.UserIDTextField.text;
    // Enter your token
    self.token = @"your_token";

    AgoraRtmClientConfig*  rtm_config = [[AgoraRtmClientConfig alloc] initWithAppId:_appID userId:_uid];

    NSError* initError = nil;
    _kit = [[AgoraRtmClientKit alloc] initWithConfig:rtm_config delegate:self error:&initError];
    if (initError != nil) {
        self.text = [NSString stringWithFormat:@"init error %@",initError];
        NSLog(@"%@", self.text);
        [self AddMsgToRecord:(self.text)];
    }
    // Log in the RTM server
    [_kit loginByToken:self.token completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
        if (errorInfo.errorCode != AgoraRtmErrorOk){
            self.text = [NSString stringWithFormat:@"Login failed for user %@. Code: %ld",self.uid, (long)errorInfo.errorCode];
            NSLog(@"%@", self.text);
        } else {
            NSLog(@"%@", self.text);
            self.text = [NSString stringWithFormat:@"Login successful for user %@. Code: %ld",self.uid, (long)errorInfo.errorCode];
        }
        [self AddMsgToRecord:(self.text)];
    }];

}

- (IBAction)Logout:(id)sender {
if (_kit != nil) {
    // Log out from the RTM server
    [_kit logout:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
        if (errorInfo == nil){
            self.text = [NSString stringWithFormat:@"Logout successful"];
            NSLog(@"%@", self.text);
            [_kit destroy];
            _kit = nil;
        } else {
            self.text = [NSString stringWithFormat:@"Logout failed. Code: %ld",(long)errorInfo.errorCode];
            NSLog(@"%@", self.text);
        }

        [self AddMsgToRecord:(self.text)];
    }];
}

- (IBAction)SubscribeChannel:(id)sender {
self.channelID = self.ChannelIDTextField.text;
if (_kit != nil) {
    // Subscribe to a channel
    [_kit subscribeWithChannel:self.channelID option:nil completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
        if(errorInfo == nil) {
            self.text = [NSString stringWithFormat:@"Successfully subscribe channel %@",self.channelID];
            NSLog(@"%@", self.text);
        } else {
            self.text = [NSString stringWithFormat:@"Failed to subscribe channel %@ Code: %ld",self.channelID, (long)errorInfo.errorCode];
            NSLog(@"%@", self.text);
        }

        [self AddMsgToRecord:(self.text)];
    }];
    }
}

- (IBAction)UnsubscribeChannel:(id)sender {
    if (_kit == nil)
        return;
    // Unsubscribe from a channel
    [_kit unsubscribeWithChannel:self.channelID completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
        if (errorInfo == nil){
            self.text = [NSString stringWithFormat:@"Leave channel successful"];
        } else {
            self.text = [NSString stringWithFormat:@"Failed to leave channel Code: %ld", (long)errorInfo.errorCode]; }

        [self AddMsgToRecord:(self.text)];
    }];
}

- (IBAction)SendMessageToMessageChannel:(id)sender {
self.channelID = self.ChannelIDTextField.text;
self.channelMsg = self.GroupMsgTextField.text;
    // Publish a message
    [_kit publish:self.channelID message:self.channelMsg option:nil completion:^(AgoraRtmCommonResponse * _Nullable response, AgoraRtmErrorInfo * _Nullable errorInfo) {
        if (errorInfo == nil)
        {
            self.text = [NSString stringWithFormat:@"Message sent to channel %@ : %@", self.channelID, self.channelMsg];        }
        else
        {
            self.text = [NSString stringWithFormat:@"Message failed to send to channel %@ : %@ ErrorCode: %ld", self.channelID, self.channelMsg, (long)errorInfo.errorCode];        }

        [self AddMsgToRecord:(self.text)];
    }];
}
@end
```

现在，你可以开始运行你的程序：

1. 保存项目。
2. 复制当前项目，使用相同的 `appId` 和不同的 `userId`。
3. 运行上面的两个项目。成功运行后，你可以在两个设备中看到 App 界面。
4. 将一个设备作为接收端，进行如下操作：
   1. 输入用户名，点击 **Login**。
   2. 输入频道名，点击 **Subscribe**。
5. 将另一个设备作为发送端，进行如下操作：
   1. 输入不同的用户名，点击 **Subscribe**。
   2. 输入相同的频道名。
   3. 输入消息，点击 **Publish MSG**。
6. 将上面的发送端和设备端互换，重复步骤 4 和 5。

如果两个设备均可收发消息，那么你已经成功集成并正确使用了 RTM 服务。

## 贯穿始终 [#贯穿始终-1]

相比于介绍如何写出代码，声网更愿意帮助你掌握上述程序编写的过程和逻辑。上述程序依次完成了以下操作，让你可以正确地收发消息：

1. 设置并初始化 RTM 对象。
2. 添加 `didReceiveMessageEvent` 事件监听函数。
3. 调用 `loginByToken` 登录了 RTM 服务。
4. 调用 `subscribeWithChannel` 订阅了一个 Message Channel。
5. 调用 `publish` 发送消息。
6. 调用 `unsubscribeWithChannel` 取消订阅一个 Message Channel。
7. 调用 `logout` 登出 RTM 系统。

## 下一步 [#下一步-1]

现在，你已经学会了如何使用 RTM Objective-C SDK 来实现 Message Channel 的收发消息功能。下一步，你可以通过 SDK 的 [API 参考](/zh-CN/api-reference/rtm/ios/configuration)了解更多功能的使用方法。
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="web" />
本文将指导你如何利用 RTM JavaScript SDK 构建简单的应用程序，内容涵盖了集成和开发的基础知识：开通声网账号、获取 SDK、发送消息、接收消息等。
## 准备工作 [#准备工作-2]

开始构建项目前，你需要完成[开通 RTM 服务](/zh-CN/realtime-media/rtm/build/rtm-initialization/enable-service)，并检查你的浏览器是否满足[平台支持](/zh-CN/realtime-media/rtm/reference/capabilities-and-compatibility/platform-support)中的最低版本要求。

## 获取 SDK [#获取-sdk]

你可以通过以下任意一种方式获取最新的 RTM JavaScript SDK。

#### 方式一：使用 CDN 添加 SDK [#方式一使用-cdn-添加-sdk]

[点击此处](https://download.agora.io/rtm2/release/Agora_RTM_JavaScript_SDK_for_Web_v2.3.0.zip)下载最新版本的 JavaScript SDK，并在你的项目中添加以下代码引用它：

```javascript
// 使用 CDN 直接将以下代码添加到你的 Web 应用中，或下载 SDK 文件后在本地引用
// 将 x.y.z 替换为具体的 SDK 版本号，如 2.3.0
// 可通过发版说明获取最新版本号
<script src="your_path_to_sdk/agora-rtm.x.y.z.min.js"></script>
```

#### 方式二：使用包管理器引入 SDK [#方式二使用包管理器引入-sdk]

1. 根据你的应用场景和开发需要，选择一种方式和工具安装 SDK。

   **场景一**：项目仅使用 RTM 产品，且没有集成声网任何版本的 RTC 产品。

   <Tabs defaultValue="npm" groupId="package" persist>
   <TabsList>
     <TabsTrigger value="npm">使用 npm</TabsTrigger>
     <TabsTrigger value="pnpm">使用 pnpm</TabsTrigger>
     <TabsTrigger value="yarn">使用 Yarn</TabsTrigger>
   </TabsList>

   <TabsContent value="npm">

   ```shell
   npm install agora-rtm
   ```

   </TabsContent>

   <TabsContent value="pnpm">

   ```shell
   pnpm add agora-rtm
   ```

   </TabsContent>

   <TabsContent value="yarn">

   ```shell
   yarn add agora-rtm
   yarn add agora-rtc-sdk-ng@4.22.0 --save-exact
   ```

   </TabsContent>
   </Tabs>

   **场景二**：项目中已经集成了 RTC 产品，需要新接入 RTM 产品。

   <Tabs defaultValue="npm" groupId="package" persist>
   <TabsList>
     <TabsTrigger value="npm">使用 npm</TabsTrigger>
     <TabsTrigger value="pnpm">使用 pnpm</TabsTrigger>
     <TabsTrigger value="yarn">使用 Yarn</TabsTrigger>
   </TabsList>

   <TabsContent value="npm">

   ```shell
   npm install agora-rtm
   ```

   </TabsContent>

   <TabsContent value="pnpm">

   ```shell
   pnpm add agora-rtm
   ```

   如果项目使用的 RTC 版本与 RTM 中依赖的 RTC 版本不一致，会收到提示：`unmet peer agora-rtc-sdk-ng@4.22.0: found 4.16.0`。此时你需要升级 RTC 的版本：

   ```shell
   pnpm update agora-rtc-sdk-ng@4.22.0
   ```

   或者通过以下命令同时保留两个版本的 RTC 库：

   ```shell
   pnpm add agora-rtc-sdk-ng-4.16@npm:agora-rtc-sdk-ng@4.16.0 --save-exact
   pnpm add agora-rtc-sdk-ng@4.22.0 --save-exact
   ```

   然后将代码中原来的 `import AgoraRTC from "agora-rtc-sdk-ng"` 替换为如下行：

   ```javascript
   import AgoraRTC from "agora-rtc-sdk-ng-4.16"
   ```

   :::warning[注意]
   保留两个版本的 RTC 库会增加项目的包体积。
   :::

   </TabsContent>

   <TabsContent value="yarn">

   ```shell
   yarn add agora-rtm
   ```

   如果项目使用的 RTC 版本与 RTM 中依赖的 RTC 版本不一致，会收到提示：`unmet peer agora-rtc-sdk-ng@4.22.0: found 4.16.0`。此时你需要升级 RTC 的版本：

   ```shell
   yarn upgrade agora-rtc-sdk-ng@4.22.0
   ```

   或者通过以下命令同时保留两个版本的 RTC 库：

   ```shell
   yarn add agora-rtc-sdk-ng-4.16@npm:agora-rtc-sdk-ng@4.16.0 --save-exact
   yarn add agora-rtc-sdk-ng@4.22.0 --save-exact
   ```

   然后将代码中原来的 `import AgoraRTC from "agora-rtc-sdk-ng"` 替换为如下行：

   ```javascript
   import AgoraRTC from "agora-rtc-sdk-ng-4.16"
   ```

   :::warning[注意]
   保留两个版本的 RTC 库会增加项目的包体积。
   :::

   </TabsContent>
   </Tabs>

   **场景三**：你的项目是全新的，且需要同时集成 RTM 和 RTC，以利用其共频道、数据优先级控制等能力。这种场景下建议你集成融合版本的 SDK。

   <Tabs defaultValue="npm" groupId="package" persist>
   <TabsList>
     <TabsTrigger value="npm">使用 npm</TabsTrigger>
     <TabsTrigger value="pnpm">使用 pnpm</TabsTrigger>
     <TabsTrigger value="yarn">使用 Yarn</TabsTrigger>
   </TabsList>

   <TabsContent value="npm">

   ```shell
   npm install agora-rtx
   ```

   </TabsContent>

   <TabsContent value="pnpm">

   ```shell
   pnpm add agora-rtx --shamefully-hoist
   ```

   </TabsContent>

   <TabsContent value="yarn">

   ```shell
   # 若版本为 2.2.0
   # yarn add agora-rtx
   # yarn add agora-rtc-sdk-ng@4.22.0 --save-exact

   # 2.2.0 及之后版本
   yarn add agora-rtx
   ```

   </TabsContent>
   </Tabs>

2. 完成安装后，在你的 `app.js` 中引入 RTM：

    ```javascript
    // 如果安装的是 agora-rtm
    import AgoraRTM from 'agora-rtm';

    // 如果安装的是融合版 agora-rtx
    // import AgoraRTM from 'agora-rtx';
    // import AgoraRTC from 'agora-rtc-sdk-ng';
    ```

## 构建项目 [#构建项目-2]

### 1. 项目配置 [#1-项目配置-2]

新建一个文件夹存放项目文件，进入文件夹中为你的 App 创建文件并安装其他依赖项。以下为在终端操作的命令行示例：

  ```shell showLineNumbers
  mkdir HelloWorld
  cd HelloWorld
  # Your app is in index.html.
  touch index.html
  ```

### 2. 初始化 RTM [#2-初始化-rtm-2]

初始化 `rtm` 时，你需要提供如下信息：

- 从控制台获取的 App ID（`appId`）。
- 你自行定义的用户 ID（`userId`），你需要保证其全局唯一性。

```html showLineNumbers
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Text Display App</title>
  <style>
    #container {
      width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    #textDisplay {
      width: 100%;
      height: 800px;
      border: 1px solid #b0b0b0;
      margin-bottom: 20px;
      overflow: auto;
      text-align: left;
      padding: 10px;
      box-sizing: border-box;
    }
    #inputContainer {
      display: flex;
      align-items: center;
    }
    #textInput {
      width: calc(100% - 100px);
      padding: 5px;
      margin-right: 10px;
    }
    #submitButton {
      width: 90px;
      padding: 5px;
    }
  </style>
  <script src="./AgoraRTM-production.js"></script>
</head>
<body>
  <script>
    const { RTM } = AgoraRTM;
    // Fill in the App ID of your project.
    const appId = "your_appId";
    // Fill in your user ID.
    const userId = "your_userId";
    // Fill in your channel name.
    let rtm;
    const msChannelName = "Chat_room";
    const buttonClick = () => {
      var input = document.getElementById("textInput");
      publishMessage(input.value);
      input.value = '';
    }
    const setupRTM = async () => {
      // Initialize the RTM client.
      try {
        rtm = new RTM(appId, userId);
      } catch (status) {
        console.log("Error");
        console.log(status);
      }

      // Add the event listener.

      // Log in the RTM server.

      // Subscribe to a channel.

    }
    // Publish a message.
    const showMessage = (user, msg) => {
      // Get text from the text box.
      const inputText = textInput.value;
      const newText = document.createTextNode(user + ": " + msg);
      const newLine = document.createElement("br");
      textDisplay.appendChild(newText);
      textDisplay.appendChild(newLine);
    }
    window.onload = setupRTM;
  </script>
  <div id="container">
    <h1>Hello RTM !</h1>
    <div>
      <div id="textDisplay"></div>
    </div>
    <div id="inputContainer">
      <input type="text" id="textInput" placeholder="Enter text">
      <button id="submitButton" onclick="buttonClick()"> Send </button>
    </div>
  </div>
</body>
</html>
```

:::info[信息]
你需要在初始化时填入项目的 App ID 和 Token。在测试阶段，为快速验证功能，我们建议你在创建声网项目时将鉴权模式设置为调试模式，然后在初始化时只需填入项目的 App ID。
:::

### 3. 添加事件监听 [#3-添加事件监听-2]

事件监听程序帮助你实现频道中消息、事件到达后的处理逻辑，添加以下代码到你的程序中以显示收到的消息或事件通知：

```javascript showLineNumbers
// Paste the following code snippet below "Add the event listener" comment
// Message event handler.
rtm.addEventListener("message", event => {
  showMessage(event.publisher, event.message);
});
// Pressence event handler.
rtm.addEventListener("presence", event => {
  if (event.eventType === "SNAPSHOT") {
    showMessage("INFO", "I Join");
  }
  else {
    showMessage("INFO", event.publisher + " is " + event.eventType);
  }
});
// Link state changed event handler.
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 affectedChannels = event.affectedChannels;
  const timestamp = event.timestamp;
  const isResumed = event.isResumed;
  showMessage("INFO", JSON.stringify(event));
});
```

### 4. 登录服务 [#4-登录服务-2]

你需要执行登录操作才能建立与 RTM 服务器的连接，然后才能调用 SDK 的其他 API。将以下代码添加到程序中，并传入你在服务端自行生成的 Token：

```javascript showLineNumbers
// Paste the following code snippet below "Log in the RTM server." comment
try {
  const result = await rtm.login({ token: 'your_token' });
  console.log(result);
} catch (status) {
  console.log(status);
}
```

### 5. 收发消息 [#5-收发消息-2]

调用 `publish` 方法向 Message Channel 发送消息后，RTM 会把该消息分发给该频道的所有订阅者，以下代码演示如何发送字符串类型的消息，将此代码片段添加到程序中：

:::info[信息]
你需要先对消息负载进行字符串序列化，才能调用 `publish` 方法发送消息。
:::

```javascript showLineNumbers
// Paste the following code snippet below "Publish a message" comment
const publishMessage = async (message) => {
  const payload = { type: "text", message: message };
  const publishMessage = JSON.stringify(payload);
  const publishOptions = { channelType: 'MESSAGE'}
  try {
    const result = await rtm.publish(msChannelName, publishMessage, publishOptions);
    showMessage(userId, publishMessage);
    console.log(result);
  } catch (status) {
    console.log(status);
  }
}
```

调用 `subscribe` 方法订阅此频道以接收此频道中的消息。将以下代码添加到程序中：

```javascript showLineNumbers
// Paste the following code snippet below "Subscribe to a channel." comment
try {
  const result = await rtm.subscribe(msChannelName);
  console.log(result);
} catch (status) {
  console.log(status);
}
```

如需了解更多收发消息的信息，查看[消息](/zh-CN/api-reference/rtm/web/message)。

### 6. 组合到一起 [#6-组合到一起]

经过上述步骤，你程序中的代码应该如下所示：

```html showLineNumbers
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Text Display App</title>
  <style>
    #container {
      width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    #textDisplay {
      width: 100%;
      height: 800px;
      border: 1px solid #b0b0b0;
      margin-bottom: 20px;
      overflow: auto;
      text-align: left;
      padding: 10px;
      box-sizing: border-box;
    }
    #inputContainer {
      display: flex;
      align-items: center;
    }
    #textInput {
      width: calc(100% - 100px);
      padding: 5px;
      margin-right: 10px;
    }
    #submitButton {
      width: 90px;
      padding: 5px;
    }
  </style>
  <script src="./AgoraRTM-production.js"></script>
</head>
<body>
  <script>
    const { RTM } = AgoraRTM;
    // Fill in the App ID of your project.
    const appId = "your_appId";
    // Fill in your user ID.
    const userId = "your_userId";
    // Fill in your channel name.
    let rtm;
    const msChannelName = "Chat_room";
    const buttonClick = () => {
      var input = document.getElementById("textInput");
      publishMessage(input.value);
      input.value = '';
    }
    const setupRTM = async () => {
      // Initialize the RTM client.
      try {
        rtm = new RTM(appId, userId);
      } catch (status) {
        console.log("Error");
        console.log(status);
      }
      // Add the event listener.
      // Message event handler.
      rtm.addEventListener("message", event => {
        showMessage(event.publisher, event.message);
      });
      // Pressence event handler.
      rtm.addEventListener("presence", event => {
        if (event.eventType === "SNAPSHOT") {
          showMessage("INFO", "I Join");
        }
        else {
          showMessage("INFO", event.publisher + " is " + event.eventType);
        }
      });
      // Link state changed event handler.
      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 affectedChannels = event.affectedChannels;
        const timestamp = event.timestamp;
        const isResumed = event.isResumed;
        showMessage("INFO", JSON.stringify(event));
      });
      // Log in the RTM server.
      try {
        const result = await rtm.login({  token: 'your_token' });
        console.log(result);
      } catch (status) {
        console.log(status);
      }
      // Subscribe to a channel.
      try {
        const result = await rtm.subscribe(msChannelName);
        console.log(result);
      } catch (status) {
        console.log(status);
      }
    }
    const publishMessage = async (message) => {
      const payload = { type: "text", message: message };
      const publishMessage = JSON.stringify(payload);
      const publishOptions = { channelType: 'MESSAGE'}
      try {
        const result = await rtm.publish(msChannelName, publishMessage, publishOptions);
        showMessage(userId, publishMessage);
        console.log(result);
      } catch (status) {
        console.log(status);
      }
    }
    const showMessage = (user, msg) => {
      // Get text from the text box.
      const inputText = textInput.value;
      const newText = document.createTextNode(user + ": " + msg);
      const newLine = document.createElement("br");
      textDisplay.appendChild(newText);
      textDisplay.appendChild(newLine);
    }
    window.onload = setupRTM;
  </script>
  <div id="container">
    <h1>Hello RTM !</h1>
    <div>
      <div id="textDisplay"></div>
    </div>
    <div id="inputContainer">
      <input type="text" id="textInput" placeholder="Enter text">
      <button id="submitButton" onclick="buttonClick()"> Send </button>
    </div>
  </div>
</body>
</html>
```

现在，你可以开始运行你的程序：

1. 保存 **index.html**，并在浏览器中运行。我们称此时看到的页面为 A 页面。
2. 复制当前项目，在 **index.html** 的 `userId` 变量中填入一个不同的用户 ID，保存后运行。我们称此时看到的页面为 B 页面。
3. 在 A 页面文本输入框中输入要发送的信息，点击 **Send** 按钮发送，然后在 B 页面中观察是否收到信息。
4. 在 B 页面文本输入框中输入要发送的信息，点击 **Send** 按钮发送，然后在 A 页面中观察是否收到信息。

如果步骤 3 和步骤 4 中都能看到正确的信息，那么你已经成功集成并正确使用了 RTM 服务。

## 贯穿始终 [#贯穿始终-2]

相比于介绍如何写出代码，声网更愿意帮助你掌握上述程序编写的过程和逻辑。上述程序依次完成了以下操作，让你可以正确地收发消息：

1. 设置并初始化 RTM 对象。
2. 添加 `message`、`presence` 和 `linkState` 事件监听函数。
3. 调用 `login` 登录了 RTM 服务。
4. 调用 `subscribe` 订阅了一个 Message Channel。
5. 调用 `publish` 发送消息。

## 下一步 [#下一步-2]

现在，你已经学会了如何使用 RTM JavaScript SDK 来实现 Message Channel 的收发消息功能。下一步，你可以通过 SDK 的 [API 参考](/zh-CN/api-reference/rtm/web/configuration)了解更多功能的使用方法。
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="harmonyos" />
本文将指导你如何利用 RTM HarmonyOS SDK 构建简单的应用程序，内容涵盖了集成和开发的基础知识：开通声网账号、获取 SDK、发送消息、接收消息等。
## 准备工作 [#准备工作-3]

开始构建项目前，你需要完成[开通 RTM 服务](/zh-CN/realtime-media/rtm/build/rtm-initialization/enable-service)，并检查你的开发环境是否满足[平台支持](/zh-CN/realtime-media/rtm/reference/capabilities-and-compatibility/platform-support)中的最低版本要求。

## 构建项目 [#构建项目-3]

### 1. 获取 SDK [#1-获取-sdk]

[点击此处](https://download.shengwang.cn/rtm2/release/RTM_ArkTS_SDK_for_HarmonyOS_v2.3.0.zip)下载最新版本的 HarmonyOS SDK。

下载完成后，解压 SDK 包，你将获得一个 `.har` 文件，这是 HarmonyOS 的静态共享包格式。

:::info[信息]
如果你同时集成了 2.2.0 及以上版本的 RTM SDK 和 4.3.0 及以上版本的 RTC SDK，请参考 [FAQ](/zh-CN/reference/faq/integration/rtm2_rtc_integration_issue) 处理集成问题。
:::

### 2. 项目配置 [#2-项目配置]

根据以下步骤配置你的项目：

1. 在 DevEco Studio 中，创建一个新的 HarmonyOS 应用项目。

2. 将下载的 `.har` 文件导入到你的项目中：
   - 在项目根目录下创建 `libs` 文件夹（如果不存在）
   - 将 `.har` 文件复制到 `libs` 文件夹中
   - 在模块的 `oh-package.json5` 文件中添加依赖：

   ```json
   {
     "dependencies": {
        "@shengwang/rtm-full": "file:./libs/AgoraRtmSDK.har"
     }
   }
   ```

3. 在模块的 `module.json5` 文件中添加网络权限：

   ```json
   {
     "module": {
       "requestPermissions": [
          {
            "name": "ohos.permission.INTERNET"
          },
          {
            "name": "ohos.permission.GET_NETWORK_INFO"
          }
       ]
     }
   }
   ```

4. 同步项目依赖，确保 SDK 正确导入。

### 3. 创建并初始化 RTM [#3-创建并初始化-rtm]

调用 RTM SDK 的任何 API 之前，需要先创建一个 `RtmClient` 对象实例。在你的项目中创建一个 ArkTS 文件（例如 `RtmManager.ets`），添加以下代码作为程序模版。你会发现此程序并不完整，不用紧张，我们在后续步骤中将一步步指导你补充完善代码。

:::info[信息]
你需要在初始化时填入已开通 RTM 服务的项目的 App ID 和 Token。在测试阶段，为快速验证功能，我们建议你在创建声网项目时将鉴权模式设置为调试模式，然后在示例中填入项目的 App ID 即可。
:::

```typescript
import { RtmClient, RtmConfig, RtmChannelType, SubscribeOptions, PublishOptions } from '@shengwang/rtm-full';
import type { LinkStateEvent, MessageEvent, PresenceEvent } from '@shengwang/rtm-full';
import { RtmError } from '@shengwang/rtm-full';

// 填入你的 App ID
const APP_ID = '<Your App ID>';
// 填入你的 Token（测试阶段可以为空字符串）
const TOKEN = '';

export class RtmManager {
  private rtmClient: RtmClient | null = null;
  private userId: string = '';

  /**
   * 初始化 RTM 客户端
   */
  async initialize(userId: string): Promise<void> {
    try {
      this.userId = userId;
      
      // 创建 RTM 配置
      const config: RtmConfig = {
        appId: APP_ID,
        userId: userId
      };

      // 创建 RTM 客户端实例
      this.rtmClient = new RtmClient(config);
      
      // 添加事件监听（后续步骤中补充）
      
      console.log('RTM client initialized successfully');
    } catch (error) {
      console.error('Failed to initialize RTM client:', error);
      if (error instanceof RtmError) {
        throw new Error(`初始化失败: ${error.reason} (code: ${error.errorCode})`);
      } else {
        throw new Error('RTM client initialization failed');
      }
    }
  }

  /**
   * 登录 RTM 服务
   */
  async login(): Promise<void> {
    // 后续步骤中补充
  }

  /**
   * 订阅频道
   */
  async subscribe(channelName: string): Promise<void> {
    // 后续步骤中补充
  }

  /**
   * 取消订阅频道
   */
  async unsubscribe(channelName: string): Promise<void> {
    // 后续步骤中补充
  }

  /**
   * 发送消息
   */
  async publish(channelName: string, message: string): Promise<void> {
    // 后续步骤中补充
  }

  /**
   * 登出 RTM 服务
   */
  async logout(): Promise<void> {
    // 后续步骤中补充
  }

  /**
   * 释放资源
   */
  release(): void {
    if (this.rtmClient) {
      this.rtmClient.release();
      this.rtmClient = null;
    }
  }
}
```

### 4. 添加事件监听 [#4-添加事件监听]

事件监听程序帮助你实现频道中消息、事件到达后的处理逻辑。在 `initialize` 方法中添加事件监听器，以显示收到的消息或事件通知：

```typescript showLineNumbers
// 在 initialize 方法中，创建 RTM 客户端后添加以下代码
// 添加事件监听（替换之前的注释）

// 监听连接状态变化
this.rtmClient.addEventListener('linkState', (event) => {
  console.log('Link state changed:', event);
  console.log(`State: ${event.previousState} -> ${event.currentState}`);
  console.log(`Reason: ${event.reason}`);
});

// 监听接收到的消息
this.rtmClient.addEventListener('message', (event) => {
  console.log('Received message:', event);
  console.log(`From: ${event.publisher}`);
  console.log(`Channel: ${event.channelName}`);
  console.log(`Message: ${event.message}`);
});

// 监听 Presence 事件（用户加入/离开）
this.rtmClient.addEventListener('presence', (event) => {
  console.log('Presence event:', event);
  console.log(`Event type: ${event.eventType}`);
  console.log(`User: ${event.publisher}`);
});
```

### 5. 登录服务 [#5-登录服务]

你需要执行登录操作才能建立与 RTM 服务器的连接，然后才能调用 SDK 的其他 API。在 `login` 方法中添加以下代码：

```typescript showLineNumbers
// 在 login 方法中添加以下代码
async login(): Promise<void> {
  if (!this.rtmClient) {
    throw new Error('RTM client not initialized');
  }

  try {
    const response = await this.rtmClient.login(TOKEN);
    console.log('Login successful:', response);
  } catch (error) {
    console.error('Login failed:', error);
    if (error instanceof RtmError) {
      throw new Error(`登录失败: ${error.reason} (code: ${error.errorCode})`);
    } else {
      throw new Error('Login failed');
    }
  }
}
```

:::info[信息]
你需要在登录时传入 Token。在测试阶段，为快速验证功能，我们建议你在创建声网项目时将鉴权模式设置为调试模式，然后在登录时传入空字符串即可。
:::

### 6. 收发消息 [#6-收发消息]

调用 `publish` 方法向 Message Channel 发送消息后，RTM 会把该消息分发给该频道的所有订阅者。以下代码演示如何发送字符串类型的消息：

:::info[信息]
你需要先对消息负载进行字符串序列化，才能调用 `publish` 方法发送消息。
:::

```typescript showLineNumbers
// 在 publish 方法中添加以下代码
async publish(channelName: string, message: string): Promise<void> {
  if (!this.rtmClient) {
    throw new Error('RTM client not initialized');
  }

  try {
    const options: PublishOptions = {
      channelType: RtmChannelType.MESSAGE,
      customType: ''
    };
    
    const response = await this.rtmClient.publish(channelName, message, options);
    console.log('Message published successfully:', response);
  } catch (error) {
    console.error('Failed to publish message:', error);
    if (error instanceof RtmError) {
      throw new Error(`发送消息失败: ${error.reason} (code: ${error.errorCode})`);
    } else {
      throw new Error('Publish failed');
    }
  }
}
```

调用 `subscribe` 方法订阅此频道以接收此频道中的消息：

```typescript showLineNumbers
// 在 subscribe 方法中添加以下代码
async subscribe(channelName: string): Promise<void> {
  if (!this.rtmClient) {
    throw new Error('RTM client not initialized');
  }

  try {
    const options: SubscribeOptions = {
      withMessage: true,
      withMetadata: false,
      withPresence: true,
      withLock: false
    };
    
    const response = await this.rtmClient.subscribe(channelName, options);
    console.log('Subscribed to channel successfully:', response);
  } catch (error) {
    console.error('Failed to subscribe to channel:', error);
    if (error instanceof RtmError) {
      throw new Error(`订阅失败: ${error.reason} (code: ${error.errorCode})`);
    } else {
      throw new Error('Subscribe failed');
    }
  }
}
```

如果你不再需要在此频道收发消息，你可以调用 `unsubscribe` 方法取消订阅此频道：

```typescript showLineNumbers
// 在 unsubscribe 方法中添加以下代码
async unsubscribe(channelName: string): Promise<void> {
  if (!this.rtmClient) {
    throw new Error('RTM client not initialized');
  }

  try {
    const response = await this.rtmClient.unsubscribe(channelName);
    console.log('Unsubscribed from channel successfully:', response);
  } catch (error) {
    console.error('Failed to unsubscribe from channel:', error);
    if (error instanceof RtmError) {
      throw new Error(`取消订阅失败: ${error.reason} (code: ${error.errorCode})`);
    } else {
      throw new Error('Unsubscribe failed');
    }
  }
}
```

如需了解更多收发消息的信息，查看[消息](/zh-CN/api-reference/rtm/harmonyos/message)。

### 7. 登出服务 [#7-登出服务]

当不再需要使用 RTM 服务时，你可以调用 `logout` 方法登出 RTM 系统：

```typescript showLineNumbers
// 在 logout 方法中添加以下代码
async logout(): Promise<void> {
  if (!this.rtmClient) {
    throw new Error('RTM client not initialized');
  }

  try {
    const response = await this.rtmClient.logout();
    console.log('Logout successful:', response);
  } catch (error) {
    console.error('Logout failed:', error);
    if (error instanceof RtmError) {
      throw new Error(`登出失败: ${error.reason} (code: ${error.errorCode})`);
    } else {
      throw new Error('Logout failed');
    }
  }
}
```

:::info[信息]
本操作会影响你账单中的 PCU 计费项。
:::

### 8. 组合到一起 [#8-组合到一起]

经过上述步骤，你的完整代码应该如下所示：

```typescript
import { RtmClient, RtmConfig, RtmChannelType, SubscribeOptions, PublishOptions } from '@shengwang/rtm-full';
import type { LinkStateEvent, MessageEvent, PresenceEvent } from '@shengwang/rtm-full';
import { RtmError } from '@shengwang/rtm-full';

// 填入你的 App ID
const APP_ID = '<Your App ID>';
// 填入你的 Token（测试阶段可以为空字符串）
const TOKEN = '';

export class RtmManager {
  private rtmClient: RtmClient | null = null;
  private userId: string = '';

  /**
   * 初始化 RTM 客户端
   */
  async initialize(userId: string): Promise<void> {
    try {
      this.userId = userId;
      
      // 创建 RTM 配置
      const config: RtmConfig = {
        appId: APP_ID,
        userId: userId
      };

      // 创建 RTM 客户端实例
      this.rtmClient = new RtmClient(config);
      
      // 监听连接状态变化
      this.rtmClient.addEventListener('linkState', (event: object) => {
        const linkEvent = event as LinkStateEvent;
        console.log('Link state changed:', linkEvent);
        console.log(`State: ${linkEvent.previousState} -> ${linkEvent.currentState}`);
        console.log(`Reason: ${linkEvent.reason}`);
      });

      // 监听接收到的消息
      this.rtmClient.addEventListener('message', (event: object) => {
        const msgEvent = event as MessageEvent;
        console.log('Received message:', msgEvent);
        console.log(`From: ${msgEvent.publisher}`);
        console.log(`Channel: ${msgEvent.channelName}`);
        console.log(`Message: ${msgEvent.message}`);
      });

      // 监听 Presence 事件（用户加入/离开）
      this.rtmClient.addEventListener('presence', (event: object) => {
        const presenceEvent = event as PresenceEvent;
        console.log('Presence event:', presenceEvent);
        console.log(`Event type: ${presenceEvent.eventType}`);
        console.log(`User: ${presenceEvent.publisher}`);
      });
      
      console.log('RTM client initialized successfully');
    } catch (error) {
      console.error('Failed to initialize RTM client:', error);
      if (error instanceof RtmError) {
        throw new Error(`初始化失败: ${error.reason} (code: ${error.errorCode})`);
      } else {
        throw new Error('RTM client initialization failed');
      }
    }
  }

  /**
   * 登录 RTM 服务
   */
  async login(): Promise<void> {
    if (!this.rtmClient) {
      throw new Error('RTM client not initialized');
    }

    try {
      const response = await this.rtmClient.login(TOKEN);
      console.log('Login successful:', response);
    } catch (error) {
      console.error('Login failed:', error);
      if (error instanceof RtmError) {
        throw new Error(`登录失败: ${error.reason} (code: ${error.errorCode})`);
      } else {
        throw new Error('Login failed');
      }
    }
  }

  /**
   * 订阅频道
   */
  async subscribe(channelName: string): Promise<void> {
    if (!this.rtmClient) {
      throw new Error('RTM client not initialized');
    }

    try {
      const options: SubscribeOptions = {
        withMessage: true,
        withMetadata: false,
        withPresence: true,
        withLock: false
      };
      
      const response = await this.rtmClient.subscribe(channelName, options);
      console.log('Subscribed to channel successfully:', response);
    } catch (error) {
      console.error('Failed to subscribe to channel:', error);
      if (error instanceof RtmError) {
        throw new Error(`订阅失败: ${error.reason} (code: ${error.errorCode})`);
      } else {
        throw new Error('Subscribe failed');
      }
    }
  }

  /**
   * 取消订阅频道
   */
  async unsubscribe(channelName: string): Promise<void> {
    if (!this.rtmClient) {
      throw new Error('RTM client not initialized');
    }

    try {
      const response = await this.rtmClient.unsubscribe(channelName);
      console.log('Unsubscribed from channel successfully:', response);
    } catch (error) {
      console.error('Failed to unsubscribe from channel:', error);
      if (error instanceof RtmError) {
        throw new Error(`取消订阅失败: ${error.reason} (code: ${error.errorCode})`);
      } else {
        throw new Error('Unsubscribe failed');
      }
    }
  }

  /**
   * 发送消息
   */
  async publish(channelName: string, message: string): Promise<void> {
    if (!this.rtmClient) {
      throw new Error('RTM client not initialized');
    }

    try {
      const options: PublishOptions = {
        channelType: RtmChannelType.MESSAGE,
        customType: ''
      };
      
      const response = await this.rtmClient.publish(channelName, message, options);
      console.log('Message published successfully:', response);
    } catch (error) {
      console.error('Failed to publish message:', error);
      if (error instanceof RtmError) {
        throw new Error(`发送消息失败: ${error.reason} (code: ${error.errorCode})`);
      } else {
        throw new Error('Publish failed');
      }
    }
  }

  /**
   * 登出 RTM 服务
   */
  async logout(): Promise<void> {
    if (!this.rtmClient) {
      throw new Error('RTM client not initialized');
    }

    try {
      const response = await this.rtmClient.logout();
      console.log('Logout successful:', response);
    } catch (error) {
      console.error('Logout failed:', error);
      if (error instanceof RtmError) {
        throw new Error(`登出失败: ${error.reason} (code: ${error.errorCode})`);
      } else {
        throw new Error('Logout failed');
      }
    }
  }

  /**
   * 释放资源
   */
  release(): void {
    if (this.rtmClient) {
      this.rtmClient.release();
      this.rtmClient = null;
    }
  }
}
```

现在，你可以在你的 HarmonyOS 应用中使用这个 `RtmManager` 类。以下是一个简单的页面示例，展示如何在 Index 页面中使用 RTM 功能：

```typescript
// Index.ets
import { RtmManager } from './RtmManager';
import { RtmChannelType } from '@shengwang/rtm-full';

@Entry
@Component
struct Index {
  @State userId: string = '';
  @State channelName: string = '';
  @State message: string = '';
  @State messageHistory: string = '';
  @State isLoggedIn: boolean = false;
  
  private rtmManager: RtmManager = new RtmManager();

  build() {
    Column() {
      // 标题
      Text('RTM 快速开始')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .margin({ top: 20, bottom: 20 })

      // 用户 ID 输入
      Row() {
        Text('用户 ID:')
          .width(80)
        TextInput({ placeholder: '请输入用户 ID' })
          .layoutWeight(1)
          .onChange((value: string) => {
            this.userId = value;
          })
      }
      .width('90%')
      .margin({ bottom: 10 })

      // 登录/登出按钮
      Row() {
        Button(this.isLoggedIn ? '登出' : '登录')
          .width('45%')
          .onClick(() => {
            if (this.isLoggedIn) {
              this.handleLogout();
            } else {
              this.handleLogin();
            }
          })
      }
      .width('90%')
      .margin({ bottom: 20 })

      Divider()
        .margin({ bottom: 20 })

      // 频道名输入
      Row() {
        Text('频道名:')
          .width(80)
        TextInput({ placeholder: '请输入频道名' })
          .layoutWeight(1)
          .onChange((value: string) => {
            this.channelName = value;
          })
      }
      .width('90%')
      .margin({ bottom: 10 })

      // 订阅/取消订阅按钮
      Row() {
        Button('订阅')
          .width('45%')
          .onClick(() => {
            this.handleSubscribe();
          })
          .enabled(this.isLoggedIn)

        Button('取消订阅')
          .width('45%')
          .onClick(() => {
            this.handleUnsubscribe();
          })
          .enabled(this.isLoggedIn)
      }
      .width('90%')
      .justifyContent(FlexAlign.SpaceBetween)
      .margin({ bottom: 20 })

      // 消息输入
      Row() {
        Text('消息:')
          .width(80)
        TextInput({ placeholder: '请输入消息内容' })
          .layoutWeight(1)
          .onChange((value: string) => {
            this.message = value;
          })
      }
      .width('90%')
      .margin({ bottom: 10 })

      // 发送消息按钮
      Button('发送消息')
        .width('90%')
        .onClick(() => {
          this.handlePublish();
        })
        .enabled(this.isLoggedIn)
        .margin({ bottom: 20 })

      Divider()
        .margin({ bottom: 10 })

      // 消息历史
      Text('消息历史:')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .alignSelf(ItemAlign.Start)
        .margin({ left: '5%', bottom: 10 })

      Scroll() {
        Text(this.messageHistory)
          .width('100%')
          .padding(10)
      }
      .width('90%')
      .height(200)
      .backgroundColor('#F0F0F0')
      .borderRadius(8)
    }
    .width('100%')
    .height('100%')
    .padding({ top: 20 })
  }

  // 登录处理
  async handleLogin() {
    if (!this.userId) {
      this.addMessage('请输入用户 ID');
      return;
    }

    try {
      await this.rtmManager.initialize(this.userId);
      await this.rtmManager.login();
      this.isLoggedIn = true;
      this.addMessage(`用户 ${this.userId} 登录成功`);
    } catch (error) {
      this.addMessage(`登录失败: ${error}`);
    }
  }

  // 登出处理
  async handleLogout() {
    try {
      await this.rtmManager.logout();
      this.rtmManager.release();
      this.isLoggedIn = false;
      this.addMessage('登出成功');
    } catch (error) {
      this.addMessage(`登出失败: ${error}`);
    }
  }

  // 订阅频道
  async handleSubscribe() {
    if (!this.channelName) {
      this.addMessage('请输入频道名');
      return;
    }

    try {
      await this.rtmManager.subscribe(this.channelName);
      this.addMessage(`订阅频道 ${this.channelName} 成功`);
    } catch (error) {
      this.addMessage(`订阅失败: ${error}`);
    }
  }

  // 取消订阅频道
  async handleUnsubscribe() {
    if (!this.channelName) {
      this.addMessage('请输入频道名');
      return;
    }

    try {
      await this.rtmManager.unsubscribe(this.channelName);
      this.addMessage(`取消订阅频道 ${this.channelName} 成功`);
    } catch (error) {
      this.addMessage(`取消订阅失败: ${error}`);
    }
  }

  // 发送消息
  async handlePublish() {
    if (!this.channelName) {
      this.addMessage('请输入频道名');
      return;
    }

    if (!this.message) {
      this.addMessage('请输入消息内容');
      return;
    }

    try {
      await this.rtmManager.publish(this.channelName, this.message);
      this.addMessage(`发送消息到 ${this.channelName}: ${this.message}`);
    } catch (error) {
      this.addMessage(`发送消息失败: ${error}`);
    }
  }

  // 添加消息到历史记录
  addMessage(msg: string) {
    const timestamp = new Date().toLocaleTimeString();
    this.messageHistory += `[${timestamp}] ${msg}\n`;
  }
}
```

这个示例展示了如何：

1. **创建 RTM 管理器实例**：在组件中创建 `RtmManager` 实例
2. **用户登录**：输入用户 ID，点击登录按钮初始化并登录 RTM
3. **订阅频道**：输入频道名，点击订阅按钮订阅消息频道
4. **发送消息**：输入消息内容，点击发送按钮向频道发送消息
5. **取消订阅**：点击取消订阅按钮退出频道
6. **用户登出**：点击登出按钮断开 RTM 连接

你也可以使用更简单的方式来测试 RTM 功能：

```typescript
// 在你的页面或组件中使用
import { RtmManager } from './RtmManager';

// 创建 RTM 管理器实例
const rtmManager = new RtmManager();

// 初始化并登录
await rtmManager.initialize('user123');
await rtmManager.login();

// 订阅频道
await rtmManager.subscribe('test-channel');

// 发送消息
await rtmManager.publish('test-channel', 'Hello, RTM!');

// 取消订阅
await rtmManager.unsubscribe('test-channel');

// 登出
await rtmManager.logout();

// 释放资源
rtmManager.release();
```

至此，你已经成功集成并正确使用了 RTM 服务。

## 贯穿始终 [#贯穿始终-3]

相比于介绍如何写出代码，声网更愿意帮助你掌握上述程序编写的过程和逻辑。上述程序依次完成了以下操作，让你可以正确地收发消息：

1. 创建并初始化 RTM 对象。
2. 添加 `linkState`、`message` 和 `presence` 事件监听函数。
3. 调用 `login` 登录了 RTM 服务。
4. 调用 `subscribe` 订阅了一个 Message Channel。
5. 调用 `publish` 发送消息。
6. 调用 `unsubscribe` 取消订阅一个 Message Channel。
7. 调用 `logout` 登出 RTM 系统。

## 下一步 [#下一步-3]

现在，你已经学会了如何使用 RTM HarmonyOS SDK 来实现 Message Channel 的收发消息功能。下一步，你可以通过 SDK 的 [API 参考](/zh-CN/api-reference/rtm/harmonyos/configuration)了解更多功能的使用方法。
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="unity" />
本文将指导你如何利用 RTM SDK 构建简单的应用程序，内容涵盖了集成和开发的基础知识：开通声网账号、获取 SDK、发送消息、接收消息等。
## 准备工作 [#准备工作-4]

开始构建项目前，你需要完成[开通 RTM 服务](/zh-CN/realtime-media/rtm/build/rtm-initialization/enable-service)，并检查你的开发环境和运行环境是否满足如下要求：

- [Unity Hub](https://unity.com/download) 任意版本

- Unity 2018.4.0 或以上版本

- 操作系统与编译器要求：

  | 开发平台 |    操作系统版本    |             编译器版本              |
  | ------ | ---------------- | ----- |
  | Android  | Android 4.1 或以上 |      Android Studio 3.0 或以上      |
  |   iOS    |   iOS 11.0 或以上   |          Xcode 9.0 或以上           |
  |  macOS   | macOS 10.10 或以上 |          Xcode 9.0 或以上           |
  | Windows  |  Windows 7 或以上  | Microsoft Visual Studio 2017 或以上 |

## 获取 SDK [#获取-sdk-1]

[点击此处](https://download.agora.io/sdk/release/Agora_Unity_RTM_SDK_v2.1.9.zip)下载最新版本的 Unity SDK，并在你的项目中导入它。

## 导入 SDK [#导入-sdk]

1. 打开 Unity Hub，点击 **New Project** 新建一个 Unity 3D 项目。

2. 点击顶部菜单栏的 **Assets** 菜单，依次选择 **Import Package**、**Custom Package** 向项目中导入你刚刚获取的 RTM Unity Package。

## 初始化 RTM [#初始化-rtm]

调用 RTM SDK 的任何 API 之前，需要先初始化一个 `IRtmClient` 对象实例。参考如下步骤创建用户界面、初始化一个 `IRtmClient` 对象实例并添加项目脚本：

:::tip[初始化配置]
初始化 RTM 的时候可以通过配置某些参数改变 `IRtmClient` 其后续行为，例如配置事件监听函数、端侧消息加密等。详见[初始配置](/zh-CN/api-reference/rtm/unity/configuration)。
:::

1. 我们为你提供了一个简单的示例项目，其中进行了简易的 UI 界面设计以便于验证功能特性。UI 界面及组件架构如下图所示：

![示例项目的 UI 界面及组件结构](https://web-cdn.agora.io/docs-portal/img/rtm2/rtm2ui.png)

   ```json showLineNumbers
   SampleScene

   - Main Camera
   - Canvas
    - Image                          // 窗口背景
    - Txt1                           // 文本标签，文本内容为 Send string message to message channel
    - InputField1                    // 消息输入框
    - Btn1                           // 发送消息按钮
    - Txt2                           // 文本标签，文本内容为 Send binary message to stream channel
    - Btn2                           // 创建 Stream Channel 按钮
    - Btn3                           // 加入 Stream Channel 按钮
    - Btn4                           // 加入 Topic 按钮
    - Btn5                           // 订阅 Topic 中的消息发布者按钮
    - InputField2                    // 消息输入框
    - Btn6                           // 发送消息按钮
    - Btn7                           // 释放 Stream Channel 按钮
    - Btn8                           // 离开 Stream Channel 按钮
    - Btn9                           // 离开 Topic 按钮
    - Btn10                          // 取消订阅 Topic 中的消息发布者按钮
    - Pannel                          // 信息输出面板
        - Txt3                        // 信息输出文字
   - EventSystem
   ```

2. 在 Unity 编辑器中，选择顶部菜单栏的 **Assets** 菜单，依次点击 **Create**、**C# Script** 为项目新建脚本，名为 **HelloWorld.cs**。

3. 在 **HelloWorld.cs** 脚本中添加以下代码作为程序模版。你会发现此程序并不完整，不用紧张，我们在后续步骤中将一步步指导你补充完善代码。

   ```csharp showLineNumbers title="Assets/Hello_world.cs"
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using Agora.Rtm;

    public class HelloWorld : MonoBehaviour
    {
        [SerializeField]
        private string appId = "your_appId";
        // Get an App ID from console portal
        private string userId = "your_userId";
        // Change the user ID by yourself

        private string msChannelName = "SeeYou";
        private string stChannelName = "Greeting";
        private string topicName = "Hello_world";

        private IRtmClient rtmClient;
        private IStreamChannel streamChannel;

        public void Awake()
        {
            Initialize();
        }

        // Initialize the RTM
        public async void Initialize()
        {
            // Initialize the RTM Client

            // Add listener
            if (rtmClient != null)
            {
                // Add message event listener
                rtmClient.OnMessageEvent += OnMessageEvent;
                // Add the presence event listener
                rtmClient.OnPresenceEvent += OnPresenceEvent;
                // Add the connection state change listener
                rtmClient.OnConnectionStateChanged += OnConnectionStateChanged;
                // Log in to the RTM server

                // Subscribe to a Message Channel

            }
        }

        private void ChangeComponentView(string[] componentsList, string viewType)
        {
            for (int i = 0; i < componentsList.Length; i++)
            {
                if (viewType == "ACTIVATE")
                GameObject.Find(componentsList[i]).GetComponent<Button>().interactable = true;
                else if (viewType == "INACTIVATE")
                GameObject.Find(componentsList[i]).GetComponent<Button>().interactable = false;
            }
        }

        private void ShowMessage(string msg)
        {
            GameObject.Find("Txt3").GetComponent<Text>().text += "\n" + msg;
        }

        // Implement the event listener handler

        // OnClick event handler for Btn1
        public async void PublishStringMessage()
        {
            // Publish string messages to Message Channel

        }

        // OnClick event handler for Btn6
        public async void PublishBinaryMessage()
        {
            // Publish binary messages to a Stream Channel

        }

        // Onclick event handler for Btn2
        public void CreateStChannel()
        {
            string[] activeComponentsList = { "Btn3", "Btn7" };
            string[] inactiveComponentList = { "Btn2", "Btn4", "Btn5", "Btn6", "Btn8", "Btn9", "Btn10" };
            // Create a Stream Channel

        }

        // Onclick event handler for Btn3
        public async void JoinStChannel()
        {
            string[] activeComponentsList = { "Btn4", "Btn5", "Btn8" };
            string[] inactiveComponentList = { "Btn2", "Btn3", "Btn6", "Btn9", "Btn10", "Btn7" };
            // Join a Stream Channel

        }

        // Onclick event handler for Btn4
        public async void JoinTopic()
        {
            string[] activeComponentsList = { "Btn6", "Btn8", "Btn9" };
            string[] inactiveComponentList = { "Btn2", "Btn3", "Btn4", "Btn7" };
            // Join a topic

        }

        // Onclick event handler for Btn5
        public async void SubscribeTopic()
        {
            string[] activeComponentsList = { "Btn8", "Btn10" };
            string[] inactiveComponentList = { "Btn2", "Btn3", "Btn5", "Btn7" };
            // Subscribe a publisher from a topic

        }

        // Onclick event handler for Btn7
        public async void ReleaseStChannel()
        {
            string[] activeComponentsList = { "Btn2" };
            string[] inactiveComponentList = { "Btn3", "Btn4", "Btn5", "Btn6", "Btn7", "Btn8", "Btn9", "Btn10" };
            // Release a Stream Channel


        }

        // Onclick event handler for Btn8
        public async void LeaveStChannel()
        {
            string[] activeComponentsList = { "Btn3", "Btn7" };
            string[] inactiveComponentList = { "Btn2", "Btn4", "Btn5", "Btn6", "Btn8", "Btn9", "Btn10" };
            // Leave a Stream Channel

        }

        // Onclick event handler for Btn9
        public async void LeaveTopic()
        {
            string[] activeComponentsList = { "Btn4", "Btn8" };
            string[] inactiveComponentList = { "Btn2", "Btn3", "Btn6", "Btn7", "Btn9" };
            // Leave a topic

        }

        // Onclick event handler for Btn10
        public async void UnsubscribeTopic()
        {
            string[] activeComponentsList = { "Btn5", "Btn8" };
            string[] inactiveComponentList = { "Btn2", "Btn3", "Btn7", "Btn10" };
            // Unsubscribe a publisher from a topic

        }

        private async void OnDestroy()
        {
            string[] activeComponentsList = { "Btn4", "Btn5", "Btn8" };
            string[] inactiveComponentList = { "Btn2", "Btn3", "Btn6", "Btn9", "Btn10", "Btn7" };
            // Dispose a Stream Channel
            if (rtmClient != null && streamChannel != null)
            {
                var result = await streamChannel.LeaveAsync();
                var status = result.Status;
                var response = result.Response;
                if (status.Error)
                {
                    ShowMessage(string.Format("{0} is failed", status.Operation));
                    ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
                }
                else
                {
                    ShowMessage("Dispose Channel Success!");
                    ChangeComponentView(activeComponentsList, "ACTIVATE");
                    ChangeComponentView(inactiveComponentList, "INACTIVATE");
                }
                var status2 = streamChannel.Dispose();
            }

            if (rtmClient != null)
            {
                var result = await rtmClient.UnsubscribeAsync(msChannelName);
                var status2 = result.Status;
                var response2 = result.Response;
                if (status3.Error)
                {
                    Debug.Log(string.Format("{0} is failed", status3.Operation));
                    Debug.Log(string.Format("The error code is {0}, because of: {1}", status3.ErrorCode, status3.Reason));
                }
                else
                {
                    Debug.Log("Unsubscribe Channel Success!");
                }
                var status4 = rtmClient.Dispose();
                Debug.Log("Dispose rtmClient Success!");
                rtmClient = null;
            }
        }
    }
   ```

## 消息格式 [#消息格式]

收发信息是 RTM 基础的能力之一，基于 RTM 实时网络，你在全球任何地方发送的信息都可以在 100 毫秒内安全可靠的送到到接收端。用户可以给一个人发送消息或一次性给成千上万人广播消息。

RTM 对在 Message 类型频道和 Stream 类型频道中发送的消息负载包体积有限制：Message 类型频道中的限制为 32 KB，Stream 类型频道中的限制为 1 KB。所有类型的频道中均支持字符串格式和二进制格式消息负载。

字符串类型消息格式：

```csharp
string message = "This is a string message";
```

二进制类型消息格式：

```csharp
byte[] message = System.Text.Encoding.Default.GetBytes("This message will be converted to binary");
```

根据你的应用场景需要，你可以选择字符串类型消息格式或者二进制类型消息格式，收到消息后通过 `MessageEvent` 中 `messageType` 字段加以区分。

你也可以使用 `JSON` 或 `Protobuf` 等方式来构建你的负载结构以便于简化业务逻辑，此部分内容可以参考[消息负载序列化](/zh-CN/realtime-media/rtm/build/message-design-and-history/serialized)。但请记住在把消息负载转交给 RTM 发送前，需要对消息负载进行序列化。

## 初始化 RTM [#初始化-rtm-1]
在 **HelloWorld.cs** 中调用 `CreateAgoraRtmClient` 方法初始化 `IRtmClient`。你需要提供如下信息：

     - 从控制台获取的 App ID（`appId`）。
     - 你需要定义用户 ID（`userId`），并保证其全局唯一性。

将以下代码添加到程序中，并将程序模版中的 `"your_appId"`、`"your_userId"` 替换成你的 `appId` 和 `userId`。

``` csharp showLineNumbers
// Paste the following code snippet below "Initialize RTM Client" comment

if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(appId))
{
    ShowMessage("We need a userId and appId to initialize!");
    return;
}
RtmLogConfig logConfig = new RtmLogConfig();
// Set the log file path.
logConfig.filePath = "myFilePath";
// Set the file size of the agora.log file.
logConfig.fileSizeInKB = 512;
// Set the log report level
logConfig.level = RTM_LOG_LEVEL.INFO;
RtmConfig config = new RtmConfig();
config.appId = appId;
config.userId = userId;
config.logConfig = logConfig;
// Create the RTM Client
try
{
    rtmClient = RtmClient.CreateAgoraRtmClient(config);
    ShowMessage("RTM Client Initialize Sucessfull");
    // Inactive unnecessary components
    string[] activeComponentsList = { "Btn2" };
    string[] inactiveComponentList = { "Btn3", "Btn4", "Btn5", "Btn6", "Btn7", "Btn8", "Btn9", "Btn10" };
    ChangeComponentView(activeComponentsList, "ACTIVATE");
    ChangeComponentView(inactiveComponentList, "INACTIVATE");
}
catch (RTMException e)
{
    ShowMessage(string.Format("{0} is failed", e.Status.Operation));
    ShowMessage(string.Format("The error code is {0}, due to: {1}", e.Status.ErrorCode, e.Status.Reason));
}
```

## 添加事件监听 [#添加事件监听]

事件监听程序帮助你实现频道中消息、事件到达后的处理逻辑，添加以下代码到你的程序中以显示收到的消息或事件通知：

``` csharp showLineNumbers
// Paste the following code snippet below "Implement the event listener handler" comment

// Implement the connection state change event handler
private void OnConnectionStateChanged(string channelName, RTM_CONNECTION_STATE state, RTM_CONNECTION_CHANGE_REASON reason)
{
    ShowMessage(string.Format("Channel:{0} connection state have changed to:{1} because of {2}", channelName, state, reason));
}

// Implement the presence event handler
private void OnPresenceEvent(PresenceEvent eve)
{
    var channelName = eve.channelName;
    var channelType = eve.channelType;
    var eventType = eve.type;
    var publisher = eve.publisher;
    var stateItems = eve.stateItems;
    var interval = eve.interval;
    var snapshot = eve.snapshot;
    Debug.Log(string.Format("The Event : {0} Happend", eventType));
    switch (eventType)
    {
        case RTM_PRESENCE_EVENT_TYPE.SNAPSHOT:
            ShowMessage(string.Format("You have sub or join channel:{0} channe type is:{1}", channelName, channelType));
            break;
        case RTM_PRESENCE_EVENT_TYPE.INTERVAL:
            ShowMessage(string.Format("The channel:{0} channe type is:{1} is now in interval mode!", channelName, channelType));
            break;
        case RTM_PRESENCE_EVENT_TYPE.REMOTE_JOIN:
            ShowMessage(string.Format("User:{0} sub or join channel:{1} channe type is:{2}", publisher, channelName, channelType));
            break;
        case RTM_PRESENCE_EVENT_TYPE.REMOTE_LEAVE:
            ShowMessage(string.Format("User:{0} unsub or leave channel:{1} channe type is:{2}", publisher, channelName, channelType));
            break;
        case RTM_PRESENCE_EVENT_TYPE.REMOTE_TIMEOUT:
            ShowMessage(string.Format("User:{0} timeout from channel:{1} channe type is:{2}", publisher, channelName, channelType));
            break;
        case RTM_PRESENCE_EVENT_TYPE.REMOTE_STATE_CHANGED:
            ShowMessage(string.Format("User:{0} state change in channel:{1} channe type is:{2}", publisher, channelName, channelType));
            break;
        case RTM_PRESENCE_EVENT_TYPE.ERROR_OUT_OF_SERVICE:
            ShowMessage(string.Format("User:{0} joined channel without presence service:{1} channe type is:{2}", publisher, channelName, channelType));
            break;
    }
}

// Implement the message event handler
private void OnMessageEvent(MessageEvent eve)
{
    var channelName = eve.channelName;
    var channelType = eve.channelType;
    var topic = eve.channelTopic;
    var publisher = eve.publisher;
    var messageType = eve.messageType;
    var customType = eve.customType;
    var message = eve.message;

    if (messageType == RTM_MESSAGE_TYPE.STRING)
    {
        var stMessage = message.GetData<string>();
        ShowMessage(string.Format("You have recieved a string type message: {0} from: {1} in channel:{2}", stMessage, publisher, channelName));
        ShowMessage(string.Format("The channel type is {0}"), channelType);
    }
    else
    {
        var biMessage = message.GetData<byte[]>();
        ShowMessage(string.Format("You have recieved a binary type message: {0},from: {1} in channel:{2}", System.BitConverter.ToString(biMessage), publisher, channelName));
        ShowMessage(string.Format("The channel type is {0}", channelType));
    }
}
```

## 登录服务 [#登录服务]

我们需要执行登录操作以建立与 RTM 服务器的连接，然后才能调用 SDK 的其他 API。调用 `LoginAsync` 方法并传入 `token` 参数以验证身份。

将以下代码添加到 **HelloWorld.cs** 中：

```csharp showLineNumbers
// Paste the following code snippet below "Log in to the RTM server" comment
var result = await rtmClient.LoginAsync(appId);
var status = result.Status;
var response = result.Response;
if (status.Error)
{
    ShowMessage(string.Format("{0} is failed", status.Operation));
    ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
}
else
{
    ShowMessage("Login Successfully");
}
```

:::info[信息]
你需要在登录时传入 Token。在测试阶段，为快速验证功能，我们建议你在创建声网项目时将鉴权模式设置为调试模式，然后在登录时只需传入项目的 App ID。
:::

## 在消息频道中收发消息 [#在消息频道中收发消息]

调用 `PublishAsync` 方法向 Message Channel 发送消息后，RTM 会把该消息分发给该频道的所有订阅者，以下代码演示如何发送 String 类型的消息，将此代码片段添加到程序中：

```csharp showLineNumbers
// Paste the following code snippet below "Publish string messages to a message channel" comment
var message = GameObject.Find("InputField1").GetComponent<InputField>().text;
if (rtmClient != null)
{
    PublishOptions options = new PublishOptions()
    {
        channelType = RTM_CHANNEL_TYPE.MESSAGE,
        customType = "PlainText"
    };
    var result = await rtmClient.PublishAsync(msChannelName, message, options);
    var status = result.Status;
    var response = result.Response;
    if (status.Error)
    {
        ShowMessage(string.Format("{0} is failed, The error code is {1}", status.Operation, status.ErrorCode));
    }
    else
    {
        ShowMessage("Publish Message Success!");
    }
}
```

调用 `SubscribeAsync` 方法订阅此频道以接收此频道中的消息。将以下代码添加到程序中：

```csharp showLineNumbers
// Paste the following code snippet below "Subscribe to a Message Channel" comment
SubscribeOptions options = new SubscribeOptions()
{
    withMessage = true,
    withPresence = true
};
var result2 = await rtmClient.SubscribeAsync(msChannelName, options);
var status2 = result2.Status;
var response2 = result2.Response;
if (status2.Error)
{
    ShowMessage(string.Format("{0} is failed", status2.Operation));
    ShowMessage(string.Format("The error code is {0}, due to: {1}", status2.ErrorCode, status2.Reason));
}
else
{
    ShowMessage(string.Format("Subscribe channel success! at Channel:{0}", response2.ChannelName));
}
```

成功订阅后，你可以通过之前监听的 `OnMessageEvent` 事件通知接收消息。

## 在流频道中收发消息 [#在流频道中收发消息]

在 Stream Channel 中的消息需要依赖 Topic 功能，你需要依次完成以下步骤：

### 创建流频道 [#创建流频道]

调用 Stream Channel 的所有方法前，你需要创建频道并获得 `IStreamChannel` 对象实例。将以下代码添加到程序中：

```csharp showLineNumbers
// Paste the following code snippet below "Create a Stream Channel" comment
if (rtmClient != null)
{
    streamChannel = rtmClient.CreateStreamChannel(stChannelName);
    ShowMessage("Create Stream Channel:" + stChannelName + "Success !");
    // Disable unnecessary components
    ChangeComponentView(activeComponentsList, "ACTIVATE");
    ChangeComponentView(inactiveComponentList, "INACTIVATE");
}
```

### 加入流频道 [#加入流频道]

加入 Stream Channel 才可以在后续进行发布和订阅消息的操作。调用 `JoinAsync` 方法并在 `options` 中传入 `token` 参数以验证身份。

:::info[信息]
当前项目在创建的时候选择的是调试模式以便于快速验证功能，所以可以用 `appId` 代替 `token` 执行加入频道操作。
:::

在 **HelloWorld.cs** 中添加如下代码：

```csharp showLineNumbers
// Paste the following code snippet below "Join a Stream Channel" comment
if (streamChannel != null)
{
    JoinChannelOptions options = new JoinChannelOptions();
    options.withPresence = true;
    options.token = appId;

    var result = await streamChannel.JoinAsync(options);
    var status = result.Status;
    var response = result.Response;
    if (status.Error)
    {
        ShowMessage(string.Format("{0} is failed", status.Operation));
        ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
    }
    else
    {
        ShowMessage(string.Format("User:{0} Join stream channel success! at Channel:{1}", response.UserId, response.ChannelName));
        ChangeComponentView(activeComponentsList, "ACTIVATE");
        ChangeComponentView(inactiveComponentList, "INACTIVATE");
    }
}
```

### 加入 Topic [#加入-topic]

调用 `JoinTopicAsync` 方法会将你注册成为 Topic 的消息发布者之一，以便于你后续向该 Topic 发送消息。将以下代码添加到频道中：

```csharp showLineNumbers
// Paste the following code snippet below "Join a topic" comment
if (streamChannel != null)
{
    JoinTopicOptions options = new JoinTopicOptions();
    options.qos = RTM_MESSAGE_QOS.ORDERED;
    options.syncWithMedia = false;

    var result = await streamChannel.JoinTopicAsync(topicName, options);
    var status = result.Status;
    var response = result.Response;
    if (status.Error)
    {
        ShowMessage(string.Format("{0} is failed", status.Operation));
        ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
    }
    else
    {
        ShowMessage(string.Format("User:{0} Join Topic:{1} success! at Channel:{2}", response.UserId, response.Topic, response.ChannelName));
        ChangeComponentView(activeComponentsList, "ACTIVATE");
        ChangeComponentView(inactiveComponentList, "INACTIVATE");
    }
}
```

### 在 Topic 中发布消息 [#在-topic-中发布消息]

成功注册为 Topic 的发布者后，你就可以调用 `PublishTopicMessageAsync` 方法在 Topic 中发送消息了，RTM 会把此消息分发给该 Topic 的所有订阅者。以下示例向你展示如何发送 Binary 类型消息：

```csharp showLineNumbers
// Paste the following code snippet below "Publish binary messages to a Stream Channel" comment
var message = GameObject.Find("InputField2").GetComponent<InputField>().text;
byte[] byteMsg = System.Text.Encoding.Default.GetBytes(message);
if (streamChannel != null)
{
    TopicMessageOptions options = new TopicMessageOptions();
    options.customType = "PlainBinary";
    options.sendTs = 0;
    var result = await streamChannel.PublishTopicMessageAsync(topicName, byteMsg, options);
    var status = result.Status;
    var response = result.Response;
    if (status.Error)
    {
        ShowMessage(string.Format("{0} is failed", status.Operation));
        ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
    }
    else
    {
        ShowMessage("Publish Binary Message Success!");
    }
}
```

### 订阅 Topic 中的发布者 [#订阅-topic-中的发布者]

你可以通过调用 `SubscribeTopicAsync` 方法订阅某个 Topic 中的一位或多位消息发布者以便于接收他们发送的消息。单个客户端在单个 Topic 中最多只能订阅 64 个消息发布者，详见 [API 调用限制](/zh-CN/realtime-media/rtm/reference/usage-limits-and-errors/api-limits)。

将以下代码添加到程序中：

```csharp showLineNumbers
// Paste the following code snippet below "Subscribe a publisher from a topic" comment
List<string> userList = new List<string>();
userList.Add("Tony");
TopicOptions options = new TopicOptions();
options.users = userList.ToArray();

var result = await streamChannel.SubscribeTopicAsync(topicName, options);
var status = result.Status;
var response = result.Response;
if (status.Error)
{
    ShowMessage(string.Format("{0} is failed", status.Operation));
    ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
}
else
{
    ShowMessage(string.Format("User:{0} Subscribe Topic:{1} success! at Channel:{2}", response.UserId, response.Topic, response.ChannelName));
    ChangeComponentView(activeComponentsList, "ACTIVATE");
    ChangeComponentView(inactiveComponentList, "INACTIVATE");
}
```

成功订阅后，你可以通过之前监听的 `OnMessageEvent` 事件通知接收消息。

### 取消订阅 Topic 中发布者 [#取消订阅-topic-中发布者]

如果你不再需要接收某 Topic 中某一位发布者的消息，调用 `UnsubscribeTopicAsync` 方法取消对他的订阅。如需取消订阅整个 Topic，在 `topicOptions` 中不填 `users` 参数即可。

将以下代码添加到程序中：

```csharp showLineNumbers
// Paste the following code snippet below "Unsubscribe a publisher from a topic" comment
List<string> userList = new List<string>();
userList.Add("Tony");

TopicOptions options = new TopicOptions();
options.users = userList.ToArray();

var result = await streamChannel.UnsubscribeTopicAsync(topicName, options);
var status = result.Status;
var response = result.Response;
if (rstatus.Error)
{
    ShowMessage(string.Format("{0} is failed", status.Operation));
    ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
}
else
{
    ShowMessage(string.Format("Unsubscribe Topic Success!"));
    ChangeComponentView(activeComponentsList, "ACTIVATE");
    ChangeComponentView(inactiveComponentList, "INACTIVATE");
}
```

### 离开 Topic [#离开-topic]

如果你不再需要在某 Topic 中发布消息，调用 `LeaveTopicAsync` 方法取消成为该 Topic 的消息发布者。

:::info[信息]
离开 Topic 不会影响你的订阅操作，你依旧可以从你订阅的 Topic 中接收消息。
:::

将以下代码添加到程序中：

```csharp showLineNumbers
// Paste the following code snippet below "Leave a topic" comment
if (streamChannel != null)
{
    var result = await streamChannel.LeaveTopicAsync(topicName);
    var status = result.Status;
    var response = result.Response;
    if (status.Error)
    {
        ShowMessage(string.Format("{0} is failed", status.Operation));
        ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
    }
    else
    {
        ShowMessage(string.Format("User:{0} Leave Topic:{1} success! at Channel:{2}", response.UserId, response.Topic, response.ChannelName));
        ChangeComponentView(activeComponentsList, "ACTIVATE");
        ChangeComponentView(inactiveComponentList, "INACTIVATE");
    }
}
```

### 退出频道 [#退出频道]

当你不再关心此频道中的动态，即不再发送和接收消息，可以调用 `LeaveTopicAsync` 方法离开该频道。

将以下代码添加到程序中：

```csharp
// Paste the following code snippet below "Release a Stream Channel" comment
if (rtmClient != null && streamChannel != null)
{
    var result = await streamChannel.LeaveAsync();
    var status = result.Status;
    var response = result.Response;
    if (status.Error)
    {
        ShowMessage(string.Format("{0} is failed", status.Operation));
        ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
    }
    else
    {
        ShowMessage("Leave Channel Success!");
        ChangeComponentView(activeComponentsList, "ACTIVATE");
        ChangeComponentView(inactiveComponentList, "INACTIVATE");
    }
}
```

### 销毁频道 [#销毁频道]

如果你不再需要某个 Stream Channel，你可以调用 `Dispose` 方法销毁该频道以释放资源。

在 **HelloWorld.cs** 中添加如下代码：

```csharp showLineNumbers
// Paste the following code snippet below "Dispose a Stream Channel" comment
var result1 = streamChannel.Dispose();
```

## 组合到一起 [#组合到一起]

经过上述步骤，你的 **HelloWorld.cs** 文件中的代码应该如下所示：

```csharp showLineNumbers

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Agora.Rtm;

public class HelloWorld : MonoBehaviour
{
    [SerializeField]
    private string appId = "your_appId";
    // Get an App ID from console portal
    private string userId = "your_userId";
    // Change the user ID by yourself
    private string msChannelName = "SeeYou";
    private string stChannelName = "Greeting";
    private string topicName = "Hello_world";
    private IRtmClient rtmClient;
    private IStreamChannel streamChannel;
    public void Awake()
    {
        Initialize();
    }
    // Initialize RTM
    public async void Initialize()
    {
        if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(appId))
        {
            ShowMessage("We need a userId and appId to initialize!");
            return;
        }
        RtmLogConfig logConfig = new RtmLogConfig();
        // Set log file path
        logConfig.filePath = "myFilePath";
        // Set the file size of the agore.log file
        logConfig.fileSizeInKB = 512;
        // Set the log report level
        logConfig.level = RTM_LOG_LEVEL.INFO;
        RtmConfig config = new RtmConfig();
        config.appId = appId;
        config.userId = userId;
        config.logConfig = logConfig;
        // Create the RTM Client
        try
        {
            rtmClient = RtmClient.CreateAgoraRtmClient(config);
            ShowMessage("RTM Client Initialize Sucessfull");
            // Inactive unnecessary components
            string[] activeComponentsList = { "Btn2" };
            string[] inactiveComponentList = { "Btn3", "Btn4", "Btn5", "Btn6", "Btn7", "Btn8", "Btn9", "Btn10" };
            ChangeComponentView(activeComponentsList, "ACTIVATE");
            ChangeComponentView(inactiveComponentList, "INACTIVATE");
        }
        catch (RTMException e)
        {
            ShowMessage(string.Format("{0} is failed", e.Status.Operation));
            ShowMessage(string.Format("The error code is {0}, due to: {1}", e.Status.ErrorCode, e.Status.Reason));
        }
        // Add listener
        if (rtmClient != null)
        {
            // Add the message event listener
            rtmClient.OnMessageEvent += OnMessageEvent;
            // Add the presence event listener
            rtmClient.OnPresenceEvent += OnPresenceEvent;
            // Add the connection state change listener
            rtmClient.OnConnectionStateChanged += OnConnectionStateChanged;
            // Log in to the RTM server
            var result = await rtmClient.LoginAsync(appId);
            var status = result.Status;
            var response = result.Response;
            if (status.Error)
            {
                ShowMessage(string.Format("{0} is failed", status.Operation));
                ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
                Debug.Log(string.Format("Login failed"));
            }
            else
            {
                ShowMessage("Login Successfully");
            }
            //Subscribe to a Message Channel
            SubscribeOptions options = new SubscribeOptions()
            {
                withMessage = true,
                withPresence = true
            };
            var result2 = await rtmClient.SubscribeAsync(msChannelName, options);
            var status2 = result2.Status;
            var response2 = result2.Response;
            if (status2.Error)
            {
                ShowMessage(string.Format("{0} is failed", status2.Operation));
                ShowMessage(string.Format("The error code is {0}, due to: {1}", status2.ErrorCode, status2.Reason));
            }
            else
            {
                ShowMessage(string.Format("Subscribe channel success! at Channel:{0}", response2.ChannelName));
            }
        }
    }
    private void ChangeComponentView(string[] componentsList, string viewType)
    {
        for (int i = 0; i < componentsList.Length; i++)
        {
            if (viewType == "ACTIVATE")
                GameObject.Find(componentsList[i]).GetComponent<Button>().interactable = true;
            else if (viewType == "INACTIVATE")
                GameObject.Find(componentsList[i]).GetComponent<Button>().interactable = false;
        }
    }
    private void ShowMessage(string msg)
    {
        GameObject.Find("Txt3").GetComponent<Text>().text += "\n" + msg;
    }
    // Implement the event listener handler
    // Implement the connection state change event handler
    private void OnConnectionStateChanged(string channelName, RTM_CONNECTION_STATE state, RTM_CONNECTION_CHANGE_REASON reason)
    {
        ShowMessage(string.Format("Channel:{0} connection state have changed to:{1} because of {2}", channelName, state, reason));
    }
    // Implement the presence event handler
    private void OnPresenceEvent(PresenceEvent eve)
    {
        var channelName = eve.channelName;
        var channelType = eve.channelType;
        var eventType = eve.type;
        var publisher = eve.publisher;
        var stateItems = eve.stateItems;
        var interval = eve.interval;
        var snapshot = eve.snapshot;
        Debug.Log(string.Format("The Event : {0} Happend", eventType));
        switch (eventType)
        {
            case RTM_PRESENCE_EVENT_TYPE.SNAPSHOT:
                ShowMessage(string.Format("You have sub or join channel:{0} channe type is:{1}", channelName, channelType));
                break;
            case RTM_PRESENCE_EVENT_TYPE.INTERVAL:
                ShowMessage(string.Format("The channel:{0} channe type is:{1} is now in interval mode!", channelName, channelType));
                break;
            case RTM_PRESENCE_EVENT_TYPE.REMOTE_JOIN:
                ShowMessage(string.Format("User:{0} sub or join channel:{1} channe type is:{2}", publisher, channelName, channelType));
                break;
            case RTM_PRESENCE_EVENT_TYPE.REMOTE_LEAVE:
                ShowMessage(string.Format("User:{0} unsub or leave channel:{1} channe type is:{2}", publisher, channelName, channelType));
                break;
            case RTM_PRESENCE_EVENT_TYPE.REMOTE_TIMEOUT:
                ShowMessage(string.Format("User:{0} timeout from channel:{1} channe type is:{2}", publisher, channelName, channelType));
                break;
            case RTM_PRESENCE_EVENT_TYPE.REMOTE_STATE_CHANGED:
                ShowMessage(string.Format("User:{0} state change in channel:{1} channe type is:{2}", publisher, channelName, channelType));
                break;
            case RTM_PRESENCE_EVENT_TYPE.ERROR_OUT_OF_SERVICE:
                ShowMessage(string.Format("User:{0} joined channel without presence service:{1} channe type is:{2}", publisher, channelName, channelType));
                break;
        }
    }
    // Implement the message event handler
    private void OnMessageEvent(MessageEvent eve)
    {
        var channelName = eve.channelName;
        var channelType = eve.channelType;
        var topic = eve.channelTopic;
        var publisher = eve.publisher;
        var messageType = eve.messageType;
        var customType = eve.customType;
        var message = eve.message;
        if (messageType == RTM_MESSAGE_TYPE.STRING)
        {
            var stMessage = message.GetData<string>();
            ShowMessage(string.Format("You have recieved a string type message: {0} from: {1} in channel:{2}", stMessage, publisher, channelName));
            ShowMessage(string.Format("The channel type is {0}", channelType));
        }
        else
        {
            var biMessage = message.GetData<byte[]>();
            ShowMessage(string.Format("You have recieved a binary type message: {0},from: {1} in channel:{2}", System.BitConverter.ToString(biMessage), publisher, channelName));
            ShowMessage(string.Format("The channel type is {0}", channelType));
        }
    }
    // OnClick Event Handler for Btn1
    public async void PublishStringMessage()
    {
        // Publish string messages to a message channel
        var message = GameObject.Find("InputField1").GetComponent<InputField>().text;
        if (rtmClient != null)
        {
            PublishOptions options = new PublishOptions()
            {
                channelType = RTM_CHANNEL_TYPE.MESSAGE,
                customType = "PlainText"
            };
            var result = await rtmClient.PublishAsync(msChannelName, message, options);
            var status = result.Status;
            var response = result.Response;
            if (status.Error)
            {
                ShowMessage(string.Format("{0} is failed, The error code is {1}", status.Operation, status.ErrorCode));
            }
            else
            {
                ShowMessage("Publish Message Success!");
            }
        }
    }
    // OnClick Event Handler for Btn6
    public async void PublishBinaryMessage()
    {
        // Publish Binary Messahe to Stream Channel
        var message = GameObject.Find("InputField2").GetComponent<InputField>().text;
        byte[] byteMsg = System.Text.Encoding.Default.GetBytes(message);
        if (streamChannel != null)
        {
            TopicMessageOptions options = new TopicMessageOptions();
            options.customType = "PlainBinary";
            options.sendTs = 0;
            var result = await streamChannel.PublishTopicMessageAsync(topicName, byteMsg, options);
            var status = result.Status;
            var response = result.Response;
            if (status.Error)
            {
                ShowMessage(string.Format("{0} is failed", status.Operation));
                ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
            }
            else
            {
                ShowMessage("Publish Binary Message Success!");
            }
        }
    }
    // Onclick event handler for Btn2
    public void CreateStChannel()
    {
        string[] activeComponentsList = { "Btn3", "Btn7" };
        string[] inactiveComponentList = { "Btn2", "Btn4", "Btn5", "Btn6", "Btn8", "Btn9", "Btn10" };
        // Create a Stream Channel
        if (rtmClient != null)
        {
            streamChannel = rtmClient.CreateStreamChannel(stChannelName);
            ShowMessage("Create Stream Channel:" + stChannelName + "Success !");
            // Disable unnecessary components
            ChangeComponentView(activeComponentsList, "ACTIVATE");
            ChangeComponentView(inactiveComponentList, "INACTIVATE");
        }
    }
    // Onclick event handler for Btn3
    public async void JoinStChannel()
    {
        string[] activeComponentsList = { "Btn4", "Btn5", "Btn8" };
        string[] inactiveComponentList = { "Btn2", "Btn3", "Btn6", "Btn9", "Btn10", "Btn7" };
        // Join a Stream Channel
        if (streamChannel != null)
        {
            JoinChannelOptions options = new JoinChannelOptions();
            options.withPresence = true;
            options.token = appId;
            var result = await streamChannel.JoinAsync(options);
            var status = result.Status;
            var response = result.Response;
            if (status.Error)
            {
                ShowMessage(string.Format("{0} is failed", status.Operation));
                ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
            }
            else
            {
                ShowMessage(string.Format("User:{0} Join stream channel success! at Channel:{1}", response.UserId, response.ChannelName));
                ChangeComponentView(activeComponentsList, "ACTIVATE");
                ChangeComponentView(inactiveComponentList, "INACTIVATE");
            }
        }
    }
    // Onclick event handler for Btn4
    public async void JoinTopic()
    {
        string[] activeComponentsList = { "Btn6", "Btn8", "Btn9" };
        string[] inactiveComponentList = { "Btn2", "Btn3", "Btn4", "Btn7" };
        // Join a topic
        if (streamChannel != null)
        {
            JoinTopicOptions options = new JoinTopicOptions();
            options.qos = RTM_MESSAGE_QOS.ORDERED;
            options.syncWithMedia = false;
            var result = await streamChannel.JoinTopicAsync(topicName, options);
            var status = result.Status;
            var response = result.Response;
            if (status.Error)
            {
                ShowMessage(string.Format("{0} is failed", status.Operation));
                ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
            }
            else
            {
                ShowMessage(string.Format("User:{0} Join Topic:{1} success! at Channel:{2}", response.UserId, response.Topic, response.ChannelName));
                ChangeComponentView(activeComponentsList, "ACTIVATE");
                ChangeComponentView(inactiveComponentList, "INACTIVATE");
            }
        }
    }
    // Onclick event handler for Btn5
    public async void SubscribeTopic()
    {
        string[] activeComponentsList = { "Btn8", "Btn10" };
        string[] inactiveComponentList = { "Btn2", "Btn3", "Btn5", "Btn7" };
        // Subscribe a publisher from a topic
        List<string> userList = new List<string>();
        userList.Add("Tony");
        TopicOptions options = new TopicOptions();
        options.users = userList.ToArray();
        var result = await streamChannel.SubscribeTopicAsync(topicName, options);
        var status = result.Status;
        var response = result.Response;
        if (status.Error)
        {
            ShowMessage(string.Format("{0} is failed", status.Operation));
            ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
        }
        else
        {
            ShowMessage(string.Format("User:{0} Subscribe Topic:{1} success! at Channel:{2}", response.UserId, response.Topic, response.ChannelName));
            ChangeComponentView(activeComponentsList, "ACTIVATE");
            ChangeComponentView(inactiveComponentList, "INACTIVATE");
        }
    }
    // Onclick event handler for Btn7
    public async void ReleaseStChannel()
    {
        string[] activeComponentsList = { "Btn2" };
        string[] inactiveComponentList = { "Btn3", "Btn4", "Btn5", "Btn6", "Btn7", "Btn8", "Btn9", "Btn10" };
        // Release a Stream Channel
        if (rtmClient != null && streamChannel != null)
        {
            var result = await streamChannel.LeaveAsync();
            var status = result.Status;
            var response = result.Response;
            if (status.Error)
            {
                ShowMessage(string.Format("{0} is failed", status.Operation));
                ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
            }
            else
            {
                ShowMessage("Dispose Channel Success!");
                ChangeComponentView(activeComponentsList, "ACTIVATE");
                ChangeComponentView(inactiveComponentList, "INACTIVATE");
            }
            var status1 = streamChannel.Dispose();
        }
    }
    // Onclick event handler for Btn8
    public async void LeaveStChannel()
    {
        string[] activeComponentsList = { "Btn3", "Btn7" };
        string[] inactiveComponentList = { "Btn2", "Btn4", "Btn5", "Btn6", "Btn8", "Btn9", "Btn10" };
        // Leave a Stream Channel
        if (streamChannel != null)
        {
            var result = await streamChannel.LeaveAsync();
            var status = result.Status;
            var response = result.Response;
            if (status.Error)
            {
                ShowMessage(string.Format("{0} is failed", status.Operation));
                ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
            }
            else
            {
                ShowMessage(string.Format("User:{0} Leave stream channel success! at Channel:{1}", response.UserId, response.ChannelName));
                ChangeComponentView(activeComponentsList, "ACTIVATE");
                ChangeComponentView(inactiveComponentList, "INACTIVATE");
            }
        }
    }
    // Onclick event handler for Btn9
    public async void LeaveTopic()
    {
        string[] activeComponentsList = { "Btn4", "Btn8" };
        string[] inactiveComponentList = { "Btn2", "Btn3", "Btn6", "Btn7", "Btn9" };
        // Leave a topic
        if (streamChannel != null)
        {
            var result = await streamChannel.LeaveTopicAsync(topicName);
            var status = result.Status;
            var response = result.Response;
            if (status.Error)
            {
                ShowMessage(string.Format("{0} is failed", status.Operation));
                ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
            }
            else
            {
                ShowMessage(string.Format("User:{0} Leave Topic:{1} success! at Channel:{2}", response.UserId, response.Topic, response.ChannelName));
                ChangeComponentView(activeComponentsList, "ACTIVATE");
                ChangeComponentView(inactiveComponentList, "INACTIVATE");
            }
        }
    }
    // Onclick event handler for Btn10
    public async void UnsubscribeTopic()
    {
        string[] activeComponentsList = { "Btn5", "Btn8" };
        string[] inactiveComponentList = { "Btn2", "Btn3", "Btn7", "Btn10" };
        // Unsubscribe a publisher from a topic
        List<string> userList = new List<string>();
        userList.Add("Tony");
        TopicOptions options = new TopicOptions();
        options.users = userList.ToArray();
        var result = await streamChannel.UnsubscribeTopicAsync(topicName, options);
        var status = result.Status;
        var response = result.Response;
        if (status.Error)
        {
            ShowMessage(string.Format("{0} is failed", status.Operation));
            ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
        }
        else
        {
            ShowMessage(string.Format("Unsubscribe Topic Success!"));
            ChangeComponentView(activeComponentsList, "ACTIVATE");
            ChangeComponentView(inactiveComponentList, "INACTIVATE");
        }
    }
    private async void OnDestroy()
    {
        string[] activeComponentsList = { "Btn4", "Btn5", "Btn8" };
        string[] inactiveComponentList = { "Btn2", "Btn3", "Btn6", "Btn9", "Btn10", "Btn7" };
        // Dispose a Stream Channel
        if (rtmClient != null && streamChannel != null)
        {
            var result = await streamChannel.LeaveAsync();
            var status = result.Status;
            var response = result.Response;
            if (status.Error)
            {
                ShowMessage(string.Format("{0} is failed", status.Operation));
                ShowMessage(string.Format("The error code is {0}, due to: {1}", status.ErrorCode, status.Reason));
            }
            else
            {
                ShowMessage("Dispose Channel Success!");
                ChangeComponentView(activeComponentsList, "ACTIVATE");
                ChangeComponentView(inactiveComponentList, "INACTIVATE");
            }
            var status1 = streamChannel.Dispose();
        }
        if (rtmClient != null)
        {
            var result = await rtmClient.UnsubscribeAsync(msChannelName);
            var status2 = result.Status;
            var response2 = result.Response;
            if (status2.Error)
            {
                Debug.Log(string.Format("{0} is failed", status2.Operation));
                Debug.Log(string.Format("The error code is {0}, because of: {1}", status2.ErrorCode, status2.Reason));
            }
            else
            {
                Debug.Log("Unsubscribe Channel Success!");
            }
            var status3 = rtmClient.Dispose();
            Debug.Log("Dispose rtmClient Success!");
            rtmClient = null;
        }
    }
}
```

现在，你可以开始运行你的程序：

1. 保存项目。
2. 在 Unity Editor 中将代码中定义的事件处理函数分别挂载到不同的 Button 组件上。
3. 复制项目，以相同的 `appId`、不同的 `userId` 再运行一个实例，验证功能。
4. 同时运行上面的两个项目。
3. 在其中的一个项目中的 **Send string messasge to message channel** 下方的输入框中输入消息，然后点击 **Send Message**，在另外一个项目中观察是否收到消息。
4. 在其中的一个项目中的 **Send binary messasge to stream channel** 下方依次点击 **Create Stream Channel**、**Join Stream Channel**、**Join Topic**、**Sub Topic Publisher** 按钮，你将激活发送文本的输入框和按钮。在输入框中输入消息，并点击 **Send Message** 按钮，在另外一个项目中观察是否收到消息。

至此，你已经成功集成并正确使用了 RTM 服务。

## 贯穿始终 [#贯穿始终-4]

相比于介绍如何写出代码，声网更愿意帮助你掌握上述程序编写的过程和逻辑。完成以上程序经历了以下几个过程：

1. 设置并初始化 RTM 对象。
2. 添加 `OnMessageEvent`、`OnPresenceEvent` 和 `OnConnectionStateChanged` 事件监听函数。
3. 对于 Message Channel：
    - 订阅了一个频道。
    - 往频道中发送了字符串消息。
4. 对于 Stream Channel：
    - 创建了一个名为 `Greeting` 的频道。
    - 加入了该频道。
    - 加入了一个名叫 `Hello_world` 的 Topic。
    - 订阅了该 Topic 中名为 `Tony` 的消息发布者（我自己）。
    - 向该 Topic 发送了一条二进制消息。
    - 取消订阅了该 Topic 中名为 `Tony` 的消息发布者（我自己）。
    - 离开了该 Topic。
    - 退出了该频道。
    - 销毁了该频道。

## 下一步 [#下一步-4]

现在，你已经学会了如何使用 RTM SDK 来实现 Message Channel 和 Stream Channel 的收发消息功能。下一步，你可以通过 SDK 的 [API 参考](/zh-CN/api-reference/rtm/unity/configuration)了解更多功能的使用方法。
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="flutter" />
本文将指导你如何利用 RTM Flutter SDK 构建简单的应用程序，内容涵盖了集成和开发的基础知识：开通声网账号、获取 SDK、发送消息、接收消息等。
同时，我们也提供了开源的实时消息示例项目供你参考，你可以前往下载或查看其中的源代码。

<Cards>
  <Card title="Agora-RTM2-QuickStart" href="https://github.com/AgoraIO/RTM2/tree/main/Agora-RTM2-QuickStart-Flutter" />
</Cards>

## 准备工作 [#准备工作-5]

开始构建项目前，你需要完成[开通 RTM 服务](/zh-CN/realtime-media/rtm/build/rtm-initialization/enable-service)，并检查你的开发环境是否满足[平台支持](/zh-CN/realtime-media/rtm/reference/capabilities-and-compatibility/platform-support)中的最低版本要求。

## 构建项目 [#构建项目-4]

### 1. 创建项目 [#1-创建项目]

根据以下步骤创建并配置项目：

1. 打开终端或命令行，执行以下命令来创建一个新的 Flutter 项目：

    ```shell
    # 创建 Flutter 项目
    flutter create getting_started
    ```

2. 使用的你的 IDE 工具打开项目，编辑 `pubspec.yaml` 文件，添加以下依赖：

    ```yaml
    dependencies:
      # 添加 RTM Flutter SDK 依赖项，请使用最新版本的 agora_rtm
      agora_rtm: ^2.2.6
    ```

   :::info[信息]
   如果你同时集成了 2.2.1 及以上版本的 RTM SDK 和 4.3.0 及以上版本的 RTC SDK，请参考 [FAQ](/zh-CN/reference/faq/integration/rtm2_rtc_integration_issue) 处理集成问题。
   :::

3. 添加依赖并保存文件后，打开终端，在项目路径下执行以下命令获取并安装依赖：

    ```shell
    flutter pub get
    ```

打开 `main.dart` 文件，删除其中所有代码，并将以下代码复制进去，作为项目的模板文件。

    ```dart
    import 'package:agora_rtm/agora_rtm.dart';
    import 'package:flutter/material.dart';
    import 'dart:convert';

    void main() async {
      WidgetsFlutterBinding.ensureInitialized();        // 等待 Widgets 初始化完成。

      // create rtm instance

      // login rtm service

      // subscribe to a channel

      // publish messages

      // unsubscribe frome a channel

      // logout rtm service

    }
   ```

:::info[信息]
（仅 iOS）在后续构建和发布 IPA 文件阶段，为避免因符号剥离而引发的问题，你还需要在 Xcode 中导航至 **Target Runner > Build Settings > Strip Style**，并将其设置为 **Non-Global Symbols**。
:::

### 2. 创建并初始化 RTM [#2-创建并初始化-rtm]

调用 RTM SDK 的任何 API 之前，需要先创建一个 <code>RtmClient</code> 对象实例。添加以下代码到 `main.dart` 文件中，注意：请将 `your_appId`、`your_userId` 替换成你的 `appId` 和 `userId`.

```dart
import 'package:agora_rtm/agora_rtm.dart';
import 'package:flutter/material.dart';
import 'dart:convert';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();        // 等待 Widgets 初始化完成。

  const userId = 'your_userId';
  const appId = 'your_appId';
  const channelName = 'getting-started';
  late RtmClient rtmClient;

  //create rtm instance
  try {
    // create rtm client
    final (status, client) = await RTM( appId, userId);
    if (status.error == true) {
        print('${status.operation} failed due to ${status.reason}, error code: ${status.errorCode}');
    } else {
        rtmClient = client;
        print('Initialize success!');
    }
  // add events listner

  } catch (e) {
    print('Initialize falid!:${e}');
  }
}
```

### 3. 添加事件监听 [#3-添加事件监听-3]

事件监听程序帮助你实现频道中消息、事件到达后的处理逻辑，添加以下代码到你的程序中以显示收到的消息或事件通知：

```dart showLineNumbers
// Paste the following code snippet below "add event listener" comment
rtmClient.addListener(
  // add message event handler
  message: (event) {
    print('recieved a message from channel: ${event.channelName}, channel type : ${event.channelType}');
    print('message content: ${utf8.decode(event.message!)}, custome type: ${event.customType}');
  },
  // add link state event handler
  linkState: (event) {
    print('link state changed from ${event.previousState} to ${event.currentState}');
    print('reason: ${event.reason}, due to operation ${event.operation}');
  });
```

### 4. 登录服务 [#4-登录服务-3]

你需要执行登录操作才能建立与 RTM 服务器的连接，然后才能调用 SDK 的其他 API。将以下代码添加到程序中：

```dart showLineNumbers
// Paste the following code snippet below "login rtm service" comment
try {
  // login rtm service
  var (status,response) = await rtmClient.login(appId);
  if (status.error == true) {
      print('${status.operation} failed due to ${status.reason}, error code: ${status.errorCode}');
  } else {
      print('login RTM success!');
  }
} catch (e) {
  print('Failed to login: $e');
}

```

:::info[信息]
你需要在登录时传入 Token。在测试阶段，为快速验证功能，我们建议你在创建声网项目时将鉴权模式设置为调试模式，然后在登录时只需传入项目的 App ID。
:::

### 5. 收发消息 [#5-收发消息-3]

调用 <code>publish</code> 方法向 Message Channel 发送消息后，RTM 会把该消息分发给该频道的所有订阅者，以下代码演示如何发送字符串类型的消息，将此代码片段添加到程序中：

:::info[信息]
你需要先对消息负载进行字符串序列化，才能调用 <code>publish</code> 方法发送消息。
:::


```dart showLineNumbers
// Paste the following code snippet below "Publish a message" comment
// Send a message every second for 100 seconds
for (var i = 0; i < 100; i++) {
  try {
    var (status, response) = await rtmClient.publish(
        channelName,
        'message number : $i',
        channelType: RtmChannelType.message,
        customType: 'PlainText'
        );
    if (status.error == true ){
      print('${status.operation} failed, errorCode: ${status.errorCode}, due to ${status.reason}');
    } else {
      print('${status.operation} success! message number:$i');
    }
  } catch (e) {
    print('Failed to publish message: $e');
  }
  await Future.delayed(Duration(seconds: 1));
}
```

调用 <code>subscribe</code> 方法订阅此频道以接收此频道中的消息。将以下代码添加到程序中：

```dart showLineNumbers
// Paste the following code snippet below "subscribe to a channel" comment
try {
  // subscribe to 'getting-started' channel
  var (status,response) = await rtmClient.subscribe(channelName);
  if (status.error == true) {
      print('${status.operation} failed due to ${status.reason}, error code: ${status.errorCode}');
  } else {
      print('subscribe channel: ${channelName} success!');
  }
} catch (e) {
  print('Failed to subscribe channel: $e');
}

```

如果你不再需要在此频道收发消息，你可以调用 <code>unsubscribe</code> 方法取消订阅此频道。

```dart showLineNumbers
// Paste the following code snippet below "unsubscribe frome a channel" comment
try {
  // unsubscribe channel
  var (status,response) = await rtmClient.unsubscribe(channelName);
  if (status.error == true) {
      print('${status.operation} failed due to ${status.reason}, error code: ${status.errorCode}');
  } else {
      print('unsubscribe success!');
  }
} catch (e) {
  print('something went wrong with logout: $e');
}
```

如需了解更多收发消息的信息，查看[消息](/zh-CN/api-reference/rtm/flutter/message)。

### 6. 登出服务 [#6-登出服务-2]

当不再需要使用 RTM 服务时，你可以调用 <code>logout</code> 方法登出 RTM 系统。将以下代码添加到程序中：

```dart
// Paste the following code snippet below "logout rtm service" comment
try {
  // logout rtm service
  var (status,response) = await rtmClient.logout();
  if (status.error == true) {
      print('${status.operation} failed due to ${status.reason}, error code: ${status.errorCode}');
  } else {
      print('logout RTM success!');
  }
} catch (e) {
  print('something went wrong with logout: $e');
}
```


:::info[信息]
本操作会影响你账单中的 PCU 计费项。
:::


### 7. 组合到一起 [#7-组合到一起-2]

经过上述步骤，你程序中的代码应该如下所示：

```dart
import 'package:flutter/material.dart';
import 'package:agora_rtm/agora_rtm.dart';
import 'dart:convert';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  const userId = 'your_userId';
  const appId = 'your_appId';
  const channelName = 'getting-started';
  late RtmClient rtmClient;

  //create rtm instance
  try {
    // create rtm client
    final (status, client) = await RTM( appId, userId);
    if (status.error == true) {
        print('${status.operation} failed due to ${status.reason}, error code: ${status.errorCode}');
    } else {
        rtmClient = client;
        print('Initialize success!');
    }

    // add events listner
    rtmClient.addListener(
    // add message event handler
    message: (event) {
      print('recieved a message from channel: ${event.channelName}, channel type : ${event.channelType}');
      print('message content: ${utf8.decode(event.message!)}, custome type: ${event.customType}');
    },
    // add link state event handler
    linkState: (event) {
      print('link state changed from ${event.previousState} to ${event.currentState}');
      print('reason: ${event.reason}, due to operation ${event.operation}');
    });
  } catch (e) {
    print('Initialize falid!:${e}');
  }

  // login rtm service
  try {
    // login rtm service
    var (status,response) = await rtmClient.login(appId);
    if (status.error == true) {
        print('${status.operation} failed due to ${status.reason}, error code: ${status.errorCode}');
    } else {
        print('login RTM success!');
    }
  } catch (e) {
    print('Failed to login: $e');
  }

  // subscribe to a channel
  try {
    // subscribe to 'getting-started' channel
    var (status,response) = await rtmClient.subscribe(channelName);
    if (status.error == true) {
        print('${status.operation} failed due to ${status.reason}, error code: ${status.errorCode}');
    } else {
        print('subscribe channel: ${channelName} success!');
    }
  } catch (e) {
    print('Failed to subscribe channel: $e');
  }

  // publish messages
  // Send a message every second for 100 seconds
  for (var i = 0; i < 100; i++) {
    try {
      var (status, response) = await rtmClient.publish(
          channelName,
          'message number : $i',
          channelType: RtmChannelType.message,
          customType: 'PlainText'
          );
      if (status.error == true ){
        print('${status.operation} failed, errorCode: ${status.errorCode}, due to ${status.reason}');
      } else {
        print('${status.operation} success! message number:$i');
      }
    } catch (e) {
      print('Failed to publish message: $e');
    }
    await Future.delayed(Duration(seconds: 1));
  }

  // unsubscribe frome a channel
  try {
    // unsubscribe channel
    var (status,response) = await rtmClient.unsubscribe(channelName);
    if (status.error == true) {
        print('${status.operation} failed due to ${status.reason}, error code: ${status.errorCode}');
    } else {
        print('unsubscribe success!');
    }
  } catch (e) {
    print('something went wrong with logout: $e');
  }

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

现在，你可以开始运行你的程序：

1. 保存项目。
2. 将目标设备连接到电脑，或者打开模拟器。
3. 打开终端，在项目路径下执行以下命令在目标设备上运行示例项目：

   ```shell
   flutter run
   ```

   成功运行后，在终端上你将看到类似以下信息：

    ```shell
    flutter: publish success! message number:0
    flutter: publish success! message number:1
    flutter: publish success! message number:2
    flutter: publish success! message number:3
    ```

4. 复制项目，在 IDE 上打开，编辑 `main.dart` 文件，修改 `userId` 保存后按照上述步骤运行第二个进程，观察终端上的输出，你将收到类似以下信息：

   ```shell
   flutter: recieved a message from channel: getting-started, channel type : RtmChannelType.message
   flutter: message content: message number : 0, custome type: PlainText
   flutter: recieved a message from channel: getting-started, channel type : RtmChannelType.message
   flutter: message content: message number : 1, custome type: PlainText
   ```

至此，你已经成功集成并正确使用了 RTM 服务。

## 贯穿始终 [#贯穿始终-5]

相比于介绍如何写出代码，声网更愿意帮助你掌握上述程序编写的过程和逻辑。上述程序依次完成了以下操作，让你可以正确地收发消息：

1. 创建并初始化 RTM 对象。
2. 添加 `linkState`、`message` 事件监听函数。
3. 调用 `login` 登录了 RTM 服务。
4. 调用 `subscribe` 订阅了一个 Message Channel。
5. 调用 `publish` 发送消息。
6. 调用 `unsubscribe` 取消订阅一个 Message Channel。
7. 调用 `logout` 登出 RTM 系统。

## 下一步 [#下一步-5]

现在，你已经学会了如何使用 RTM Flutter SDK 来实现 Message Channel 的收发消息功能。下一步，你可以通过 SDK 的 [API 参考](/zh-CN/api-reference/rtm/flutter/configuration)了解更多功能的使用方法。
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="cpp" />
本文将从 Linux 环境视角指导你如何利用 RTM C++ SDK 构建简单的应用程序，内容涵盖了集成和开发的基础知识：开通声网账号、获取 SDK、发送消息、接收消息等。对于 Windows 平台，我们提供了完整的 Visual Studio 工程，开发者可以直接参考工程配置，来运行你的第一个 RTM 程序。

<Cards>
  <Card title="Linux 示例项目" href="https://github.com/AgoraIO/RTM2/tree/main/Agora-RTM2-QuickStart-Linux-C%2B%2B" />
  <Card title="Windows 示例项目" href="https://github.com/AgoraIO/RTM2/tree/main/Agora-RTM2-QuickStart-Windows-C%2B%2B" />
</Cards>

## 准备工作 [#准备工作-6]

开始构建项目前，你需要完成[开通 RTM 服务](/zh-CN/realtime-media/rtm/build/rtm-initialization/enable-service)，并检查你的开发环境是否满足[平台支持](/zh-CN/realtime-media/rtm/reference/capabilities-and-compatibility/platform-support)中的最低版本要求。

## 构建项目 [#构建项目-5]

### 1. 项目配置 [#1-项目配置-3]

根据以下步骤配置你的项目：

1. 新建一个目录 `RTMProject`，目录中包含以下文件夹：

   ```shell
   RTMProject/
   ├── include
   └── lib
   ```

2. [点击此处](https://download.shengwang.cn/rtm2/release/RTM_C%2B%2B_SDK_for_Windows_v2.3.0.zip)下载最新版本的 C++ SDK 并解压。将 SDK 包中的如下文件复制到项目的对应目录下：

- 将 `/rtm/sdk/libagora_rtm_sdk.so` 文件复制到项目的 `lib` 目录下。
- 将 `/rtm/sdk/high_level_api/include` 路径下的所有 `*.h` 文件复制到项目的 `include` 目录下。

   :::warning[注意]
   如果你同时集成了 2.2.0 及以上版本的 RTM SDK 和 4.3.0 及以上版本的 RTC SDK，请手动删除更低版本的动态库文件。
   :::

3. 在 `RTMProject` 目录下新建 `CMakeLists.txt` 和 `build.sh` 文件，以便后续使用 CMake 编译项目。文件内容如下：

   ```text
   # CMakeLists.txt
   cmake_minimum_required (VERSION 2.8)
   project(RTMQuickStart)
   set(TARGET_NAME rtm_demo)
   set(SOURCES rtm_quick_start.cpp)
   set(HEADERS)
   set(TARGET_BUILD_TYPE "Debug")
   set(CMAKE_CXX_FLAGS "-fPIC -O2 -g -std=c++11 -msse2")
   include_directories(${CMAKE_SOURCE_DIR}/include)
   link_directories(${CMAKE_SOURCE_DIR}/lib)
   add_executable(${TARGET_NAME} ${SOURCES} ${HEADERS})
   target_link_libraries(${TARGET_NAME} agora_rtm_sdk pthread)
   ```

   ```shell
   # build.sh
   rm -fr build
   mkdir -p build
   cd build
   cmake ..
   make
   cd ..
   ```

### 2. 创建并初始化 RTM [#2-创建并初始化-rtm-1]

调用 RTM SDK 的任何 API 之前，需要先创建一个 `IRtmClient` 对象实例，再初始化该实例。在 `RTMProject` 目录下新建 `RTMQuickStart.cpp` 文件，添加以下代码作为程序模版。你会发现此程序并不完整，不用紧张，我们在后续步骤中将一步步指导你补充完善代码。

:::info[信息]
你需要在初始化时填入已开通 RTM 服务的项目的 App ID 和 Token。在测试阶段，为快速验证功能，我们建议你在创建声网项目时将鉴权模式设置为调试模式，然后在示例中填入项目的 App ID 即可。
:::

```cpp
#include <iostream>
#include <memory>
#include <string>
#include <exception>

#include "IAgoraRtmClient.h"

// Pass in your App ID here
const std::string APP_ID = "<Your App ID>";


// Terminal color codes for UBUNTU/LINUX
#define RESET   "\033[0m"
#define BLACK   "\033[30m"      /* Black */
#define RED     "\033[31m"      /* Red */
#define GREEN   "\033[32m"      /* Green */
#define YELLOW  "\033[33m"      /* Yellow */
#define BLUE    "\033[34m"      /* Blue */
#define MAGENTA "\033[35m"      /* Magenta */
#define CYAN    "\033[36m"      /* Cyan */
#define WHITE   "\033[37m"      /* White */
#define BOLDBLACK   "\033[1m\033[30m"      /* Bold Black */
#define BOLDRED     "\033[1m\033[31m"      /* Bold Red */
#define BOLDGREEN   "\033[1m\033[32m"      /* Bold Green */
#define BOLDYELLOW  "\033[1m\033[33m"      /* Bold Yellow */
#define BOLDBLUE    "\033[1m\033[34m"      /* Bold Blue */
#define BOLDMAGENTA "\033[1m\033[35m"      /* Bold Magenta */
#define BOLDCYAN    "\033[1m\033[36m"      /* Bold Cyan */
#define BOLDWHITE   "\033[1m\033[37m"      /* Bold White */

using namespace agora::rtm;

class RtmEventHandler : public IRtmEventHandler {
public:
  // Add the event listener

private:
  void cbPrint(const char* fmt, ...) {
    printf("\x1b[32m<< RTM async callback: ");
    va_list args;
    va_start(args, fmt);
    vprintf(fmt, args);
    va_end(args);
    printf(" >>\x1b[0m\n");
  }
};

class RtmDemo {
public:
  RtmDemo()
    : eventHandler_(new RtmEventHandler()),
      rtmClient_(nullptr) { Init(); }

  void Init() {
    std::cout << YELLOW << "Please enter userID (literal \"null\" or starting"
    << "with space is not allowed, no more than 64 characters!):" << std::endl;

    std::string userID;
    std::getline(std::cin, userID);

    RtmConfig config;
    config.appId = APP_ID.c_str();
    config.userId = userID.c_str();
    config.eventHandler = eventHandler_.get();

    // Create an IRtmClient instance
    int errorCode = 0;
    rtmClient_ = createAgoraRtmClient(config, errorCode);
    if (!rtmClient_ || errorCode != 0) {
      std::cout << RED <<"error creating rtm service!" << std::endl;
      exit(0);
    }
  }

  void start() {
    // Log in the RTM server
  }

  // Log out from the RTM server

  // Subscribe to a channel

  // Unsubscribe from a channel

  // Publish a message

  // Sample codes for the user interface
  void mainMeun() {
    bool quit  = false;
    while (!quit) {
      std::cout << WHITE
                << "1: subscribe channel\n"
                << "2: unsubscribe channel\n"
                << "3: publish message\n"
                << "4: logout" << std::endl;
      std::cout << YELLOW <<"please input your choice" << std::endl;
      std::string input;
      std::getline(std::cin, input);
      int32_t choice = 0;
      try {
        choice = std::stoi(input);
      } catch(...) {
        std::cout <<RED << "invalid input" << std::endl;
        continue;
      }
      switch (choice)
      {
        case 1: {
          std::cout << YELLOW << "please input dst channel id" << std::endl;
          std::string dst;
          std::getline(std::cin, dst);
          subscribeChannel(dst);
          break;
        }
        case 2: {
          std::cout << YELLOW << "please input dst channel id" << std::endl;
          std::string dst;
          std::getline(std::cin, dst);
          unsubscribeChannel(dst);
          break;
        }
        case 3: {
          std::cout << YELLOW << "please input channel id" << std::endl;
          std::string channel;
          std::getline(std::cin, channel);
          groupChat(channel);
          break;
        }
        case 4: {
          logout();
          return;
        }
        default: {
          std::cout <<RED << "invalid input" << std::endl;
          break;
        }
      }
    }
  }


  void groupChat(const std::string& channelName) {
    std::string message;
    while (true) {
      std::cout << YELLOW << "please input message "
                << "or input \"quit\" to leave group chat"
                << std::endl;
      std::getline(std::cin, message);
      if (message.compare("quit") == 0) {
        return;
      } else {
        publishMessage(channelName, message);
        std::this_thread::sleep_for(std::chrono::seconds(1));
      }
    }
  }

private:
  std::unique_ptr<IRtmEventHandler> eventHandler_;
  IRtmClient* rtmClient_;
};

int main(int argc, const char * argv[]) {
  RtmClient messaging;
  messaging.login();
  return 0;
}
```

### 3. 添加事件监听 [#3-添加事件监听-4]

事件监听程序帮助你实现频道中消息、事件到达后的处理逻辑，添加以下代码到你的程序中以显示收到的消息或事件通知：

```cpp showLineNumbers
// Paste the following code snippet below "Add the event listener" comment
void onLoginResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) override {
  cbPrint("onLoginResult, request id: %lld, errorCode: %d", requestId, errorCode);
}

void onLogoutResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) {
  cbPrint("onLogoutResult, request id: %lld, errorCode: %d", requestId, errorCode);
}

void onLinkStateEvent(const LinkStateEvent& event) override {
  cbPrint("onLinkStateEvent, state: %d -> %d, operation: %d, reason code: %d, reason: %s", event.previousState, event.currentState, event.operation, event.reasonCode, event.reason);
}

void onPublishResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) override {
  cbPrint("onPublishResult request id: %lld result: %d", requestId, errorCode);
}

void onMessageEvent(const MessageEvent &event) override {
  cbPrint("receive message from: %s, message: %s", event.publisher, event.message);
}

void onSubscribeResult(const uint64_t requestId, const char *channelName, RTM_ERROR_CODE errorCode) override {
  cbPrint("onSubscribeResult: channel:%s, request id: %lld result: %d", channelName, requestId, errorCode);
}

void onUnsubscribeResult(const uint64_t requestId, const char *channelName, RTM_ERROR_CODE errorCode) override {
  cbPrint("onUnsubscribeResult: channel:%s, request id: %lld result: %d", channelName, requestId, errorCode);
}
```

### 4. 登录服务 [#4-登录服务-4]

你需要执行登录操作才能建立与 RTM 服务器的连接，然后才能调用 SDK 的其他 API。将以下代码添加到程序中：

```cpp showLineNumbers
// Paste the following code snippet below "Log in the RTM server" comment
uint64_t requestId = 0;
rtmClient_->login(APP_ID.c_str(), requestId);

// Sample codes for the user interface
mainMeun();
std::cout << YELLOW << "quit ? yes/no" << std::endl;
std::string input;
std::getline(std::cin, input);
if (input.compare("yes") == 0) {
  exit(0);
}
```

:::info[信息]
你需要在登录时传入 Token。在测试阶段，为快速验证功能，我们建议你在创建声网项目时将鉴权模式设置为调试模式，然后在登录时只需传入项目的 App ID。
:::

### 5. 收发消息 [#5-收发消息-4]

调用 `publish` 方法向 Message Channel 发送消息后，RTM 会把该消息分发给该频道的所有订阅者，以下代码演示如何发送字符串类型的消息，将此代码片段添加到程序中：

:::info[信息]
你需要先对消息负载进行字符串序列化，才能调用 `publish` 方法发送消息。
:::

```cpp showLineNumbers
// Paste the following code snippet below "Publish a message" comment
void publishMessage(const std::string& channelName, const std::string& message) {
  PublishOptions options;
  options.messageType = RTM_MESSAGE_TYPE_STRING;
  uint64_t requestId;
  rtmClient_->publish(channelName.c_str(), message.c_str(), message.size(), options, requestId);
}
```

调用 `subscribe` 方法订阅此频道以接收此频道中的消息。将以下代码添加到程序中：

```cpp showLineNumbers
// Paste the following code snippet below "Subscribe to a channel" comment
void subscribeChannel(const std::string& channelName) {
  uint64_t requestId = 0;
  rtmClient_->subscribe(channelName.c_str(), SubscribeOptions(), requestId);
}
```

如果你不再需要在此频道收发消息，你可以调用 `unsubscribe` 方法取消订阅此频道。

```cpp showLineNumbers
// Paste the following code snippet below "Unsubscribe from a channel" comment
void unsubscribeChannel(const std::string& channelName) {
  uint64_t requestId = 0;
  rtmClient_->unsubscribe(channelName.c_str(), requestId);
}
```

如需了解更多收发消息的信息，查看[消息](/zh-CN/api-reference/rtm/cpp/message)。

### 6. 登出服务 [#6-登出服务-3]

当不再需要使用 RTM 服务时，你可以调用 `logout` 方法登出 RTM 系统。将以下代码添加到程序中：

```cpp
// Paste the following code snippet below "Log out from the RTM server" comment
void logout() {
  uint64_t requestId = 0;
  rtmClient_->logout(requestId);
}
```

:::info[信息]
本操作会影响你账单中的 PCU 计费项。
:::

### 7. 组合到一起 [#7-组合到一起-3]

经过上述步骤，你程序中的代码应该如下所示：

```cpp
#include <iostream>
#include <memory>
#include <string>
#include <exception>

#include "IAgoraRtmClient.h"

// Pass in your App ID here
const std::string APP_ID = "<Your App ID>";

// Terminal color codes for UBUNTU/LINUX
#define RESET   "\033[0m"
#define BLACK   "\033[30m"      /* Black */
#define RED     "\033[31m"      /* Red */
#define GREEN   "\033[32m"      /* Green */
#define YELLOW  "\033[33m"      /* Yellow */
#define BLUE    "\033[34m"      /* Blue */
#define MAGENTA "\033[35m"      /* Magenta */
#define CYAN    "\033[36m"      /* Cyan */
#define WHITE   "\033[37m"      /* White */
#define BOLDBLACK   "\033[1m\033[30m"      /* Bold Black */
#define BOLDRED     "\033[1m\033[31m"      /* Bold Red */
#define BOLDGREEN   "\033[1m\033[32m"      /* Bold Green */
#define BOLDYELLOW  "\033[1m\033[33m"      /* Bold Yellow */
#define BOLDBLUE    "\033[1m\033[34m"      /* Bold Blue */
#define BOLDMAGENTA "\033[1m\033[35m"      /* Bold Magenta */
#define BOLDCYAN    "\033[1m\033[36m"      /* Bold Cyan */
#define BOLDWHITE   "\033[1m\033[37m"      /* Bold White */

using namespace agora::rtm;

class RtmEventHandler : public IRtmEventHandler {
public:
  // Add the event listener
  void onLoginResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) override {
    cbPrint("onLoginResult, request id: %lld, errorCode: %d", requestId, errorCode);
  }

  void onLogoutResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) {
    cbPrint("onLogoutResult, request id: %lld, errorCode: %d", requestId, errorCode);
  }

  void onLinkStateEvent(const LinkStateEvent& event) override {
    cbPrint("onLinkStateEvent, state: %d -> %d, operation: %d, reason code: %d, reason: %s", event.previousState, event.currentState, event.operation, event.reasonCode, event.reason);
  }

  void onPublishResult(const uint64_t requestId, RTM_ERROR_CODE errorCode) override {
    cbPrint("onPublishResult request id: %lld result: %d", requestId, errorCode);
  }

  void onMessageEvent(const MessageEvent &event) override {
    cbPrint("receive message from: %s, message: %s", event.publisher, event.message);
  }

  void onSubscribeResult(const uint64_t requestId, const char *channelName, RTM_ERROR_CODE errorCode) override {
    cbPrint("onSubscribeResult: channel:%s, request id: %lld result: %d", channelName, requestId, errorCode);
  }

  void onUnsubscribeResult(const uint64_t requestId, const char *channelName, RTM_ERROR_CODE errorCode) override {
    cbPrint("onUnsubscribeResult: channel:%s, request id: %lld result: %d", channelName, requestId, errorCode);
  }

private:
  void cbPrint(const char* fmt, ...) {
    printf("\x1b[32m<< RTM async callback: ");
    va_list args;
    va_start(args, fmt);
    vprintf(fmt, args);
    va_end(args);
    printf(" >>\x1b[0m\n");
  }
};

class RtmDemo {
public:
  RtmDemo()
    : eventHandler_(new RtmEventHandler()),
      rtmClient_(nullptr) { Init(); }

  void Init() {
    std::cout << YELLOW << "Please enter userID (literal \"null\" or starting"
    << "with space is not allowed, no more than 64 characters!):" << std::endl;

    std::string userID;
    std::getline(std::cin, userID);

    RtmConfig config;
    config.appId = APP_ID.c_str();
    config.userId = userID.c_str();
    config.eventHandler = eventHandler_.get();
    // Create an IRtmClient instance
    int errorCode = 0;
    rtmClient_ = createAgoraRtmClient(config, errorCode);
    if (!rtmClient_ || errorCode != 0) {
      std::cout << RED <<"error creating rtm service!" << std::endl;
      exit(0);
    }
  }

  void start() {
    // Log in the RTM server
    uint64_t requestId = 0;
    rtmClient_->login(APP_ID.c_str(), requestId);

    // Sample codes for the user interface
    mainMeun();

    std::cout << YELLOW << "quit ? yes/no" << std::endl;
    std::string input;
    std::getline(std::cin, input);
    if (input.compare("yes") == 0) {
      exit(0);
    }
  }

  // Log out from the RTM server
  void logout() {
    uint64_t requestId = 0;
    rtmClient_->logout(requestId);
  }

  // Subscribe to a channel
  void subscribeChannel(const std::string& channelName) {
    uint64_t requestId = 0;
    rtmClient_->subscribe(channelName.c_str(), SubscribeOptions(), requestId);
  }

  // Unsubscribe from a channel
  void unsubscribeChannel(const std::string& channelName) {
    uint64_t requestId = 0;
    rtmClient_->unsubscribe(channelName.c_str(), requestId);
  }

  // Publish a message
  void publishMessage(const std::string& channelName, const std::string& message) {
    PublishOptions options;
    options.messageType = RTM_MESSAGE_TYPE_STRING;
    uint64_t requestId;
    rtmClient_->publish(channelName.c_str(), message.c_str(), message.size(), options, requestId);
  }

  // Sample codes for the user interface
  void mainMeun() {
    bool quit  = false;
    while (!quit) {
      std::cout << WHITE
                << "1: subscribe channel\n"
                << "2: unsubscribe channel\n"
                << "3: publish message\n"
                << "4: logout" << std::endl;
      std::cout << YELLOW <<"please input your choice: " << std::endl;
      std::string input;
      std::getline(std::cin, input);
      int32_t choice = 0;
      try {
        choice = std::stoi(input);
      } catch(...) {
        std::cout <<RED << "invalid input" << std::endl;
        continue;
      }

      switch (choice)
      {
        case 1: {
          std::cout << YELLOW << "please input channel name:" << std::endl;
          std::string channelName;
          std::getline(std::cin, channelName);
          subscribeChannel(channelName);
          std::this_thread::sleep_for(std::chrono::seconds(1));
          break;
        }
        case 2: {
          std::cout << YELLOW << "please input channel name:" << std::endl;
          std::string channelName;
          std::getline(std::cin, channelName);
          unsubscribeChannel(channelName);
          std::this_thread::sleep_for(std::chrono::seconds(1));
          break;
        }
        case 3: {
          std::cout << YELLOW << "please input channel name:" << std::endl;
          std::string channelName;
          std::getline(std::cin, channelName);
          groupChat(channelName);
          break;
        }
        case 4: {
          logout();
          return;
        }
        default: {
          std::cout <<RED << "invalid input" << std::endl;
          break;
        }
      }
    }
  }

  void groupChat(const std::string& channelName) {
    std::string message;
    while (true) {
      std::cout << YELLOW << "please input message "
                << "or input \"quit\" to leave group chat"
                << std::endl;
      std::getline(std::cin, message);
      if (message.compare("quit") == 0) {
        return;
      } else {
        publishMessage(channelName, message);
        std::this_thread::sleep_for(std::chrono::seconds(1));
      }
    }
  }

private:
  std::unique_ptr<IRtmEventHandler> eventHandler_;
  IRtmClient* rtmClient_;
};

int main(int argc, const char * argv[]) {
  RtmClient messaging;
  messaging.login();
  return 0;
}
```

现在，你可以开始运行你的程序：

1. 保存项目。
2. 在命令行工具中，运行以下命令编译项目：

   ```shell
   $ sh build.sh
   ```

3. 编译完成后，运行以下命令进入 `build` 目录并运行编译完成的可执行文件：

   ```shell
   $ cd build
   $ ./rtm_demo
   ```

4. 运行项目。成功运行后，你可以在命令行工具中看到消息收发的提示信息。

:::info[信息]
如果提示找不到 `libaosl.so`，需要添加一下动态库的加载路径，例如 `export LD_LIBRARY_PATH=./lib:$LD_LIBRARY_PATH`，其中，`./lib` 为存放RTM SDK `so` 库的路径。
:::

至此，你已经成功集成并正确使用了 RTM 服务。

## 贯穿始终 [#贯穿始终-6]

相比于介绍如何写出代码，声网更愿意帮助你掌握上述程序编写的过程和逻辑。上述程序依次完成了以下操作，让你可以正确地收发消息：

1. 创建并初始化 RTM 对象。
2. 添加 `onLoginResult`、`onLinkStateEvent`、`onPublishResult`、`onMessageEvent` 和 `onSubscribeResult` 事件监听函数。
3. 调用 `login` 登录了 RTM 服务。
4. 调用 `subscribe` 订阅了一个 Message Channel。
5. 调用 `publish` 发送消息。
6. 调用 `unsubscribe` 取消订阅一个 Message Channel。
7. 调用 `logout` 登出 RTM 系统。

## 下一步 [#下一步-6]

现在，你已经学会了如何使用 RTM C++ SDK 来实现 Message Channel 的收发消息功能。下一步，你可以通过 SDK 的 [API 参考](/zh-CN/api-reference/rtm/cpp/configuration)了解更多功能的使用方法。
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="swift" />
本文将指导你如何利用 RTM Swift SDK 构建简单的应用程序，内容涵盖了集成和开发的基础知识：开通声网账号、获取 SDK、发送消息、接收消息等。
## 准备工作 [#准备工作-7]

开始构建项目前，你需要完成[开通 RTM 服务](/zh-CN/realtime-media/rtm/build/rtm-initialization/enable-service)，并检查你的开发环境和运行环境是否满足[平台支持](/zh-CN/realtime-media/rtm/reference/capabilities-and-compatibility/platform-support)中的最低版本要求。

## 构建项目 [#构建项目-6]

### 1. 项目配置 [#1-项目配置-4]

根据以下步骤配置你的项目：

1. 在 Xcode 中创建一个 iOS 平台下的 **Single View App**，项目设置如下：
   - **Product Name** 设为 `RtmQuickstart`。
   - **Organization Identifier** 设为 `agora`。
   - **User Interface** 选择 **Storyboard**。
   - **Language** 选择 **Swift**。
2. 通过以下任意一种方式获取最新的 RTM Swift SDK 并导入你的项目。

<Tabs defaultValue="cdn">
<TabsList>
  <TabsTrigger value="cdn">使用 CDN</TabsTrigger>
  <TabsTrigger value="cocoapods">使用 Cocoapods</TabsTrigger>
  <TabsTrigger value="spm">通过 SPM 集成 SDK</TabsTrigger>
</TabsList>

<TabsContent value="cdn">

1. [点击此处](https://download.shengwang.cn/rtm2/release/RTM_OC_SDK_v2.3.0.zip)下载最新版本的 Objective-C SDK。
2. 复制 SDK 包中 `/libs` 路径下所有的 `xcframework` 文件至项目路径下。
3. 打开 Xcode，进入 **TARGETS > Project Name > General > Frameworks, Libraries, and Embedded Content** 菜单。
4. 点击 **+ > Add Other… > Add Files** 添加所有的 `xcframework` 文件，并确保添加的动态库 **Embed** 属性设置为 **Embed & Sign**。

添加完成后，项目会自动链接所需系统库。

</TabsContent>

<TabsContent value="cocoapods">

1. 开始前请确保你已安装 Cocoapods，如尚未安装 Cocoapods，参考 [Getting Started with CocoaPods](https://guides.cocoapods.org/using/getting-started.html#getting-started) 安装说明。
2. 在终端里进入项目根目录，并运行 `pod init` 命令。项目文件夹下会生成一个 `Podfile` 文本文件。
3. 打开 `Podfile` 文件，修改文件为如下内容。注意将 `Your App` 替换为你的 Target 名称。

   ```ruby
   platform :ios, '11.0'
   target 'Your App' do
      # 对于 2.2.8 及之后版本:
      pod 'ShengWang-Rtm', '2.3.0', :subspecs => ['RtmKit']
      # 对于 2.2.7 版本:
      # pod 'ShengwangRtm', 'x.y.z'
      # 对于 2.2.6 及之前版本:
      # pod 'AgoraRtm', 'x.y.z'
      # 如需获取 2.2.0 之前的版本，请将包名修改为 AgoraRtm_iOS
   end
   ```

4. 在终端内运行 `pod install` 命令安装声网 SDK。成功安装后，Terminal 中会显示 `Pod installation complete!`。
5. 成功安装后，项目文件夹下会生成一个后缀为 `.xcworkspace` 的文件，通过 Xcode 打开该文件进行后续操作。

</TabsContent>

<TabsContent value="spm">

使用如下链接，通过 SPM (Swift Package Manager) 集成 SDK：

```text
https://github.com/AgoraIO/AgoraRtm_Apple.git
```

</TabsContent>
</Tabs>

   :::info[信息]
   如果你同时集成了 2.2.0 及以上版本的 RTM SDK 和 4.3.0 及以上版本的 RTC SDK，请参考 [FAQ](/zh-CN/reference/faq/integration/rtm2_rtc_integration_issue) 处理集成问题。
   :::

### 2. 初始化 RTM [#2-初始化-rtm-3]

调用 RTM SDK 的任何 API 之前，需要先创建一个 `AgoraRtmClientKit` 对象实例，再初始化该实例。在 `ContentView.swift` 文件中，添加以下代码作为程序模版。你会发现此程序并不完整，不用紧张，我们在后续步骤中将一步步指导你补充完善代码。

:::info[信息]
你需要将示例中的 `<#YOUR APPID#>` 字段替换成你项目的 App ID。
:::

```swift
import SwiftUI
import Combine
import AgoraRtmKit

struct Message: Identifiable, Equatable {
    let id = UUID()
    let content: String
}

class ChatViewModel: NSObject, ObservableObject, AgoraRtmClientDelegate{
    // Initialize the AgoraRtmClientKit instance
    var appid: String = <#YOUR APPID#>
    var rtmKit: AgoraRtmClientKit? = nil
    @Published var username: String = ""
    @Published var message: String = ""
    @Published var channel: String = ""
    @Published var messages: [Message] = []

    // Add the event listener

    // Log in the RTM server

    // Log out from the RTM server

    // Subscribe to a channel

    // Unsubscribe from a channel

    // Publish a message
}

// Set user interface
struct ContentView: View {
    @StateObject private var viewModel = ChatViewModel()

    var body: some View {
        VStack {
            TextField("input user name", text: $viewModel.username)
                .padding()
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .font(.title)
            HStack {
                Button(action: viewModel.login) {
                    Text("login")
                        .font(.title)
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }

                Button(action: viewModel.logout) {
                    Text("logout")
                        .font(.title)
                        .padding()
                        .background(Color.red)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
            }

            TextField("channel name", text: $viewModel.channel)
                .padding()
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .font(.title)
            HStack {
                Button(action: viewModel.subscribe) {
                    Text("subscribe")
                        .font(.title)
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }

                Button(action: viewModel.unsubscribe) {
                    Text("unsubscribe")
                        .font(.title)
                        .padding()
                        .background(Color.red)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
            }

            TextField("input message", text: $viewModel.message)
                .padding()
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .font(.title)

            Button(action: viewModel.sendMessage) {
                Text("send")
                    .font(.title)
                    .padding()
                    .background(Color.green)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }

            // display log
            ScrollViewReader { scrollProxy in
                            List(viewModel.messages) { message in
                                Text(message.content)
                                    .id(message.id)
                            }
                            .onChange(of: viewModel.messages) { _ in
                                if let lastMessage = viewModel.messages.last {
                                    withAnimation {
                                        scrollProxy.scrollTo(lastMessage.id, anchor: .bottom)
                                    }
                                }
                            }
                        }
            .padding()

        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```

如需了解更多初始化的信息，查看[初始配置](/zh-CN/api-reference/rtm/swift/configuration)。

### 3. 添加事件监听 [#3-添加事件监听-5]

事件监听程序帮助你实现频道中消息、事件到达后的处理逻辑，添加以下代码到你的程序中以显示收到的消息或事件通知：

```swift showLineNumbers
// Paste the following code snippet below "Add the event listener" comment
func rtmKit(_ rtmKit: AgoraRtmClientKit, didReceiveLinkStateEvent event: AgoraRtmLinkStateEvent) {
    addToMessageList(str: "RTM link state change current state is: \(event.currentState.rawValue) previous state is :\(event.previousState.rawValue)")
}
func rtmKit(_ rtmKit: AgoraRtmClientKit, didReceiveMessageEvent event: AgoraRtmMessageEvent) {
    addToMessageList(str: "Message received.\n channel: \(event.channelName), publisher: \(event.publisher),  message content: \(event.message.stringData!)")
}
```

### 4. 登录服务 [#4-登录服务-5]

你需要执行登录操作才能建立与 RTM 服务器的连接，然后才能调用 SDK 的其他 API。将以下代码添加到程序中：

```swift showLineNumbers
// Paste the following code snippet below "Log in the RTM server" comment
func login() {
    if rtmKit != nil {
        addToMessageList(str: "RTM alreay login! Logout first!")
        return
    }
    let config = AgoraRtmClientConfig(appId: appid, userId: username);
    rtmKit = try! AgoraRtmClientKit(config, delegate: self)
    rtmKit?.login(token, completion: { response, error in
        if error != nil {
            self.addToMessageList(str: "login failed error code is \(error?.errorCode.rawValue), reason is \(error?.reason)")
        } else {
            self.addToMessageList(str: "\(self.username) login succsss")
        }
    })
}
```

### 5. 收发消息 [#5-收发消息-5]

调用 `publish` 方法向 Message Channel 发送消息后，RTM 会把该消息分发给该频道的所有订阅者，以下代码演示如何发送字符串类型的消息，将此代码片段添加到程序中：

:::info[信息]
你需要先对消息负载进行字符串序列化，才能调用 `publish` 方法发送消息。
:::

```swift showLineNumbers
// Paste the following code snippet below "Publish a message" comment
func sendMessage() {
    guard !message.isEmpty else { return }
    if (rtmKit != nil) {
        rtmKit?.publish(channelName: channel, message: message, option: nil, completion: { response, error in
            if error != nil {
                self.addToMessageList(str: "publish failed error code is \(error?.errorCode.rawValue), reason is \(error?.reason)")
            } else {
                self.addToMessageList(str: "publish message to channel: \(self.channel) succsss")
            }
        })
    }
    message = ""
}

func addToMessageList(str: String) {
    messages.append(Message(content: str))
}
```

调用 `subscribe` 方法订阅此频道以接收此频道中的消息。将以下代码添加到程序中：

```swift showLineNumbers
// Paste the following code snippet below "Subscribe to a channel" comment
func subscribe() {
    rtmKit?.subscribe(channelName: channel, option: nil, completion: { response, error in
        if error != nil {
            self.addToMessageList(str: "subscribe channel: \(self.channel) failed error code is \(error?.errorCode.rawValue), reason is \(error?.reason)")
        } else {
            self.addToMessageList(str: "susbcribe channel: \(self.channel) succsss")
        }
    })
}
```

如果你不再需要在此频道收发消息，你可以调用 `unsubscribe` 方法取消订阅此频道。将以下代码添加到程序中：

``` swift showLineNumbers
// Paste the following code snippet below "Unsubscribe from a channel" comment
func unsubscribe() {
    rtmKit?.unsubscribe(channel, completion: { response, error in
        if error != nil {
            self.addToMessageList(str: "unsubscribe channel: \(self.channel) failed error code is \(error?.errorCode.rawValue), reason is \(error?.reason)")
        } else {
            self.addToMessageList(str: "unsusbcribe channel: \(self.channel) succsss")
        }
    })
}
```

如需了解更多收发消息的信息，查看[消息](/zh-CN/api-reference/rtm/swift/message)。

### 6. 登出服务 [#6-登出服务-4]

当不再需要使用 RTM 服务时，你可以调用 `logout` 方法登出 RTM 系统。将以下代码添加到程序中：

```swift showLineNumbers
// Paste the following code snippet below "Log out from the RTM server" comment
func logout() {
    if rtmKit == nil {
        addToMessageList(str: "RTM alreay logout!")
        return
    }
    rtmKit?.logout()
    rtmKit?.destroy()
    rtmKit = nil
    addToMessageList(str: "RTM logout!")
}
```

:::info[信息]
本操作会影响你账单中的 PCU 计费项。
:::

### 7. 组合到一起 [#7-组合到一起-4]

经过上述步骤，你程序中的代码应该如下所示：

```swift
import SwiftUI
import Combine
import AgoraRtmKit

struct Message: Identifiable, Equatable {
    let id = UUID()
    let content: String
}

class ChatViewModel: NSObject, ObservableObject, AgoraRtmClientDelegate{
    // Initialize the AgoraRtmClientKit instance
    var appid: String = <#YOUR APPID#>
    var rtmKit: AgoraRtmClientKit? = nil
    @Published var username: String = ""
    @Published var message: String = ""
    @Published var channel: String = ""
    @Published var messages: [Message] = []

    // Add the event listener
    func rtmKit(_ rtmKit: AgoraRtmClientKit, didReceiveLinkStateEvent event: AgoraRtmLinkStateEvent) {
        addToMessageList(str: "RTM link state change current state is: \(event.currentState.rawValue) previous state is :\(event.previousState.rawValue)")
    }
    func rtmKit(_ rtmKit: AgoraRtmClientKit, didReceiveMessageEvent event: AgoraRtmMessageEvent) {
        addToMessageList(str: "Message received.\n channel: \(event.channelName), publisher: \(event.publisher),  message content: \(event.message.stringData!)")
    }

    // Log in the RTM server
    func login() {
        if rtmKit != nil {
            addToMessageList(str: "RTM alreay login! Logout first!")
            return
        }
        let config = AgoraRtmClientConfig(appId: appid, userId: username);
        rtmKit = try! AgoraRtmClientKit(config, delegate: self)
        rtmKit?.login(appid, completion: { response, error in
            if error != nil {
                self.addToMessageList(str: "login failed error code is \(error?.errorCode.rawValue), reason is \(error?.reason)")
            } else {
                self.addToMessageList(str: "\(self.username) login succsss")
            }
        })
    }

    // Log out from the RTM server
    func logout() {
        if rtmKit == nil {
            addToMessageList(str: "RTM alreay logout!")
            return
        }
        rtmKit?.logout()
        rtmKit?.destroy()
        rtmKit = nil
        addToMessageList(str: "RTM logout!")
    }

    // Subscribe to a channel
    func subscribe() {
        rtmKit?.subscribe(channelName: channel, option: nil, completion: { response, error in
            if error != nil {
                self.addToMessageList(str: "subscribe channel: \(self.channel) failed error code is \(error?.errorCode.rawValue), reason is \(error?.reason)")
            } else {
                self.addToMessageList(str: "susbcribe channel: \(self.channel) succsss")
            }
        })
    }

    // Unsubscribe from a channel
    func unsubscribe() {
        rtmKit?.unsubscribe(channel, completion: { response, error in
            if error != nil {
                self.addToMessageList(str: "unsubscribe channel: \(self.channel) failed error code is \(error?.errorCode.rawValue), reason is \(error?.reason)")
            } else {
                self.addToMessageList(str: "unsusbcribe channel: \(self.channel) succsss")
            }
        })
    }

    // Publish a message
    func sendMessage() {
        guard !message.isEmpty else { return }
        if (rtmKit != nil) {
            rtmKit?.publish(channelName: channel, message: message, option: nil, completion: { response, error in
                if error != nil {
                    self.addToMessageList(str: "publish failed error code is \(error?.errorCode.rawValue), reason is \(error?.reason)")
                } else {
                    self.addToMessageList(str: "publish message to channel: \(self.channel) succsss")
                }
            })
        }
        message = ""
    }

    func addToMessageList(str: String) {
        messages.append(Message(content: str))
    }
}

// Set user interface
struct ContentView: View {
    @StateObject private var viewModel = ChatViewModel()

    var body: some View {
        VStack {
            TextField("input user name", text: $viewModel.username)
                .padding()
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .font(.title)
            HStack {
                Button(action: viewModel.login) {
                    Text("login")
                        .font(.title)
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }

                Button(action: viewModel.logout) {
                    Text("logout")
                        .font(.title)
                        .padding()
                        .background(Color.red)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
            }

            TextField("channel name", text: $viewModel.channel)
                .padding()
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .font(.title)
            HStack {
                Button(action: viewModel.subscribe) {
                    Text("subscribe")
                        .font(.title)
                        .padding()
                        .background(Color.blue)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }

                Button(action: viewModel.unsubscribe) {
                    Text("unsubscribe")
                        .font(.title)
                        .padding()
                        .background(Color.red)
                        .foregroundColor(.white)
                        .cornerRadius(10)
                }
            }

            TextField("input message", text: $viewModel.message)
                .padding()
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .font(.title)

            Button(action: viewModel.sendMessage) {
                Text("send")
                    .font(.title)
                    .padding()
                    .background(Color.green)
                    .foregroundColor(.white)
                    .cornerRadius(10)
            }

            // display log
            ScrollViewReader { scrollProxy in
                            List(viewModel.messages) { message in
                                Text(message.content)
                                    .id(message.id)
                            }
                            .onChange(of: viewModel.messages) { _ in
                                if let lastMessage = viewModel.messages.last {
                                    withAnimation {
                                        scrollProxy.scrollTo(lastMessage.id, anchor: .bottom)
                                    }
                                }
                            }
                        }
            .padding()

        }
        .padding()
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}
```

现在，你可以开始运行你的程序：

1. 保存项目。
2. 复制当前项目，使用相同的 `appId` 和不同的 `userId`。
3. 运行上面的两个项目。成功运行后，你可以在两个设备中看到 App 界面。
4. 将一个设备作为接收端，进行如下操作：
   1. 输入用户名，点击 **login**。
   2. 输入频道名，点击 **subscribe**。
5. 将另一个设备作为发送端，进行如下操作：
   1. 输入不同的用户名，点击 **subscribe**。
   2. 输入相同的频道名。
   3. 输入消息，点击 **send**。
6. 将上面的发送端和设备端互换，重复步骤 4 和 5。

如果两个设备均可收发消息，那么你已经成功集成并正确使用了 RTM 服务。

## 贯穿始终 [#贯穿始终-7]

相比于介绍如何写出代码，声网更愿意帮助你掌握上述程序编写的过程和逻辑。上述程序依次完成了以下操作，让你可以正确地收发消息：

1. 设置并初始化 RTM 对象。
2. 添加 `didReceiveMessageEvent` 事件监听函数。
3. 调用 `login` 登录了 RTM 服务。
4. 调用 `subscribe` 订阅了一个 Message Channel。
5. 调用 `publish` 发送消息。
6. 调用 `unsubscribe` 取消订阅一个 Message Channel。
7. 调用 `logout` 登出 RTM 系统。

## 下一步 [#下一步-7]

现在，你已经学会了如何使用 RTM Swift SDK 来实现 Message Channel 的收发消息功能。下一步，你可以通过 SDK 的 [API 参考](/zh-CN/api-reference/rtm/swift/configuration)了解更多功能的使用方法。
<_PlatformProcessedMarker close="true" />

<_PlatformProcessedMarker groupMode="structured" canonicalPlatform="web" platform="mini-program" />
本文介绍如何在微信小程序项目中集成 RTM 微信小程序 SDK，并实现基础的 Message Channel 消息收发。

:::caution[注意]
本 SDK 专为微信小程序运行环境构建，不能作为 RTM Web SDK 的直接替代品使用。当前小程序 SDK 不支持 `StreamChannel`、Topic、`cloudProxy`，也不支持私有化部署和私有化配置。
:::

## 准备工作 [#准备工作-8]

开始前，请确保你已完成以下准备：

- 已创建支持 npm 包的微信小程序项目。
- 已安装当前稳定版本的微信开发者工具。
- 开发环境中已安装 Node.js 和 npm。
- 如需在主小程序 appservice 中加载 RTM SDK，微信小程序基础库版本需为 2.13.0 或以上。
- 已完成[开通服务](/zh-CN/realtime-media/rtm/build/rtm-initialization/enable-service)，并获取项目的 App ID。
- 生产环境中已准备由业务服务器生成的 RTM Token。
- 已将 SDK 所需的 request 和 socket 域名添加到微信公众平台的服务器域名白名单。

## 1. 创建声网项目 [#1-创建声网项目]

安装 RTM SDK 前，请先在[声网控制台](https://console.shengwang.cn/)创建项目。初始化 SDK 时需要使用项目的 App ID。在生产环境中，请使用由业务服务器生成的 RTM Token。

:::info[信息]
使用不同 App ID 的客户端无法互通。需要互发消息的所有客户端必须使用相同的 App ID。
:::

## 2. 安装 RTM SDK [#2-安装-rtm-sdk]

中国大陆地区使用声网服务的项目，安装 `shengwang-rtm-miniapp`：

```bash
npm install shengwang-rtm-miniapp
```

中国大陆以外地区使用 Agora 服务的项目，安装 `agora-rtm-miniapp`：

```bash
npm install agora-rtm-miniapp
```

安装后，将已安装软件包中的 WebAssembly 资源复制到小程序项目根目录下的 `wasm/` 目录。请根据安装的软件包运行对应命令：

```bash
node ./node_modules/shengwang-rtm-miniapp/scripts/load-wasm.js
```

```bash
node ./node_modules/agora-rtm-miniapp/scripts/load-wasm.js
```

安装软件包并复制资源后，在微信开发者工具中执行**构建 npm**。

## 3. 导入 RTM SDK [#3-导入-rtm-sdk]

以下示例使用 `shengwang-rtm-miniapp`：

```javascript
import AgoraRTM from 'shengwang-rtm-miniapp';
```

## 4. 初始化 RTM 客户端实例 [#4-初始化-rtm-客户端实例]

使用 App ID 和用户 ID 创建 `RTM` 实例：

```javascript
import AgoraRTM from 'shengwang-rtm-miniapp';

const { RTM } = AgoraRTM;
const appId = 'your-app-id';
const userId = 'your-user-id';
const messageChannelName = 'chat-room';
let rtm;

try {
  rtm = new RTM(appId, userId);
} catch (error) {
  console.error('RTM 初始化失败：', error);
}
```

请将 `your-app-id` 和 `your-user-id` 替换为项目的 App ID 和当前用户的用户 ID。

## 5. 添加事件监听 [#5-添加事件监听]

登录前注册事件监听，以便小程序及时处理收到的消息和连接状态变化。

```javascript
rtm.addEventListener('message', event => {
  console.log('收到消息：', event.publisher, event.message);
});

rtm.addEventListener('presence', event => {
  console.log('Presence 事件：', event);
});

rtm.addEventListener('linkState', event => {
  console.log('连接状态变化：', event.currentState, event.reasonCode);
});
```

## 6. 登录 RTM 服务 [#6-登录-rtm-服务]

SDK 初始化完成后，登录 RTM 服务。如果在登录前调用需要连接的 RTM 操作，SDK 会返回 `RTM_ERROR_NOT_LOGIN (-10002)`。

```javascript
try {
  await rtm.login({ token: 'your-token' });
  console.log('RTM 登录成功');
} catch (error) {
  console.error('RTM 登录失败：', error);
}
```

## 7. 订阅频道 [#7-订阅频道]

发送消息前先订阅 Message Channel，以便小程序接收其他用户发布的消息：

```javascript
try {
  await rtm.subscribe(messageChannelName);
  console.log('已订阅频道：', messageChannelName);
} catch (error) {
  console.error('订阅失败：', error);
}
```

## 8. 发布消息 [#8-发布消息]

向频道发布字符串消息。对象类型的消息内容需要先序列化，再调用 `publish`。

```javascript
const message = JSON.stringify({ text: '你好，微信小程序' });

try {
  await rtm.publish(messageChannelName, message);
  console.log('消息发送成功');
} catch (error) {
  console.error('消息发送失败：', error);
}
```

## 9. 取消订阅 [#9-取消订阅]

小程序不再需要接收该频道的消息时，取消订阅：

```javascript
try {
  await rtm.unsubscribe(messageChannelName);
} catch (error) {
  console.error('取消订阅失败：', error);
}
```

## 更多信息 [#更多信息]

至此，你已经完成 RTM 小程序 SDK 的安装和 WebAssembly 资源准备，并实现了登录及 Message Channel 消息收发。

当前 [RTM JavaScript API 参考](/zh-CN/api-reference/rtm/web/configuration) 可用于理解 API 概念。微信小程序 SDK 与 Web SDK API 范围不同，使用具体 API 前，请以已安装的 `shengwang-rtm-miniapp` 包中的类型定义为准。

如有意见、问题或功能建议，请发送邮件至 rtm-support@agora.io。

## 附录：微信小程序域名白名单 [#附录微信小程序域名白名单]

微信小程序项目需要配置以下 13 个域名。其中，12 个 HTTPS 域名需要添加到微信公众平台的 **request 合法域名**，1 个 WSS 域名需要添加到 **socket 合法域名**。

### request 合法域名 [#request-合法域名]

AP 域名用于登录、边缘节点分配和配置拉取，通过 `wx.request` 访问：

```text
https://ap-web-1.agora.io;
https://ap-web-2.agora.io;
https://ap-web-3.agora.io;
https://ap-web-4.agora.io;
https://web-1.ap.sd-rtn.com;
https://web-2.ap.sd-rtn.com;
https://web-3.ap.sd-rtn.com;
https://web-4.ap.sd-rtn.com;
```

事件上报域名用于 SDK 事件上报，通过 `wx.request` 访问：

```text
https://webcollector-rtm.agora.io;
https://rtm.statscollector.sd-rtn.com;
```

日志上报域名在开启 `logUpload` 后用于 SDK 日志上报，通过 `wx.request` 访问：

```text
https://logservice-rtm.agora.io;
https://rtm.logservice.sd-rtn.com;
```

### socket 合法域名 [#socket-合法域名]

RTM 长连接通过 `wx.connectSocket` 访问：

```text
wss://miniapp.agoraio.cn;
```

SDK 不使用 `wx.uploadFile` 或 `wx.downloadFile`，因此无需为这两个接口额外配置域名白名单。
<_PlatformProcessedMarker close="true" />
</_PlatformTabsGroup>
