Mongoose 文件传输示例实战:流式 POST 文件上传客户端与服务器
2026/9/23 2:48:27 网站建设 项目流程
  • 嵌入式
  • 网络
  • 通信
  • 物联网

【免费下载链接】mongoose

Embedded web server, with TCP/IP network stack, MQTT and Websocket

项目地址:https://gitcode.com/gh_mirrors/mon/mongoose
点击查看免费下载

本文围绕 Mongoose 嵌入式网络库自带的 file-transfer 示例展开,完整讲解一个"最小 HTTP 客户端 + HTTP 服务器"组合:客户端以单个 POST 请求上传文件并主动将流量切分为小数据块发送,服务器则在收到请求后手动接管数据处理、边收边写盘,从而避免将整个(可能很大的)文件缓存在内存中。读完本文,你将掌握 Mongoose 事件驱动的 HTTP 请求接管机制(MG_EV_HTTP_HDRS)、Basic Auth 认证的收发实现、流式分块上传的编程模型,以及一套可直接复制运行、支持命令行配置的完整上传方案。

示例概览:一个"数据边到边写"的上传链路

示例代码位于 tutorials/http/file-transfer,共包含四个关键文件:

  • server.c:HTTP 服务器。监听全部网卡的 8090 端口,将web_root目录作为静态资源根目录,把upload目录作为上传落盘目录。
  • client.c:HTTP 客户端。读取本地文件,用单个 POST 请求上传到服务器,并通过MG_EV_WRITE事件把流量切分为MG_IO_SIZE大小的小块逐批发送。
  • Makefile:一条命令同时构建serverclient两个可执行文件,兼容 POSIX 与 Windows(MinGW)环境。
  • mongoose.c/mongoose.h:本示例使用的 Mongoose 单文件源码(仓库根目录 mongoose.c / mongoose.h 的打包副本)。

该示例的设计动机非常明确:常规的 HTTP 服务器在处理请求时会先把整个请求体缓冲进内存,如果用户上传的是一个几百 MB 的文件,而设备只有几十 KB RAM,这种做法必然失败。因此服务器主动在收到 HTTP 头(MG_EV_HTTP_HDRS)的时机接管连接,把随后到达的 body 数据"边收边写"到磁盘,全程不缓存完整文件。上传操作还要求 Basic Auth 认证,默认用户名/密码为user/pass,客户端与服务器均可通过命令行参数修改,只有通过认证的请求才能写入文件。

构建:一条 make 命令产出两个可执行文件

原文档建议先配置好开发环境,然后进入示例目录构建。在当前仓库中的实际操作路径为:

cd tutorials/http/file-transfer make clean all

执行后会在当前目录生成serverclient两个可执行文件。其中make all的默认行为是"构建后直接启动服务器"(见 Makefile 中的all: example目标与$(RUN) ./$(SPROG) $(SARGS)),因此想手动控制启动顺序时,可以分别执行:

make server make client

从 Makefile 可以看到构建细节:

  • 服务器与客户端分别由server.c mongoose.cclient.c mongoose.c编译,公共编译选项为CFLAGS = -W -Wall -Wextra -g -I.
  • 通过CFLAGS_MONGOOSE变量预留了 Mongoose 构建选项(例如-DMG_ENABLE_LINES),可按需追加;
  • 在 Windows 上(ifeq ($(OS),Windows_NT))会自动切换到gcc+-lws2_32,输出server.exe/client.exe,删除命令也切换为cmd /C del /Q /F /S,说明该示例可在 Windows 下用 MinGW 直接构建;
  • make clean会清理两个程序及所有中间产物。

运行:三步完成一次认证上传

1. 启动服务器

在一个终端中启动服务器(默认监听所有网卡的 8090 端口):

./server

正常启动时输出如下日志:

6332b7 2 server.c:157:main Mongoose version : v7.12 6332b7 2 server.c:159:main Listening on : http://0.0.0.0:8090 6332b7 2 server.c:160:main Web root : [/home/mongoose/examples/file-transfer/web_root] 6332b7 2 server.c:161:main Uploading to : [/home/mongoose/examples/file-transfer/upload]

(日志中的路径以你实际构建目录为准。)可以看到服务器做了三件事:创建事件管理器、以http://0.0.0.0:8090为监听地址调用mg_http_listen()、然后进入mg_mgr_poll()无限事件循环,直到收到SIGINT/SIGTERM信号才退出(见 server.c 的main()函数)。

