OpenHarmony海思WS63星闪平台:使用Mongoose 网路库文件下载封装与断点续传实现

文件下载是常见的需求,在嵌入式中使用Mongoose 网路库可以进行一次封装更通用些,且受限于网络环境下载可能中断,本工程实践实现一种断点续传方案。


本文对应工程实现:src/network/http_lfs_download.c / http_lfs_download.h。在 Mongoose 事件模型上做了一层阻塞式 封装:HTTP 流式写入 LittleFS (经 littlefs_adapt / fs_adapt_*),支持 Range 断点续传临时文件 + rename 原子落盘 、以及 Content-Length / Content-Range 层面的完整性校验。

1. 设计目标与适用场景

目标 说明
与业务解耦 不绑定字库/OTA;上层只传 urlbase_dirbasename
断点续传 存在未完成的临时文件时,带 Range: bytes=N- 请求,服务端返回 206 后从偏移 N 追加写
原子替换 默认先写 basename.part(后缀可配),成功后再 rename 覆盖正式文件,避免半成品直接顶替旧文件
校验 已解析到期望总大小时,对比「已写字节数」与「最终文件 stat 大小」

约束(与当前工程 Mongoose 配置一致):

  • URL 仅支持 http://MG_TLS=0 时无 HTTPS)。
  • basename 不得含 /..,长度受限(实现中 ≤ 48)。
  • 需已挂载 LittleFS,且 fs_adapt_* 可用。

2. 对外 API

2.1 参数结构体 http_lfs_download_param_t

c 复制代码
typedef struct {
    const char *url;          /* 仅 http:// */
    const char *base_dir;     /* 如 "/system" */
    const char *basename;     /* 本地文件名,如 "font.bin" */
    const char *part_suffix;  /* ATOMIC 时临时文件后缀;NULL/"" 等价 ".part";DIRECT 时忽略 */
    http_lfs_store_mode_t store_mode;
} http_lfs_download_param_t;

2.2 落盘模式 http_lfs_store_mode_t

  • HTTP_LFS_STORE_ATOMIC(默认 0)

    • 写入路径:base_dir/basename + part_suffix(默认 basename.part)。
    • 成功后:delete(正式)rename(临时 → 正式)
    • 中断时:正式文件仍为上一次完整版本(若曾成功过);临时文件保留,供下次续传。
  • HTTP_LFS_STORE_DIRECT(1)

    • 直接写 base_dir/basename临时文件与 rename。
    • 中断可能留下不完整 正式文件;断点逻辑仍可能基于该文件大小发 Range(若你选择 DIRECT 且文件已存在非零大小,行为与 ATOMIC 类似,但损坏的「正式」文件会参与续传,需自行权衡)。

2.3 阻塞接口

c 复制代码
int http_lfs_download_blocking(const http_lfs_download_param_t *param);
返回值 含义
0 成功
-1 失败(参数、HTTP 非 200/206、写盘、rename、校验失败等)
HTTP_LFS_RES_FALLBACK_FULL(-2) 仅内部使用 :如 416 Range Not SatisfiableContent-Range 与本地偏移不一致 时触发删掉临时文件并全量重下http_lfs_download_blocking 内部 goto retry_connect 后对外仍返回 0 或 -1

调用线程内会 mg_mgr_initmg_http_connect → 轮询 mg_mgr_poll + osal_msleep(1) 直到完成或总超时,属于同步阻塞封装,适合独立下载任务线程。


3. 断点续传实现要点

3.1 何时启用 Range

http_lfs_download_blocking 中:

  1. fs_adapt_stat(临时路径, &sz) 成功且 sz > 0 ,且本会话尚未禁用 Range,则:
    • resume_offset = sz
    • try_range = true
  2. 续传(无临时文件或大小为 0):会 fs_adapt_delete(临时路径) ,从空文件开始全量下载(200 + O_TRUNC)。

临时路径在 ATOMIC 模式下为 base_dir/basename + part_suffix(默认 .part)。

3.2 HTTP 请求

MG_EV_CONNECT 中若 try_range && resume_offset > 0,在 GET 中增加:

http 复制代码
Range: bytes=<resume_offset>-

