序列化框架对比: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