iOS IMLib 快速开始

本页用 5 个步骤完成 iOS 端 IMLib 接入:控制台取 AppKey / Token、导入 SDK、初始化、建立连接、收发消息与会话。示例方法名与参数来自冻结的 sdk-bindings.json。

  1. 控制台获取 AppKey 与 Token

    在控制台创建应用并获取 AppKey;用户 Token 由业务服务先注册用户(user.register)、再调用 user.token.issue 签发后下发到客户端(两步都需要服务端签名)。

    客户端只持有 Token,不得内置 AppSecret;Token 泄露时可调用服务端接口吊销。

  2. 导入 SDK

    按「导入 SDK」安装依赖;Swift Package 名 ImpIaaSIM(IMLib)与 ImpIaaSIMKit(IMKit);Objective-C 通过 #import <ImpIaaSIM/ImpIaaSIM.h> 使用同一套实现。

  3. 初始化客户端

    使用 AppKey(与服务端地址)初始化;初始化幂等:同 appkey 重复调用返回同一实例,不建立连接。

  4. 建立连接并订阅事件

    使用 Token 调用 connect,成功后触发 CONNECTED;离线消息在 hello 后自动补发。

  5. 收发消息与会话

    注册对端用户后发送第一条文本消息、监听 MESSAGES 事件并拉取会话列表。

1. 控制台获取 AppKey 与 Token​

凭据获取方式使用位置
AppKey控制台 → 应用 → 基本信息初始化客户端(options.appkey)
用户 Token业务服务注册用户(user.register)后调用 user.token.issue 签发(需签名)connect(token)
AppSecret控制台 → 应用 → 密钥(仅服务端保存)服务端签发 Token / 调用 Open API
禁止硬编码

AppSecret 只能保存在业务服务端;客户端产物中不得出现 AppSecret。示例中的 TOKEN 表示服务端签发的用户 Token。

本地服务端地址

本页及后续示例统一使用 https://im.example.com(服务端缺省端口)。

示例使用占位地址 https://im.example.com;本地联调请替换为你实际启动的服务端地址与端口(客户端与服务端必须指向同一地址)。

本地从零跑通:创建应用 → 注册用户 → 签发 Token(服务端签名)​

Token 必须由业务服务用 AppSecret 签名后调用 Open API 签发;下面脚本可直接复制(Node.js 18+),第 1 步创建应用,第 2 步注册两个用户并签发各自的 Token。

JavaScript
// 0) 启动本地服务端(另开终端):
//    IM_ADDR=127.0.0.1:18081 IM_DATA_DIR=/tmp/im-data IM_ADMIN_SECRET=dev-admin //      ./target/debug/im-server
import { createHash, randomBytes } from 'node:crypto';

const BASE = 'http://127.0.0.1:18081';   // 本地联调;生产替换为你的域名
const APP_KEY = 'demo';
const APP_SECRET = process.env.IM_APP_SECRET; // 上一步创建应用返回;仅服务端持有

// 1) 创建应用(已有应用可跳过)
const app = await fetch(`${BASE}/api/admin/apps`, {
  method: 'POST',
  headers: { 'X-Admin-Secret': 'dev-admin', 'content-type': 'application/json' },
  body: JSON.stringify({ appkey: APP_KEY, name: 'Demo' }),
}).then((r) => r.json());
console.log('app =', app);

// 2) 注册用户 + 签发 Token(签名头:sha1(appSecret + nonce + timestamp))
function signedHeaders() {
  const nonce = randomBytes(16).toString('hex');
  const timestamp = Date.now();
  const signature = createHash('sha1').update(`${APP_SECRET}${nonce}${timestamp}`).digest('hex');
  return {
    'App-Key': APP_KEY, Nonce: nonce, Timestamp: String(timestamp), Signature: signature,
    'content-type': 'application/json',
  };
}
async function call(path, body) {
  const res = await fetch(`${BASE}/im/v2${path}`, { method: 'POST', headers: signedHeaders(), body: JSON.stringify(body) });
  return res.json();
}
for (const [userId, name] of [['ios_alice', 'Alice'], ['ios_bob', 'Bob']]) {
  console.log(await call('/user/register', { userId, name }));       // user.register
  console.log(await call('/user/token/issue', { userId }));          // user.token.issue → data.token
}
禁止使用未签名入口换 Token

不要调用 /api/apps/{appkey}/users 这类未签名接口换 Token:它只在 IM_ALLOW_ANON_WRITE=true(默认仅 loopback 的开发模式)时开放,属于开发期便利入口,生产环境必须关闭。

客户端只接受业务服务下发的 Token,不得内置 AppSecret;匿名入口的 Token 不受签名审计保护,泄露后无法追溯调用方。

两个用户要分别用各自的 Token 连接(两个进程 / 两台模拟器);data.token 即 connect(token:) 的参数。

2. 导入 SDK​

CocoaPods
# CocoaPods(Podfile)
platform :ios, '13.0'
target 'YourApp' do
  pod 'ImpIaaSIM', '~> 1.0'      # IMLib:无 UI 通信库
  pod 'ImpIaaSIMKit', '~> 1.0'   # IMKit:可选,UI 组件
end
# 安装后打开 ImpIaaSIM.xcworkspace

SDK 未发布时用本地路径集成;下面命令可直接复制(与 集成方式 同源)。

本仓库本地集成
# 方式 A:SPM 本地路径(Package.swift)
.package(path: "../sdk/ios")

# 方式 B:CocoaPods 本地 podspec
pod 'ImpIaaSIM', :path => '../sdk/ios'
pod 'ImpIaaSIMKit', :path => '../sdk/ios'