仍带 HostConnection: closeAccept: */* (与当前源码一致;若需与 font_download.c 一样去掉 Connection: close 或加 User-Agent,可在此统一改)。

3.3 响应处理

  • 416 :认为 Range 无法满足,fd_abort(..., HTTP_LFS_RES_FALLBACK_FULL) ,外层 关闭本次连接重新发起一次全量下载 (并 range_disabled_for_session 避免再次 Range 死循环)。
  • 200 / 206 以外:失败。
  • 206 Partial Content
    • 必须存在 Content-Range ,且解析出的区间起点 bytes <start>-<end>/<total>resume_offset 一致,否则 回退全量
    • 打开临时文件 O_WRONLY | O_CREAT O_TRUNC),fs_adapt_seek(fd, resume_offset, SEEK_SET)bytes_written 初始化为 resume_offset
    • expected_bytes = Content-Length + resume_offset(整文件逻辑总字节数)。
  • 200 OK (服务端忽略 Range):O_TRUNC 从头写;若本地曾以为要续传,日志会提示 server ignored range

3.4 传输编码

  • 支持 Content-Lengthchunked直到关闭(无 CL 非 chunked)。
  • 断点续传 依赖明确的 CL + 206 + Content-Range 组合做总长度校验;chunked/Until-Close 路径下 expected_known 可能为 false,校验策略与 CL 路径不同(见源码 fd_finish_ok)。

3.5 完成与校验

fd_finish_ok 中:

  1. expected_knownbytes_written 必须等于 expected_bytes
  2. ATOMICrename 临时 → 正式 后,对 final_pathfs_adapt_stat ,与 expected_bytes 比对。
  3. 失败则删正式/临时并置错。

4. 内部结构(便于对照源码)

  • hd_ctx_t :保存 URL、base_dir / final_path / path(当前写入路径)resume_offsettry_rangebytes_writtenexpected_bytesmode(CL/chunked/close)等。
  • hd_ev_handler :处理 MG_EV_OPEN / MG_EV_POLL(连接超时、正文空闲超时) / MG_EV_CONNECT(发 GET) / MG_EV_HTTP_HDRS(解析状态与头、打开文件) / MG_EV_READ / MG_EV_CLOSE / MG_EV_ERROR
  • 正文消费逻辑与早期 font_download.c 同类:recv 缓冲写 Flash、删已消费字节、更新 last_body_ms

可调宏(源码顶部,可按 WiFi 大文件经验调大,与 font_download.c 思路类似):

  • HD_BODY_IDLE_MS(默认 30000):两次收到 body 数据的最大间隔。
  • HD_OVERALL_MS(默认 300000):单次连接总等待时间。
  • HD_MG_POLL_MS (默认 10):mg_mgr_poll 阻塞参数,可通过 #ifndef 覆盖。

代码封装实现:

c 复制代码
/**
 * @file http_lfs_download.c
 * @brief Mongoose HTTP:流式下载到 LittleFS(Range、校验、.part 原子替换),与业务无关。
 */

#include "http_lfs_download.h"

 #include <ctype.h>
 #include <fcntl.h>
 #include <limits.h>
 #include <stdbool.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <string.h>
 
 #include "cmsis_os2.h"
 #include "littlefs_adapt.h"
 #include "mongoose_config.h"
 #include "mongoose.h"
 #include "soc_osal.h"
 
 #if defined(__has_include)
 #if __has_include("mongoose_protocol.h")
 #include "mongoose_protocol.h"
 #define FD_MG_LOCK()   mongoose_api_lock()
 #define FD_MG_UNLOCK() mongoose_api_unlock()
 #endif
 #endif
 #ifndef FD_MG_LOCK
 #define FD_MG_LOCK()   ((void)0)
 #define FD_MG_UNLOCK() ((void)0)
 #endif
 
