3. 传输协议优化:选对路子,省一半时间

传输协议这块,说白了就是固件从服务器到设备走哪条路。我见过太多项目,固件升级慢得离谱,结果一查,根本不是带宽问题,是协议没选对。今天咱们就把 HTTP/HTTPS、MQTT 掰开揉碎聊清楚,再讲讲断点续传和多线程分片这些实战技巧。

核心观点:没有最好的协议,只有最合适的场景。HTTP 适合大文件、高吞吐;MQTT 适合小包、低功耗、弱网络。选错了,后面怎么优化都白搭。

3.1 HTTP/HTTPS vs MQTT:到底差在哪?

先看一张对比表,心里有个底:

对比维度 HTTP/HTTPS MQTT
传输模型 请求-响应(客户端主动拉) 发布-订阅(服务器主动推)
协议开销 头部大(300-800 字节) 头部极小(2-14 字节)
连接方式 短连接为主,每次请求新建 TCP 长连接,保持心跳
适用文件大小 任意大小(支持 Range 分片) 建议 < 10MB(消息体限制)
断点续传 原生支持(HTTP Range) 需应用层自己实现
安全性 HTTPS(TLS 加密) MQTT over TLS
典型场景 手机 App、PC 端固件升级 IoT 传感器、低功耗设备

我个人习惯,如果设备内存够(> 512KB)、网络稳定,优先上 HTTPS。为什么?因为 HTTP 的 Range 头天然支持断点续传,省去很多应用层开发工作。但如果是电池供电的 IoT 设备,MQTT 的低功耗优势就体现出来了——一次连接可以反复用,不像 HTTP 每次都要三次握手。

我的经验:曾经有个智能门锁项目,用 HTTPS 做升级,每次都要重新建立 TLS 握手,光握手就耗掉 2-3 秒。换成 MQTT 长连接后,升级时间直接砍半。但注意,MQTT 的 QoS 级别要选对——QoS 1 就够了,QoS 2 太费流量。

3.2 断点续传机制:别让用户从头再来

断点续传,说白了就是「上次传到哪里,这次接着传」。你想想看,一个 10MB 的固件,下载到 90% 突然断网了,要是从头再来,用户不骂娘才怪。

HTTP 的断点续传实现非常简单,核心就是 Range 请求头:

// 客户端请求:从第 1024 字节开始下载
GET /firmware_v2.bin HTTP/1.1
Host: ota.example.com
Range: bytes=1024-

// 服务器响应
HTTP/1.1 206 Partial Content
Content-Range: bytes 1024-1048575/1048576
Content-Length: 1047552

客户端需要做的就三件事:

  1. 记录已下载字节数——写到 Flash 的特定扇区,或者存到文件系统里
  2. 发起 Range 请求——带上已下载的偏移量
  3. 校验续传内容——用哈希值确认数据没被篡改

避坑指南:我曾经遇到过一个坑——服务器返回的是 200 而不是 206。查了半天,发现是 CDN 没配置支持 Range。所以上线前一定要用 curl 测试一下:curl -I -H "Range: bytes=0-100" http://your-server/firmware.bin,看返回是不是 206。

MQTT 的断点续传就麻烦一些。因为 MQTT 本身没有 Range 概念,你得自己在应用层实现。我的做法是这样的:

  • 把固件分成固定大小的块(比如 1KB 一块)
  • 每个块分配一个序号(0, 1, 2, ...)
  • 客户端收到块后,把序号写入 Flash 位图
  • 重连后,客户端发布一个请求消息,告诉服务器「我收到第 0-500 块了,从 501 开始发」
// MQTT 断点续传示例(伪代码)
// 客户端请求续传
topic: device/001/ota/resume
payload: {"device_id":"001","last_block":500}

// 服务器响应
topic: device/001/ota/data
payload: {"block_id":501,"data":"base64_encoded_chunk"}

3.3 多线程下载与分片策略:把带宽吃满

单线程下载,说白了就是「一条路走到黑」。网络延迟高的时候,大部分时间都在等 ACK。多线程下载就是同时开几条路,把带宽利用率拉满。

分片策略的核心问题就两个:分多大?分多少片?

参数 推荐值 说明
分片大小 16KB - 256KB 太小了增加请求次数,太大了失去分片意义
并发线程数 2 - 4(嵌入式设备) 取决于 CPU 性能和内存大小
分片对齐 按 Flash 扇区对齐 避免跨扇区写入,减少擦除次数

我建议的分片下载流程是这样的:

  1. 获取固件元信息——总大小、哈希值、分片数
  2. 计算分片范围——每个线程负责一段连续的字节范围
  3. 并行发起 Range 请求——每个线程独立下载自己的分片
  4. 写入 Flash——按分片偏移量直接写入,不需要排序
  5. 校验完整性——所有分片下载完成后,计算整体哈希
// 多线程分片下载核心逻辑(C 伪代码)
#define CHUNK_SIZE 65536  // 64KB 一片
#define NUM_THREADS 4

typedef struct {
    uint32_t start;
    uint32_t end;
    uint8_t *buffer;
} download_chunk_t;

void ota_download_multi_thread(const char *url, uint32_t total_size) {
    uint32_t chunk_count = total_size / CHUNK_SIZE;
    if (total_size % CHUNK_SIZE) chunk_count++;
    
    // 分配任务给 4 个线程
    for (int i = 0; i < NUM_THREADS; i++) {
        for (int j = i; j < chunk_count; j += NUM_THREADS) {
            download_chunk_t chunk;
            chunk.start = j * CHUNK_SIZE;
            chunk.end = min(chunk.start + CHUNK_SIZE, total_size) - 1;
            // 发起 HTTP Range 请求
            http_range_get(url, chunk.start, chunk.end, chunk.buffer);
            // 直接写入 Flash(按偏移量)
            flash_write(chunk.start, chunk.buffer, chunk.end - chunk.start + 1);
        }
    }
}

我的经验:分片大小我一般选 64KB。为什么?因为很多 Flash 芯片的页大小就是 64KB,对齐了写入效率最高。另外,并发数别贪多——嵌入式设备内存本来就紧张,4 个线程已经能跑满大部分 4G 模块的带宽了。我曾经试过开 8 个线程,结果内存爆了,系统直接重启。

3.4 综合策略:什么时候用什么方案?

说了这么多,到底怎么选?我画了张决策图,你一看就明白:

传输协议与分片策略决策流程 固件升级需求 设备类型? 低功耗 / 弱网络 MQTT + 应用层续传 高性能 / 大文件 HTTPS + Range 续传 单线程下载 分片大小? 内存 < 256KB 16KB 分片 / 2 线程 内存 >= 256KB 64KB 分片 / 4 线程 校验 → 升级 说明 MQTT 场景:固件 < 2MB HTTPS 场景:固件 >= 2MB

最后说一句,传输优化不是孤立的。协议选好了,分片策略定好了,还得跟后面的 Flash 写入、校验策略配合。比如分片大小最好跟 Flash 扇区对齐,不然写入的时候要多做一次读-改-写,性能直接打折扣。

我曾经踩过的坑:有个项目用 MQTT 传 8MB 的固件,分片设成 256 字节一片,结果光消息头就占了 30% 的流量。后来改成 4KB 一片,流量直接降下来。记住,MQTT 的消息头虽然小,但架不住数量多——分片太小,协议开销反而会吃掉带宽。