第4章:通信协议设计——基于MQTT/HTTP的升级包下发

好,咱们接着聊。上一章讲了升级包的存储结构,那包是怎么从云端下来的呢?这就涉及到通信协议的选择了。我个人习惯把通信协议比作「运输车」——MQTT是那种轻便的快递三轮车,HTTP则是标准厢式货车。选哪个,得看你的路况和货物大小。

4.1 协议选型:MQTT vs HTTP

先说说MQTT。我在做一款便携式制氧机时,设备用的是2G模组,带宽窄、流量贵。MQTT的报文头只有2个字节,心跳保活机制也很省电。说白了,它天生为物联网而生。

HTTP呢?我遇到过客户要求用HTTPS下载固件,因为他们的安全合规部门只认TLS。HTTP的优势在于穿透性好,防火墙基本都放行,而且支持断点续传——这个在升级大文件时特别重要。

我的建议是:

  • 小包升级(<1MB):优先MQTT,省流量、省电
  • 大包升级(>1MB):用HTTP,支持断点续传更靠谱
  • 混合方案:MQTT下发升级通知和元信息,HTTP拉取实际固件包
实战技巧:我曾经在一个项目中,MQTT的QoS设成了2(保证到达),结果设备在弱网环境下反复重传,把电池耗光了。后来改成QoS 1 + 应用层ACK确认,问题解决。你想想看,协议本身没问题,但场景适配很重要。

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多台制氧机,这个模式帮了大忙。

注意:MQTT的Payload大小有限制(通常256KB)。如果固件包超过这个限制,必须分块发送。我曾经见过有人把整个4MB固件塞进一个MQTT报文里,结果服务器直接断开连接——嗯,血的教训。

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 安全校验:防篡改与防重放

通信协议设计里,安全是绕不开的坎。我总结了三道防线:

  1. 传输层加密:MQTT over TLS / HTTPS,这是基础
  2. 应用层签名:固件包用私钥签名,设备用公钥验签
  3. 防重放:每个包携带时间戳+随机数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,设备不自己生成。
避坑指南:我曾经在一个项目中,设备上报升级进度时用了「百分比」字段。结果因为整数除法精度问题,进度条卡在99%不动了。后来改成上报「已下载字节数」和「总字节数」,由云端计算百分比。嗯,细节决定成败。

好了,通信协议这块就聊到这儿。下一章咱们讲升级过程中的状态机管理——说白了,就是设备在升级时到底在干什么,每一步该怎么走。到时候见。