#define HD_BODY_IDLE_MS      30000UL
#define HD_CONNECT_MS        30000UL
#define HD_OVERALL_MS        300000UL
#ifndef HD_MGR_POLL_MS
#define HD_MGR_POLL_MS       10
#endif
#define HD_PATH_MAX          96
#define HD_URL_MAX           256
 
 typedef enum {
     FD_BODY_NONE = 0,
     FD_BODY_CONTENT_LENGTH,
     FD_BODY_CHUNKED,
     FD_BODY_UNTIL_CLOSE,
 } fd_body_mode_t;
 
 typedef struct {
     int fd;
     int result;
     volatile bool done;
 
     fd_body_mode_t mode;
     size_t content_remaining;
     size_t chunk_payload_left;
 
     bool hdrs_ok;
     uint64_t last_body_ms;
     uint64_t connect_deadline_ms;
     size_t bytes_written;
     size_t expected_bytes;
     bool expected_known;
     bool try_range;
     size_t resume_offset;
     char base_dir[HD_PATH_MAX];
     char final_path[HD_PATH_MAX];
     char path[HD_PATH_MAX];
     char url_buf[HD_URL_MAX];
     bool direct_write;
 } hd_ctx_t;
 
 static int fd_parse_content_length(const struct mg_str *s, size_t *out)
 {
     char tmp[32];
     if (s == NULL || s->buf == NULL) {
         return -1;
     }
     size_t n = s->len < sizeof(tmp) - 1U ? s->len : sizeof(tmp) - 1U;
     memcpy(tmp, s->buf, n);
     tmp[n] = '\0';
     char *endp = NULL;
     unsigned long v = strtoul(tmp, &endp, 10);
     if (endp == tmp) {
         return -1;
     }
     *out = (size_t)v;
     return 0;
 }
 
 static bool fd_is_hex_line_char(int c)
 {
     return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
 }
 
 static int fd_parse_chunk_size_line(const char *s, size_t line_len, unsigned long *size_out)
 {
     size_t i = 0;
     while (i < line_len && fd_is_hex_line_char((unsigned char)s[i])) {
         i++;
     }
     if (i == 0U) {
         return -1;
     }
     char tmp[12];
     if (i >= sizeof(tmp)) {
         return -1;
     }
     memcpy(tmp, s, i);
     tmp[i] = '\0';
     char *endp = NULL;
     unsigned long v = strtoul(tmp, &endp, 16);
     if (endp == tmp || v == ULONG_MAX) {
         return -1;
     }
     *size_out = v;
     return 0;
 }
 
 static bool fd_header_contains_token_ci(const struct mg_str *s, const char *token)
 {
     size_t i;
     size_t j;
     size_t slen;
     size_t tlen;
 
     if (s == NULL || s->buf == NULL || token == NULL) {
         return false;
     }
     slen = s->len;
     tlen = strlen(token);
     if (tlen == 0U || slen < tlen) {
         return false;
     }
 
     for (i = 0; i + tlen <= slen; i++) {
         for (j = 0; j < tlen; j++) {
             char a = (char)tolower((unsigned char)s->buf[i + j]);
             char b = (char)tolower((unsigned char)token[j]);
             if (a != b) {
                 break;
             }
         }
         if (j == tlen) {
             return true;
         }
     }
     return false;
 }
 
 static int fd_parse_content_range_start(const struct mg_str *s, size_t *start_out)
 {
     char tmp[64];
     unsigned long start_ul = 0;
     unsigned long end_ul = 0;
     unsigned long total_ul = 0;
     if (s == NULL || s->buf == NULL || start_out == NULL) {
         return -1;
     }
     size_t n = s->len < sizeof(tmp) - 1U ? s->len : sizeof(tmp) - 1U;
     memcpy(tmp, s->buf, n);
     tmp[n] = '\0';
 
     /* e.g. "bytes 123-456/789" */
     if (sscanf(tmp, "bytes %lu-%lu/%lu", &start_ul, &end_ul, &total_ul) < 2) {
         return -1;
     }
     (void)end_ul;
     (void)total_ul;
     *start_out = (size_t)start_ul;
     return 0;
 }
 
 static void fd_abort(struct mg_connection *c, hd_ctx_t *ctx, int err)
 {
     if (ctx->done) {
         return;
     }
     if (ctx->fd >= 0) {
         (void)fs_adapt_close(ctx->fd);
         ctx->fd = -1;
     }
     ctx->result = err;
     ctx->done = true;
     c->is_closing = 1;
 }
 
 static void fd_finish_ok(struct mg_connection *c, hd_ctx_t *ctx)
 {
     unsigned int final_size = 0U;
 
     if (ctx->done) {
         return;
     }
     if (ctx->fd >= 0) {
         (void)fs_adapt_sync(ctx->fd);
         (void)fs_adapt_close(ctx->fd);
         ctx->fd = -1;
     }
 
     /*
      * 第一层完整性校验:接收写入总字节必须等于 HTTP 头推导的期望总长
      * (200 + Content-Length) 或 (206 + resume_offset + Content-Length)。
      */
     if (ctx->expected_known && (ctx->bytes_written != ctx->expected_bytes)) {
         ctx->result = -1;
         ctx->done = true;
         c->is_closing = 1;
         printf("[HttpLfs] size mismatch: got=%u expected=%u\r\n",
             (unsigned int)ctx->bytes_written, (unsigned int)ctx->expected_bytes);
         return;
     }
 
     /* ATOMIC:先删旧正式文件,再把临时文件 rename 覆盖;DIRECT:已在目标路径上写完 */
     if (!ctx->direct_write) {
         (void)fs_adapt_delete(ctx->final_path);
         if (fs_adapt_rename(ctx->path, ctx->final_path) != 0) {
             (void)fs_adapt_delete(ctx->path);
             ctx->result = -1;
             ctx->done = true;
             c->is_closing = 1;
             printf("[HttpLfs] rename failed: %s -> %s\r\n", ctx->path, ctx->final_path);
             return;
         }
     }
 
     /* 第二层完整性校验:落盘后的最终文件大小再对比一次 */
     if (ctx->expected_known) {
         if (fs_adapt_stat(ctx->final_path, &final_size) != 0 ||
             (size_t)final_size != ctx->expected_bytes) {
             (void)fs_adapt_delete(ctx->final_path);
             ctx->result = -1;
             ctx->done = true;
             c->is_closing = 1;
             printf("[HttpLfs] final file size verify failed: stat=%u expected=%u\r\n",
                 final_size, (unsigned int)ctx->expected_bytes);
             return;
         }
     }
 
     ctx->result = 0;
     ctx->done = true;
     if (ctx->expected_known) {
         printf("[HttpLfs] http done %u/%u bytes\r\n",
             (unsigned int)ctx->bytes_written, (unsigned int)ctx->expected_bytes);
     } else {
         printf("[HttpLfs] http done %u bytes (until-close)\r\n",
             (unsigned int)ctx->bytes_written);
     }
     c->is_closing = 1;
 }
 
 static void fd_strip_http_headers(struct mg_connection *c, struct mg_http_message *hm)
 {
     size_t hdr_len = (size_t)(hm->body.buf - (char *)c->recv.buf);
     if (hdr_len > c->recv.len) {
         return;
     }
     mg_iobuf_del(&c->recv, 0, hdr_len);
 }
 
 static void fd_consume_content_length(struct mg_connection *c, hd_ctx_t *ctx)
 {
     while (ctx->content_remaining > 0U && c->recv.len > 0U) {
         size_t n = c->recv.len;
         if (n > ctx->content_remaining) {
             n = ctx->content_remaining;
         }
         int w = fs_adapt_write(ctx->fd, (const char *)c->recv.buf, (unsigned int)n);
         if (w < 0 || (size_t)w != n) {
             fd_abort(c, ctx, -1);
             return;
         }
         mg_iobuf_del(&c->recv, 0, n);
         ctx->content_remaining -= n;
         ctx->bytes_written += n;
         ctx->last_body_ms = mg_millis();
     }
     if (ctx->content_remaining == 0U) {
         fd_finish_ok(c, ctx);
     }
 }
 
 static void fd_consume_until_close(struct mg_connection *c, hd_ctx_t *ctx)
 {
     while (c->recv.len > 0U) {
         size_t n = c->recv.len;
         int w = fs_adapt_write(ctx->fd, (const char *)c->recv.buf, (unsigned int)n);
         if (w < 0 || (size_t)w != n) {
             fd_abort(c, ctx, -1);
             return;
         }
         mg_iobuf_del(&c->recv, 0, n);
         ctx->bytes_written += n;
         ctx->last_body_ms = mg_millis();
     }
 }
 
 static void fd_consume_chunked(struct mg_connection *c, hd_ctx_t *ctx)
 {
     for (;;) {
         if (c->recv.len == 0U) {
             return;
         }
         if (ctx->chunk_payload_left == 0U) {
             char *buf = (char *)c->recv.buf;
             size_t i = 0;
             while (i + 1U < c->recv.len && !(buf[i] == '\r' && buf[i + 1U] == '\n')) {
                 i++;
             }
             if (i + 1U >= c->recv.len) {
                 return;
             }
             unsigned long csz = 0;
             if (fd_parse_chunk_size_line(buf, i, &csz) != 0) {
                 fd_abort(c, ctx, -1);
                 return;
             }
             mg_iobuf_del(&c->recv, 0, i + 2U);
             if (csz == 0UL) {
                 fd_finish_ok(c, ctx);
                 return;
             }
             ctx->chunk_payload_left = (size_t)csz;
             ctx->last_body_ms = mg_millis();
             continue;
         }
 
         size_t n = c->recv.len;
         if (n > ctx->chunk_payload_left) {
             n = ctx->chunk_payload_left;
         }
         int w = fs_adapt_write(ctx->fd, (const char *)c->recv.buf, (unsigned int)n);
         if (w < 0 || (size_t)w != n) {
             fd_abort(c, ctx, -1);
             return;
         }
         mg_iobuf_del(&c->recv, 0, n);
         ctx->chunk_payload_left -= n;
         ctx->bytes_written += n;
         ctx->last_body_ms = mg_millis();
 
         if (ctx->chunk_payload_left == 0U) {
             if (c->recv.len < 2U) {
                 return;
             }
             if (((char *)c->recv.buf)[0] != '\r' || ((char *)c->recv.buf)[1] != '\n') {
                 fd_abort(c, ctx, -1);
                 return;
             }
             mg_iobuf_del(&c->recv, 0, 2U);
         }
     }
 }
 
 static void fd_dispatch_body(struct mg_connection *c, hd_ctx_t *ctx)
 {
     switch (ctx->mode) {
         case FD_BODY_CONTENT_LENGTH:
             fd_consume_content_length(c, ctx);
             break;
         case FD_BODY_CHUNKED:
             fd_consume_chunked(c, ctx);
             break;
         case FD_BODY_UNTIL_CLOSE:
             fd_consume_until_close(c, ctx);
             break;
         default:
             break;
     }
 }
 
