一、导语
TUTK 提供的 P2PTunnel 服务,类似于 VPN 服务。P2PTunnel 服务启动后,将通过 TUTK 的私有协议,将上层传入的数据转发到对端,而且不需要知道对端的IP。
P2PTunnel 模块,可以内嵌至厂商的 APP 程序内,也可以独立做成一个模块。对于一些基于 TCP/IP 的标准或者私有服务,比如 HTTP、SSH、FTP、Telnet、RTSP,APP 端只需要简单几行代码,就可以完成接入,实现外网访问内网设备。二、核心工作原理
P2PTunnel Agent(APP端)的核心作用是:通过 TUTK 服务器与内网设备的 P2PTunnel Server 建立 P2P 隧道,将 APP 本地端口与设备端服务端口映射,APP 访问本地映射端口即可穿透至内网设备的目标服务,无需关注设备真实IP。
2.1 P2PTunnel 模块工作示意图(APP端)

2.2 P2PTunnel Agent 使用流程图

图 2:APP端 Agent 初始化、连线、映射、释放完整流程
三、核心开发步骤(APP端 Agent)
以下为 P2PTunnel Agent(APP端)的核心开发流程,包含初始化、P2P连线、端口映射、资源释放等关键步骤,适配 SDK 全版本,支持多设备连接场景。
3.1 Agent 初始化(必选)
步骤1、设置 SDK 许可证密钥
启动 Agent 前需先通过 TUTK 提供的许可证密钥激活 SDK,否则后续接口调用会失败。
// 设置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;
}说明:
sdk_license_key 需妥善保管,避免泄露;APP 启动时仅需调用一次。步骤2、初始化 P2PTunnel Agent 模块
根据 SDK 版本选择对应的初始化接口,新版本支持局域网直连模式(提升传输速度,需设备端配合开启)。
// 定义最大支持的设备连接数(自定义,如8台设备)
#define MAX_SERVER_CONNECT_NUM 8
// 初始化P2PTunnel模块(根据SDK版本选择接口)
#if _USE_SDK_VERSION_BELOW_4_3_5_0_
// 旧版本初始化接口(SDK版本 < 4.3.5.0)
// 参数:最大支持的设备连接数
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)
// 参数1:最大支持的设备连接数
// 参数2:1=开启局域网直连(提升速度,局域网内数据不加密);0=关闭
// 注意:设备端需同样设置为1才能启用直连模式,否则不生效
ret = P2PTunnelAgentInitialize2(MAX_SERVER_CONNECT_NUM, 1);
if (ret != TUTK_ER_NoERROR) {
printf("P2PTunnelAgentInitialize2() error[%d]\n", ret);
return -1;
}
#endif说明:初始化接口仅需在 APP 启动时调用一次,多次调用可能导致资源冲突。
步骤3、注册隧道状态回调函数
注册回调函数以监听 P2P 会话状态变更(如连接建立、断开、异常),便于 APP 同步UI状态。
// 注册P2P隧道状态回调函数 // 参数1:回调函数指针(需自定义实现,如TunnelStatusCB) // 参数2:自定义参数(传递给回调函数,如APP上下文信息) P2PTunnelAgent_GetStatus(TunnelStatusCB, (void*)args);
说明:
TunnelStatusCB 需按 SDK 定义的函数原型实现,示例:void TunnelStatusCB(int SID, int status, void* pArg),其中 SID 为会话ID,status 为状态码。3.2 建立 P2P 隧道连接(必选)
调用扩展连接接口
通过设备 UID 发起 P2P 连接,支持新版本 DTLS 加密协议,同时兼容旧版本设备(自动降级为旧接口)。
// 设备信息结构体(存储设备UID、认证信息等,自定义)
typedef struct {
char uid[64]; // 设备唯一标识(由设备端提供)
char username[32]; // 设备认证用户名
char password[64]; // 设备认证密码
// 其他扩展字段...
} sTunnelInfo;
// 初始化设备信息(示例:从APP配置或用户输入获取)
sTunnelInfo tunnelInfo = {
.uid = "DEVICE_UID_123456", // 目标设备UID
.username = "tutk_agent", // 预设用户名
.password = "agent_pass123" // 预设密码
};
// 尝试通过新版本扩展接口建立P2P隧道连接
int SID = P2PTunnelAgent_Connect_Ex(
tunnelInfo.uid, // 目标设备UID
TunnelClientAuthentication, // 客户端认证回调函数(填充账号密码)
(void*)&tunnelInfo // 自定义参数(传递设备信息给回调)
);
// 处理"远程不支持DTLS"错误(旧版本设备兼容逻辑)
if (SID == TUNNEL_ER_REMOTE_NOT_SUPPORT_DTLS) {
printf("远程设备为旧版本SDK,不支持DTLS,切换至旧版本连接接口\n");
// 初始化旧版本认证数据结构(按旧协议要求)
sAuthData authData = {0}; // 自动初始化缓冲区为0
int nDeviceErr = 0; // 接收设备端返回的错误码
// 填充旧版本默认认证信息(需与设备端旧版本认证逻辑匹配)
strncpy(authData.szUsername, "Tutk.com", sizeof(authData.szUsername) - 1);
strncpy(authData.szPassword, "P2P Platform", sizeof(authData.szPassword) - 1);
// 调用旧版本连接接口重试
SID = P2PTunnelAgent_Connect(
tunnelInfo.uid, // 目标设备UID
(void*)&authData, // 旧版本认证数据
sizeof(sAuthData), // 认证数据长度
&nDeviceErr // 输出设备端错误码(便于排查)
);
// 打印旧版本连接结果
printf("旧版本接口连接结果 - UID: %s, 会话ID(SID): %d, 设备错误码: %d\n",
tunnelInfo.uid, SID, nDeviceErr);
}
// 处理最终连接结果
if (SID < 0) {
printf("P2P连接失败,错误码: %d(参考SDK文档错误码说明)\n", SID);
return -1;
} else {
printf("P2P连接成功,会话ID(SID): %d(后续操作需使用该SID)\n", SID);
}说明:连接成功后返回的
SID 为会话唯一标识,后续端口映射、断开连接等操作需传入该 SID。实现客户端认证回调函数
回调函数用于向 SDK 填充设备认证的用户名和密码,支持按设备区分不同认证信息(核心!)。
/**
* P2P隧道客户端认证回调函数
* @brief 向SDK填充设备认证的用户名和密码,支持多设备差异化认证
* @param cszAccount 输出参数:用户名缓冲区(SDK读取该缓冲区进行认证)
* @param nAccountMaxLength cszAccount缓冲区最大长度(含字符串终止符'\0')
* @param cszPassword 输出参数:密码缓冲区(SDK读取该缓冲区进行认证)
* @param nPasswordMaxLength cszPassword缓冲区最大长度(含字符串终止符'\0')
* @param pArg 自定义参数:传递的sTunnelInfo结构体指针(区分不同设备)
*/
// 全局默认认证信息(适用于无自定义信息的设备)
#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
) {
// 将自定义参数转换为设备信息结构体(区分不同设备)
sTunnelInfo *deviceInfo = (sTunnelInfo *)pArg;
// 校验参数有效性
if (cszAccount == NULL || cszPassword == NULL || nAccountMaxLength == 0 || nPasswordMaxLength == 0) {
printf("[%s] 无效参数:缓冲区为空或长度为0\n", __func__);
return;
}
// 按设备填充认证信息(核心差异化逻辑)
if (deviceInfo != NULL && strlen(deviceInfo->uid) > 0) {
// 有自定义设备信息:使用设备专属的用户名密码
printf("[%s] 设备认证 - UID: %s, 用户名: %s\n",
__func__, deviceInfo->uid, deviceInfo->username);
// 安全填充用户名(避免缓冲区溢出)
strncpy(cszAccount, deviceInfo->username, nAccountMaxLength - 1);
cszAccount[nAccountMaxLength - 1] = '\0';
// 安全填充密码(避免缓冲区溢出)
strncpy(cszPassword, deviceInfo->password, nPasswordMaxLength - 1);
cszPassword[nPasswordMaxLength - 1] = '\0';
} else {
// 无自定义设备信息:使用全局默认认证信息
printf("[%s] 无设备自定义信息,使用默认认证\n", __func__);
strncpy(cszAccount, DEFAULT_TUNNEL_USERNAME, nAccountMaxLength - 1);
cszAccount[nAccountMaxLength - 1] = '\0';
strncpy(cszPassword, DEFAULT_TUNNEL_PASSWORD, nPasswordMaxLength - 1);
cszPassword[nPasswordMaxLength - 1] = '\0';
}
}关键说明:通过
pArg 传递设备专属信息,实现多设备差异化认证(不同设备可使用不同账号密码)。3.3 端口映射(核心步骤)
步骤1、建立本地端口与设备端口的映射
将 APP 本地端口与设备端服务端口绑定,APP 访问本地端口即可穿透至设备端对应服务(如本地10001→设备22端口(SSH))。
/**
* 建立P2P隧道端口映射
* @param SID 已建立的P2P会话ID(连接成功返回的SID)
* @param local_port APP本地端口(自定义,需未被占用)
* @param remote_port 设备端服务端口(如SSH=22、HTTP=80、RTSP=554)
* @return 映射索引(>=0成功,<0失败)
*/
// 示例:映射本地10001端口 → 设备22端口(SSH服务)
int local_port = 10001;
int remote_port = 22;
int mapIndex = P2PTunnelAgent_PortMapping(SID, local_port, remote_port);
// 处理映射结果
if (mapIndex < 0) {
// 映射失败:输出详细信息(常见原因:本地端口被占用、SID无效)
printf("端口映射失败 - SID: %d, 本地端口: %d → 设备端口: %d, 错误码: %d\n",
SID, local_port, remote_port, mapIndex);
// 解决方案:本地端口被占用时,更换本地端口(如10002、10003)
local_port = 10002;
mapIndex = P2PTunnelAgent_PortMapping(SID, local_port, remote_port);
if (mapIndex >= 0) {
printf("更换本地端口后映射成功 - 本地端口: %d → 设备端口: %d, 映射索引: %d\n",
local_port, remote_port, mapIndex);
}
} else {
// 映射成功:记录映射索引(后续解除映射需使用)
printf("端口映射成功 - SID: %d, 本地端口: %d → 设备端口: %d, 映射索引: %d\n",
SID, local_port, remote_port, mapIndex);
// 提示:APP可通过访问 127.0.0.1:%d 访问设备服务(如127.0.0.1:10001)
printf("访问方式:127.0.0.1:%d(等同于访问设备端 %d 端口)\n", local_port, remote_port);
}重要提醒
端口映射成功后,需妥善保存 mapIndex(映射索引),后续解除映射必须使用该索引,否则会导致本地端口长期占用。
步骤2、多设备端口映射示例
若 APP 需连接多个设备,通过不同本地端口区分,实现同时访问多个设备的同一服务端口。
// 多设备端口映射示例(3台设备,均访问80端口(HTTP服务))
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}; // 存储3台设备的SID
int deviceMapIndexes[3] = {0}; // 存储3台设备的映射索引
int localPorts[3] = {10001, 10002, 10003}; // 3个不同本地端口
int remotePort = 80; // 设备端HTTP服务端口
// 循环建立多设备连接和映射
for (int i = 0; i < 3; i++) {
// 建立P2P连接(复用之前的连接逻辑)
deviceSIDs[i] = P2PTunnelAgent_Connect_Ex(
multiDeviceInfo[i].uid,
TunnelClientAuthentication,
(void*)&multiDeviceInfo[i]
);
if (deviceSIDs[i] < 0) {
printf("设备%d(UID:%s)连接失败\n", i+1, multiDeviceInfo[i].uid);
continue;
}
// 建立端口映射
deviceMapIndexes[i] = P2PTunnelAgent_PortMapping(deviceSIDs[i], localPorts[i], remotePort);
if (deviceMapIndexes[i] >= 0) {
printf("设备%d映射成功 - 访问 127.0.0.1:%d → 设备%d的80端口\n",
i+1, localPorts[i], i+1);
}
}说明:多设备场景下,通过“不同本地端口+不同SID+不同映射索引”实现区分,APP 访问对应本地端口即可定位到目标设备。
3.4 解除端口映射(必选,避免端口占用)
单个映射解除
不需要访问设备服务时,需及时解除端口映射,释放本地端口资源。
// 通过映射索引解除单个端口映射(mapIndex为映射成功时返回的索引)
P2PTunnelAgent_StopPortMapping(mapIndex);
printf("已解除映射索引%d对应的端口映射\n", mapIndex);多个映射批量解除
多设备场景下,可通过索引数组批量解除多个端口映射,简化操作。
// 批量解除多个端口映射(示例:解除3个设备的映射)
int mapIndexArray[3] = {deviceMapIndexes[0], deviceMapIndexes[1], deviceMapIndexes[2]};
// 调用批量解除接口(参数:映射索引数组,数组长度)
P2PTunnelAgent_StopPortMapping_byIndexArray(mapIndexArray, 3);
printf("已批量解除3个端口映射\n");注意
断开设备连接前,必须先解除所有端口映射,否则会导致本地端口无法释放,后续无法重复使用该端口。
3.5 断开 P2P 连接(必选)
方式1、优雅断开(推荐)
等待缓冲区数据发送完成后再断开连接,避免数据丢失(适用于文件传输、数据同步等场景)。
// 优雅断开连接(参数:会话ID SID)
P2PTunnelAgent_Disconnect(SID);
printf("已优雅断开SID=%d的P2P连接(等待数据发送完成)\n", SID);方式2、强制断开
不等待数据发送,直接断开连接(适用于紧急退出、网络异常等场景)。
// 强制断开连接(参数:会话ID SID)
P2PTunnelAgent_Abort(SID);
printf("已强制断开SID=%d的P2P连接(不等待数据发送)\n", SID);说明:两种方式任选其一,建议优先使用优雅断开(
P2PTunnelAgent_Disconnect),确保数据完整性。3.6 Agent 反初始化(必选)
步骤1、释放 Agent 资源
APP 退出时,调用反初始化接口释放 P2PTunnel Agent 模块的所有资源,避免内存泄漏。
// Agent 反初始化(APP退出时仅需调用一次)
P2PTunnelAgentDeInitialize();
printf("P2PTunnel Agent 反初始化完成\n");说明:反初始化前需确保所有端口映射已解除、所有 P2P 连接已断开,否则可能导致资源释放不彻底。
四、FAQ(常见问题)
如果本地端口被占用了,该如何处理?
换一个未被占用的本地端口即可。建议 APP 内置端口检测逻辑(如尝试10001→10002→10003...),自动选择可用端口。
如果 Agent 需要连接多个设备,该如何区分不同的设备?
通过不同的本地端口区分。例如:3台设备均提供 HTTP 服务(80端口),APP 可使用 10001、10002、10003 分别映射这3台设备的80端口,访问对应本地端口即可定位到目标设备。同时,通过 SID(会话ID)和映射索引分别管理不同设备的连接和映射。
P2PTunnelAPIs 可以使用 IOTCAPIs 的 API 吗?
可以,调用 IOTCAPIs 接口时,传入 P2P 会话的 SID(连接成功返回的会话ID)即可,数据会通过 P2P 隧道传输。
P2PTunnelAPIs 可以与其他模块(AV、RDT)一起使用吗?
可以,但集成复杂度较高,容易出现资源冲突或兼容性问题,通常不建议混合使用。若需同时实现音视频传输和隧道功能,建议优先使用 P2PTunnel 承载音视频 TCP 流。
五、相关资源
以下为 TUTK P2P SDK 相关开发文档,便于扩展学习和实操:
