7、MessagePack:MsgPack简介、msgpack-c库的使用、高效二进制序列化

聊完了JSON和CBOR,咱们今天来聊聊另一个在嵌入式圈子里很常见的二进制序列化方案——MessagePack。说实话,我第一次接触MsgPack是在一个物联网网关项目里。当时设备上报的数据量特别大,用JSON传,带宽吃紧,延迟也高。后来一查,发现MsgPack能把同样的数据压缩到原来的一半甚至更少。嗯,从那天起,我就开始认真研究它了。

7.1 MsgPack是什么?

MessagePack,简称MsgPack,是一种高效的二进制序列化格式。它和JSON很像——支持相同的数据类型,比如整数、浮点数、字符串、数组、字典等。但区别在于,JSON是文本格式,而MsgPack是二进制格式。

说白了,MsgPack就是JSON的二进制版本。它用更少的字节来表达同样的数据。你想想看,JSON里一个数字"12345"要占5个字节,但在MsgPack里,一个整数可能只占1个字节(如果值很小的话)。这就是它高效的原因。

核心特点:

  • 紧凑:数据体积小,节省存储和带宽
  • 快速:编码解码速度比JSON快很多
  • 跨语言:几乎所有主流语言都有实现
  • 自描述:不需要外部Schema,数据本身包含类型信息

7.2 msgpack-c库的使用

在C语言里,最常用的MsgPack库就是msgpack-c。这个库由MsgPack官方维护,稳定性和性能都很好。我在项目中用过好几个版本,从早期的0.5.x到现在的3.x,接口变化不小,但核心思想没变。

7.2.1 安装与引入

安装很简单。如果你用的是Linux,直接通过包管理器安装就行:

# Ubuntu/Debian
sudo apt-get install libmsgpack-dev

# macOS
brew install msgpack-c

如果你喜欢从源码编译,也可以去GitHub上拉下来自己编译。我个人习惯用包管理器,省事。

在代码里引入头文件:

#include <msgpack.h>

7.2.2 基本序列化操作

咱们先看一个最简单的例子——序列化一个整数和一个字符串。

#include <stdio.h>
#include <msgpack.h>

int main() {
    // 创建一个缓冲区
    msgpack_sbuffer buffer;
    msgpack_sbuffer_init(&buffer);

    // 创建一个打包器
    msgpack_packer packer;
    msgpack_packer_init(&packer, &buffer, msgpack_sbuffer_write);

    // 打包一个整数
    msgpack_pack_int(&packer, 42);

    // 打包一个字符串
    msgpack_pack_str(&packer, 5);
    msgpack_pack_str_body(&packer, "hello", 5);

    // 现在buffer.data里就是二进制数据了
    printf("序列化后的数据长度: %zu 字节\n", buffer.size);

    // 清理
    msgpack_sbuffer_destroy(&buffer);
    return 0;
}

你看,代码很直观。先初始化一个缓冲区,然后创建一个打包器,接着用msgpack_pack_xxx系列函数往里塞数据。最后数据就在buffer.data里了。

小技巧: 如果你要打包多个字段,建议先定义一个结构体,然后用msgpack_pack_arraymsgpack_pack_map来组织数据。这样反序列化时也更容易对应上。

7.2.3 反序列化操作

反序列化同样简单。咱们把刚才打包的数据再解析回来:

#include <stdio.h>
#include <msgpack.h>

int main() {
    // 假设这是从网络或文件里收到的数据
    unsigned char data[] = {0x2a, 0xa5, 0x68, 0x65, 0x6c, 0x6c, 0x6f};
    size_t data_len = sizeof(data);

    // 创建反序列化对象
    msgpack_unpacked unpacked;
    msgpack_unpacked_init(&unpacked);

    // 开始解析
    if (msgpack_unpack_next(&unpacked, data, data_len, NULL) == MSGPACK_UNPACK_SUCCESS) {
        msgpack_object obj = unpacked.data;

        // 第一个对象是整数
        if (obj.type == MSGPACK_OBJECT_POSITIVE_INTEGER) {
            printf("整数: %lu\n", obj.via.u64);
        }

        // 继续解析下一个
        if (msgpack_unpack_next(&unpacked, data, data_len, NULL) == MSGPACK_UNPACK_SUCCESS) {
            obj = unpacked.data;
            if (obj.type == MSGPACK_OBJECT_STR) {
                printf("字符串: %.*s\n", (int)obj.via.str.size, obj.via.str.ptr);
            }
        }
    }

    msgpack_unpacked_destroy(&unpacked);
    return 0;
}

这里要注意一点:msgpack_unpack_next每次只解析一个对象。如果你打包了多个值,就需要多次调用它。我曾经在这个地方踩过坑——以为一次调用就能解析完所有数据,结果只拿到了第一个字段。

7.3 高效二进制序列化的核心原理

MsgPack为什么高效?咱们来拆解一下它的编码规则。

