27、序列化与反序列化:结构体序列化、Protocol Buffers简介、JSON序列化
聊到文件操作,有个话题绕不开——序列化与反序列化。
说白了,就是把内存里的数据变成一串字节流,存到文件里或者发到网络上。反过来,从字节流恢复成原来的数据结构,就是反序列化。
我刚开始做嵌入式开发时,觉得这事很简单:直接把结构体用 fwrite 写进文件不就行了?后来踩了不少坑,才明白这里面的门道有多深。
为什么不能直接写结构体?
你想想看,结构体在内存里是怎么存的?
- 内存对齐:编译器会在成员之间插入填充字节。同一份结构体,32位和64位系统下大小可能不一样。
- 字节序:大端和小端机器上,多字节整数的存储顺序是反的。
- 指针问题:结构体里的指针,存的是地址。换个进程,地址就失效了。
我在项目中遇到过这样的事:两台设备通过串口通信,A设备写了一个结构体发过去,B设备读出来全是乱码。查了半天,发现是字节序不一致。从那以后,我再也不敢直接 memcpy 结构体了。
⚠️ 注意:不要直接用
fwrite(&struct, sizeof(struct), 1, fp) 做序列化。跨平台、跨语言时,这几乎一定会出问题。
手动序列化:最朴素的做法
最可靠的办法,是一个成员一个成员地处理。我习惯写一对函数:serialize() 和 deserialize()。
// 定义一个简单的结构体
typedef struct {
uint32_t id;
char name[32];
float temperature;
} SensorData;
// 序列化:把结构体写入缓冲区
int serialize_sensor(const SensorData *data, uint8_t *buf, size_t buf_size) {
size_t offset = 0;
// 写入 id(4字节,小端序)
buf[offset++] = (data->id >> 0) & 0xFF;
buf[offset++] = (data->id >> 8) & 0xFF;
buf[offset++] = (data->id >> 16) & 0xFF;
buf[offset++] = (data->id >> 24) & 0xFF;
// 写入 name(固定32字节)
memcpy(buf + offset, data->name, 32);
offset += 32;
// 写入 temperature(4字节 float,按 IEEE 754 处理)
uint32_t temp_bits;
memcpy(&temp_bits, &data->temperature, sizeof(temp_bits));
buf[offset++] = (temp_bits >> 0) & 0xFF;
buf[offset++] = (temp_bits >> 8) & 0xFF;
buf[offset++] = (temp_bits >> 16) & 0xFF;
buf[offset++] = (temp_bits >> 24) & 0xFF;
return offset; // 返回实际写入的字节数
}
// 反序列化:从缓冲区恢复结构体
int deserialize_sensor(SensorData *data, const uint8_t *buf, size_t buf_size) {
size_t offset = 0;
// 读取 id
data->id = 0;
data->id |= (uint32_t)buf[offset++] << 0;
data->id |= (uint32_t)buf[offset++] << 8;
data->id |= (uint32_t)buf[offset++] << 16;
data->id |= (uint32_t)buf[offset++] << 24;
// 读取 name
memcpy(data->name, buf + offset, 32);
offset += 32;
// 读取 temperature
uint32_t temp_bits = 0;
temp_bits |= (uint32_t)buf[offset++] << 0;
temp_bits |= (uint32_t)buf[offset++] << 8;
temp_bits |= (uint32_t)buf[offset++] << 16;
temp_bits |= (uint32_t)buf[offset++] << 24;
memcpy(&data->temperature, &temp_bits, sizeof(temp_bits));
return offset;
}
💡 我的习惯:手动序列化时,我会在数据流开头加一个魔数(Magic Number)和版本号。这样反序列化时可以校验格式,也方便以后升级协议。
Protocol Buffers:Google 的序列化方案
手动序列化虽然可控,但写起来太累。尤其是结构体字段一多,维护起来很痛苦。
这时候就该 Protocol Buffers 出场了。这是 Google 开源的高效序列化框架,在嵌入式领域也很有用。
它的工作流程是这样的:
- 写一个
.proto文件,定义数据结构。 - 用
protoc编译器生成 C 代码。 - 在代码里调用生成的序列化/反序列化函数。
举个例子,定义一个传感器数据结构:
// sensor.proto
syntax = "proto3";
message SensorData {
uint32 id = 1;
string name = 2;
float temperature = 3;
int32 battery_level = 4;
}
然后生成 C 代码:
protoc --c_out=. sensor.proto
生成的代码里会有这些函数:
sensor_data__pack()— 序列化到缓冲区sensor_data__unpack()— 反序列化sensor_data__free_unpacked()— 释放内存
使用起来很简单:
#include "sensor.pb-c.h"
SensorData data = SENSOR_DATA__INIT;
data.id = 1001;
data.name = "temp_sensor_01";
data.temperature = 25.6f;
data.battery_level = 85;
// 计算序列化后的大小
size_t size = sensor_data__get_packed_size(&data);
uint8_t *buf = malloc(size);
// 序列化
sensor_data__pack(&data, buf);
// 反序列化
SensorData *decoded = sensor_data__unpack(NULL, size, buf);
if (decoded != NULL) {
printf("ID: %u, Temp: %.2f\n", decoded->id, decoded->temperature);
sensor_data__free_unpacked(decoded, NULL);
}
free(buf);
🔑 核心优势:
- 序列化后的数据比 JSON 小 3-10 倍
- 解析速度极快,适合资源受限的嵌入式设备
- 支持向后兼容:可以加字段而不破坏旧数据
JSON 序列化:人类可读的格式
Protocol Buffers 虽然高效,但调试起来不方便。你没法直接打开一个 .bin 文件看里面是什么。
JSON 就不一样了。它是纯文本,一眼就能看懂。在嵌入式里,我常用 cJSON 这个轻量级库。
#include "cJSON.h"
// 序列化:结构体转 JSON 字符串
char* sensor_to_json(const SensorData *data) {
cJSON *root = cJSON_CreateObject();
cJSON_AddNumberToObject(root, "id", data->id);
cJSON_AddStringToObject(root, "name", data->name);
cJSON_AddNumberToObject(root, "temperature", data->temperature);
cJSON_AddNumberToObject(root, "battery_level", data->battery_level);
char *json_str = cJSON_Print(root);
cJSON_Delete(root);
return json_str; // 记得 free()
}
// 反序列化:JSON 字符串转结构体
int json_to_sensor(const char *json_str, SensorData *data) {
cJSON *root = cJSON_Parse(json_str);
if (root == NULL) return -1;
cJSON *item;
item = cJSON_GetObjectItem(root, "id");
if (item && cJSON_IsNumber(item)) data->id = item->valueint;
item = cJSON_GetObjectItem(root, "name");
if (item && cJSON_IsString(item)) strncpy(data->name, item->valuestring, 32);
item = cJSON_GetObjectItem(root, "temperature");
if (item && cJSON_IsNumber(item)) data->temperature = item->valuedouble;
cJSON_Delete(root);
return 0;
}
⚠️ 嵌入式使用注意:cJSON 会动态分配内存。在 RAM 很小的 MCU 上,要小心堆碎片。我一般会限制 JSON 字符串的最大长度,并做内存池管理。
三种方案怎么选?
我个人的经验是这样的:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 跨平台通信,性能敏感 | Protocol Buffers | 体积小、解析快、有版本管理 |
| 调试阶段,或与 Web 交互 | JSON | 人类可读,方便排查问题 |
| 资源极度受限(如 8 位 MCU) | 手动序列化 | 无依赖,完全可控,代码量最小 |
| 需要长期存储的配置文件 | JSON | 可编辑,可版本控制 |
知识体系图
下面这张图帮你理清序列化的核心逻辑:
避坑指南
最后分享几个我踩过的坑:
- 浮点数精度:不同平台对 float 的 IEEE 754 实现可能略有差异。序列化时最好转成整数(比如乘以 1000)再传输。
- 字符串长度:手动序列化时,字符串一定要带长度字段,或者用固定长度。不要依赖
strlen去判断结束。 - 版本兼容:我曾经升级了协议,加了新字段,结果旧设备解析新数据直接崩溃。后来我学乖了,每个数据包都带版本号。
- 内存管理:反序列化时,如果结构体里有指针(比如动态字符串),一定要记得分配和释放内存。cJSON 和 protobuf-c 都会 malloc,别忘了 free。
嗯,序列化这事,说难不难,说简单也不简单。关键是选对工具,想清楚边界条件。希望这些经验能帮你少走弯路。
公众号:蓝海资料掘金营,微信deep3321