第4章:通信协议设计——基于MQTT/HTTP的升级包下发
好,咱们接着聊。上一章讲了升级包的存储结构,那包是怎么从云端下来的呢?这就涉及到通信协议的选择了。我个人习惯把通信协议比作「运输车」——MQTT是那种轻便的快递三轮车,HTTP则是标准厢式货车。选哪个,得看你的路况和货物大小。
4.1 协议选型:MQTT vs HTTP
先说说MQTT。我在做一款便携式制氧机时,设备用的是2G模组,带宽窄、流量贵。MQTT的报文头只有2个字节,心跳保活机制也很省电。说白了,它天生为物联网而生。
HTTP呢?我遇到过客户要求用HTTPS下载固件,因为他们的安全合规部门只认TLS。HTTP的优势在于穿透性好,防火墙基本都放行,而且支持断点续传——这个在升级大文件时特别重要。
我的建议是:
- 小包升级(<1MB):优先MQTT,省流量、省电
- 大包升级(>1MB):用HTTP,支持断点续传更靠谱
- 混合方案:MQTT下发升级通知和元信息,HTTP拉取实际固件包
4.2 数据包格式定义
不管用哪种协议,数据包格式必须统一。我习惯把包分成三层:
| 层级 | 名称 | 说明 |
|---|---|---|
| L1 | 传输层 | MQTT Topic / HTTP URL + Header |
| L2 | 应用层 | JSON 元数据 + 二进制固件块 |
| L3 | 校验层 | CRC32 / SHA256 签名 |
嗯,这里重点讲L2应用层的格式。我定义了一个通用结构:
{
"cmd": "ota_update", // 命令类型
"version": "2.1.0", // 目标版本
"fw_size": 524288, // 固件总大小(字节)
"block_size": 1024, // 每块大小
"block_total": 512, // 总块数
"block_index": 0, // 当前块索引(从0开始)
"block_data": "base64...", // 块数据(Base64编码)
"checksum": "sha256hex...", // 整包SHA256
"timestamp": 1712345678 // 时间戳,防重放
}
- 分块传输:每块独立校验,失败只重传该块
- Base64编码:避免二进制数据在JSON中乱码
- 时间戳:防止攻击者截获旧包重放
4.3 MQTT主题设计
MQTT的主题(Topic)设计得好,能省很多事。我一般按设备维度来组织:
// 下行(云端→设备)
ota/{device_id}/cmd // 升级命令
ota/{device_id}/data // 固件数据块
// 上行(设备→云端)
ota/{device_id}/status // 升级状态上报
ota/{device_id}/log // 升级日志
为什么会这样设计?因为MQTT支持通配符订阅。云端可以订阅 ota/+/status 来监控所有设备的升级进度。我在一个项目里管理3000多台制氧机,这个模式帮了大忙。
4.4 HTTP接口设计
用HTTP时,我倾向于RESTful风格。接口定义如下:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /api/v1/ota/{device_id}/info | 查询是否有新版本 |
| GET | /api/v1/ota/{device_id}/firmware | 下载固件(支持Range头) |
| POST | /api/v1/ota/{device_id}/report | 上报升级结果 |
这里重点说断点续传。HTTP的 Range 头天然支持:
// 设备请求:从第1024字节开始下载
GET /api/v1/ota/device_001/firmware HTTP/1.1
Range: bytes=1024-2047
// 服务器响应
HTTP/1.1 206 Partial Content
Content-Range: bytes 1024-2047/524288
Content-Length: 1024
[二进制数据...]
我个人习惯在设备端维护一个 downloaded_size 变量,每次启动下载时先检查本地已存了多少字节,然后从断点处继续。这样即使下载过程中断电重启,也不会从头再来。
4.5 安全校验:防篡改与防重放
通信协议设计里,安全是绕不开的坎。我总结了三道防线:
- 传输层加密:MQTT over TLS / HTTPS,这是基础
- 应用层签名:固件包用私钥签名,设备用公钥验签
- 防重放:每个包携带时间戳+随机数nonce
签名流程(我常用的方案):
1. 云端计算固件包的SHA256哈希
2. 用RSA私钥对哈希值加密,得到签名
3. 将签名附加在固件包尾部下发
4. 设备收到后,用公钥解密签名得到哈希值
5. 设备本地计算固件包哈希,比对是否一致
你可能会问:为什么不用MD5?我在2018年做过一次安全审计,发现MD5已经被破解了——攻击者可以构造两个不同文件但MD5相同。从那以后,我所有项目都强制用SHA256。
4.6 实战中的坑与对策
最后分享几个我踩过的坑:
- 坑1:MQTT QoS设置不当——QoS 0可能丢包,QoS 2可能阻塞。我一般用QoS 1 + 应用层ACK确认。
- 坑2:HTTP超时设置——下载大固件时,默认30秒超时肯定不够。我习惯设成300秒,并且每30秒发一次心跳。
- 坑3:时间戳不同步——设备RTC不准,导致防重放校验失败。解决方案:用服务器下发的timestamp,设备不自己生成。
好了,通信协议这块就聊到这儿。下一章咱们讲升级过程中的状态机管理——说白了,就是设备在升级时到底在干什么,每一步该怎么走。到时候见。