Skip to content

连接设备端

这页讲的是设备端真正进入可连接状态之后的主线:设置 device_secret_key、启动设备端 SDK、等待客户端连接、由你的业务服务端签发 token、客户端发起连接,以及设备端在 on_conn_accepted(hconn) 里拿到这条连接。

先准备这几项

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

特别注意:持有device_secret_key即表示拥有对应device_id的设备端的全部访问权限,务必保密且安全地存储。

连接设备端的场景中,客户端传入的 remote_id 就直接使用目标设备的 device_id

先看整体流程

  1. 设备端设置 device_secret_key,并用 device_id 启动 SDK。
  2. 你的业务服务端先判断当前主体是否允许访问目标设备,再按公开的自定义签名算法签发连接 token
  3. 客户端拿到 token 后,用目标 remote_id 发起连接。
  4. 设备端在 on_conn_accepted(hconn) 里拿到这条连接,后续收发都围绕这条 hconn 展开。

第一步:设置 device_secret_key 并启动设备端 SDK

如果你还没把 C SDK 放进工程,先看集成到设备端。这页从设备端工程已经接好、并且已经有 TIRTCCALLBACKS 回调集合开始。

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

  1. 调用 TiRtcInit() 初始化 SDK。
  2. 调用 TiRtcSetOption(TIRTC_OPT_DEVICE_SECRET_KEY, ...) 设置 device_secret_key
  3. 调用 TiRtcStart(device_id, &kCallbacks) 启动设备端。
  4. 等待 on_event() 收到 TIRTC_EVENT_SYS_STARTED,表示设备端已经启动完成,可以等待客户端连接。

示例:

c
#include <string.h>
#include "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_and_wait_connection(const char *device_id, const char *device_secret_key)
{
    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;
    }

    return TiRtcStart(device_id, &kCallbacks);
}

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

第二步:业务服务端签发 token

服务端签发前,先做你自己的授权判断:当前主体能不能访问这个目标设备。如果允许,再签发这次连接所需的短时 token

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

token 里至少要表达这些信息

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

公开的自定义签名算法

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

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

计算过程如下:

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

payload_b64 = base64url(payload_json)
device_sig = HMAC_SHA256(device_secret_key, payload_b64)
app_sig = HMAC_SHA256(SecretKeyId, payload_b64 + "." + device_sig)
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, deviceSecretKey, subject string, ttlSeconds int64) (string, error) {
	now := time.Now().Unix()
	nonce, err := randomNonce()
	if err != nil {
		return "", err
	}

	claims := ConnectTokenClaims{
		Subject:   subject,
		Scope:     "connect:" + 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)
	deviceSig := hmacB64(deviceSecretKey, payloadB64)
	appSig := hmacB64(secretKeyID, payloadB64+"."+deviceSig)
	return "v1." + payloadB64 + "." + appSig, nil
}

第三步:客户端拿 token 发起连接

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

kotlin
val conn = TiRtcConn()
val code = conn.connect(
  remoteId = remoteId,
  token = token,
)
// `connect(...)` 返回 0 只表示建连请求已提交,真正结果以后续状态回调为准。
if (code != 0) {
  Log.e("TiRTC", "connect request failed code=$code")
}
swift
let conn = TiRtcConn(delegate: nil)
let code = conn.connect(to: remoteId, token: token)
// `connect(...)` 返回 0 只表示建连请求已提交,真正结果以后续状态回调为准。
if code != 0 {
    print("connect request failed: \(code)")
}
dart
final TiRtcConn conn = TiRtcConn();
final int code = conn.connect(remoteId: remoteId, token: token);
// `connect(...)` 返回 0 只表示建连请求已提交,真正结果以后续状态回调为准。
if (code != 0) {
  debugPrint('connect request failed: $code');
}

后续的播放、命令收发和流消息,都是基于这条连接继续展开。

第四步:设备端在 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 使用,不要在客户端自行拼装、解析或改写它。

相关文档

TiRTC 开发文档