Skip to content

连接设备

本页说明设备端、业务服务端和客户端如何共同建立并管理一条连接。先看完三端怎样配合,再按你负责的部分完成接入。

如果还没有接入 SDK,先从概览选择设备端和客户端平台。连接成功后,再实现音视频、语音对讲、命令消息或流消息。

三端怎样完成一次连接

  1. 设备端启动 SDK 前,先设置设备密钥 device_secret_keyclient_idclient_id 是设备端启动时上报的标识;启动 SDK 时还需要传入设备标识 device_id

    c
    TiRtcSetOption(TIRTC_OPT_DEVICE_SECRET_KEY, device_secret_key, ...);
    TiRtcSetOption(TIRTC_OPT_CLIENT_ID, client_id, ...);
    TiRtcStart(device_id, ...);
  2. 业务服务端判断当前主体是否允许访问目标设备,再使用应用级凭证标识 AccessKeyId、应用级密钥 SecretKeyId 和目标设备标识 remoteId 等参数签发本次连接凭证 token

    go
    // remoteID 填目标设备的 device_id
    token, err := GenerateConnectToken(accessKeyID, secretKeyID, remoteID, ...)
  3. 客户端取得 token,用要连接的目标标识 remote_id 发起连接。

    c
    // remote_id 填目标设备的 device_id
    TiRtcConnect(remote_id, token, ...);
  4. 连接建立后,客户端和设备端分别收到成功信号。两端从这时开始使用同一条连接。

  5. 任意一端主动结束连接,或连接发生错误后,两端都停止使用这条连接并完成各自的清理。

设备端启动和 token 签发可以分别准备,没有先后依赖。签发 token 时,目标设备不需要在线;客户端实际发起连接时,设备端才需要已经启动并在线。

按你负责的部分阅读

三端可以分别开发,但需要使用同一组设备标识和授权目标。联调前先一起确认下面的连接参数。

开始前先对齐连接参数

项目用途
device_id标识是哪一台设备
device_secret_key设备级密钥;设备端启动时会用到
AppId客户端应用的唯一标识
client_id设备端启动必填的硬件或生产标识
AccessKeyId应用级凭证标识
SecretKeyId应用级密钥;服务端签发时使用
remote_id客户端这次要连接的目标标识

如果你还没有这些应用级凭证或设备身份,先看申请开通

持有 device_secret_key 即表示拥有对应 device_id 的设备端访问权限。只在设备端安全存储,不要写入客户端代码或下发给客户端。

连接设备端的场景中,客户端传入的 remote_id 就直接使用目标设备的 device_id。例如目标设备的 device_idPRODFENGXXXX,客户端 remote_id 就填 PRODFENGXXXX

设备端:启动并管理连接

如果你还没把 C SDK 放进工程,先看 C SDK 接入。后续内容从设备端工程已经接好、并且已经有 TIRTCCALLBACKS 回调集合开始。

启动并等待连接

设备端进入可连接状态需要完成这几个动作:

  1. 调用 TiRtcInit() 初始化 SDK(无需网络,可以在未连通互联网之前调用,比如配网成功前)。
  2. 调用 TiRtcSetOption(TIRTC_OPT_DEVICE_SECRET_KEY, ...) 设置 device_secret_key
  3. 调用 TiRtcSetOption(TIRTC_OPT_CLIENT_ID, ...) 设置 client_id
  4. 调用 TiRtcStart(device_id, &kCallbacks) 启动设备端(注意此步骤要求连通互联网)。
  5. 等待 on_event() 收到 TIRTC_EVENT_SYS_STARTED,表示设备端已经启动完成,可以等待客户端连接。

client_id 是设备端启动必填项。请在 TiRtcStart(device_id, ...) 前调用 TiRtcSetOption(TIRTC_OPT_CLIENT_ID, ...) 设置。

client_id 必须满足以下规则:

  • 必须设置,未设置会导致启动失败。
  • 长度必须为 1 到 64 个可打印 ASCII 字符。
  • 取值要稳定、可追溯,并能在你的生产或售后系统里定位到一台实物设备。
  • 同一个 device_id 首次启动成功后,会绑定当次上报的 client_id;后续启动同一个 device_id 时,必须继续使用这个 client_id,否则启动会失败。

可以直接使用生产序列号、MAC、ICCID、IMEI 或芯片标识;没有单一编号时,可以组合产品线、批次和序列号,例如 IPC-A1-20260709-000123

示例:

c
#include <string.h>
#include <tirtc/tiRTC.h>

static void on_conn_accepted(tirtc_conn_t hconn);
static void on_conn_error(tirtc_conn_t hconn, int error);
static void on_disconnected(tirtc_conn_t hconn);

