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 知识体系结构图

序列化与反序列化知识体系 序列化/反序列化 cJSON (JSON) 创建/解析 内存管理 跨平台适配 nanopb (Protobuf) .proto 定义 编码/解码 字段约束 选择依据:可读性 vs 性能 vs 带宽 JSON:人类可读,调试方便,适合配置文件和日志 Protobuf:二进制紧凑,解析快,适合高频通信和带宽受限场景

18.5 实战经验总结

我在一个智能家居项目里,同时用了两种方案:

  • 设备配置:用 JSON 存配置文件,方便运维人员手动修改。
  • 实时数据上报:用 Protobuf,每秒上报 100 次传感器数据,带宽节省了 70%。

我的建议:如果你刚开始做嵌入式通信,先用 JSON 把功能跑通。等性能瓶颈出现了,再换成 Protobuf。别一开始就追求极致性能,开发效率也很重要。

曾经有个项目,我图省事直接用 JSON 传高频数据。结果 MCU 的 CPU 占用率飙到 80%,解析 JSON 成了瓶颈。后来换成 nanopb,CPU 占用率降到 15%。嗯,选对工具真的很重要。

最后提醒一句:无论用哪种方案,都要做好版本兼容。我见过太多因为字段增减导致解析失败的事故。建议在消息头里加个版本号字段,方便以后升级。

公众号:蓝海资料掘金营,微信deep3321