文件互传中的数据帧设计

文件互传中的数据帧设计
前言前面文件互传的各个业务的设计实现分析的差不多了这里回顾分析下文件传输过程中文件数据帧的设计消息帧的设计以及其他的一些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字节(indexfileNameLength) 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]); // 把 bufferoffset 位置强转为 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轮填充数据 index0 offset0 fileItem 指向 buffer0 fileItem-index htonl(0) 0x00000000 fileItem-fileNameLength htonl(11) 0x0000000B offset 0 8 8 memcpy(fileItem-fileName, photo1.jpg, 11) offset 8 11 19 index1 offset19 fileItem 指向 buffer19 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.jpg5字节但占了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 指向的地址 ┌──────────┬──────────┬─────────────────────┐ │ index0 │ len11 │ 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 就是 buffer8 的别名为什么使用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 // 下一个成员偏移011 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