static void on_event(int event, const void *data, int len)
{
    (void)data;
    (void)len;

    if (event == TIRTC_EVENT_SYS_STARTED) { /* SDK 已启动完成,可以等待客户端连接 */ }
}

static const TIRTCCALLBACKS kCallbacks = {
    .on_event = on_event,
    .on_conn_accepted = on_conn_accepted,
    .on_conn_error = on_conn_error,
    .on_disconnected = on_disconnected,
};

int start_device(const char *device_id, const char *device_secret_key, const char *client_id)
{
    int code = TiRtcInit();
    if (code != 0) { return code; }

    code = TiRtcSetOption(TIRTC_OPT_DEVICE_SECRET_KEY, device_secret_key, (uint32_t)strlen(device_secret_key));
    if (code != 0) { return code; }

    code = TiRtcSetOption(TIRTC_OPT_CLIENT_ID, client_id, (uint32_t)strlen(client_id));
    if (code != 0) { return code; }

    return TiRtcStart(device_id, &kCallbacks);
}

TiRtcStart() 返回 0 只表示启动请求已提交。真正启动成功以 TIRTC_EVENT_SYS_STARTED 为准。只有设备端已经启动完成后,客户端才可能真正连上来。

启动完成后,设备会等待客户端发起连接。连接建立或结束时,再按下一节处理对应回调。

接收和结束连接

客户端建立连接后,设备端会收到 on_conn_accepted(hconn)。保存这里传入的 hconn,后续收发都使用这条连接句柄。

这个回调没有返回值,C SDK 也没有单独的“拒绝连接”接口。如果业务层不接受这个客户端,设备端需要关闭这条刚建立的连接。

一条来自客户端的连接,最终有两种结束方式:

  • 设备端主动关闭: 业务层决定不保留刚建立的连接,或者不再需要与当前客户端通信。
  • 连接因错误失效: 客户端主动断开、异常退出或网络中断后,SDK 会在检测到连接关闭或超时时回调 on_conn_error

两种情况都要停止收发,把 hconn 投递到业务线程,再调用 TiRtcDisconnect(hconn)。下面的示例让它们共用同一个释放队列:

c
// 以下辅助函数由设备端应用实现。
void save_connection(tirtc_conn_t hconn);
void enqueue_disconnect_once(tirtc_conn_t hconn);
void record_connection_error(tirtc_conn_t hconn, int error);
void remove_connection(tirtc_conn_t hconn);

static void on_conn_accepted(tirtc_conn_t hconn)
{
    save_connection(hconn);
}

static void on_conn_error(tirtc_conn_t hconn, int error)
{
    record_connection_error(hconn, error);
    enqueue_disconnect_once(hconn);
}

void close_connection(tirtc_conn_t hconn)
{
    enqueue_disconnect_once(hconn);
}

int disconnect_worker(tirtc_conn_t hconn)
{
    return TiRtcDisconnect(hconn);
}

static void on_disconnected(tirtc_conn_t hconn)
{
    remove_connection(hconn);
}

业务层不接受刚建立的连接或主动关闭现有连接时,调用 close_connection(hconn)。同一个 hconn 只能提交一次 TiRtcDisconnect()disconnect_worker 必须运行在 SDK 回调之外。

on_conn_error 触发或 TiRtcDisconnect() 返回 0 后,都不要再使用这个 hconn。如果需要确认 SDK 已完成内部释放,可以等待 on_disconnected;该回调返回后,SDK 自动释放连接对象。

服务端:签发 token

开发联调时,可以在受控开发环境中运行 CLI 的 token issue 命令,为目标设备生成一次性连接 token,并直接用于当前客户端联调。 每次重新连接或连接失败后重试,都重新运行 token issue,不要重复使用之前的 token。 生产环境仍应由你的业务服务端完成登录鉴权、设备归属判断和 token 签发。

服务端签发前,应先按业务场景完成授权判断,例如确认当前主体是否允许访问目标设备。确认允许后,再签发本次连接所需的 token

  • remote_id 表示这次连接的目标标识。
  • 常见设备连接场景里,remote_id 直接使用目标设备的 device_id,例如 PRODFENGXXXX
  • 服务端签发时写入 scope 的目标,也应当和客户端随后传入的 remote_id 保持一致。

token 里至少要表达这些信息

字段说明
sub这次请求的主体标识,例如你的 user_id 或其他稳定主键
scope这次授权的目标范围;常见写法是 connect:device://<remote_id>
iss当前应用的 AccessKeyId
iat签发时间
exp过期时间
nonce每次签发都不同的随机值

公开的自定义签名算法

最终下发给客户端的字符串格式是:

text
token = "v1.<payload_b64>.<app_sig>"

计算过程如下:

text
payload_json = {
  "sub": <subject>,
  "scope": "connect:device://" + <remote_id>,
  "iss": <AccessKeyId>,
  "iat": <issued_at>,
  "exp": <expires_at>,
  "nonce": <random_nonce>
}

