4、应用层协议设计:JSON/Protobuf 在嵌入式中的取舍与实现

应用层协议,说白了就是设备之间“说人话”的规则。在嵌入式系统里,我们经常要在 JSON 和 Protobuf 之间做选择。这不是一个简单的“谁更好”的问题,而是“谁更适合你的场景”。

我早年做过一个智能网关项目,一开始图省事全用了 JSON,结果到了后期,内存和带宽双双告急。后来换成了 Protobuf,虽然开发时多花了点功夫,但产品稳定多了。今天我就把这段经验拆开来讲。

4.1 两种协议的底层差异

先看一张对比表,把核心差异说清楚:

维度 JSON Protobuf
数据格式 纯文本,人类可读 二进制,不可读
编码体积 较大(含大量冗余字符) 极小(字段编号+Varint编码)
解析速度 较慢(字符串解析) 极快(直接内存映射)
内存占用 高(需构建DOM树) 低(流式解析,零拷贝)
Schema要求 无(自描述) 必须预定义 .proto 文件
向后兼容 天然支持(新增字段不影响旧解析器) 需遵循字段编号规则
调试便利性 极高(curl直接看) 低(需工具解码)

为什么会这样?JSON 的每个键名都要重复传输,比如 "temperature": 25.3 这串字符,光键名就占了十几个字节。而 Protobuf 用字段编号代替键名,比如字段 1 代表 temperature,传输时只写 0x08 0xCB 0x01 三个字节。你想想看,在 115200 波特率的串口上,这个差距有多明显。

4.2 什么时候选 JSON?

我个人习惯把 JSON 用在三个场景:

  • 调试阶段:开发初期,协议还没定型,JSON 改起来最快。我经常在串口助手里直接改 JSON 字符串测试,不用重新编译固件。
  • 配置管理:设备启动时读取的配置文件,比如 WiFi 密码、服务器地址。这些数据量小,变化频繁,JSON 的灵活性正好派上用场。
  • 与云端交互:如果对接的是 RESTful API,JSON 是事实标准。强行用 Protobuf 反而增加网关转换的复杂度。
小技巧:在资源紧张的 MCU 上解析 JSON,建议用 jsmn 或 cJSON 这类轻量库。它们不动态分配内存,而是基于 token 数组做解析,能有效避免内存碎片。

4.3 什么时候必须上 Protobuf?

反过来,Protobuf 也有它不可替代的地方:

  • 高频率数据上报:比如传感器每秒上报 100 次数据。用 JSON 的话,光序列化就能吃掉 30% 的 CPU。
  • 低带宽链路:LoRa、NB-IoT 这类窄带通信,每字节都珍贵。Protobuf 能把 200 字节的 JSON 压缩到 30 字节。
  • 多平台互操作:如果你的系统涉及 C、Java、Python 等多种语言,Protobuf 的代码生成器能自动生成各语言的数据结构,省去手写解析器的麻烦。
避坑指南:我曾经在一个项目里把 Protobuf 用于 OTA 升级包的描述文件,结果因为 .proto 文件版本没对齐,导致旧设备解析新包时崩溃。记住:Protobuf 的兼容性依赖严格的字段编号管理,新增字段只能用新编号,绝对不能复用旧编号。

4.4 嵌入式中的 Protobuf 实现要点

如果你决定用 Protobuf,有几点要注意:

  1. 使用 Nanopb:Google 官方的 protobuf-c 对 MCU 来说太重了。Nanopb 是专为嵌入式优化的版本,生成的代码体积小,支持静态内存分配。
  2. 避免动态消息:在 .proto 文件中尽量少用 repeated 和 map 类型,它们会引入动态内存分配。用固定长度的数组代替。
  3. 预计算最大尺寸:用 Nanopb 的 pb_encode 前,先调用 pb_get_encoded_size 算出最大缓冲区需求,避免溢出。

下面是一个典型的 Nanopb 编码示例:

// sensor.proto
message SensorData {
    required uint32 id = 1;
    required float temperature = 2;
    required float humidity = 3;
    optional uint32 battery = 4 [default = 100];
}

// 编码端代码
#include <pb_encode.h>
#include "sensor.pb.h"

uint8_t buffer[64];
SensorData data = {
    .id = 1,
    .temperature = 25.3,
    .humidity = 60.5,
    .battery = 85
};

pb_ostream_t stream = pb_ostream_from_buffer(buffer, sizeof(buffer));
if (!pb_encode(&stream, SensorData_fields, &data)) {
    // 编码失败处理
}
// stream.bytes_written 就是实际编码长度

4.5 混合使用策略

在实际产品中,我很少只用一种协议。更常见的做法是:

  • 控制通道用 JSON:用于设备发现、参数查询、调试指令。这些交互频率低,但需要人类可读。
  • 数据通道用 Protobuf:用于传感器数据、状态上报、日志批量传输。这些数据量大,对效率要求高。

这样做的好处是,调试时用 JSON 通道直接看设备状态,生产运行时用 Protobuf 通道保证性能。两个通道互不干扰。

4.6 知识体系图

下面这张图总结了本章的核心决策逻辑:

应用层协议选型决策树 应用层协议选型 JSON(文本协议) Protobuf(二进制协议) 调试/配置 低频控制 云端API 高频数据 低带宽链路 多平台互操作 混合使用策略 控制通道 → JSON(可读性优先) 数据通道 → Protobuf(性能优先) 嵌入式 Protobuf 实现要点 ① 使用 Nanopb 轻量库 ② 避免动态消息类型 ③ 预计算最大编码尺寸 ④ 严格管理字段编号

4.7 性能实测数据

最后给一组我在 STM32F407 上实测的数据,供你参考:

测试项 JSON (cJSON) Protobuf (Nanopb) 差异倍数
编码 10 个传感器数据 2.3 ms 0.4 ms 5.8x
解码 10 个传感器数据 3.1 ms 0.3 ms 10.3x
编码后数据体积 512 字节 68 字节 7.5x
峰值堆内存占用 1.2 KB 128 字节 9.4x

嗯,数据摆在这里,怎么选你应该心里有数了。记住:没有银弹,只有最适合你当前场景的方案。