文件互传中的数据帧设计
·
前言
前面文件互传的各个业务的设计实现分析的差不多了,这里回顾分析下文件传输过程中,文件数据帧的设计,消息帧的设计以及其他的一些open Harmony关于内存对齐的设计。看完这篇就会理解为什么面试官老是揪着int占用几字节问不放了,哈哈哈。
消息帧设计
就比如文件发送完以后,软总线会把发送文件列表发送给接收端核对是否全部发完
发送文件列表给接收端源码
int32_t FileListToBuffer(const char **destFile, uint32_t fileCnt, FileListBuffer *outbufferInfo)
{
if (destFile == NULL || outbufferInfo == NULL || fileCnt == 0) {
TRANS_LOGE(TRANS_FILE, "invalid param");
return SOFTBUS_INVALID_PARAM;
}
int32_t errCode = SOFTBUS_OK;
uint32_t totalLength = 0;
uint32_t offset = 0;
for (uint32_t i = 0; i < fileCnt; i++) {
size_t fileNameLength = strlen(destFile[i]);
if (fileNameLength == 0 || fileNameLength > MAX_FILE_PATH_NAME_LEN) {
TRANS_LOGE(TRANS_FILE, "bad file name at index=%{public}" PRIu32, i);
return SOFTBUS_INVALID_PARAM;
} else {
totalLength += fileNameLength;
}
}
size_t bufferSize = totalLength + (sizeof(struct FileListItem) * fileCnt);
uint8_t *buffer = (uint8_t *)SoftBusCalloc(bufferSize);
if (buffer == NULL) {
TRANS_LOGE(TRANS_FILE, "calloc filelist failed");
return SOFTBUS_MALLOC_ERR;
}
for (uint32_t index = 0; index < fileCnt; index++) {
uint32_t fileNameSize = strlen(destFile[index]);
struct FileListItem *fileItem = (struct FileListItem *)(buffer + offset);
fileItem->index = htonl(index);
fileItem->fileNameLength = htonl(fileNameSize);
offset += sizeof(struct FileListItem);
// note: no \0 here
if (memcpy_s(fileItem->fileName, bufferSize - offset, destFile[index], fileNameSize) != EOK) {
TRANS_LOGE(TRANS_FILE, "copy file name failed!");
errCode = SOFTBUS_MEM_ERR;
break;
}
offset += fileNameSize;
}
if (errCode != SOFTBUS_OK) {
SoftBusFree(buffer);
return errCode;
}
outbufferInfo->buffer = buffer;
outbufferInfo->bufferSize = offset;
return SOFTBUS_OK;
}
逐行解析
int32_t FileListToBuffer(const char **destFile, uint32_t fileCnt, FileListBuffer *outbufferInfo)
{
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 参数校验
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
if (destFile == NULL || outbufferInfo == NULL || fileCnt == 0) {
return SOFTBUS_INVALID_PARAM;
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 第1轮循环:计算所有文件名的总长度 ⭐
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
uint32_t totalLength = 0;
for (uint32_t i = 0; i < fileCnt; i++) {
size_t fileNameLength = strlen(destFile[i]);
if (fileNameLength == 0 || fileNameLength > MAX_FILE_PATH_NAME_LEN) {
return SOFTBUS_INVALID_PARAM; // 文件名不能为空或超长
}
totalLength += fileNameLength; // 累加文件名长度
}
// 假设 destFile = ["photo1.jpg", "doc.pdf"]
// totalLength = 11 + 7 = 18
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 分配缓冲区 ⭐
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
size_t bufferSize = totalLength + (sizeof(struct FileListItem) * fileCnt);
// = 18 + 8*2 = 34 字节
// ↑文件名 ↑每个Item头部8字节(index+fileNameLength)
uint8_t *buffer = (uint8_t *)SoftBusCalloc(bufferSize);
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 第2轮循环:填充二进制数据 ⭐⭐
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
uint32_t offset = 0;
for (uint32_t index = 0; index < fileCnt; index++) {
uint32_t fileNameSize = strlen(destFile[index]);
// 把 buffer+offset 位置强转为 FileListItem 指针
struct FileListItem *fileItem = (struct FileListItem *)(buffer + offset);
// 填充头部(转网络字节序)
fileItem->index = htonl(index);
fileItem->fileNameLength = htonl(fileNameSize);
// offset 跳过 Item 头部(8字节)这里为什么传入结构体的大小就能跳过头部后面有讲到
offset += sizeof(struct FileListItem);
// 拷贝文件名到 fileName[0] 的位置(不带 \0)
memcpy_s(fileItem->fileName, bufferSize - offset, destFile[index], fileNameSize);
// ↑ 目标地址 ↑ 剩余空间 ↑ 源数据 ↑ 长度
// offset 跳过文件名
offset += fileNameSize;
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// 输出结果
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
outbufferInfo->buffer = buffer;
outbufferInfo->bufferSize = offset;
return SOFTBUS_OK;
}
例子
输入:destFile = ["photo1.jpg", "doc.pdf"],fileCnt = 2
第1轮:计算总长度
strlen("photo1.jpg") = 11
strlen("doc.pdf") = 7
totalLength = 18
分配:bufferSize = 18 + 8*2 = 34 字节
第2轮:填充数据
index=0:
offset=0
fileItem 指向 buffer+0
fileItem->index = htonl(0) = 0x00000000
fileItem->fileNameLength = htonl(11) = 0x0000000B
offset = 0 + 8 = 8
memcpy(fileItem->fileName, "photo1.jpg", 11)
offset = 8 + 11 = 19
index=1:
offset=19
fileItem 指向 buffer+19
fileItem->index = htonl(1) = 0x00000001
fileItem->fileNameLength = htonl(7) = 0x00000007
offset = 19 + 8 = 27
memcpy(fileItem->fileName, "doc.pdf", 7)
offset = 27 + 7 = 34
输出:
buffer → 34字节的二进制数据
bufferSize = 34
最终二进制布局

为什么需要两轮循环
第1轮:算总长度 → 才知道要分配多少内存
第2轮:填充数据 → 往分配好的内存里写
不能合并成一轮:
第1个文件时还不知道总长度 → 无法确定分配多少
如果先分配一个估算值 → 可能不够或浪费
两轮循环是最稳妥的方式
柔性数组设计
发送的帧里面,如果帧需要包含的数据长度不确定怎么办,这个项目提供了柔性数组的设计解决。
方案1:固定大小数组(浪费空间)
struct FileListItem {
uint32_t index;
uint32_t fileNameLength;
char fileName[256]; // 固定256字节
};
→ 文件名只有 "a.jpg"(5字节),但占了256字节 → 浪费251字节
→ 文件名超过256字节就放不下 → 限制
方案2:指针(两次内存分配)
struct FileListItem {
uint32_t index;
uint32_t fileNameLength;
char *fileName; // 指针
};
→ 需要单独 malloc 文件名 → 两次内存操作
→ 序列化时麻烦:要序列化结构体 + 单独序列化字符串
方案3:柔性数组(最佳)✅
struct FileListItem {
uint32_t index; // 4字节
uint32_t fileNameLength; // 4字节
char fileName[0]; // 0字节占位符
};
→ 不占空间
→ 实际数据紧跟在后面
→ 长度由 fileNameLength 决定
柔性数组是什么
struct FileListItem {
uint32_t index; // 4字节
uint32_t fileNameLength; // 4字节
char fileName[0]; // ← 这就是柔性数组
};
在这里,柔性数组只是一个标识符,是一个大小为0的数组,不占用结构体空间。
普通数组:
char name[32]; // 占32字节,不管你用不用
→ 浪费空间(文件名可能只有5字节)
→ 限制长度(超过32就放不下)
柔性数组:
char name[0]; // 占0字节,只是一个标记
→ 实际数据紧跟在结构体后面
→ 长度由 fileNameLength 告诉你
→ 不浪费空间,不限制长度

项目中的效果
// 分配内存:结构体(8) + 文件名(11) = 19字节
uint8_t *buffer = SoftBusCalloc(sizeof(FileListItem) + strlen("photo1.jpg"));
// 内存长这样:
// buffer 指向的地址:
┌──────────┬──────────┬─────────────────────┐
│ index=0 │ len=11 │ p h o t o 1 . j p g │
│ (4字节) │ (4字节) │ (11字节) │
└──────────┴──────────┴─────────────────────┘
0 8 19
// 使用:
FileListItem *item = (FileListItem *)buffer;
item->index = 0; // 前4字节
item->fileNameLength = 11; // 第5-8字节
memcpy(item->fileName, "photo1.jpg", 11); // 从第9字节开始写
// ↑ fileName 就是 buffer+8 的别名
为什么使用memcpy而不是strcpy
// note: no \0 here(代码注释明确说了)
memcpy(item->fileName, destFile[index], fileNameSize);
// ↑ 没有 \0 结尾!节省1字节
strcpy(item->fileName, destFile[index]);
// ↑ 会自动加 \0 → 多1字节 → 网络传输浪费带宽
1字节对齐
这里就涉及到项目对内存对齐的处理了。当一个变量不满默认的4字节或8字节的时候,编译器会自动进行补齐,但是这样有两个坏处,第一帧变长,第二两端内容对不上了,这是为了保障发送过去的数据和接收到的数据保持一致。
发送端(1字节对齐):
[index(4)][len(4)][fileName...]
第8字节开始就是文件名
接收端(默认4字节对齐):
[index(4)][len(4)][填充(4)][fileName...]
第12字节才开始文件名 ← 位置对不上!
→ 两端结构体布局不一致 → 解析错误 💥
1字节对齐后:
两端都是 [index(4)][len(4)][fileName...]
→ 布局一致 → 正确解析 ✅
对齐规则详解
默认对齐是什么
默认对齐值 = 编译器决定,通常等于该平台上最大的基本类型对齐要求
常见平台:
x86 / ARM (32位):默认4字节对齐
x86_64 / ARM64: 默认8字节对齐
可以用 #pragma pack(N) 或 __attribute__((aligned(N))) 手动指定
判断对齐值的公式
成员的对齐值 = min(成员自身大小, 结构体当前对齐值)
例1:默认4字节对齐
struct Example {
char a; // 大小1,对齐值=min(1,4)=1 → 偏移0
// 下一个成员偏移=0+1=1
int b; // 大小4,对齐值=min(4,4)=4 → 偏移必须是4的倍数
// 当前偏移=1,不是4的倍数
// → 插入3字节填充到偏移4
};
内存布局:
偏移: 0 1 2 3 4 5 6 7
内容: [a] [填充][填充][填充] [ b b b b ]
sizeof = 8
内存对齐在编码中的具体操作
#pragma pack(push, 1) // 保存当前设置,设为1字节对齐
struct FileListItem {
uint32_t index; // 偏移0,大小4
uint32_t fileNameLength; // 偏移4,大小4(紧接index)
char fileName[0]; // 偏移8,大小0
}; // sizeof = 8,无填充!
#pragma pack(pop) // 恢复之前的设置
对比默认对齐(假设4字节):
struct FileListItem {
uint32_t index; // 偏移0
uint32_t fileNameLength; // 偏移4
char fileName[0]; // 偏移8
}; // sizeof = 8(这个例子没区别)
但如果有个 char 成员在中间就有区别了:
#pragma pack(push, 1)
struct BadExample {
char type; // 偏移0
uint32_t index; // 偏移1(紧接type,无填充!)
}; // sizeof = 5
默认4字节对齐:
struct BadExample {
char type; // 偏移0
// 偏移1-3填充
uint32_t index; // 偏移4
}; // sizeof = 8
更多推荐


所有评论(0)