payload_b64 = base64url(payload_json)
app_sig = HMAC_SHA256(SecretKeyId, payload_b64)
token = "v1." + payload_b64 + "." + app_sig

这里有两个边界需要保持清楚:

  • SecretKeyIddevice_secret_key 只留在受控服务端或设备端环境里,不下发给客户端。
  • 客户端只消费最终的 token,不参与签名。

Go 示例

go
package tirtc

import (
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"time"
)

type ConnectTokenClaims struct {
	Subject   string `json:"sub"`
	Scope     string `json:"scope"`
	Issuer    string `json:"iss"`
	IssuedAt  int64  `json:"iat"`
	ExpiresAt int64  `json:"exp"`
	Nonce     string `json:"nonce"`
}

func hmacB64(key, content string) string {
	h := hmac.New(sha256.New, []byte(key))
	_, _ = h.Write([]byte(content))
	return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
}

func randomNonce() (string, error) {
	buf := make([]byte, 16)
	if _, err := rand.Read(buf); err != nil {
		return "", err
	}
	return base64.RawURLEncoding.EncodeToString(buf), nil
}

func GenerateConnectToken(accessKeyID, secretKeyID, remoteID, subject string, ttlSeconds int64) (string, error) {
	now := time.Now().Unix()
	nonce, err := randomNonce()
	if err != nil {
		return "", err
	}

	claims := ConnectTokenClaims{
		Subject:   subject,
		Scope:     "connect:device://" + remoteID,
		Issuer:    accessKeyID,
		IssuedAt:  now,
		ExpiresAt: now + ttlSeconds,
		Nonce:     nonce,
	}

	payloadJSON, err := json.Marshal(claims)
	if err != nil {
		return "", err
	}

	payloadB64 := base64.RawURLEncoding.EncodeToString(payloadJSON)
	appSig := hmacB64(secretKeyID, payloadB64)
	return "v1." + payloadB64 + "." + appSig, nil
}

客户端:发起并管理连接

发起连接

这里的 remote_id 要和服务端签发时绑定的目标一致。常见场景里,这个值就是目标设备的 device_id,例如 PRODFENGXXXX

c
// 先调用 TiRtcInit(),设置 TIRTC_OPT_APP_ID,
// 再调用 TiRtcStart(NULL, &kClientCallbacks),并等待 TIRTC_EVENT_SYS_STARTED。
static tirtc_conn_t client_hconn;

// 由客户端应用实现:记录错误,并把失效连接投递到业务线程释放。
void record_connection_error(tirtc_conn_t hconn, int error);
void enqueue_disconnect_once(tirtc_conn_t hconn);

static void on_client_event(int event, const void *data, int len)
{
    (void)data;
    (void)len;
    if (event == TIRTC_EVENT_SYS_STARTED) { /* 可以发起连接 */ }
}

// 应用层应为每次连接设置超时。如果超时后才收到成功回调,
// 应把回调返回的 hconn 投递到业务线程释放。
static void connect_cb(int error, tirtc_conn_t hconn, void *user_data)
{
    (void)user_data;
    if (error != 0) {
        /* 记录或上报 error。连接失败时 hconn 无效。 */
        return;
    }

    client_hconn = hconn;
}

static void on_conn_error(tirtc_conn_t hconn, int error)
{
    record_connection_error(hconn, error);
    if (client_hconn == hconn) {
        client_hconn = NULL;
    }
    // 这里只投递任务;业务线程随后调用 TiRtcDisconnect(hconn)。
    enqueue_disconnect_once(hconn);
}

static void on_disconnected(tirtc_conn_t hconn)
{
    if (client_hconn == hconn) {
        client_hconn = NULL;
    }
}

static const TIRTCCALLBACKS kClientCallbacks = {
    .on_event = on_client_event,
    .on_conn_error = on_conn_error,
    .on_disconnected = on_disconnected,
};

int connect_device(const char *remote_id, const char *token)
{
    // 返回 0 只表示请求已提交。
    int code = TiRtcConnect(remote_id, token, connect_cb, NULL);
    if (code != 0) {
        /* 连接请求未能提交,记录或上报 code。 */
    }
    return code;
}
kotlin
val conn = TiRtcConn()
// 先注册状态回调,再发起连接。
conn.onStateChanged = TiRtcConnStateListener { state, errorCode ->
  when (state) {
    TiRtcConnState.CONNECTED -> Log.i("TiRTC", "connected")
    TiRtcConnState.DISCONNECTED -> Log.w("TiRTC", "disconnected errorCode=$errorCode")
    else -> Log.d("TiRTC", "state=$state errorCode=$errorCode")
  }
}

