序列化框架对比:JSON vs XML vs Protobuf vs MsgPack vs 自定义格式

做嵌入式开发这些年,我接触过的序列化方案少说也有七八种。每次新项目启动,总有人问我:「到底该用哪种?」说实话,没有银弹。每种方案都有自己的脾气,选对了事半功倍,选错了后期改起来想哭。

今天我就把这五种主流方案拉出来遛遛。JSON、XML、Protobuf、MsgPack,再加上我们自己手写的自定义格式。咱们从实际工程角度,看看它们各自擅长什么、短板在哪。

先看一张全景图

下面这张图是我自己整理的,把五种方案的定位和核心差异画了出来。你一看就明白它们各自在哪个维度上发力。

序列化框架对比全景图 可读性 → 性能/紧凑度 → XML 可读性高 体积大 JSON 平衡之选 生态好 MsgPack 紧凑二进制 兼容JSON Protobuf 高性能 需IDL 自定义 极致性能 灵活 可读性高 性能高

从图上能看出来,XML和JSON偏左上方,可读性好但性能一般。Protobuf和自定义格式在右下角,性能拉满但可读性差。MsgPack卡在中间,算是个折中方案。

JSON:最通用的选择

JSON现在基本是互联网的事实标准了。我最早用JSON是在做Web后端的时候,前后端通信全靠它。后来做嵌入式,发现很多云平台也支持JSON上报数据。

优点很明显:

  • 人类可读——调试的时候直接看字符串,不用额外工具
  • 语言支持广泛——几乎每种语言都有现成的解析库
  • 自描述——字段名都在数据里,不需要额外的schema文件

但缺点也扎心:

  • 体积大——字段名重复出现,比如{"temperature":25.5},光字段名就占了十几个字节
  • 解析慢——字符串解析在MCU上很吃资源
  • 无类型约束——数字和字符串混在一起,容易踩坑
我的建议:如果项目需要和外部系统对接,或者调试阶段频繁改字段,用JSON最省心。但如果是资源受限的MCU,建议只在调试接口用JSON,正式通信换别的。

XML:老当益壮,但有点重

XML我接触得早,刚入行那会儿做工业控制,设备配置全是XML格式。说实话,XML的严谨性确实好,有DTD或XSD约束,结构清晰。

但它的缺点也让人头疼:

  • 标签冗余严重——一个字段被开始标签和结束标签包两遍
  • 解析开销大——DOM解析吃内存,SAX解析写起来又麻烦
  • 在嵌入式领域水土不服——我见过有人在Cortex-M0上跑XML解析器,结果RAM直接爆了

XML现在主要用在配置文件、文档存储这些场景。如果你在做嵌入式,我个人不太推荐XML做通信协议——除非你资源非常充裕,比如跑Linux的高端芯片。

Protobuf:Google出品,性能怪兽

Protobuf是我在做一个车联网项目时开始用的。当时需要把车辆状态数据(几百个字段)以1Hz频率上报,JSON搞不定带宽和延迟,就换成了Protobuf。

它的核心优势:

  • 体积极小——用变长编码和字段编号代替字段名,数据量能压缩到JSON的1/3甚至更少
  • 解析极快——二进制格式,直接内存拷贝,不需要字符串解析
  • 强类型约束——.proto文件定义了每个字段的类型,跨语言通信不会出错
  • 向前向后兼容——加字段不影响旧版本解析

但要注意几个坑:

  • 需要编译步骤——写.proto文件,然后用protoc生成代码,流程多了几步
  • 调试不方便——二进制数据没法直接看,得用protoc --decode_raw转成文本
  • 库体积不小——完整Protobuf库在MCU上可能偏大,但有个Nanopb版本专门为嵌入式优化

避坑指南:我曾经在一个项目里直接用完整版Protobuf库,结果Flash占用多了30KB。后来换成Nanopb,只用了不到8KB。所以嵌入式上用Protobuf,记得选Nanopb或者自己裁剪。

MsgPack:JSON的二进制表亲

MsgPack的设计理念很有意思——它保持了JSON的数据模型(数组、map、字符串、数字),但用二进制编码代替文本。说白了,就是JSON的二进制版本。

