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 本身没有断点续传的概念。我一般这样实现:

  1. 设备本地维护一个 log_offset 文件,记录已成功上报的日志位置。
  2. 每次上报时,从 log_offset 开始读取日志。
  3. 上报成功后,更新 log_offset
  4. 如果上报失败,下次重连后从上次位置继续。

代码示意(伪代码):

// 伪代码: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 还是二进制?怎么压缩?到时候见。