static void hd_ev_handler(struct mg_connection *c, int ev, void *ev_data)
 {
     hd_ctx_t *ctx = (hd_ctx_t *)c->fn_data;
     if (ctx == NULL) {
         return;
     }
 
     if (ev == MG_EV_OPEN) {
         (void)ev_data;
         ctx->connect_deadline_ms = mg_millis() + HD_CONNECT_MS;
         return;
     }
 
     if (ev == MG_EV_POLL) {
         if (!ctx->hdrs_ok) {
             if (mg_millis() > ctx->connect_deadline_ms &&
                 (c->is_connecting || c->is_resolving)) {
                 mg_error(c, "http_lfs connect timeout");
             }
             return;
         }
         if (ctx->done) {
             return;
         }
         if ((mg_millis() - ctx->last_body_ms) > HD_BODY_IDLE_MS) {
             mg_error(c, "http_lfs body idle timeout");
         }
         return;
     }
 
     if (ev == MG_EV_CONNECT) {
         /*
          * mg_url_host() 不含端口;非 80 端口必须在 Host 中带 :port,否则虚拟主机/网关可能直接断连。
          */
         struct mg_str host = mg_url_host(ctx->url_buf);
         unsigned short port = mg_url_port(ctx->url_buf);
         if (port == 80U) {
             if (ctx->try_range && ctx->resume_offset > 0U) {
                 mg_printf(c,
                           "GET %s HTTP/1.1\r\n"
                           "Host: %.*s\r\n"
                           "Connection: close\r\n"
                           "Accept: */*\r\n"
                           "Range: bytes=%u-\r\n"
                           "\r\n",
                           mg_url_uri(ctx->url_buf), (int)host.len, host.buf,
                           (unsigned int)ctx->resume_offset);
             } else {
                 mg_printf(c,
                           "GET %s HTTP/1.1\r\n"
                           "Host: %.*s\r\n"
                           "Connection: close\r\n"
                           "Accept: */*\r\n"
                           "\r\n",
                           mg_url_uri(ctx->url_buf), (int)host.len, host.buf);
             }
         } else {
             if (ctx->try_range && ctx->resume_offset > 0U) {
                 mg_printf(c,
                           "GET %s HTTP/1.1\r\n"
                           "Host: %.*s:%u\r\n"
                           "Connection: close\r\n"
                           "Accept: */*\r\n"
                           "Range: bytes=%u-\r\n"
                           "\r\n",
                           mg_url_uri(ctx->url_buf), (int)host.len, host.buf, (unsigned int)port,
                           (unsigned int)ctx->resume_offset);
             } else {
                 mg_printf(c,
                           "GET %s HTTP/1.1\r\n"
                           "Host: %.*s:%u\r\n"
                           "Connection: close\r\n"
                           "Accept: */*\r\n"
                           "\r\n",
                           mg_url_uri(ctx->url_buf), (int)host.len, host.buf, (unsigned int)port);
             }
         }
         return;
     }
 
     if (ev == MG_EV_HTTP_HDRS) {
         struct mg_http_message *hm = (struct mg_http_message *)ev_data;
         int st = mg_http_status(hm);
         if (st == 416 && ctx->try_range) {
             printf("[HttpLfs] range not satisfiable, fallback full download\r\n");
             fd_abort(c, ctx, HTTP_LFS_RES_FALLBACK_FULL);
             return;
         }
         if (st != 200 && st != 206) {
             printf("[HttpLfs] unexpected http status=%d\r\n", st);
             fd_abort(c, ctx, -1);
             return;
         }
 
         struct mg_str *te = mg_http_get_header(hm, "Transfer-Encoding");
         bool chunked = fd_header_contains_token_ci(te, "chunked");
         struct mg_str *clh = mg_http_get_header(hm, "Content-Length");
         struct mg_str *crh = mg_http_get_header(hm, "Content-Range");
         size_t cl = 0;
 
         if (clh != NULL && fd_parse_content_length(clh, &cl) == 0) {
             printf("[HttpLfs] http %d, te=%s, cl=%u, range_try=%u off=%u\r\n",
                 st, (te != NULL) ? "yes" : "no", (unsigned int)cl,
                 (unsigned int)(ctx->try_range ? 1U : 0U), (unsigned int)ctx->resume_offset);
         } else {
             printf("[HttpLfs] http %d, te=%s, cl=n/a, range_try=%u off=%u\r\n",
                 st, (te != NULL) ? "yes" : "no",
                 (unsigned int)(ctx->try_range ? 1U : 0U), (unsigned int)ctx->resume_offset);
         }
 
         if (chunked) {
             ctx->mode = FD_BODY_CHUNKED;
         } else if (clh != NULL) {
             if (fd_parse_content_length(clh, &cl) != 0) {
                 fd_abort(c, ctx, -1);
                 return;
             }
             ctx->mode = FD_BODY_CONTENT_LENGTH;
             ctx->content_remaining = cl;
             ctx->expected_known = true;
             ctx->expected_bytes = cl + ((st == 206) ? ctx->resume_offset : 0U);
         } else {
             ctx->mode = FD_BODY_UNTIL_CLOSE;
         }
 
         if (fs_adapt_mkdir(ctx->base_dir) != 0) {
             fd_abort(c, ctx, -1);
             return;
         }
 
         if (st == 206) {
             size_t range_start = 0U;
             if (ctx->resume_offset == 0U || crh == NULL ||
                 fd_parse_content_range_start(crh, &range_start) != 0 ||
                 range_start != ctx->resume_offset) {
                 printf("[HttpLfs] bad content-range, fallback full\r\n");
                 fd_abort(c, ctx, HTTP_LFS_RES_FALLBACK_FULL);
                 return;
             }
             ctx->fd = fs_adapt_open(ctx->path, O_WRONLY | O_CREAT);
         } else {
             ctx->fd = fs_adapt_open(ctx->path, O_WRONLY | O_CREAT | O_TRUNC);
         }
         if (ctx->fd < 0) {
             fd_abort(c, ctx, -1);
             return;
         }
         if (st == 206) {
             if (fs_adapt_seek(ctx->fd, (int)ctx->resume_offset, SEEK_SET) < 0) {
                 fd_abort(c, ctx, -1);
                 return;
             }
             ctx->bytes_written = ctx->resume_offset;
             printf("[HttpLfs] resume from offset=%u\r\n", (unsigned int)ctx->resume_offset);
         } else if (ctx->resume_offset > 0U) {
             printf("[HttpLfs] server ignored range, restart from zero\r\n");
         }
 
         fd_strip_http_headers(c, hm);
         ctx->hdrs_ok = true;
         ctx->last_body_ms = mg_millis();
         fd_dispatch_body(c, ctx);
         return;
     }
 
     if (ev == MG_EV_READ) {
         if (!ctx->hdrs_ok || ctx->done) {
             return;
         }
         fd_dispatch_body(c, ctx);
         return;
     }
 
     if (ev == MG_EV_CLOSE) {
         if (ctx->done) {
             return;
         }
         if (!ctx->hdrs_ok) {
             ctx->result = -1;
             ctx->done = true;
             return;
         }
         if (ctx->mode == FD_BODY_UNTIL_CLOSE) {
             if (ctx->fd >= 0) {
                 fd_finish_ok(c, ctx);
             } else {
                 ctx->result = -1;
                 ctx->done = true;
             }
             return;
         }
         if (ctx->expected_known) {
             printf("[HttpLfs] http closed early: got %u/%u bytes, remain=%u\r\n",
                 (unsigned int)ctx->bytes_written, (unsigned int)ctx->expected_bytes,
                 (unsigned int)ctx->content_remaining);
         } else {
             printf("[HttpLfs] http closed early in non-until-close mode, got=%u bytes\r\n",
                 (unsigned int)ctx->bytes_written);
         }
         fd_abort(c, ctx, -1);
         return;
     }
 
     if (ev == MG_EV_ERROR) {
         if (ev_data != NULL) {
             printf("[HttpLfs] MG_EV_ERROR: %s\r\n", (const char *)ev_data);
         } else {
             printf("[HttpLfs] MG_EV_ERROR\r\n");
         }
         fd_abort(c, ctx, -1);
     }
 }
 
