多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Mongoose 传输图片(Transfer-Encoding: chunked):用 mg_send_http_chunk 分块推送大图并验证 Content-Length 缺失场景

Mongoose 传输图片(Transfer-Encoding: chunked):用 mg_send_http_chunk 分块推送大图并验证 Content-Length 缺失场景 1. Mongoose 传大图踩坑记为什么 Content-Length 会翻车先说结论Mongoose 嵌入式 HTTP 服务端传大图最容易翻车的不是网络库本身而是你对 HTTP 报文体的理解。我见过太多人写mg_printf(conn, HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\n, len)然后mg_send一把梭小图没事一上几 MB 的 JPEG 就出现客户端卡死、图片下半截灰掉、浏览器一直转圈。核心检索词先摆出来Mongoose 用 mg_send_http_chunk 实现 Transfer-Encoding: chunked 分块推送大图这件事能解决什么问题它让服务端在不知道最终图片总长度、或者不想一次性把整张图读进内存的情况下依然能把图片流式发给浏览器。适合谁适合在 ESP32、STM32、Linux 边缘盒子上跑 Mongoose 做本地 Web 配置页、图传、抓拍预览的嵌入式开发者。Content-Length方案的本质是响应头里先声明「我这次要发 N 字节」客户端按 N 字节收收满就结束。问题在于图片是动态生成的比如摄像头抓拍、缩放、加水印你在写响应头的那一刻还不知道最终字节数图片文件很大一次性malloc整块内存嵌入式设备直接 OOM你算错了长度客户端就会一直等剩下的字节直到超时。而Transfer-Encoding: chunked把「长度」这件事从头部挪到了报文体里每个数据块前面用十六进制写清本块多长最后用一个长度为 0 的块表示结束。客户端不需要预先知道总长边收边解析。Mongoose 把这个协议细节封装成了mg_send_http_chunk()你只管喂数据它负责拼十六进制长度行和 CRLF。下面这张表先把两种方案摆在一起对比后面再上代码。维度Content-LengthTransfer-Encoding: chunked总长度是否需预知必须不需要内存占用通常需整块缓冲可分块流式发送结束标志收满 N 字节收到长度 0 的块适用场景静态小文件、已知长度大图、动态生成、流式Mongoose APImg_send / mg_printfmg_send_http_chunk注意一个坑Transfer-Encoding和Content-Length不要同时出现在同一个响应里RFC 7230 明确规定二者互斥同时出现时客户端行为未定义很多解析器会直接报错或忽略 chunked。你在改代码时务必把原来那行Content-Length删干净。2. TaoToken 前置准备给调试链路配一个稳定入口写嵌入式 HTTP 服务端最烦的不是写代码而是调试链路本身不稳定抓包工具、日志分析、模型辅助排查各走各的。我习惯在动手前先把调试用的模型入口配好遇到mg_send_http_chunk发出去客户端却解析失败时能快速把原始报文贴进去问。这里用的是 TaoToken官网入口是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 这个不加 UTM。它提供的是兼容 OpenAI 风格的接口所以你在任何支持自定义 Base URL 的客户端里都能接。先拿 Key进控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 创建一个。创建后那串sk-开头的字符串只显示一次复制存好。如果你只是想验证模型通不通直接用模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 发一句话即可不用写代码。但做嵌入式调试我更推荐把它接进编辑器或命令行这样贴报文、贴报错更顺手。长期做编码和 Agent 任务的话Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 比按次调用更划算适合你反复调 Mongoose 回调、反复问 HTTP 报文格式的场景。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各客户端的配置示例。如果你用 Claude Code 这类工具参考 https://taotoken.net/claudecode?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 的说明配 Anthropic 兼容入口。为什么要在讲 Mongoose 之前先配这个因为 chunked 的调试非常依赖「把原始字节流看清楚」。你抓到的报文里十六进制长度行、CRLF、结束块肉眼很容易看错一位。把原始报文丢给模型做逐字节解释比你自己数\r\n快得多。这一步不是必须但能省掉大量来回试错的时间。3. 可复制配置mg_send_http_chunk 分块发送完整代码这一节是全文重点直接给能跑的代码。假设你已经用 Mongoose 起了一个 HTTP 服务事件处理函数里判断路由。下面这段是核心逻辑路径和参数都按你实际项目改。先看响应头。chunked 的关键就是头部这一行且不能再带Content-Length// 只发头部声明使用分块编码 mg_printf(connection, HTTP/1.1 200 OK\r\n Content-Type: image/jpeg\r\n Transfer-Encoding: chunked\r\n Connection: close\r\n \r\n);注意Content-Type要写对图片是image/jpeg或image/png写错浏览器可能不渲染。Connection: close在嵌入式场景下建议加上避免长连接占资源。然后是分块发送。假设你已经把图片读进image.data()长度是flength// 第一块发送图片数据 mg_send_http_chunk(connection, image.data(), flength); // 可选如果图片是分多次读的可以循环发多块 // while ((n read_next_block(buf, sizeof(buf))) 0) { // mg_send_http_chunk(connection, buf, n); // } // 结束块长度 0数据为空表示响应体结束 mg_send_http_chunk(connection, , 0);mg_send_http_chunk的签名是void mg_send_http_chunk(struct mg_connection *c, const char *buf, size_t len)。它内部会做三件事把len转成十六进制字符串、拼上\r\n、再拼数据、再拼\r\n。当len 0时它发的是0\r\n\r\n也就是结束标志。如果你要分多块发比如边读文件边发写法是这样FILE *fp fopen(C:\\Users\\l110272\\Desktop\\1.jpg, rb); if (fp NULL) { mg_printf(connection, HTTP/1.1 404 Not Found\r\n\r\n); return; } mg_printf(connection, HTTP/1.1 200 OK\r\n Content-Type: image/jpeg\r\n Transfer-Encoding: chunked\r\n \r\n); char buf[4096]; size_t n; while ((n fread(buf, 1, sizeof(buf), fp)) 0) { mg_send_http_chunk(connection, buf, n); } fclose(fp); // 必须发结束块否则客户端一直等 mg_send_http_chunk(connection, , 0);这段代码的价值在于内存里永远只有一个 4KB 的缓冲区多大的图都不会 OOM。这就是 chunked 相比 Content-Length 最实在的好处。再给一个对照的 Content-Length 写法方便你理解差异// Content-Length 方案必须先知道总长度 mg_printf(connection, HTTP/1.1 200 OK\r\n Content-Type: image/jpeg\r\n Content-Length: %d\r\n \r\n, (int) flength); mg_send(connection, image.data(), flength);对比一下就很清楚Content-Length 方案里flength必须在写头部时就确定而且image.data()得整块在内存里。chunked 方案把这两条约束都解除了。如果你用 Codex 或类似工具配置auth.json时记得三件套要写全Base URL 填https://taotoken.net/apiKey 填你创建的sk-串Model ID 按文档里给的填。Cline 里配 MCP 也是同理Base URL、Key、Model ID 一个都不能少缺一个就会报认证失败。4. 验证请求抓包看 chunked 报文长什么样代码写完不算完得验证客户端真的按 chunked 解析了。最直接的办法是抓包看原始字节流。用 Wireshark 抓本地回环或网卡过滤http找到你的响应。一个正确的 chunked 响应体长这样十六进制长度行 CRLF 数据 CRLFHTTP/1.1 200 OK Content-Type: image/jpeg Transfer-Encoding: chunked 1000 4096 字节的图片数据 1000 4096 字节的图片数据 ... 0注意最后那个0后面跟一个空行就是0\r\n\r\n。如果你抓到的报文里数据块前面没有十六进制长度行说明你用的是mg_send而不是mg_send_http_chunk客户端会把它当成 Content-Length 缺失的响应直接报错。用 curl 验证更简单curl -v -o out.jpg http://192.168.1.100/api/Image-v会打印响应头你应该看到 HTTP/1.1 200 OK Content-Type: image/jpeg Transfer-Encoding: chunked如果看到的是 Content-Length: 11368说明你头部没改对。如果 curl 卡住不返回八成是忘了发结束块mg_send_http_chunk(connection, , 0)。再验证图片完整性比对字节数ls -l out.jpg # 和源文件大小对比应该一致浏览器验证直接访问http://设备IP/api/Image图片能完整显示、右键另存为后能正常打开就说明 chunked 解析没问题。如果图片显示一半就断检查是不是中间某次mg_send_http_chunk的len传错了或者循环里提前 break 了。还有一个细节Mongoose 的mg_send_http_chunk在发送时会自动处理连接的写缓冲。如果你在回调里连续发很多块注意 Mongoose 的发送是异步的数据先进缓冲再刷出。嵌入式设备缓冲小块太大会导致mg_send返回 0缓冲满。这时候要么减小块大小要么用mg_send的返回值做流控。实测下来4KB 一块在 ESP32 上比较稳。5. 常见报错排查401、local proxy failed、reading choices这一节把真实会撞上的报错列出来对照着查。报错一客户端报net::ERR_INCOMPLETE_CHUNKED_ENCODING这是最典型的。原因几乎都是没发结束块。chunked 协议规定必须用长度 0 的块收尾你只发了数据块就 return客户端永远等不到结束标志。检查你的代码路径确保每条分支最后都调用了mg_send_http_chunk(connection, , 0)。报错二401 Unauthorized这个通常不是 Mongoose 的问题而是你在调试链路里配的模型入口认证失败。检查三件套Base URL 是不是https://taotoken.net/apiKey 是不是完整的sk-串Model ID 有没有写对。三者缺一或写错都会 401。如果你用 Claude Code参考 https://taotoken.net/claudecode?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 里的 Anthropic 兼容配置别把 OpenAI 风格的 Base URL 填进去。报错三local proxy failed这个报错一般出现在你本地调试工具走代理时。检查你的工具配置里有没有多余的代理设置把代理关掉直连https://taotoken.net/api。嵌入式设备本身不涉及这个但你在 PC 上用工具抓包、调模型时容易撞上。报错四reading choices相关解析错误这是模型返回体解析失败通常是你把非 OpenAI 格式的响应喂给了期望 OpenAI 格式的客户端。确认你调的是兼容接口响应里应该有choices字段。如果返回的是别的结构说明 Base URL 或路径拼错了。报错五图片显示但颜色错乱 / 下半截灰这不是 chunked 的锅是Content-Type写错或数据块边界错位。检查Content-Type是不是image/jpeg以及每次mg_send_http_chunk的len是不是真实数据长度。如果你传了flength但image.data()实际只有一半就会多发垃圾字节。报错六Mongoose 回调里mg_send_http_chunk返回后连接立刻关闭检查是不是在MG_EV_HTTP_REQUEST回调里同步发完就 return 了而 Mongoose 还没把缓冲刷出去。这种情况要么在回调里等MG_EV_SEND要么确保Connection: close让 Mongoose 发完再关。嵌入式场景下把发送逻辑放在MG_EV_HTTP_REQUEST里同步做完通常没问题但如果数据量特别大建议分片并在MG_EV_SEND里继续。排查顺序建议先看客户端报什么错再抓包看原始报文最后对照代码。90% 的 chunked 问题都能靠「有没有结束块」和「头部有没有误带 Content-Length」这两条定位。6. 继续调试与接入把链路固定下来Mongoose 传大图这件事代码本身不复杂难的是把调试链路固定成一套可复用的流程。我的做法是抓包工具常开模型入口配好遇到报文看不懂就贴进去逐字节问。模型对话入口 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 适合快速验证一句话接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里有各客户端配置API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 管理你的凭证长期做编码任务用 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 更省。最后留一个实用技巧在 Mongoose 里调试 chunked可以在每次mg_send_http_chunk前加一行日志打印本次发送的len发完结束块再打一行。这样你对照抓包结果一眼就能看出哪一块长度对不上。嵌入式设备没有方便的调试器时串口日志就是你的抓包工具。
返回列表