2. 运行客户端上传文件

在另一个终端执行客户端,默认会把文件以foo.txt为名上传到http://localhost:8090/upload/foo.txt

./client -f Makefile ok

输出ok即代表上传成功——这是服务器在完成落盘后返回的响应体(详见下文服务器状态机)。如果想在同一个终端里先启动服务器再开客户端,可以先把服务器放到后台运行。

3. 用 curl 验证服务器兼容性

服务器并不绑定专用客户端,它支持任何标准 HTTP 客户端上传,例如 curl:

curl -su user:pass http://localhost:8090/upload/foo.txt --data-binary @Makefile

-u user:pass提供 Basic Auth 凭据,--data-binary @Makefile以二进制原始数据方式上传本机文件,请求体不会做任何改写。这条命令足以证明:服务器端实现的是标准 HTTP/1.x 协议语义,而非自定义私有协议。

命令行参数:客户端与服务端的完整配置表

示例的默认行为是使用硬编码的用户名和密码;直接不带参数运行./server./client会打印 usage 帮助信息。以下为两个程序支持的全部参数。

服务器参数(server.c)

参数含义默认值
-u NAME允许上传的用户名user
-p PWD密码pass
-d DIR静态文件服务目录web_root
-D DIR上传文件存储目录upload
-s SIZE允许的最大文件大小(字节)10000
-l ADDR监听地址http://0.0.0.0:8090
-v LEVEL调试级别,取值 0~4MG_LL_INFO

例如把最大上传大小放宽到 1 MB、监听 9000 端口:

./server -s 1000000 -l http://0.0.0.0:9000

服务器启动时还会把-d/-D指定的相对目录通过realpath()转换为绝对路径,以消除包含..的相对路径带来的歧义(server.cmain()中的处理逻辑)。

客户端参数(client.c)

参数含义默认值
-f NAME要发送的本地文件(必填,缺省直接打印 usage 退出)
-u NAME用户名user
-p PWD密码pass
-U URL完整的服务器 URL,包含目标文件名http://localhost:8090/upload/foo.txt
-v LEVEL调试级别,取值 0~4MG_LL_INFO

典型用法:修改目标文件名与凭据后上传:

./client -f firmware.bin -U http://192.168.1.10:8090/upload/fw.bin -u admin -p secret

注意:客户端会先调用mg_fs_posix.st()获取文件大小,文件不存在或大小为 0 时会直接报open failed退出(见 client.cmain())。

服务器端实现:在 MG_EV_HTTP_HDRS 时机接管连接

服务器核心逻辑集中在handle_uploads()回调中,其巧妙之处在于在 HTTP body 完整到达之前就接管连接

拦截 /upload 请求并关闭默认 HTTP 处理器