7.3.1 整数编码

MsgPack对整数做了非常精细的优化:

值范围 编码格式 占用字节
0 ~ 127 单字节(高位为0) 1字节
-32 ~ -1 单字节(高位为111) 1字节
其他8位整数 0xcc + 数据 2字节
其他16位整数 0xcd + 数据 3字节
其他32位整数 0xce + 数据 5字节
其他64位整数 0xcf + 数据 9字节

你看,对于小整数,MsgPack只用1个字节就搞定了。而JSON里,就算你只传一个"0",也得占2个字节(引号+数字)。这就是差距。

7.3.2 字符串编码

字符串的编码也很聪明。短字符串(长度小于32)只用一个字节表示长度+类型,后面直接跟字符串内容。长一点的字符串用额外字节存长度。

举个例子:字符串"hi"在MsgPack里是0xa2 0x68 0x69,总共3个字节。而在JSON里是"hi",4个字节。虽然只省了1个字节,但数据量大了以后,这个差距就很可观了。

7.4 实战:用MsgPack封装传感器数据

咱们来写一个实际点的例子。假设你有一个温度传感器,需要上报数据:

#include <stdio.h>
#include <string.h>
#include <msgpack.h>

typedef struct {
    int sensor_id;
    float temperature;
    float humidity;
    char location[32];
} sensor_data_t;

// 序列化传感器数据
void serialize_sensor_data(sensor_data_t *data, msgpack_sbuffer *buffer) {
    msgpack_packer packer;
    msgpack_packer_init(&packer, buffer, msgpack_sbuffer_write);

    // 打包成map,方便扩展
    msgpack_pack_map(&packer, 4);

    // sensor_id
    msgpack_pack_str(&packer, 9);
    msgpack_pack_str_body(&packer, "sensor_id", 9);
    msgpack_pack_int(&packer, data->sensor_id);

    // temperature
    msgpack_pack_str(&packer, 11);
    msgpack_pack_str_body(&packer, "temperature", 11);
    msgpack_pack_float(&packer, data->temperature);

    // humidity
    msgpack_pack_str(&packer, 8);
    msgpack_pack_str_body(&packer, "humidity", 8);
    msgpack_pack_float(&packer, data->humidity);

    // location
    msgpack_pack_str(&packer, 8);
    msgpack_pack_str_body(&packer, "location", 8);
    size_t len = strlen(data->location);
    msgpack_pack_str(&packer, len);
    msgpack_pack_str_body(&packer, data->location, len);
}

int main() {
    sensor_data_t data = {
        .sensor_id = 1,
        .temperature = 25.6f,
        .humidity = 68.3f,
        .location = "room_a"
    };

    msgpack_sbuffer buffer;
    msgpack_sbuffer_init(&buffer);

    serialize_sensor_data(&data, &buffer);

    printf("序列化后数据大小: %zu 字节\n", buffer.size);
    printf("同等JSON数据大约: 80+ 字节\n");

    // 这里可以把buffer.data发送到网络或写入文件

    msgpack_sbuffer_destroy(&buffer);
    return 0;
}

这个例子跑下来,序列化后的数据大概只有30多字节。同样的数据用JSON,至少80字节起步。在物联网场景下,这个差距意味着每月能省下不少流量费。

注意: 使用msgpack_pack_float时,要确保你的平台支持IEEE 754单精度浮点数。大多数现代处理器都支持,但一些老旧的嵌入式MCU可能有问题。我曾经在STM32F103上遇到过浮点数编码不一致的情况,后来改用整数(放大100倍)来传输温度值才解决。

7.5 知识体系总览

下面这张图帮你梳理了MsgPack的核心知识点:

MessagePack 二进制序列化格式 紧凑 · 快速 · 跨语言 msgpack-c 库 JSON的二进制版 自描述格式 整数优化编码 字符串紧凑存储 打包器/解包器 缓冲区管理 实战:传感器数据序列化

7.6 避坑指南

最后,分享几个我在实际项目中踩过的坑:

  • 字节序问题:MsgPack默认使用大端序(网络字节序)。如果你的MCU是小端序(比如ARM Cortex-M系列),编码解码时库会自动处理,但如果你自己手动解析二进制数据,一定要注意字节序转换。
  • 浮点数精度:MsgPack支持32位和64位浮点数。如果你用msgpack_pack_float打包,但接收端用msgpack_unpack_double解析,精度会丢失。我曾经因为这个原因,导致温度数据在服务器端显示总是差0.1度。
  • 内存管理msgpack_sbuffer内部会动态分配内存。如果你在中断服务函数里使用它,要小心内存分配失败的问题。我建议在初始化时预分配足够大的缓冲区,避免运行时动态扩展。

好了,关于MessagePack的内容就聊到这里。它确实是一个很实用的工具,尤其是在带宽和存储受限的嵌入式场景里。希望这些经验能帮你少走一些弯路。


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