19、序列化与物联网:MQTT协议中的序列化、传感器数据打包
说到物联网,我脑子里第一个蹦出来的就是MQTT协议。这玩意儿在嵌入式圈子里太常见了,几乎成了IoT通信的事实标准。MQTT本身轻量、省带宽,但真正让数据在设备间跑起来的,还是序列化这个幕后英雄。
说白了,传感器采集到的温度、湿度、气压这些原始数据,都是结构体或者数组。你要把它们发到云端,就得先打包成字节流。这个过程,就是序列化。反过来,云端下发的控制指令,也得解析成设备能识别的格式,这就是反序列化。
我在做智能家居项目时,遇到过不少因为序列化没处理好导致的通信问题。嗯,今天咱们就聊聊MQTT协议里的序列化实战,重点放在传感器数据打包上。
MQTT为什么需要序列化?
MQTT协议本身只定义了消息的发布/订阅模型,以及QoS等级、主题等控制字段。它不关心你发的payload里装的是什么。你可以发纯文本、JSON、XML,也可以发二进制流。
但物联网设备资源有限——RAM小、Flash小、CPU主频低。JSON这种文本格式虽然可读性好,但解析起来开销大,数据膨胀也严重。你想想看,一个温度值25.6,用JSON表示是"temperature":25.6,至少20个字节。如果用二进制序列化,一个float就4个字节。
所以,在嵌入式物联网场景下,我更倾向于用自定义的二进制序列化格式。效率高、体积小、解析快。
传感器数据打包的常见模式
传感器数据通常包含多个字段。比如一个温湿度传感器,数据可能长这样:
typedef struct {
uint8_t device_id; // 设备ID
uint16_t timestamp; // 时间戳(相对时间)
float temperature; // 温度
float humidity; // 湿度
uint8_t crc; // 校验和
} sensor_data_t;
这个结构体在内存里占多少字节?1 + 2 + 4 + 4 + 1 = 12字节。但别忘了结构体对齐。在我的经验里,很多新手在这里栽过跟头——结构体成员之间可能有填充字节,导致序列化出来的数据跟预期不一样。
注意:结构体对齐是序列化的头号陷阱。不同编译器、不同架构的对齐规则可能不同。直接memcpy发送结构体,在跨平台通信时必出问题。
我建议的做法是:手动按字节打包,不要依赖编译器对齐。下面是我常用的打包函数:
void pack_sensor_data(uint8_t *buf, const sensor_data_t *data) {
buf[0] = data->device_id;
buf[1] = (data->timestamp >> 8) & 0xFF;
buf[2] = data->timestamp & 0xFF;
// 温度:float转4字节,大端序
uint32_t temp_bits;
memcpy(&temp_bits, &data->temperature, sizeof(float));
buf[3] = (temp_bits >> 24) & 0xFF;
buf[4] = (temp_bits >> 16) & 0xFF;
buf[5] = (temp_bits >> 8) & 0xFF;
buf[6] = temp_bits & 0xFF;
// 湿度同理
uint32_t hum_bits;
memcpy(&hum_bits, &data->humidity, sizeof(float));
buf[7] = (hum_bits >> 24) & 0xFF;
buf[8] = (hum_bits >> 16) & 0xFF;
buf[9] = (hum_bits >> 8) & 0xFF;
buf[10] = hum_bits & 0xFF;
// CRC
buf[11] = data->crc;
}
你看,每个字节都明确指定了位置和顺序。这样不管在什么平台上,打包出来的数据都是一致的。
MQTT消息中的序列化实践
MQTT的payload就是一段字节数组。我们把打包好的二进制数据直接塞进去就行。发布消息的代码大概是这样:
uint8_t mqtt_buf[64];
sensor_data_t sensor;
// 采集传感器数据
sensor.device_id = 0x01;
sensor.timestamp = get_relative_time();
sensor.temperature = read_temperature();
sensor.humidity = read_humidity();
sensor.crc = calc_crc8((uint8_t*)&sensor, sizeof(sensor) - 1);
// 打包
pack_sensor_data(mqtt_buf, &sensor);
// 发布到MQTT主题
mqtt_publish("sensor/data", mqtt_buf, 12, QOS1);
这里有个细节:CRC校验。我曾经在一个项目中忽略了校验,结果数据在传输过程中被干扰,云端收到了错误的数据,导致整个控制逻辑跑偏。从那以后,我每个传感器数据包都带CRC。
小技巧:CRC8算法简单,适合资源受限的设备。可以用查表法实现,速度很快。我一般用多项式0x07(CRC-8-ATM)。
反序列化:从字节流还原数据
接收端拿到字节流后,要按同样的规则解析回来。注意,顺序和大小端必须跟打包时完全一致。
void unpack_sensor_data(const uint8_t *buf, sensor_data_t *data) {
data->device_id = buf[0];
data->timestamp = ((uint16_t)buf[1] << 8) | buf[2];
// 温度
uint32_t temp_bits = ((uint32_t)buf[3] << 24) |
((uint32_t)buf[4] << 16) |
((uint32_t)buf[5] << 8) |
buf[6];
memcpy(&data->temperature, &temp_bits, sizeof(float));
// 湿度同理
uint32_t hum_bits = ((uint32_t)buf[7] << 24) |
((uint32_t)buf[8] << 16) |
((uint32_t)buf[9] << 8) |
buf[10];
memcpy(&data->humidity, &hum_bits, sizeof(float));
// CRC
data->crc = buf[11];
}
解析完后,别忘了校验CRC。如果CRC对不上,说明数据在传输过程中被篡改或损坏了,应该丢弃这条消息。
更灵活的方案:TLV格式
如果传感器数据类型经常变化,或者需要支持多种传感器,我推荐用TLV(Type-Length-Value)格式。每个字段都带一个类型标识和长度,解析起来更灵活。
// TLV格式示例
// [Type:1字节][Length:2字节][Value:Length字节]
typedef enum {
TLV_TYPE_TEMPERATURE = 0x01,
TLV_TYPE_HUMIDITY = 0x02,
TLV_TYPE_PRESSURE = 0x03,
TLV_TYPE_TIMESTAMP = 0x04,
} tlv_type_t;
TLV的好处是:你可以只发送有变化的传感器数据,不用每次都发完整结构体。比如温度变了,就只发温度字段。云端收到后,根据Type知道是什么数据,根据Length知道数据长度,然后解析Value。
我在一个农业物联网项目里用过TLV,传感器种类有十几种,每种数据长度还不一样。用TLV后,协议扩展变得非常简单,加一种新传感器只需要定义一个新的Type值就行。
序列化与MQTT QoS的配合
MQTT有3个QoS等级。序列化后的数据包大小直接影响QoS的选择。QoS0不确认,适合丢包不敏感的数据;QoS1至少一次,适合大部分传感器数据;QoS2恰好一次,适合控制指令。
我个人习惯:传感器上报用QoS0或QoS1,控制指令用QoS2。序列化后的数据包越小,QoS2的额外开销占比就越大。所以控制指令的序列化要尽量精简。
SVG:传感器数据序列化与MQTT通信流程
避坑指南:我踩过的几个坑
- 大小端问题:我曾经在ARM Cortex-M和x86服务器之间通信,ARM默认小端,x86也是小端,但有些MIPS或PowerPC是大端。所以统一用大端序(网络字节序)最保险。
- 浮点数精度:float在IEEE754标准下是4字节,但不同平台对NaN、Infinity的处理可能不同。我建议传感器数据用定点数代替浮点数,比如温度乘以100存为int16_t。
- CRC校验范围:CRC要覆盖整个数据包,包括设备ID和时间戳。我见过有人只校验数据部分,结果头部被篡改了也不知道。
- MQTT消息长度:MQTT最大payload是256MB,但实际网络中MTU通常只有1500字节。如果传感器数据包太大,要考虑分片或者压缩。
总结
MQTT协议中的序列化,说白了就是让传感器数据在设备间高效、可靠地传输。二进制序列化是嵌入式物联网的首选,TLV格式提供了灵活性,CRC校验保证了数据完整性。
我在实际项目中,通常会把序列化和反序列化封装成独立的模块,这样换协议或者换传感器时,只需要改这个模块就行。嗯,代码复用性很重要。
最后提醒一句:序列化方案确定后,一定要写文档。别问我为什么——我曾经看着自己半年前写的代码,愣是没看懂打包格式。好记性不如烂笔头啊。
公众号:蓝海资料掘金营,微信deep3321