if (ev == MG_EV_HTTP_HDRS) { struct mg_http_message *hm = (struct mg_http_message *) ev_data; if (mg_match(hm->uri, mg_str("/upload/#"), NULL)) { c->pfn = NULL; // Silence HTTP protocol handler, we'll take over ...

MG_EV_HTTP_HDRS事件表示"已收到完整 HTTP 头但 body 未必完整"(事件定义见 src/event.h)。mg_match()用通配模式/upload/#匹配 URI 前缀;一旦命中,就通过c->pfn = NULL静默掉 mongoose 内置的 HTTP 协议处理器,把连接的字节流控制权完全交给自己的回调。

从底层看,这正是 Mongoose HTTP 协议层的既有机制:在 src/http.c 的http_cb()中,解析出头部后会调用mg_call(c, MG_EV_HTTP_HDRS, &hm)通知用户;如果用户修改了c->recv中的数据(c->recv.len != old_len),mongoose 会主动"洗手"——将c->pfn置空并脱离该连接,不再尝试按 HTTP 语义继续解析。示例正是利用这一点,把后续到达的原始 body 字节直接交给自己的代码处理。

认证、限长与路径校验(一条完整的安全检查链)

接管之后,服务器依次执行四道检查,任一失败都会回复错误并标记c->is_draining = 1(告诉 mongoose 响应发送完毕后关闭连接):

  1. 认证:调用authuser(hm),内部通过mg_http_creds(hm, user, sizeof(user), pass, sizeof(pass))解析Authorization头,并与命令行配置的s_user/s_pass比对。失败则返回403 Denied
  2. 限长hm->body.len > s_max_size时返回400 Too long(默认上限 10000 字节)。
  3. 文件名:URI 恰好为/upload/hm->uri.len == 8)时返回400 Name required;拼接出的完整路径超过MG_PATH_MAX时返回400 Path is too long
  4. 路径安全:用snprintf拼接出upload/<文件名>后调用mg_path_is_sane()校验,防止..等路径穿越攻击,不合法则返回400 Invalid pathmg_path_is_sane()的实现位于 src/util.c,是 mongoose 在文件服务、OTA、dash 等模块中通用的路径安全防线(src/http.c、src/ota.c 等均有调用)。

全部通过后,先fs->rm()删除可能存在的同名旧文件,再以MG_FS_WRITE打开目标文件,把文件句柄存入struct upload_statefp字段,记录expected = hm->body.len(Content-Length 给出的总字节数),最后调用mg_iobuf_del(&c->recv, 0, hm->head.len)把已消费的 HTTP 头从接收缓冲中删掉——这正是上一节所述"修改 recv 数据触发 mongoose 脱管"的实际动作。

边收边写:由 MG_EV_READ 驱动的流式落盘状态机

if (us->expected > 0 && c->recv.len > 0) { us->received += c->recv.len; if (us->fp) fs->wr(us->fp, c->recv.buf, c->recv.len); // Write to file c->recv.len = 0; // Delete received data if (us->received >= us->expected) { mg_http_reply(c, 200, NULL, "%lu ok\n", us->received); if (us->fp) fs->cl(us->fp); // Close file memset(us, 0, sizeof(*us)); // Cleanup upload state c->is_draining = 1; // Close after response sent } }

这段代码对MG_EV_READ(新数据到达)和MG_EV_HTTP_HDRS(头之后紧跟的 body 数据)都会执行:只要recv缓冲里有数据,就把整块数据直接写入文件,然后清零recv.len继续等待下一块。当累计received达到expected时,回复200+%lu ok\n(这就是客户端打印的ok),关闭文件、清空上传状态并排空连接。

关键收益在于:无论文件多大,接收缓冲中同一时刻只有一小块数据,内存占用与文件大小无关,只取决于单次到达的数据量。struct upload_state本身被存放在连接的c->data指针所指区域,随连接生命周期自动管理(定义见 server.c)。

非上传请求仍走标准静态服务

} else if (ev == MG_EV_HTTP_MSG && c->pfn != NULL) { struct mg_http_message *hm = (struct mg_http_message *) ev_data; struct mg_http_serve_opts opts = {0}; opts.root_dir = s_root_dir; mg_http_serve_dir(c, hm, &opts); }

cb()回调中对普通请求(c->pfn未被置空,即未被接管)使用mg_http_serve_dir()提供web_root目录的静态文件服务。因此同一端口上,"上传接口 + 静态页面"两者共存:浏览器访问/能看到web_root下的页面,向/upload/xxx发 POST 则走流式写入路径。

客户端实现:连接、认证与分块发送

MG_EV_CONNECT 时机组装请求

客户端在MG_EV_CONNECT(已与服务器建立 TCP 连接)事件中,用mg_printf()直接拼装 HTTP 请求行与头:

mg_printf(c, "POST %s HTTP/1.0\r\n" "Host: %.*s\r\n" "Content-Type: octet-stream\r\n" "Content-Length: %d\r\n", mg_url_uri(s_url), (int) host.len, host.buf, fsize); mg_http_bauth(c, s_user, s_pass); // Add Basic auth header mg_printf(c, "%s", "\r\n"); // End HTTP headers

mg_url_uri()/mg_url_host()-U指定的 URL 中拆出路径与主机名;Content-Length填的是本地文件大小fsize;之后调用mg_http_bauth()追加认证头,最后输出空行结束头部。

mg_http_bauth()的实现在 src/http.c:它直接把Authorization: Basic前缀、user:pass拼接后做 Base64 编码,写入发送缓冲并补上\r\n。也就是说,Authorization: Basic base64(user:pass)是客户端与服务端之间的认证契约。

MG_EV_WRITE 时机按 MG_IO_SIZE 分块发送

} else if (ev == MG_EV_WRITE && c->send.len < MG_IO_SIZE) { uint8_t *buf = alloca(MG_IO_SIZE); size_t len = MG_IO_SIZE - c->send.len; len = fsize < len ? fsize : len; fd->fs->rd(fd->fd, buf, len); mg_send(c, buf, len); fsize -= len;

MG_EV_WRITE表示发送缓冲被部分清空、可以继续写入数据。此时从本地文件读取最多MG_IO_SIZE字节(减去仍在发送缓冲中的残余量),用mg_send()送出,如此反复直到文件读完。MG_IO_SIZE是 mongoose 的 IO 缓冲增长粒度,在 src/config.h 中默认定义为 512 字节,而 POSIX 平台(src/arch_unix.h)覆盖为 16384,因此桌面环境下每次实际发送约 16 KB。这正是文档所述"shaping traffic to send small data chunks"——把一个大文件拆成一批小数据块按序发送,避免一次性把整个文件塞进发送缓冲。

收到响应后结束事件循环

MG_EV_HTTP_MSG中打印响应体(即服务器返回的ok),随后关闭文件、设置done标志让主循环退出;MG_EV_ERROR分支则负责连接失败/超时时的清理。连接超时由MG_EV_POLL中记录的mg_millis() + s_timeout_ms(默认 1500 ms)判断,超时会通过mg_error()断开。

客户端主流程非常精简(client.cmain()):mg_mgr_init()mg_http_connect(&mgr, s_url, fn, &done)→ 循环mg_mgr_poll(&mgr, 50)直到done为真 →mg_mgr_free(),全程事件驱动,无阻塞等待。

关键 API 与机制小结

API / 机制作用出处
MG_EV_HTTP_HDRS收到完整 HTTP 头但 body 未齐时触发,是接管请求的最佳时机src/event.h、src/http.c
c->pfn = NULL静默内置 HTTP 协议处理器,由用户代码接管原始字节流server.c
mg_http_creds()服务端解析 Authorization(支持 Basic / Bearer / Cookie / query 多种来源)src/http.c
mg_http_bauth()客户端生成Authorization: Basic base64(user:pass)src/http.c
mg_path_is_sane()校验文件路径,防御..路径穿越src/util.c
mg_match(uri, "/upload/#")通配匹配上传 URIserver.c
mg_http_reply()发送带状态码的 HTTP 响应src/http.c
mg_http_serve_dir()静态目录服务(非上传请求)server.c

值得强调的是mg_http_creds()的通用性:从源码(src/http.c)可以看到,它依次尝试Authorization: BasicAuthorization: Bearer、Cookie 中的access_token以及 query 参数中的access_token,因此同一套服务器代码稍作修改即可扩展为 Bearer Token 或 cookie 会话认证,而不必改动协议处理框架。

安全边界与适用限制

最后梳理本示例体现的工程约束,这些也是把示例移植到产品时应当保留的底线:

  • 认证是上传的门禁:未通过authuser()的请求一律403 Denied,且连接随即关闭。生产环境应至少将凭据改为强口令,并建议切换到 HTTPS(mongoose 支持 TLS,参见仓库根目录 tls.h 及相关驱动)。
  • 大小限制-s参数(默认 10000 字节)在接收前就依据 Content-Length 拦截超限请求,避免磁盘被撑爆;但由于检查发生在头部阶段,若客户端使用Transfer-Encoding: chunked且不声明长度,则需要额外的累计计数逻辑。
  • 路径安全mg_path_is_sane()MG_PATH_MAX双重把关,杜绝路径穿越与超长路径。
  • 内存恒定:流式写盘确保内存占用与文件大小解耦,这是本示例在低 RAM 嵌入式设备上可行的根本原因;对应地,客户端也通过MG_IO_SIZE分块发送,避免大文件整体进缓冲。

如果需要在此基础上继续扩展,可以参考仓库中 tutorials/http/file-upload-html-form、tutorials/http/file-upload-multiple-posts 与 tutorials/http/file-upload-single-post 等示例,它们展示了 HTML 表单、多 POST 分片上传等不同形态的落盘方案;Mongoose 的 HTTP 协议核心实现与所有相关事件定义则可直接阅读 src/http.c 与 src/http.h。

  • 嵌入式
  • 网络
  • 通信
  • 物联网

【免费下载链接】mongoose

Embedded web server, with TCP/IP network stack, MQTT and Websocket

项目地址:https://gitcode.com/gh_mirrors/mon/mongoose
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询