# 方式 C:手工链接 framework(模拟器 x86_64)
FW="../sdk/ios/build/ImpIaaSIM.framework"
SDK=$(xcrun --sdk iphonesimulator --show-sdk-path)
xcrun swiftc -sdk "$SDK" -target x86_64-apple-ios18.0-simulator   -F ../sdk/ios/build -I "$FW/Modules"   -framework ImpIaaSIM -framework UIKit -framework Foundation   -Xlinker -rpath -Xlinker @executable_path/Frameworks   -o MyApp main.swift
# 注意:只给 -F 时 Swift 编译器看不到 Swift 声明,必须同时加 -I "$FW/Modules"

组装可安装的 .app​

simctl 安装所需的最小 .app
# 把可执行文件组装成可安装的 .app(模拟器)
APP=MyApp.app
mkdir -p "$APP/Frameworks"
cp MyApp "$APP/MyApp"
cp Info.plist "$APP/Info.plist"          # 至少含 CFBundleIdentifier / CFBundleExecutable / MinimumOSVersion
cp -R "$FW" "$APP/Frameworks/ImpIaaSIM.framework"   # 等价于 Xcode 的 Embed & Sign
codesign --force --deep --sign - "$APP"
xcrun simctl install booted "$APP"
xcrun simctl launch --console booted com.example.MyApp
Embed & Sign

手工组包时把 ImpIaaSIM.framework 放入 .app/Frameworks 即等价 Embed;链接参数里的 -rpath @executable_path/Frameworks 负责运行时查找,缺一不可。

3. 初始化​

import ImpIaaSIM
let options = IMPClientOptions(appkey: "YOUR_APP_KEY", serverUrl: "https://im.example.com")
let client = ImpIaaSIM.client(options: options)
let status = client.getConnectionStatus()
print(status)
#import <ImpIaaSIM/ImpIaaSIM.h>

// 工厂:initWithAppkey:serverUrl:(应用内单例见 ImpIaaSIM.client)
IMPOSClient *client = [[IMPOSClient alloc] initWithAppkey:@"YOUR_APP_KEY"
                                                serverUrl:@"https://im.example.com"];

4. 建立连接​

let res = await client.connect(token: "TOKEN")
if res.code != 0 { print(res.msg ?? "") }
[client connectWithToken:TOKEN completion:^(IMPOSResult *result) {
    if (result.code != 0) {
        NSLog(@"connect failed: %ld %@", (long)result.code, result.msg);
        return;
    }
    NSLog(@"connected as %@", result.data.userId);
}];

5. 收发消息与会话​

收发验证需要两个已注册用户:先注册对端(例如 user2)并签发它的 Token,再用对端账号建连接收。示例中的 user2 是“已存在的另一个用户”,不是任意字符串。

let res = await client.sendTextMessage(conversationType: .privateChat, targetId: "user2", text: "你好,NI PaaS", options: nil)
if res.code != 0 { print(res.msg ?? "") }
[client sendTextMessageWithConversationType:1 /* 单聊 */
                                targetId:@"user2"
                                    text:@"你好,NI PaaS"
                                 options:nil
                              completion:^(IMPOSResult *result) {
    if (result.code != 0) { NSLog(@"send failed: %@", result.msg); return; }
    [self appendMessage:result.data];
}];

[client getConversationListWithCompletion:^(IMPOSResult *result) {
    if (result.code == 0) { self.conversations = result.data; [self.tableView reloadData]; }
}];

监听消息(Swift)​

收消息需要注册监听器;回调在 SDK 事件线程投递,UI 更新请切回主线程。

Swift(IMPEventListener)
final class MessageEventListener: IMPEventListener {
    func onEvent(_ payload: IMPEventPayload) {
        if case .messages(let messages) = payload {
            for message in messages {
                print("收到消息:\(message.messageId) 会话:\(message.conversationType)")
            }
        }
    }
}

let listener = MessageEventListener()
client.addEventListener(.messages, listener: listener)
// 退出页面时移除,避免重复回调
client.removeEventListener(.messages, listener: listener)
发送给未注册用户会断开连接

单聊发送给不存在的用户时,服务端返回 1004(用户不存在),当前连接可能因此被断开,随后发送调用返回 3001(连接已中断)。

这不是网络故障:注册对端用户(user.register)并重连后即可正常发送。排查时先确认两个 userId 都已注册且属于同一个 appkey。

响应不是 JSON / 404?

如果脚本报 Unexpected token '<' 或看到 HTML,通常是服务端地址或端口不对(示例使用占位地址;本地联调要替换为实际端口)。

先用浏览器或 curl 访问服务端根路径确认可达,再检查 serverUrl 是否与启动参数一致。

双端验证:两个进程 / 两个浏览器 profile

同一进程内对同一个 appkey 初始化只会得到一个客户端实例(初始化幂等):把两个账号塞进同一个实例会互相覆盖登录态,典型报错是 “cannot send to self”。

实测:同一进程内建两个客户端实例(不同 appkey 或先 destroy 再 create)可以互发;同 origin 的两个浏览器标签共享 deviceId,第二个客户端可能收不到投递。

因此本机验证“收发”请开两个进程,或开两个浏览器 profile(各自独立 localStorage/deviceId),每个进程/Profile 只登录一个用户。

验收证据(截图 / 录屏)​

Shell
# 模拟器安装并启动(把 <Demo.app> / <bundle-id> 换成你的产物)
xcrun simctl boot "iPhone 16 Pro" || true
xcrun simctl install booted <Demo.app>
xcrun simctl launch --console booted <bundle-id>

# 截图 + 录屏(真实运行)
xcrun simctl io booted screenshot ios-screen.png
xcrun simctl io booted recordVideo --codec h264 ios-run.mp4

下一步​