4、日志传输协议设计:MQTT/CoAP/HTTP协议选型、QoS策略、断点续传与重传机制
日志传输,说白了就是设备怎么把肚子里的话「说」给云平台听。选什么协议、怎么保证不丢、断了怎么续——这些坑我基本都踩过一遍。今天咱们就掰开揉碎聊聊。
4.1 三大协议选型:MQTT vs CoAP vs HTTP
先给结论:物联网日志传输,MQTT 是首选,CoAP 是备胎,HTTP 是最后的选择。为什么?我一个个说。
| 特性 | MQTT | CoAP | HTTP |
|---|---|---|---|
| 传输层 | TCP(可靠) | UDP(轻量) | TCP(可靠) |
| 协议开销 | 2字节最小头 | 4字节最小头 | 头部较大 |
| QoS支持 | 0/1/2 三级 | 0/1 两级 | 无原生QoS |
| 适用场景 | 频繁上报、双向通信 | 极低功耗、受限网络 | 大包上传、非实时 |
| 断点续传 | 需应用层实现 | 需应用层实现 | Range头支持 |
MQTT 是我个人最常用的。它天生为物联网设计,发布/订阅模型让日志路由特别灵活。我在一个智能电表项目里,每天几万台设备上报日志,MQTT 扛得稳稳的。QoS 1 基本够用,QoS 2 太费资源,一般不用。
CoAP 呢?它基于 UDP,适合 NB-IoT 这类窄带场景。但有个问题——日志通常需要可靠传输,CoAP 的 QoS 1 只保证「至少一次」,可能重复。我建议只在电池供电、上报频率极低(比如一天一次)的设备上用。
HTTP 嘛……说实话,我一般只在设备固件升级、大文件日志导出时才用。它的 Range 头天然支持断点续传,这点比 MQTT 方便。但每次请求都要建连,对频繁上报来说太奢侈了。
我的选型建议:
- WiFi/以太网设备 → MQTT(QoS 1)
- NB-IoT/低功耗设备 → CoAP(应用层加确认)
- 大文件/固件升级 → HTTP(Range续传)
4.2 QoS 策略:不是越高越好
很多新手一上来就选 QoS 2,觉得「最可靠」。其实这是个误区。QoS 2 的确认机制很重,网络差的时候反而容易堵死。
我遇到过最典型的案例:一个温湿度传感器项目,选了 QoS 2,结果设备在弱网环境下频繁重传,电池三天就耗光了。后来改成 QoS 1 + 应用层去重,电池撑了半年。
那怎么选?我总结了个经验:
- QoS 0:日志不重要,丢了也无所谓(比如调试日志)
- QoS 1:大部分场景的首选。保证至少一次,但可能重复。应用层做去重就行。
- QoS 2:只有金融、医疗等对数据一致性要求极高的场景才用。代价是吞吐量下降 30%-50%。
小技巧: 我习惯在日志消息里加一个全局唯一的序列号(比如设备ID+时间戳+自增序号)。这样即使 QoS 1 重复投递,服务端也能轻松去重。成本极低,效果极好。
4.3 断点续传:日志断了怎么办?
设备上报日志时,网络突然断了——这是家常便饭。断点续传的核心思路就一句话:记录进度,下次接着传。
具体怎么做?我分 MQTT 和 HTTP 两种情况说。
4.3.1 MQTT 场景下的断点续传
MQTT 本身没有断点续传的概念。我一般这样实现:
- 设备本地维护一个
log_offset文件,记录已成功上报的日志位置。 - 每次上报时,从
log_offset开始读取日志。 - 上报成功后,更新
log_offset。 - 如果上报失败,下次重连后从上次位置继续。
代码示意(伪代码):
// 伪代码:MQTT断点续传
uint32_t offset = read_offset_from_flash();
while (has_more_log(offset)) {
char* log_data = read_log_from(offset, MAX_PACKET_SIZE);
int ret = mqtt_publish("device/log", log_data, QoS1);
if (ret == SUCCESS) {
offset += strlen(log_data);
save_offset_to_flash(offset);
} else {
// 网络异常,等待重连
break;
}
}
注意: 写入 flash 的频率不能太高。我建议每成功上报 10 条日志才写一次 offset,或者用定时器每 30 秒写一次。否则 flash 很快就写坏了。
4.3.2 HTTP 场景下的断点续传
HTTP 就简单多了。用 Range 头就行:
// 客户端请求:从第1024字节开始续传
GET /upload/log.bin HTTP/1.1
Host: cloud.example.com
Range: bytes=1024-
服务端返回 206 Partial Content,带上剩余数据。客户端收到后继续追加写入本地文件。
我记得有个项目,设备日志文件动不动就几十 MB,用 HTTP Range 续传,配合 MD5 校验,从来没出过问题。
4.4 重传机制:别让设备「死磕」
重传机制设计不好,设备容易陷入「死循环」。我见过最夸张的案例:一个设备在弱网环境下,重传了 200 多次,最后把云平台的连接数打满了。
我的经验是:指数退避 + 最大重试次数。
具体策略:
- 第一次重试:等 1 秒
- 第二次重试:等 2 秒
- 第三次重试:等 4 秒
- ……
- 最大间隔:不超过 60 秒
- 最大重试次数:5 次(超过后丢弃日志,记录失败计数)
代码示例:
// 伪代码:指数退避重传
int max_retries = 5;
int base_delay = 1; // 秒
for (int i = 0; i < max_retries; i++) {
int ret = mqtt_publish(topic, payload, QoS1);
if (ret == SUCCESS) {
break;
}
int delay = min(base_delay * (1 << i), 60);
sleep(delay);
}
// 超过最大重试次数,记录日志到本地缓存
if (i == max_retries) {
save_to_local_cache(payload);
log_error("日志上报失败,已缓存到本地");
}
核心原则:
- 不要无限重试——设备资源有限
- 重试间隔要递增——避免网络拥塞
- 失败后要有降级策略——比如缓存到本地,等网络好了再上报
4.5 综合方案:一个实际案例
最后,我分享一个实际项目的方案。一个工业网关项目,每天上报 10 万条日志:
- 协议:MQTT,QoS 1
- 去重:每条日志带 64 位唯一 ID(设备 MAC + 时间戳 + 序号)
- 断点续传:本地 SQLite 记录已上报的日志 ID,断网后从断点续传
- 重传:指数退避,最多重试 5 次,失败后写入本地缓存文件
- 缓存策略:本地最多缓存 1000 条日志,超过则丢弃最旧的
这个方案跑了两年,日志丢失率低于 0.01%。嗯,基本可以忽略不计。
好了,日志传输协议这块就聊到这儿。下一节咱们讲讲日志格式设计——JSON 还是二进制?怎么压缩?到时候见。