int http_lfs_download_blocking(const http_lfs_download_param_t *param)
{
    size_t resume_bytes = 0U;
    bool try_range_once = false;
    bool range_disabled_for_session = false;
    const char *part_suffix;
    bool direct;

    if (param == NULL || param->url == NULL || param->base_dir == NULL || param->basename == NULL) {
        return -1;
    }
    direct = (param->store_mode == HTTP_LFS_STORE_DIRECT);
    part_suffix = (param->part_suffix != NULL && param->part_suffix[0] != '\0') ? param->part_suffix : ".part";

retry_connect:
    if (param->url[0] == '\0' || param->base_dir[0] == '\0' || param->basename[0] == '\0') {
        return -1;
    }
    if (strncmp(param->url, "http://", 7) != 0) {
        return -1;
    }
    if (strchr(param->basename, '/') != NULL || strstr(param->basename, "..") != NULL) {
        return -1;
    }
    if (strlen(param->basename) > 48U) {
        return -1;
    }
    if (strstr(param->base_dir, "..") != NULL) {
        return -1;
    }

    struct mg_mgr mgr;
    mg_mgr_init(&mgr);

    hd_ctx_t *ctx = (hd_ctx_t *)calloc(1, sizeof(*ctx));
    if (ctx == NULL) {
        mg_mgr_free(&mgr);
        return -1;
    }
    ctx->fd = -1;
    ctx->result = -1;
    if (snprintf(ctx->url_buf, sizeof(ctx->url_buf), "%s", param->url) >= (int)sizeof(ctx->url_buf)) {
        free(ctx);
        mg_mgr_free(&mgr);
        return -1;
    }
    if (snprintf(ctx->base_dir, sizeof(ctx->base_dir), "%s", param->base_dir) >= (int)sizeof(ctx->base_dir)) {
        free(ctx);
        mg_mgr_free(&mgr);
        return -1;
    }
    if (snprintf(ctx->final_path, sizeof(ctx->final_path), "%s/%s", ctx->base_dir, param->basename) >=
        (int)sizeof(ctx->final_path)) {
        free(ctx);
        mg_mgr_free(&mgr);
        return -1;
    }
    ctx->direct_write = direct;
    if (direct) {
        if (snprintf(ctx->path, sizeof(ctx->path), "%s/%s", ctx->base_dir, param->basename) >= (int)sizeof(ctx->path)) {
            free(ctx);
            mg_mgr_free(&mgr);
            return -1;
        }
    } else {
        if (snprintf(ctx->path, sizeof(ctx->path), "%s/%s%s", ctx->base_dir, param->basename, part_suffix) >=
            (int)sizeof(ctx->path)) {
            free(ctx);
            mg_mgr_free(&mgr);
            return -1;
        }
    }

    /*
     * Range 续传:若存在临时文件,则先尝试从其大小处续传。
     * 416 等情况会回退全量(HTTP_LFS_RES_FALLBACK_FULL)。
     */
    {
        unsigned int sz = 0U;
        if (!range_disabled_for_session && try_range_once == false &&
            fs_adapt_stat(ctx->path, &sz) == 0 && sz > 0U) {
            resume_bytes = (size_t)sz;
            try_range_once = true;
        }
    }
    ctx->try_range = try_range_once;
    ctx->resume_offset = try_range_once ? resume_bytes : 0U;
    if (!ctx->try_range) {
        (void)fs_adapt_delete(ctx->path);
    }

    FD_MG_LOCK();
    struct mg_connection *c = mg_http_connect(&mgr, ctx->url_buf, hd_ev_handler, ctx);
    FD_MG_UNLOCK();
    if (c == NULL) {
        free(ctx);
        mg_mgr_free(&mgr);
        return -1;
    }

    uint64_t overall_deadline = mg_millis() + HD_OVERALL_MS;
    while (!ctx->done && mg_millis() < overall_deadline) {
        FD_MG_LOCK();
        mg_mgr_poll(&mgr, HD_MGR_POLL_MS);
        FD_MG_UNLOCK();
        osal_msleep(1U);
    }

    if (!ctx->done) {
        FD_MG_LOCK();
        mg_error(c, "http_lfs overall timeout");
        mg_mgr_poll(&mgr, HD_MGR_POLL_MS);
        FD_MG_UNLOCK();
        while (!ctx->done && mg_millis() < overall_deadline + 2000UL) {
            FD_MG_LOCK();
            mg_mgr_poll(&mgr, HD_MGR_POLL_MS);
            FD_MG_UNLOCK();
            osal_msleep(1U);
        }
    }

    int res = ctx->result;
    FD_MG_LOCK();
    mg_mgr_free(&mgr);
    FD_MG_UNLOCK();
    free(ctx);
    if (res == HTTP_LFS_RES_FALLBACK_FULL) {
        range_disabled_for_session = true;
        try_range_once = false;
        resume_bytes = 0U;
        goto retry_connect;
    }
    return res;
}

