一、模块概述
RDT(Reliable Data Transfer)是 TUTK P2P SDK 中的可靠传输模块,提供通用的 RDT_Read 和 RDT_Write 数据传输接口,适用于需要稳定传输数据的场景(如设备控制指令、文件传输等)。
核心特性说明:
- 发送端/接收端均内置缓冲区,缓存传输中或待处理的数据;
- 传输机制:数据不会严格按"一帧一帧"传输,可能存在「单帧分片」或「多帧合并」的情况;
- 关键要求:接收端必须通过自定义帧格式解析数据,实现分片重组,才能获取完整的业务数据。
二、RDT帧设计
为实现数据分片重组,需自定义统一的帧格式,确保发送端和接收端遵循相同的解析规则。推荐帧结构分为5个部分,兼顾完整性校验和扩展性:
(一)参考帧结构
// 帧结构总览(字节数固定部分+可变部分)
frmBegin[4] | frmInfo[5] | data[dataSize] | frmEnd[2]
// 各部分说明
1. frmBegin[4]:帧起始标志(固定4字节)
- 作用:标识一帧数据的开始,用于同步帧边界
- 示例:0xAA 0xBB 0xCC 0xDD(可自定义,需两端一致)
2. frmInfo[5]:帧信息头(固定5字节,含业务核心信息)
- 子结构:type[1] + dataSize[4]
- type[1]:数据类型(1字节,自定义业务类型,如0x01=控制指令、0x02=文件数据)
- dataSize[4]:数据体长度(4字节,大端/小端需两端统一,推荐大端)
3. data[dataSize]:业务数据体(可变长度,由dataSize指定)
- 存储实际需要传输的业务数据(如指令内容、文件分片等)
4. frmEnd[2]:帧结束标志(固定2字节)
- 作用:标识一帧数据的结束,用于校验帧完整性
- 示例:0xEE 0xFF(可自定义,需两端一致)
说明:帧结构的固定部分(frmBegin、frmInfo、frmEnd)总长度为11字节(也可根据实际场景自行扩展),可变部分长度由dataSize动态指定,确保解析时可准确定位各字段边界。
(二)帧结构自定义说明
- 核心约束:
frmBegin、frmInfo、frmEnd的长度和格式需在发送端、接收端完全一致,否则会导致解析失败; - 扩展建议:若需更多业务字段(如校验码、序列号),可扩展
frmInfo长度(如改为8字节),示例:
frmInfo[8] = type[1] + seq[2] + dataSize[4] + crc[1](seq=序列号,crc=数据校验码); - 注意:帧标志(frmBegin/frmEnd)需避免与业务数据重复,防止误识别帧边界。
三、帧读取方法
接收端需按「帧起始标志 → 帧信息头 → 业务数据 → 帧结束标志」的顺序读取数据,通过循环重试机制处理分片传输场景,确保获取完整帧。
以下为各环节的标准实现(C语言示例),依赖 RDT 核心接口 RDT_Read(rdt_channel_id, buf, len, timeout)。
1. 读取帧起始标志(frmBegin)
读取固定4字节的帧起始标志,用于定位帧的开始位置。
// 全局变量:RDT通道ID(需在建立RDT连接后获取,如IOTC_Connect_RDT返回的通道ID)
int rdt_channel_id = -1;
// 帧起始标志定义(需与发送端一致)
#define FRAME_BEGIN_SIZE 4
const unsigned char FRAME_BEGIN[] = {0xAA, 0xBB, 0xCC, 0xDD};
/**
* 读取帧起始标志
* @return 0=读取成功且标志匹配,负数=错误码(-1=缓冲区不足,-2=标志不匹配,其他=RDT_Read错误码)
*/
int readFrameBegin() {
int readSize = 0; // 单次读取字节数
int rest = FRAME_BEGIN_SIZE; // 剩余需读取的字节数
unsigned char buffer[FRAME_BEGIN_SIZE] = {0}; // 接收缓冲区
// 循环读取完整的起始标志(处理分片)
while (rest > 0) {
readSize = RDT_Read(
rdt_channel_id,
buffer + (FRAME_BEGIN_SIZE - rest), // 当前写入位置
rest,
1000 // 超时时间(ms),可根据业务调整
);
// 处理读取结果
if (readSize == RDT_ER_TIMEOUT) {
// 超时:继续重试(避免因分片传输导致读取失败)
continue;
} else if (readSize < 0) {
// 非超时错误(如通道关闭):返回RDT错误码
return readSize;
}
rest -= readSize;
}
// 验证起始标志是否匹配(防止误识别)
if (memcmp(buffer, FRAME_BEGIN, FRAME_BEGIN_SIZE) != 0) {
return -2; // 标志不匹配,返回自定义错误码
}
return 0; // 读取成功且标志有效
}
2. 读取帧信息头(frmInfo)
读取固定5字节的帧信息头,解析出数据类型(type)和数据体长度(dataSize),为后续数据读取做准备。
// 帧信息头固定大小(5字节)
#define FRAME_INFO_SIZE 5
/**
* 读取帧信息头
* @param frmInfo 接收帧信息的缓冲区(需提前分配至少FRAME_INFO_SIZE字节)
* @param frmInfoBufferSize 缓冲区实际大小(用于合法性校验)
* @return 0=读取成功,负数=错误码(-1=参数非法,其他=RDT_Read错误码)
*/
int readFrameInfo(char* frmInfo, size_t frmInfoBufferSize) {
// 参数合法性校验(避免空指针和缓冲区溢出)
if (frmInfo == NULL || frmInfoBufferSize < FRAME_INFO_SIZE) {
return -1;
}
int readSize = 0;
int rest = FRAME_INFO_SIZE;
memset(frmInfo, 0, frmInfoBufferSize); // 清空缓冲区
// 循环读取完整的帧信息头
while (rest > 0) {
readSize = RDT_Read(
rdt_channel_id,
frmInfo + (FRAME_INFO_SIZE - rest),
rest,
1000
);
if (readSize == RDT_ER_TIMEOUT) {
continue;
} else if (readSize < 0) {
return readSize;
}
rest -= readSize;
}
return 0;
}
3. 解析帧信息头(parseFrameInfo)
从读取到的 frmInfo 中解析出 type(数据类型)和 dataSize(数据体长度),需注意字节序转换(若为大端传输)。
#include// 用于ntohl函数(网络字节序转主机字节序)
/**
* 解析帧信息头
* @param frmInfo 已读取的帧信息头缓冲区(长度>=FRAME_INFO_SIZE)
* @param outType 输出参数:数据类型(需提前分配内存)
* @param outDataSize 输出参数:数据体长度(需提前分配内存)
* @return 0=解析成功,-1=参数非法
*/
int parseFrameInfo(const char* frmInfo, int* outType, int* outDataSize) {
if (frmInfo == NULL || outType == NULL || outDataSize == NULL) {
return -1;
}
// 解析数据类型(1字节,直接读取)
*outType = (unsigned char)frmInfo[0];
// 解析数据体长度(4字节,假设发送端为网络字节序,转换为主机字节序)
uint32_t dataSizeNet = 0;
memcpy(&dataSizeNet, frmInfo + 1, 4); // frmInfo[1]~frmInfo[4]为dataSize
*outDataSize = ntohl(dataSizeNet); // 大端转主机字节序(若为小端则用ntohs/直接读取)
return 0;
}
4. 读取业务数据体(data)
根据解析出的 dataSize 读取完整的业务数据,确保缓冲区大小足够容纳数据体。
/**
* 读取业务数据体
* @param dataBuf 接收数据的缓冲区(需提前分配>=dataSize字节)
* @param dataBufSize 缓冲区实际大小(用于合法性校验)
* @param dataSize 预期读取的数据体长度(从frmInfo解析获得)
* @return 实际读取的字节数(正常=dataSize),负数=错误码(-1=参数非法,其他=RDT_Read错误码)
*/
int readOneFrameData(char* dataBuf, size_t dataBufSize, int dataSize) {
// 参数合法性校验(防止缓冲区溢出和非法参数)
if (dataBuf == NULL || dataBufSize < (size_t)dataSize || dataSize <= return="" int="" readsize="0;" rest="" totalread="0;" while=""> 0) {
readSize = RDT_Read(
rdt_channel_id,
dataBuf + (dataSize - rest), // 当前写入位置
rest,
1000
);
if (readSize == RDT_ER_TIMEOUT) {
continue;
} else if (readSize < 0) {
return readSize;
}
totalRead += readSize;
rest -= readSize;
}
return totalRead;
}
5. 读取帧结束标志(frmEnd)
读取固定2字节的帧结束标志,验证帧的完整性。
// 帧结束标志定义(需与发送端一致)
#define FRAME_END_SIZE 2
const unsigned char FRAME_END[] = {0xEE, 0xFF};
/**
* 读取帧结束标志
* @return 0=读取成功且标志匹配,负数=错误码(-2=标志不匹配,其他=RDT_Read错误码)
*/
int readFrameEnd() {
int readSize = 0;
int rest = FRAME_END_SIZE;
unsigned char buffer[FRAME_END_SIZE] = {0};
// 循环读取完整的结束标志
while (rest > 0) {
readSize = RDT_Read(
rdt_channel_id,
buffer + (FRAME_END_SIZE - rest),
rest,
1000
);
if (readSize == RDT_ER_TIMEOUT) {
continue;
} else if (readSize < 0) {
return readSize;
}
rest -= readSize;
}
// 验证结束标志是否匹配
if (memcmp(buffer, FRAME_END, FRAME_END_SIZE) != 0) {
return -2;
}
return 0;
}
四、完整读取示例
将上述分步读取方法整合,实现持续监听并解析RDT数据帧的完整流程,包含错误处理和异常兼容。
// 业务数据缓冲区大小(根据实际最大dataSize调整,示例:100KB)
#define MAX_DATA_BUFFER_SIZE 100 * 1024
// 业务数据处理函数(示例:根据数据类型分发处理)
void handleBusinessData(int dataType, const char* dataBuf, int dataSize) {
switch (dataType) {
case 0x01: // 控制指令类型
printf("收到控制指令,长度:%d字节,内容:%s\n", dataSize, dataBuf);
// TODO:执行控制指令逻辑
break;
case 0x02: // 文件数据类型
printf("收到文件数据,长度:%d字节\n", dataSize);
// TODO:写入文件或拼接文件分片
break;
default:
printf("收到未知类型数据,类型:%d,长度:%d字节\n", dataType, dataSize);
break;
}
}
// 主循环:持续读取并解析RDT数据帧
int rdtFrameReadLoop() {
char frmInfo[FRAME_INFO_SIZE] = {0};
char dataBuffer[MAX_DATA_BUFFER_SIZE] = {0};
int dataType = 0;
int dataSize = 0;
int ret = 0;
// 循环监听数据(直到通道关闭或主动退出)
while (1) {
// 1. 读取帧起始标志
ret = readFrameBegin();
if (ret < 0) {
if (ret == -2) {
printf("帧起始标志不匹配,跳过异常数据\n");
continue; // 标志不匹配,跳过当前数据,继续下一轮监听
} else {
printf("读取帧起始标志失败,错误码:%d\n", ret);
break; // RDT通道错误,退出循环
}
}
// 2. 读取帧信息头
ret = readFrameInfo(frmInfo, sizeof(frmInfo));
if (ret < 0) {
printf("读取帧信息头失败,错误码:%d\n", ret);
break;
}
// 3. 解析帧信息头
ret = parseFrameInfo(frmInfo, &dataType, &dataSize);
if (ret < 0) {
printf("解析帧信息头失败\n");
continue;
}
// 4. 校验数据大小合法性(防止缓冲区溢出)
if (dataSize <= 0="" datasize=""> MAX_DATA_BUFFER_SIZE) {
printf("无效的数据大小:%d字节(超出缓冲区限制),跳过当前帧\n", dataSize);
// 跳过当前帧的剩余数据(避免影响后续帧解析)
// TODO:可选实现:读取并丢弃当前帧的data和frmEnd
continue;
}
// 5. 读取业务数据体
ret = readOneFrameData(dataBuffer, sizeof(dataBuffer), dataSize);
if (ret < 0) {
printf("读取业务数据失败,错误码:%d\n", ret);
break;
}
if (ret != dataSize) {
printf("数据体不完整(预期:%d字节,实际:%d字节),跳过当前帧\n", dataSize, ret);
continue;
}
// 6. 读取帧结束标志
ret = readFrameEnd();
if (ret < 0) {
if (ret == -2) {
printf("帧结束标志不匹配,跳过当前帧\n");
continue;
} else {
printf("读取帧结束标志失败,错误码:%d\n", ret);
break;
}
}
// 7. 处理完整的业务数据
handleBusinessData(dataType, dataBuffer, dataSize);
// 清空缓冲区(避免残留数据影响下一轮解析)
memset(dataBuffer, 0, sizeof(dataBuffer));
}
printf("RDT帧读取循环退出\n");
return ret;
}
说明:完整示例包含参数校验、异常处理、缓冲区管理等关键逻辑,可直接集成到实际项目中,只需根据业务需求修改
MAX_DATA_BUFFER_SIZE和handleBusinessData函数。示例说明
- 流程完整性:覆盖「帧起始 → 帧信息 → 业务数据 → 帧结束」的全链路读取和校验;
- 错误处理:针对标志不匹配、数据大小异常、超时等场景做兼容,避免程序崩溃;
- 扩展性:
handleBusinessData函数可根据实际业务需求扩展(如指令解析、文件存储等); - 注意:需在调用
rdtFrameReadLoop前确保rdt_channel_id已通过 RDT 连接接口(如IOTC_Connect_RDT)获取并有效。
