18、序列化与反序列化:JSON (cJSON)、Protocol Buffers (nanopb) 的跨平台实践
嵌入式开发里,数据怎么在不同设备之间传?怎么存?这问题我遇到过太多次了。说白了,序列化就是把内存里的结构体变成一串字节流,反序列化就是再变回来。今天咱们聊聊两个主流方案:cJSON 和 nanopb。
核心观点:JSON 适合人类阅读、调试方便;Protobuf 适合性能敏感、带宽有限的场景。选哪个,看你的实际需求。
18.1 为什么需要序列化?
你想想看,两个不同架构的芯片通信,一个是大端,一个是小端。直接发结构体?那肯定出问题。序列化就是为了解决这个——它定义了一套通用的数据交换格式,跟平台无关。
我在做物联网网关项目时,传感器数据要上报到云端。一开始直接发二进制结构体,结果服务器那边解析出来全是乱码。后来改用 JSON,虽然多了点开销,但调试起来方便太多了。
18.2 cJSON:轻量级 JSON 解析器
cJSON 是我个人用得最多的 JSON 库。它只有一个 .c 和一个 .h 文件,移植起来特别方便。说白了,它就是帮你把 JSON 字符串和 C 结构体之间做转换。
18.2.1 基本用法
// 创建一个 JSON 对象
cJSON *root = cJSON_CreateObject();
cJSON_AddStringToObject(root, "device_id", "sensor_01");
cJSON_AddNumberToObject(root, "temperature", 25.6);
cJSON_AddNumberToObject(root, "humidity", 68.3);
// 序列化成字符串
char *json_str = cJSON_Print(root);
printf("JSON: %s\n", json_str);
// 释放内存
cJSON_Delete(root);
free(json_str);
嗯,这里要注意:cJSON_Print 返回的字符串需要手动 free。我刚开始用的时候经常忘记,导致内存泄漏。
18.2.2 解析 JSON
const char *input = "{\"device_id\":\"sensor_01\",\"temperature\":25.6}";
cJSON *root = cJSON_Parse(input);
if (root == NULL) {
printf("解析失败\n");
return;
}
cJSON *temp = cJSON_GetObjectItem(root, "temperature");
if (cJSON_IsNumber(temp)) {
printf("温度: %f\n", temp->valuedouble);
}
cJSON_Delete(root);
小技巧:解析嵌套 JSON 时,可以用 cJSON_GetObjectItem 链式调用。比如 cJSON_GetObjectItem(cJSON_GetObjectItem(root, "data"), "value")。
18.2.3 跨平台注意事项
cJSON 本身是跨平台的,但有几个坑要注意:
- 内存分配:默认用 malloc/free。在 FreeRTOS 里,我建议重写 cJSON_Hooks,改用 heap 管理函数。
- 浮点数精度:不同平台浮点数表示有差异。我遇到过 ARM 和 x86 之间传浮点数,小数点后第 6 位对不上的情况。
- 字符编码:cJSON 默认支持 UTF-8。如果你用 GBK,记得先转码。
18.3 nanopb:专为嵌入式优化的 Protobuf
Protocol Buffers 是 Google 出的,比 JSON 更紧凑、解析更快。nanopb 是它的嵌入式版本,代码量极小,适合资源受限的 MCU。
18.3.1 定义 .proto 文件
syntax = "proto3";
message SensorData {
string device_id = 1;
float temperature = 2;
float humidity = 3;
int32 battery_level = 4;
}
然后用 nanopb 的生成器编译:
nanopb_generator.py sensor.proto
会生成 sensor.pb.h 和 sensor.pb.c 两个文件。
18.3.2 序列化与反序列化
#include "sensor.pb.h"
// 序列化
SensorData data = SensorData_init_zero;
strcpy(data.device_id, "sensor_01");
data.temperature = 25.6;
data.humidity = 68.3;
data.battery_level = 85;
uint8_t buffer[128];
pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
bool status = pb_encode(&stream, SensorData_fields, &data);
if (status) {
printf("编码成功,长度: %d\n", stream.bytes_written);
}
// 反序列化
SensorData decoded = SensorData_init_zero;
pb_istream_t istream = pb_istream_from_buffer(buffer, stream.bytes_written);
status = pb_decode(&istream, SensorData_fields, &decoded);
if (status) {
printf("设备ID: %s\n", decoded.device_id);
}
警告:nanopb 的字符串字段默认是固定长度数组。如果 .proto 里没指定 max_size,默认是 128 字节。我遇到过字符串被截断的坑,记得在 .proto 里显式声明:
string device_id = 1 [(nanopb).max_size = 32];
18.3.3 性能对比
| 特性 | cJSON | nanopb |
|---|---|---|
| 序列化大小 | 约 80 字节 | 约 20 字节 |
| 解析时间 (1000次) | 约 15ms | 约 3ms |
| 代码体积 | 约 8KB | 约 4KB |
| 可读性 | 高 | 低 |
| 跨平台性 | 好 | 好 |
从表里能看出来,nanopb 在性能和体积上都有优势。但 JSON 的可读性更好,调试时直接用文本看,不用额外工具。
18.4 知识体系结构图
18.5 实战经验总结
我在一个智能家居项目里,同时用了两种方案:
- 设备配置:用 JSON 存配置文件,方便运维人员手动修改。
- 实时数据上报:用 Protobuf,每秒上报 100 次传感器数据,带宽节省了 70%。
我的建议:如果你刚开始做嵌入式通信,先用 JSON 把功能跑通。等性能瓶颈出现了,再换成 Protobuf。别一开始就追求极致性能,开发效率也很重要。
曾经有个项目,我图省事直接用 JSON 传高频数据。结果 MCU 的 CPU 占用率飙到 80%,解析 JSON 成了瓶颈。后来换成 nanopb,CPU 占用率降到 15%。嗯,选对工具真的很重要。
最后提醒一句:无论用哪种方案,都要做好版本兼容。我见过太多因为字段增减导致解析失败的事故。建议在消息头里加个版本号字段,方便以后升级。