建立与设备端的连接
完整连接流程包括:设置 device_secret_key 和 client_id、启动设备端 SDK、等待客户端连接、由你的业务服务端签发 token、客户端发起连接,以及设备端在 on_conn_accepted(hconn) 里拿到这条连接。
先准备这几项
| 项目 | 用途 |
|---|---|
device_id | 标识是哪一台设备 |
device_secret_key | 设备级密钥;设备端启动时会用到 |
AppId | 客户端应用的唯一标识 |
client_id | 设备端启动必填的硬件或生产标识 |
AccessKeyId | 应用级凭证标识 |
SecretKeyId | 应用级密钥;服务端签发时使用 |
remote_id | 客户端这次要连接的目标标识 |
特别注意:持有device_secret_key即表示拥有对应device_id的设备端的全部访问权限,务必保密且安全地存储。
连接设备端的场景中,客户端传入的 remote_id 就直接使用目标设备的 device_id。例如目标设备的 device_id 是 PRODFENGXXXX,客户端 remote_id 就填 PRODFENGXXXX。
先看整体流程
- 设备端设置
device_secret_key和client_id,并用device_id启动 SDK。 - 你的业务服务端先判断当前主体是否允许访问目标设备,再按公开的自定义签名算法签发连接
token。 - 客户端拿到
token后,用目标remote_id发起连接。 - 设备端在
on_conn_accepted(hconn)里拿到这条连接,后续收发都围绕这条hconn展开。
设备端启动和 token 签发可以分别准备,没有先后依赖。签发 token 时,目标设备不需要在线;客户端实际发起连接时,设备端才需要已经启动并在线。
第一步:配置设备身份并启动 SDK
如果你还没把 C SDK 放进工程,先看 C SDK 接入。后续内容从设备端工程已经接好、并且已经有 TIRTCCALLBACKS 回调集合开始。
设备端进入可连接状态需要完成这几个动作:
- 调用
TiRtcInit()初始化 SDK(无需网络,可以在未连通互联网之前调用,比如配网成功前)。 - 调用
TiRtcSetOption(TIRTC_OPT_DEVICE_SECRET_KEY, ...)设置device_secret_key。 - 调用
TiRtcSetOption(TIRTC_OPT_CLIENT_ID, ...)设置client_id。 - 调用
TiRtcStart(device_id, &kCallbacks)启动设备端(注意此步骤要求连通互联网)。 - 等待
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。
示例:
#include <string.h>
#include <tirtc/tiRTC.h>
static tirtc_conn_t current_hconn;
static void on_event(int event, const void *data, int len)
{
(void)data;
(void)len;
if (event == TIRTC_EVENT_SYS_STARTED) { /* SDK 已启动完成,可以等待客户端连接 */ }
}
static void on_conn_accepted(tirtc_conn_t hconn)
{
current_hconn = hconn;
}
static const TIRTCCALLBACKS kCallbacks = {
.on_event = on_event,
.on_conn_accepted = on_conn_accepted,
};
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 为准。只有设备端已经启动完成后,客户端才可能真正连上来。
第二步:业务服务端签发 token
开发联调时,客户端开发人员可以在受控开发环境中运行 DevTools 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 | 每次签发都不同的随机值 |
公开的自定义签名算法
最终下发给客户端的字符串格式是:
token = "v1.<payload_b64>.<app_sig>"计算过程如下:
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这里有两个边界需要保持清楚:
SecretKeyId和device_secret_key只留在受控服务端或设备端环境里,不下发给客户端。- 客户端只消费最终的
token,不参与签名。
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
}第三步:客户端拿 token 发起连接
这里的 remote_id 要和服务端签发时绑定的目标一致。常见场景里,这个值就是目标设备的 device_id,例如 PRODFENGXXXX。 Android、HarmonyOS、iOS、Flutter 和 React Native 应先注册连接状态回调,再调用 connect(...)。Web 以 Promise 的 resolve/reject 表达连接结果。
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,
)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,
});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)")
}
}
}
let observer = ConnectionObserver()
let conn = TiRtcConn(delegate: observer)
conn.connect(remoteId: remoteId, token: token)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);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)}`);
}const conn = new TiRtcConn();
try {
await conn.connect({
deviceId: remoteId,
token,
});
// Promise resolve 表示连接已成功建立。
} catch (error) {
console.error('connect failed:', error);
}后续的播放、命令收发和流消息,都是基于这条连接继续展开。
第四步:设备端在 on_conn_accepted 里拿到这条连接
客户端这次连接建立后,设备端会进入 on_conn_accepted(hconn) 回调。这里拿到的 hconn,就是后续命令收发、流消息和音视频收发要继续使用的那条连接句柄。
如果你的设备端一直没有收到 on_conn_accepted(hconn),优先回头检查三件事:
- 设备端是否已经收到
TIRTC_EVENT_SYS_STARTED。 - 客户端连接时传入的
remote_id,是否和服务端签发时绑定的是同一个目标。 - 客户端使用的
token是否仍然有效。
常见对齐点
- 设备端以
TIRTC_EVENT_SYS_STARTED作为启动完成信号。 - 服务端签发时绑定的目标标识,和客户端连接时传入的
remote_id,必须指向同一个目标。 - 客户端把
token当成 opaque string 使用,不要在客户端自行拼装、解析或改写它。