5. 字库下载使用示例

font.bin 下载到 LittleFS /system/font.bin ,中断后可从 /system/font.bin.part 续传,成功后原子替换:

c 复制代码
#include "http_lfs_download.h"

static int download_font_to_system(void)
{
    http_lfs_download_param_t p = {
        .url = "http://192.168.1.100/static/font.bin",
        .base_dir = "/system",
        .basename = "font.bin",
        .part_suffix = ".part",   /* 可改为 NULL 使用默认 ".part" */
        .store_mode = HTTP_LFS_STORE_ATOMIC,
    };

    int r = http_lfs_download_blocking(&p);
    if (r != 0) {
        printf("font download failed, ret=%d\r\n", r);
        return -1;
    }
    printf("font.bin ready under /system\r\n");
    return 0;
}

首次下载 :无 .part 时先删再下,得到完整 font.bin.part → 校验通过 → font.bin
中途断电/断网 :保留 font.bin.part (已有字节数 N ),再次调用同一参数:自动 Range: bytes=N-206 后 seek 到 N 继续写,完成后 rename 为 font.bin

若业务必须 直接写 font.bin(不推荐用于需断点场景):

c 复制代码
p.store_mode = HTTP_LFS_STORE_DIRECT;
p.basename = "font.bin";
/* part_suffix 忽略;续传基于已存在的 /system/font.bin 大小 */

