二、核心開發(fā)步驟(APP端 Agent)
以下為 P2PTunnel Agent(APP端)的核心開發(fā)流程,包含初始化、P2P連線、端口映射、資源釋放等關(guān)鍵步驟,適配 SDK 全版本,支持多設(shè)備連接場(chǎng)景。
(一)Agent 初始化(必選)
1. 設(shè)置 SDK 許可證密鑰
啟動(dòng) Agent 前需先通過(guò) TUTK 提供的許可證密鑰激活 SDK,否則后續(xù)接口調(diào)用會(huì)失敗。
// 設(shè)置SDK許可證密鑰(由 TUTK 官方提供)
int ret = TUTK_SDK_Set_License_Key(sdk_license_key);
if (ret != TUTK_ER_NoERROR) {
printf("TUTK_SDK_Set_License_Key() error[%d]\n", ret);
return -1;
}
說(shuō)明:sdk_license_key 需妥善保管,避免泄露;APP 啟動(dòng)時(shí)僅需調(diào)用一次。
2. 初始化 P2PTunnel Agent 模塊
根據(jù) SDK 版本選擇對(duì)應(yīng)的初始化接口,新版本支持局域網(wǎng)直連模式(提升傳輸速度,需設(shè)備端配合開啟)。
// 定義最大支持的設(shè)備連接數(shù)(自定義,如8臺(tái)設(shè)備)
#define MAX_SERVER_CONNECT_NUM 8
// 初始化P2PTunnel模塊(根據(jù)SDK版本選擇接口)
#if _USE_SDK_VERSION_BELOW_4_3_5_0_
// 舊版本初始化接口(SDK版本 < 4.3.5.0)
// 參數(shù):最大支持的設(shè)備連接數(shù)
ret = P2PTunnelAgentInitialize(MAX_SERVER_CONNECT_NUM);
if (ret != TUTK_ER_NoERROR) {
printf("P2PTunnelAgentInitialize() error[%d]\n", ret);
return -1;
}
#else
// 新版本初始化接口(SDK版本 >= 4.3.5.0)
// 參數(shù)1:最大支持的設(shè)備連接數(shù)
// 參數(shù)2:1=開啟局域網(wǎng)直連(提升速度,局域網(wǎng)內(nèi)數(shù)據(jù)不加密);0=關(guān)閉
// 注意:設(shè)備端需同樣設(shè)置為1才能啟用直連模式,否則不生效
ret = P2PTunnelAgentInitialize2(MAX_SERVER_CONNECT_NUM, 1);
if (ret != TUTK_ER_NoERROR) {
printf("P2PTunnelAgentInitialize2() error[%d]\n", ret);
return -1;
}
#endif
說(shuō)明:初始化接口僅需在 APP 啟動(dòng)時(shí)調(diào)用一次,多次調(diào)用可能導(dǎo)致資源沖突。
3. 注冊(cè)隧道狀態(tài)回調(diào)函數(shù)
注冊(cè)回調(diào)函數(shù)以監(jiān)聽 P2P 會(huì)話狀態(tài)變更(如連接建立、斷開、異常),便于 APP 同步UI狀態(tài)。
// 注冊(cè)P2P隧道狀態(tài)回調(diào)函數(shù)
// 參數(shù)1:回調(diào)函數(shù)指針(需自定義實(shí)現(xiàn),如TunnelStatusCB)
// 參數(shù)2:自定義參數(shù)(傳遞給回調(diào)函數(shù),如APP上下文信息)
P2PTunnelAgent_GetStatus(TunnelStatusCB, (void*)args);
說(shuō)明:TunnelStatusCB 需按 SDK 定義的函數(shù)原型實(shí)現(xiàn),示例:void TunnelStatusCB(int SID, int status, void* pArg),其中 SID 為會(huì)話ID,status 為狀態(tài)碼。
(二)建立 P2P 隧道連接(必選)
1. 調(diào)用擴(kuò)展連接接口
通過(guò)設(shè)備 UID 發(fā)起 P2P 連接,支持新版本 DTLS 加密協(xié)議,同時(shí)兼容舊版本設(shè)備(自動(dòng)降級(jí)為舊接口)。
// 設(shè)備信息結(jié)構(gòu)體(存儲(chǔ)設(shè)備UID、認(rèn)證信息等,自定義)
typedef struct {
char uid[64]; // 設(shè)備唯一標(biāo)識(shí)(由設(shè)備端提供)
char username[32]; // 設(shè)備認(rèn)證用戶名
char password[64]; // 設(shè)備認(rèn)證密碼
// 其他擴(kuò)展字段...
} sTunnelInfo;
// 初始化設(shè)備信息(示例:從APP配置或用戶輸入獲?。?
sTunnelInfo tunnelInfo = {
.uid = "DEVICE_UID_123456", // 目標(biāo)設(shè)備UID
.username = "tutk_agent", // 預(yù)設(shè)用戶名
.password = "agent_pass123" // 預(yù)設(shè)密碼
};
// 嘗試通過(guò)新版本擴(kuò)展接口建立P2P隧道連接
int SID = P2PTunnelAgent_Connect_Ex(
tunnelInfo.uid, // 目標(biāo)設(shè)備UID
TunnelClientAuthentication, // 客戶端認(rèn)證回調(diào)函數(shù)(填充賬號(hào)密碼)
(void*)&tunnelInfo // 自定義參數(shù)(傳遞設(shè)備信息給回調(diào))
);
// 處理"遠(yuǎn)程不支持DTLS"錯(cuò)誤(舊版本設(shè)備兼容邏輯)
if (SID == TUNNEL_ER_REMOTE_NOT_SUPPORT_DTLS) {
printf("遠(yuǎn)程設(shè)備為舊版本SDK,不支持DTLS,切換至舊版本連接接口\n");
// 初始化舊版本認(rèn)證數(shù)據(jù)結(jié)構(gòu)(按舊協(xié)議要求)
sAuthData authData = {0}; // 自動(dòng)初始化緩沖區(qū)為0
int nDeviceErr = 0; // 接收設(shè)備端返回的錯(cuò)誤碼
// 填充舊版本默認(rèn)認(rèn)證信息(需與設(shè)備端舊版本認(rèn)證邏輯匹配)
strncpy(authData.szUsername, "Tutk.com", sizeof(authData.szUsername) - 1);
strncpy(authData.szPassword, "P2P Platform", sizeof(authData.szPassword) - 1);
// 調(diào)用舊版本連接接口重試
SID = P2PTunnelAgent_Connect(
tunnelInfo.uid, // 目標(biāo)設(shè)備UID
(void*)&authData, // 舊版本認(rèn)證數(shù)據(jù)
sizeof(sAuthData), // 認(rèn)證數(shù)據(jù)長(zhǎng)度
&nDeviceErr // 輸出設(shè)備端錯(cuò)誤碼(便于排查)
);
// 打印舊版本連接結(jié)果
printf("舊版本接口連接結(jié)果 - UID: %s, 會(huì)話ID(SID): %d, 設(shè)備錯(cuò)誤碼: %d\n",
tunnelInfo.uid, SID, nDeviceErr);
}
// 處理最終連接結(jié)果
if (SID < 0) {
printf("P2P連接失敗,錯(cuò)誤碼: %d(參考SDK文檔錯(cuò)誤碼說(shuō)明)\n", SID);
return -1;
} else {
printf("P2P連接成功,會(huì)話ID(SID): %d(后續(xù)操作需使用該SID)\n", SID);
}
說(shuō)明:連接成功后返回的 SID 為會(huì)話唯一標(biāo)識(shí),后續(xù)端口映射、斷開連接等操作需傳入該 SID。
2. 實(shí)現(xiàn)客戶端認(rèn)證回調(diào)函數(shù)
回調(diào)函數(shù)用于向 SDK 填充設(shè)備認(rèn)證的用戶名和密碼,支持按設(shè)備區(qū)分不同認(rèn)證信息(核心?。?/p>
/**
* P2P隧道客戶端認(rèn)證回調(diào)函數(shù)
* @brief 向SDK填充設(shè)備認(rèn)證的用戶名和密碼,支持多設(shè)備差異化認(rèn)證
* @param cszAccount 輸出參數(shù):用戶名緩沖區(qū)(SDK讀取該緩沖區(qū)進(jìn)行認(rèn)證)
* @param nAccountMaxLength cszAccount緩沖區(qū)最大長(zhǎng)度(含字符串終止符'\0')
* @param cszPassword 輸出參數(shù):密碼緩沖區(qū)(SDK讀取該緩沖區(qū)進(jìn)行認(rèn)證)
* @param nPasswordMaxLength cszPassword緩沖區(qū)最大長(zhǎng)度(含字符串終止符'\0')
* @param pArg 自定義參數(shù):傳遞的sTunnelInfo結(jié)構(gòu)體指針(區(qū)分不同設(shè)備)
*/
// 全局默認(rèn)認(rèn)證信息(適用于無(wú)自定義信息的設(shè)備)
#define DEFAULT_TUNNEL_USERNAME "default_user"
#define DEFAULT_TUNNEL_PASSWORD "default_pass"
void TunnelClientAuthentication(
char *cszAccount,
uint32_t nAccountMaxLength,
char *cszPassword,
uint32_t nPasswordMaxLength,
const void *pArg
) {
// 將自定義參數(shù)轉(zhuǎn)換為設(shè)備信息結(jié)構(gòu)體(區(qū)分不同設(shè)備)
sTunnelInfo *deviceInfo = (sTunnelInfo *)pArg;
// 校驗(yàn)參數(shù)有效性
if (cszAccount == NULL || cszPassword == NULL || nAccountMaxLength == 0 || nPasswordMaxLength == 0) {
printf("[%s] 無(wú)效參數(shù):緩沖區(qū)為空或長(zhǎng)度為0\n", __func__);
return;
}
// 按設(shè)備填充認(rèn)證信息(核心差異化邏輯)
if (deviceInfo != NULL && strlen(deviceInfo->uid) > 0) {
// 有自定義設(shè)備信息:使用設(shè)備專屬的用戶名密碼
printf("[%s] 設(shè)備認(rèn)證 - UID: %s, 用戶名: %s\n",
__func__, deviceInfo->uid, deviceInfo->username);
// 安全填充用戶名(避免緩沖區(qū)溢出)
strncpy(cszAccount, deviceInfo->username, nAccountMaxLength - 1);
cszAccount[nAccountMaxLength - 1] = '\0';
// 安全填充密碼(避免緩沖區(qū)溢出)
strncpy(cszPassword, deviceInfo->password, nPasswordMaxLength - 1);
cszPassword[nPasswordMaxLength - 1] = '\0';
} else {
// 無(wú)自定義設(shè)備信息:使用全局默認(rèn)認(rèn)證信息
printf("[%s] 無(wú)設(shè)備自定義信息,使用默認(rèn)認(rèn)證\n", __func__);
strncpy(cszAccount, DEFAULT_TUNNEL_USERNAME, nAccountMaxLength - 1);
cszAccount[nAccountMaxLength - 1] = '\0';
strncpy(cszPassword, DEFAULT_TUNNEL_PASSWORD, nPasswordMaxLength - 1);
cszPassword[nPasswordMaxLength - 1] = '\0';
}
}
關(guān)鍵說(shuō)明:通過(guò) pArg 傳遞設(shè)備專屬信息,實(shí)現(xiàn)多設(shè)備差異化認(rèn)證(不同設(shè)備可使用不同賬號(hào)密碼)。
(三)端口映射(核心步驟)
1. 建立本地端口與設(shè)備端口的映射
將 APP 本地端口與設(shè)備端服務(wù)端口綁定,APP 訪問(wèn)本地端口即可穿透至設(shè)備端對(duì)應(yīng)服務(wù)(如本地10001→設(shè)備22端口(SSH))。
/**
* 建立P2P隧道端口映射
* @param SID 已建立的P2P會(huì)話ID(連接成功返回的SID)
* @param local_port APP本地端口(自定義,需未被占用)
* @param remote_port 設(shè)備端服務(wù)端口(如SSH=22、HTTP=80、RTSP=554)
* @return 映射索引(>=0成功,<0失?。?
*/
// 示例:映射本地10001端口 → 設(shè)備22端口(SSH服務(wù))
int local_port = 10001;
int remote_port = 22;
int mapIndex = P2PTunnelAgent_PortMapping(SID, local_port, remote_port);
// 處理映射結(jié)果
if (mapIndex < 0) {
// 映射失?。狠敵鲈敿?xì)信息(常見原因:本地端口被占用、SID無(wú)效)
printf("端口映射失敗 - SID: %d, 本地端口: %d → 設(shè)備端口: %d, 錯(cuò)誤碼: %d\n",
SID, local_port, remote_port, mapIndex);
// 解決方案:本地端口被占用時(shí),更換本地端口(如10002、10003)
local_port = 10002;
mapIndex = P2PTunnelAgent_PortMapping(SID, local_port, remote_port);
if (mapIndex >= 0) {
printf("更換本地端口后映射成功 - 本地端口: %d → 設(shè)備端口: %d, 映射索引: %d\n",
local_port, remote_port, mapIndex);
}
} else {
// 映射成功:記錄映射索引(后續(xù)解除映射需使用)
printf("端口映射成功 - SID: %d, 本地端口: %d → 設(shè)備端口: %d, 映射索引: %d\n",
SID, local_port, remote_port, mapIndex);
// 提示:APP可通過(guò)訪問(wèn) 127.0.0.1:%d 訪問(wèn)設(shè)備服務(wù)(如127.0.0.1:10001)
printf("訪問(wèn)方式:127.0.0.1:%d(等同于訪問(wèn)設(shè)備端 %d 端口)\n", local_port, remote_port);
}
重要提醒
端口映射成功后,需妥善保存 mapIndex(映射索引),后續(xù)解除映射必須使用該索引,否則會(huì)導(dǎo)致本地端口長(zhǎng)期占用。
2. 多設(shè)備端口映射示例
若 APP 需連接多個(gè)設(shè)備,通過(guò)不同本地端口區(qū)分,實(shí)現(xiàn)同時(shí)訪問(wèn)多個(gè)設(shè)備的同一服務(wù)端口。
// 多設(shè)備端口映射示例(3臺(tái)設(shè)備,均訪問(wèn)80端口(HTTP服務(wù)))
sTunnelInfo multiDeviceInfo[3] = {
{.uid = "DEVICE_UID_001", .username = "user1", .password = "pass1"},
{.uid = "DEVICE_UID_002", .username = "user2", .password = "pass2"},
{.uid = "DEVICE_UID_003", .username = "user3", .password = "pass3"}
};
int deviceSIDs[3] = {0}; // 存儲(chǔ)3臺(tái)設(shè)備的SID
int deviceMapIndexes[3] = {0}; // 存儲(chǔ)3臺(tái)設(shè)備的映射索引
int localPorts[3] = {10001, 10002, 10003}; // 3個(gè)不同本地端口
int remotePort = 80; // 設(shè)備端HTTP服務(wù)端口
// 循環(huán)建立多設(shè)備連接和映射
for (int i = 0; i < 3; i++) {
// 建立P2P連接(復(fù)用之前的連接邏輯)
deviceSIDs[i] = P2PTunnelAgent_Connect_Ex(
multiDeviceInfo[i].uid,
TunnelClientAuthentication,
(void*)&multiDeviceInfo[i]
);
if (deviceSIDs[i] < 0) {
printf("設(shè)備%d(UID:%s)連接失敗\n", i+1, multiDeviceInfo[i].uid);
continue;
}
// 建立端口映射
deviceMapIndexes[i] = P2PTunnelAgent_PortMapping(deviceSIDs[i], localPorts[i], remotePort);
if (deviceMapIndexes[i] >= 0) {
printf("設(shè)備%d映射成功 - 訪問(wèn) 127.0.0.1:%d → 設(shè)備%d的80端口\n",
i+1, localPorts[i], i+1);
}
}
說(shuō)明:多設(shè)備場(chǎng)景下,通過(guò)“不同本地端口+不同SID+不同映射索引”實(shí)現(xiàn)區(qū)分,APP 訪問(wèn)對(duì)應(yīng)本地端口即可定位到目標(biāo)設(shè)備。
(四)解除端口映射(必選,避免端口占用)
1. 單個(gè)映射解除
不需要訪問(wèn)設(shè)備服務(wù)時(shí),需及時(shí)解除端口映射,釋放本地端口資源。
// 通過(guò)映射索引解除單個(gè)端口映射(mapIndex為映射成功時(shí)返回的索引)
P2PTunnelAgent_StopPortMapping(mapIndex);
printf("已解除映射索引%d對(duì)應(yīng)的端口映射\n", mapIndex);
2. 多個(gè)映射批量解除
多設(shè)備場(chǎng)景下,可通過(guò)索引數(shù)組批量解除多個(gè)端口映射,簡(jiǎn)化操作。
// 批量解除多個(gè)端口映射(示例:解除3個(gè)設(shè)備的映射)
int mapIndexArray[3] = {deviceMapIndexes[0], deviceMapIndexes[1], deviceMapIndexes[2]};
// 調(diào)用批量解除接口(參數(shù):映射索引數(shù)組,數(shù)組長(zhǎng)度)
P2PTunnelAgent_StopPortMapping_byIndexArray(mapIndexArray, 3);
printf("已批量解除3個(gè)端口映射\n");
注意
斷開設(shè)備連接前,必須先解除所有端口映射,否則會(huì)導(dǎo)致本地端口無(wú)法釋放,后續(xù)無(wú)法重復(fù)使用該端口。
(五)斷開 P2P 連接(必選)
1. 優(yōu)雅斷開(推薦)
等待緩沖區(qū)數(shù)據(jù)發(fā)送完成后再斷開連接,避免數(shù)據(jù)丟失(適用于文件傳輸、數(shù)據(jù)同步等場(chǎng)景)。
// 優(yōu)雅斷開連接(參數(shù):會(huì)話ID SID)
P2PTunnelAgent_Disconnect(SID);
printf("已優(yōu)雅斷開SID=%d的P2P連接(等待數(shù)據(jù)發(fā)送完成)\n", SID);
2. 強(qiáng)制斷開
不等待數(shù)據(jù)發(fā)送,直接斷開連接(適用于緊急退出、網(wǎng)絡(luò)異常等場(chǎng)景)。
// 強(qiáng)制斷開連接(參數(shù):會(huì)話ID SID)
P2PTunnelAgent_Abort(SID);
printf("已強(qiáng)制斷開SID=%d的P2P連接(不等待數(shù)據(jù)發(fā)送)\n", SID);
說(shuō)明:兩種方式任選其一,建議優(yōu)先使用優(yōu)雅斷開(P2PTunnelAgent_Disconnect),確保數(shù)據(jù)完整性。
(六)Agent 反初始化(必選)
1. 釋放 Agent 資源
APP 退出時(shí),調(diào)用反初始化接口釋放 P2PTunnel Agent 模塊的所有資源,避免內(nèi)存泄漏。
// Agent 反初始化(APP退出時(shí)僅需調(diào)用一次)
P2PTunnelAgentDeInitialize();
printf("P2PTunnel Agent 反初始化完成\n");
說(shuō)明:反初始化前需確保所有端口映射已解除、所有 P2P 連接已斷開,否則可能導(dǎo)致資源釋放不徹底。