它的特点:

  • 兼容JSON数据结构——JSON能表达的数据,MsgPack都能表达
  • 体积比JSON小——省掉了引号、冒号、逗号这些冗余字符
  • 解析比JSON快——二进制格式,不需要字符串解析
  • 有现成的C库——mpack、msgpack-c都挺好用

但MsgPack也有局限:

  • 仍然有字段名开销——不像Protobuf用编号代替字段名,MsgPack还是存字段名字符串
  • 生态不如JSON——虽然不少语言支持,但工具链和社区资源少一些
  • 类型系统不够强——没有Protobuf那种严格的类型约束

我个人的使用场景:当项目需要比JSON更紧凑,但又不想引入Protobuf那套编译流程时,MsgPack是个不错的中间选择。

自定义格式:极致性能的终极方案

说到自定义格式,可能有人觉得「这不就是自己造轮子吗?」嗯,确实有造轮子的风险。但在某些场景下,自定义格式是唯一的选择。

我举个例子:之前做一个传感器采集节点,MCU只有2KB RAM,Flash 32KB。JSON库放不下,Protobuf库也放不下。最后我手写了一个二进制协议:

// 自定义二进制格式示例
// 协议定义:
// 字节0: 帧头 0xAA
// 字节1: 数据长度
// 字节2: 消息类型
// 字节3-N: 负载数据
// 字节N+1: CRC校验

typedef struct {
    uint8_t header;      // 0xAA
    uint8_t length;      // 负载长度
    uint8_t msg_type;    // 消息类型
    uint8_t payload[64]; // 负载
    uint8_t crc;         // 校验和
} __attribute__((packed)) CustomFrame;

// 序列化
void serialize_temperature(CustomFrame *frame, float temp) {
    frame->header = 0xAA;
    frame->length = sizeof(float);
    frame->msg_type = 0x01; // 温度消息
    memcpy(frame->payload, &temp, sizeof(float));
    frame->crc = calc_crc(frame, sizeof(CustomFrame) - 1);
}

// 反序列化
float deserialize_temperature(CustomFrame *frame) {
    if (frame->header != 0xAA) return -1;
    if (frame->msg_type != 0x01) return -1;
    if (frame->crc != calc_crc(frame, sizeof(CustomFrame) - 1)) return -1;
    float temp;
    memcpy(&temp, frame->payload, sizeof(float));
    return temp;
}

自定义格式的优势:

  • 极致紧凑——没有冗余字节,每个bit都用在刀刃上
  • 解析最快——直接内存操作,没有解析开销
  • 完全可控——想怎么设计就怎么设计,不受第三方库限制

但风险也大:

  • 开发成本高——要自己处理字节序、对齐、校验、版本兼容
  • 调试困难——没有现成的工具,得自己写解析脚本
  • 跨语言麻烦——如果C端和Python端通信,两边都得实现一遍协议
注意:自定义格式适合「协议固定、双方都是C语言、资源极度受限」的场景。如果协议经常变,或者需要和多种语言对接,别自己造轮子,用Protobuf更靠谱。

横向对比:一张表说清楚

维度 JSON XML Protobuf MsgPack 自定义
数据体积 大 很大 极小 较小 极小
解析速度 慢 很慢 极快 快 极快
人类可读 好 好 差 差 差
跨语言支持 极好 好 好 好 差
嵌入式适配 一般 差 好(Nanopb) 好 极好
开发效率 高 中 中 高 低
版本兼容 手动处理 手动处理 内置支持 手动处理 手动处理

我总结的选型建议

说了这么多,到底怎么选?我根据自己的经验,给几个参考:

  1. 和云端通信、调试阶段——用JSON。方便、通用、生态好。
  2. 工业控制、配置文件——用XML。虽然重,但严谨性无可替代。
  3. 高性能通信、跨语言、字段多——用Protobuf。性能好,兼容性强。
  4. 比JSON紧凑、又不想引入编译流程——用MsgPack。折中方案。
  5. 资源极度受限、协议固定——用自定义格式。极致性能,但要做好文档。

其实选型没有标准答案。我见过有人用JSON在8位MCU上跑,也见过有人用Protobuf做百万级并发服务器。关键是你得清楚自己的约束条件——RAM多少?Flash多少?带宽多少?开发周期多长?想清楚这些,答案自然就有了。

嗯,序列化框架对比就聊到这儿。每种方案都有它的脾气,选对了就是好工具。


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