6. 与 font_download.c 的关系

项目 font_download.c http_lfs_download.c
路径 固定 /system + 单文件名 任意 base_dir + basename
断点 无 Range,每次 O_TRUNC Range + .part + 416 回退
原子性 直接写目标文件 默认 临时文件 + rename
适用 简单一次性拉取 通用大文件 / 字库 / 资源包

字库场景建议:优先使用 http_lfs_download_blocking ;仅在不需要续传、希望代码路径极简时再用专用 font_download


7. 服务端与调试建议

  • 服务器需支持 Range / 206 Partial Content 且返回正确 Content-Range(静态文件服务器如 Nginx、多数对象存储一般支持)。
  • 若总是 200 且从头传:检查 CDN/网关是否剥离 Range;实现上仍会 截断重写 临时文件,不会错误拼接双份内容。
  • 日志关键字:[HttpLfs],便于串口排查 status、CL、resume offset、size mismatch、rename failed

8. 小结

本封装在 Mongoose 上以 mg_http_connect + 状态机回调 实现 HTTP/1.1 下载 ,通过 临时文件 + Range/206/Content-Range 完成 断点续传 ,并以 rename + 双重长度校验 保证 ATOMIC 模式下的落盘一致性。字库等大体量文件可 base_dir="/system"basename="font.bin" 直接复用;超时与 poll 间隔可按现场网络在 http_lfs_download.c中调整。