conn.connect(
  remoteId = remoteId,
  token = token,
)
ts
import { TiRtcConn, TiRtcConnState } from 'tirtc/Index';

const conn = new TiRtcConn();
// 先注册状态回调,再发起连接。
conn.onStateChanged = (state: TiRtcConnState, errorCode: number): void => {
  if (state === TiRtcConnState.connected) {
    console.info('connected');
    return;
  }
  if (state === TiRtcConnState.disconnected) {
    console.warn(`disconnected errorCode=${errorCode}`);
    return;
  }
  console.info(`state=${state} errorCode=${errorCode}`);
};

conn.connect({
  remoteId,
  token,
});
swift
final class ConnectionObserver: NSObject, TiRtcConnDelegate {
    func conn(_ conn: TiRtcConn, didChangeState state: TiRtcConnState, errorCode: Int32) {
        switch state {
        case .connected:
            print("connected")
        case .disconnected:
            print("disconnected: \(errorCode)")
        default:
            print("state: \(state), errorCode: \(errorCode)")
        }
    }
}

// 创建连接对象时注册 delegate,再发起连接。
let observer = ConnectionObserver()
let conn = TiRtcConn(delegate: observer)
conn.connect(remoteId: remoteId, token: token)
dart
final TiRtcConn conn = TiRtcConn();
// 先注册状态回调,再发起连接。
conn.onStateChanged = (TiRtcConnState state, int errorCode) {
  if (state == TiRtcConnState.connected) {
    debugPrint('connected');
    return;
  }
  if (state == TiRtcConnState.disconnected) {
    debugPrint('disconnected errorCode=$errorCode');
    return;
  }
  debugPrint('state=$state errorCode=$errorCode');
};

conn.connect(remoteId: remoteId, token: token);
tsx
import {TiRtc, TiRtcConn, TiRtcConnState} from 'tirtc-react-native';

const conn = new TiRtcConn();
// 先注册状态回调,再发起连接。
conn.onStateChanged = (state, errorCode) => {
  if (state === TiRtcConnState.connected) {
    console.info('connected');
    return;
  }
  if (state === TiRtcConnState.disconnected) {
    console.warn(`disconnected ${TiRtc.formatError(errorCode)}`);
    return;
  }
  console.info(`state=${state} errorCode=${errorCode}`);
};

const code = conn.connect(remoteId, token);
if (code !== 0) {
  console.warn(`connect failed: ${TiRtc.formatError(code)}`);
}
javascript
const conn = new TiRtcConn();

try {
  await conn.connect({
    deviceId: remoteId,
    token,
  });
  // Promise resolve 表示连接已成功建立。
} catch (error) {
  console.error('connect failed:', error);
}

维护连接状态

不同客户端 SDK 使用的连接对象和状态回收方式有所不同。

常规客户端

客户端通过 TiRtcConn 的状态管理连接:

  • 连接状态为 CONNECTEDconnected 时,才能继续使用这条连接。
  • 连接失败、设备端关闭连接或客户端调用 disconnect() 后,状态会变为 DISCONNECTEDdisconnected。此时应停止基于这条连接继续操作。
  • 如果不再使用这个 TiRtcConn 对象,调用 dispose() 释放对象。disconnect() 只结束当前连接,不等于释放对象。

C SDK 客户端

C SDK 客户端主动结束连接时,调用 TiRtcDisconnect(hconn)。连接建立后如果收到 on_conn_error,把 hconn 投递到业务线程,再调用 TiRtcDisconnect()。连接尚未建立时,connect_cb 返回的 hconn 无效,不需要释放。

Web

Web 通过 connect(...) 返回的 Promise 判断连接是否建立。主动结束连接时调用 disconnect()

常规客户端和 Web 的连接对象与音视频输出对象彼此独立。调用 disconnect() 不会自动解除已经创建的音视频输出;停止播放时,还需要解除音视频输出的绑定,具体见播放来自设备端的音视频

确认连接已经建立

设备端和客户端都出现对应的成功信号,才表示这条连接可以开始承载音视频或消息:

连接建立的标志后续使用的对象
设备端 C SDK收到 on_conn_accepted(hconn)回调传入的 hconn
常规客户端连接状态变为 CONNECTEDconnectedTiRtcConn 对象
C SDK 客户端connect_cb 返回 error == 0回调返回的 hconn
Webconnect(...) 返回的 Promise resolveTiRtcConn 对象

token 签发成功只表示客户端取得了连接凭证,不表示连接已经建立。设备端收到 TIRTC_EVENT_SYS_STARTED 也只表示 SDK 已启动完成,可以等待连接。

连接成功后,可以继续实现播放音视频语音对讲收发命令消息收发流消息。如果两端没有出现对应的成功信号,按照排查接入问题中的连接阶段检查配置与日志。

TiRTC 开发文档