关联源文件:src/network/http_lfs_download.csrc/network/http_lfs_download.h;参考:src/network/font_download.csrc/littlefs/littlefs_adapt.c

相关推荐
王码码20355 小时前
Flutter 组件 inappwebview_cookie_manager 适配 鸿蒙Harmony 实战 - 驾驭核心大 Web 容器缓存隧道、构建金融级政企应用绝对防串号跨域大隔离基座
flutter·harmonyos·鸿蒙·openharmony·inappwebview_cookie_manager
左手厨刀右手茼蒿5 小时前
Flutter 组件 ews 的适配 鸿蒙Harmony 实战 - 驾驭企业级 Exchange Web Services 协议、实现鸿蒙端政企办公同步与高安通讯隔离方案
flutter·harmonyos·鸿蒙·openharmony
键盘鼓手苏苏5 小时前
Flutter 组件 spry 适配鸿蒙 HarmonyOS 实战:轻量化 Web 框架,构建高性能端侧微服务与 Middleware 治理架构
flutter·harmonyos·鸿蒙·openharmony
亚历克斯神9 小时前
Flutter 组件 t_stats 的适配 鸿蒙Harmony 实战 - 驾驭高性能统计学运算、实现鸿蒙端海量数据实时态势感知与工业级描述性统计方案
flutter·harmonyos·鸿蒙·openharmony·t_stats
键盘鼓手苏苏9 小时前
Flutter 组件 angel3_orm_mysql 的适配 鸿蒙Harmony 实战 - 驾驭专业 ORM 映射引擎、实现鸿蒙端与 MySQL 数据库的透明映射与高性能 SQL 审计方案
flutter·harmonyos·鸿蒙·openharmony·angel3_orm_mysql
左手厨刀右手茼蒿9 小时前
Flutter 组件 serverpod_swagger 的适配 鸿蒙Harmony 实战 - 驾驭 API 文档自动化、实现鸿蒙端全栈联调与 Swagger UI 动态审计方案
flutter·harmonyos·鸿蒙·openharmony·serverpod_swagger
钛态9 小时前
Flutter 三方库 discord_interactions 的鸿蒙化适配指南 - 在 OpenHarmony 打造高效的社交机器人交互底座
flutter·harmonyos·鸿蒙·openharmony·discord_interactions
加农炮手Jinx9 小时前
Flutter 组件 dascade 的适配 鸿蒙Harmony 实战 - 驾驭级联式异步数据流、实现鸿蒙端响应式 UI 状态泵与复杂业务逻辑解耦方案
flutter·harmonyos·鸿蒙·openharmony
里欧跑得慢9 小时前
Flutter 组件 postgres_crdt 的适配 鸿蒙Harmony 实战 - 驾驭分布式无冲突复制数据类型、实现鸿蒙端高性能离线对等同步架构方案
flutter·harmonyos·鸿蒙·openharmony·postgres_crdt