送文件 server端:從零搭建可復(fù)用的文件傳輸服務(wù))
1. mongoose tcp發(fā)送文件 server端先搞清楚要解決什么問(wèn)題如果你正在用 mongoose 寫(xiě)一個(gè) TCP server想讓它在收到客戶端指令后把本地文件穩(wěn)定地推過(guò)去那你大概率會(huì)遇到幾個(gè)很現(xiàn)實(shí)的問(wèn)題文件大了怎么分塊、發(fā)完怎么讓對(duì)端知道“發(fā)完了”、發(fā)送過(guò)程中連接斷了怎么辦、以及怎么驗(yàn)證真的收全了。mongoose 本身是一個(gè)很輕量的網(wǎng)絡(luò)庫(kù)它不會(huì)幫你把文件傳輸協(xié)議也一起設(shè)計(jì)好所以這部分邏輯得自己補(bǔ)。mongoose tcp發(fā)送文件 server端 這個(gè)場(chǎng)景核心不是“怎么調(diào) API”而是“怎么設(shè)計(jì)一個(gè)可復(fù)用、可校驗(yàn)、可排障的傳輸流程”。我見(jiàn)過(guò)太多示例代碼是直接把整個(gè)文件讀進(jìn)內(nèi)存再mg_send小文件沒(méi)問(wèn)題一旦上到幾十 MB 甚至上百 MB內(nèi)存直接飆上去嵌入式設(shè)備根本扛不住。所以這篇內(nèi)容會(huì)圍繞一個(gè)可落地的實(shí)現(xiàn)路徑來(lái)講先約定幀格式再做分塊發(fā)送最后做接收端校驗(yàn)和吞吐驗(yàn)證。適合誰(shuí)看做嵌入式網(wǎng)關(guān)、邊緣設(shè)備、輕量級(jí)文件同步服務(wù)的同學(xué)用 C/C 寫(xiě) TCP 服務(wù)、又不想引入重型框架的同學(xué)以及已經(jīng)用 mongoose 跑通了 echo server想進(jìn)一步做文件傳輸?shù)耐瑢W(xué)。你需要的基礎(chǔ)是會(huì)用 mongoose 的mg_mgr、mg_bind、mg_send知道MG_EV_RECV和MG_EV_CLOSE大概在什么時(shí)候觸發(fā)。先明確一個(gè)設(shè)計(jì)原則TCP 是字節(jié)流沒(méi)有消息邊界。所以你不能假設(shè)“一次mg_send對(duì)應(yīng)一次recv”。必須自己在應(yīng)用層定義幀結(jié)構(gòu)。我采用的方案是4 字節(jié)小端長(zhǎng)度前綴 JSON 頭 原始文件字節(jié)。JSON 頭里帶文件名、文件大小、分塊大小、校驗(yàn)方式。這樣接收端先讀 4 字節(jié)拿到頭長(zhǎng)度再讀頭解析出文件大小然后按字節(jié)數(shù)收文件體收滿即完成。這個(gè)約定一旦定下來(lái)server 端和 client 端就能解耦后面換語(yǔ)言實(shí)現(xiàn)也不影響。還有一個(gè)容易被忽略的點(diǎn)mongoose 的mg_send是往發(fā)送緩沖區(qū)里追加數(shù)據(jù)不是阻塞發(fā)送。所以你不能在一個(gè)循環(huán)里無(wú)腦塞幾百 MB得關(guān)注mg_connection的發(fā)送隊(duì)列。mongoose 提供了mg_send的返回值實(shí)際入隊(duì)字節(jié)數(shù)以及可以通過(guò)nc-send_mbuf.len觀察積壓。合理的做法是分塊發(fā)送每塊比如 8KB 到 64KB發(fā)完一塊后讓出事件循環(huán)等MG_EV_POLL或下一次可寫(xiě)時(shí)再繼續(xù)。這樣既不會(huì)撐爆內(nèi)存也不會(huì)把事件循環(huán)卡死。下面這張表是我在實(shí)際項(xiàng)目里對(duì)比過(guò)的分塊大小選擇你可以參考分塊大小內(nèi)存占用吞吐表現(xiàn)適用場(chǎng)景4 KB低一般內(nèi)存極緊張的 MCU16 KB較低較好嵌入式 Linux 網(wǎng)關(guān)64 KB中等好普通服務(wù)端256 KB較高很好局域網(wǎng)大文件選 16KB 或 64KB 通常是比較穩(wěn)的折中。接下來(lái)進(jìn)入具體實(shí)現(xiàn)。2. TaoToken 前置為什么文件傳輸服務(wù)也需要模型能力兜底你可能會(huì)問(wèn)一個(gè) TCP 文件傳輸 server跟大模型有什么關(guān)系關(guān)系在于當(dāng)你的傳輸服務(wù)跑在邊緣設(shè)備上日志、報(bào)錯(cuò)、協(xié)議解析這些環(huán)節(jié)往往需要一個(gè)能快速解釋和生成代碼的助手。比如接收端報(bào)reading choices之類的解析錯(cuò)誤或者 mongoose 返回local proxy failed你希望有個(gè)地方能直接把報(bào)錯(cuò)貼進(jìn)去問(wèn)清楚而不是翻半天文檔。我自己的做法是把 TaoToken 當(dāng)成一個(gè)“隨叫隨到的協(xié)議排障助手”。它的模型對(duì)話入口可以直接貼 C 代碼和報(bào)錯(cuò)讓它幫你定位是幀解析錯(cuò)了還是緩沖區(qū)沒(méi)清。對(duì)于長(zhǎng)期做嵌入式網(wǎng)絡(luò)開(kāi)發(fā)的人來(lái)說(shuō)Coding Plan 更適合因?yàn)槟銜?huì)反復(fù)需要生成和改寫(xiě) mongoose 事件處理邏輯。而 API Keys 和接入文檔則是你把它接進(jìn)自己工具鏈的入口。這里要強(qiáng)調(diào)一點(diǎn)TaoToken 不是用來(lái)替代你的編輯器或編譯器的它解決的是“理解”和“生成”的問(wèn)題。你的 mongoose server 還是得自己編譯、自己跑、自己抓包驗(yàn)證。模型能幫你的是解釋mg_send的返回值語(yǔ)義、幫你寫(xiě)一個(gè)校驗(yàn)函數(shù)、幫你分析為什么接收端少收了 4 個(gè)字節(jié)。如果你只是偶爾查一下報(bào)錯(cuò)用模型對(duì)話就夠了如果你要把它接進(jìn) CI 或者自己的腳本里做自動(dòng)化代碼檢查那就走 API。地址我放在下面按需取用模型對(duì)話https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chatCoding Planhttps://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding_planAPI Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi_keys接入文檔https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdocAPI 的基礎(chǔ)地址是https://taotoken.net/api這個(gè)不帶 UTM直接用于代碼里配置 Base URL。如果你用的是 Claude Code 這類工具做代碼潤(rùn)色它對(duì)應(yīng)的接入方式在文檔里有說(shuō)明核心還是三件套Base URL、Key、Model ID。這三樣配齊才能讓工具真正跑起來(lái)而不是只停留在“連上后就能用”的空話。回到文件傳輸本身。為什么我要在第二節(jié)講這個(gè)因?yàn)閷?shí)際排障時(shí)你面對(duì)的不是一個(gè)孤立的mg_send調(diào)用而是一整條鏈路mongoose 事件循環(huán)、TCP 緩沖區(qū)、對(duì)端解析、文件落盤(pán)。任何一環(huán)出問(wèn)題表現(xiàn)都可能是“文件傳了一半”。這時(shí)候有一個(gè)能快速解釋報(bào)錯(cuò)、生成校驗(yàn)代碼的助手能省很多時(shí)間。但記住最終驗(yàn)證必須靠你自己的抓包和校驗(yàn)邏輯模型只是加速理解。3. 可復(fù)制配置mongoose TCP server 分塊發(fā)送完整代碼這一節(jié)直接給可復(fù)制的代碼和配置。先約定幀格式再給 server 端實(shí)現(xiàn)。幀結(jié)構(gòu)如下[4字節(jié)小端頭長(zhǎng)度][JSON頭][文件原始字節(jié)...]JSON 頭示例{ msg: 0, fileName: data_0.mp4, fileSize: 10485760, chunkSize: 16384, checksum: crc32 }server 端收到客戶端發(fā)來(lái)的請(qǐng)求后讀取本地文件先發(fā)頭再分塊發(fā)文件體。關(guān)鍵點(diǎn)是不要在MG_EV_RECV里一次性把整個(gè)文件讀完發(fā)完而是用一個(gè)發(fā)送狀態(tài)機(jī)在MG_EV_POLL里持續(xù)推進(jìn)。下面是一個(gè)可編譯的完整示例基于 mongoose 7.x#include mongoose.h #include stdio.h #include string.h #include stdlib.h #define CHUNK_SIZE 16384 struct send_state { FILE *fp; long file_size; long sent; int header_sent; char file_name[256]; }; static void send_file_header(struct mg_connection *c, struct send_state *st) { char json[512]; int json_len snprintf(json, sizeof(json), {\msg\:0,\fileName\:\%s\,\fileSize\:%ld,\chunkSize\:%d,\checksum\:\crc32\}, st-file_name, st-file_size, CHUNK_SIZE); uint32_t len_le (uint32_t)json_len; mg_send(c, len_le, 4); mg_send(c, json, json_len); st-header_sent 1; } static void send_file_chunk(struct mg_connection *c, struct send_state *st) { char buf[CHUNK_SIZE]; size_t n fread(buf, 1, CHUNK_SIZE, st-fp); if (n 0) { mg_send(c, buf, n); st-sent n; } if (st-sent st-file_size) { fclose(st-fp); st-fp NULL; MG_INFO((file send done: %ld bytes, st-sent)); } } static void ev_handler(struct mg_connection *c, int ev, void *ev_data) { struct send_state *st (struct send_state *)c-fn_data; if (ev MG_EV_RECV) { struct mg_str *data (struct mg_str *)ev_data; if (data-len 4) return; uint32_t head_len 0; memcpy(head_len,>gcc server.c mongoose.c -o file_server -lpthreadWindows 下用 MSVC 或 MinGW 類似注意路徑改成實(shí)際文件路徑。這段代碼的關(guān)鍵設(shè)計(jì)點(diǎn)第一MG_EV_RECV里只做請(qǐng)求解析和狀態(tài)初始化不直接發(fā)文件。第二MG_EV_POLL里檢查c-send_mbuf.len只有積壓小于 64KB 時(shí)才繼續(xù)發(fā)下一塊避免內(nèi)存暴漲。第三MG_EV_CLOSE里清理文件句柄防止泄漏。第四頭長(zhǎng)度用 4 字節(jié)小端接收端按同樣規(guī)則解析。如果你用的是 mongoose 的 JSON 配置方式比如某些集成場(chǎng)景對(duì)應(yīng)的 settings 片段可以寫(xiě)成{ tcp_server: { listen: tcp://0.0.0.0:18888, chunk_size: 16384, send_high_water: 65536, file_root: D:/IMG/video } }這個(gè) JSON 不是 mongoose 原生配置而是我建議你在自己項(xiàng)目里抽出來(lái)的配置層方便換端口、換分塊大小、換文件根目錄。把chunk_size和send_high_water做成可配置后面調(diào)吞吐會(huì)方便很多。4. 驗(yàn)證請(qǐng)求與成功結(jié)果接收端校驗(yàn)和吞吐測(cè)試代碼跑起來(lái)只是第一步真正要確認(rèn)的是“文件傳對(duì)了”。這一節(jié)給接收端的校驗(yàn)步驟和吞吐驗(yàn)證方法。先寫(xiě)一個(gè)簡(jiǎn)單的接收端用 Python 快速驗(yàn)證不用編譯 Cimport socket import struct import json import hashlib HOST 127.0.0.1 PORT 18888 s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) s.sendall(struct.pack(I, 2) b{}) head_len struct.unpack(I, s.recv(4))[0] head b while len(head) head_len: head s.recv(head_len - len(head)) meta json.loads(head) print(meta:, meta) file_size meta[fileSize] received 0 md5 hashlib.md5() with open(recv_ meta[fileName], wb) as f: while received file_size: chunk s.recv(min(16384, file_size - received)) if not chunk: break f.write(chunk) md5.update(chunk) received len(chunk) print(received:, received, expected:, file_size) print(md5:, md5.hexdigest()) s.close()運(yùn)行后你應(yīng)該看到received和expected相等并且本地生成的文件能正常播放或打開(kāi)。如果received小于expected說(shuō)明發(fā)送端提前關(guān)了連接或者接收端循環(huán)條件寫(xiě)錯(cuò)了。吞吐驗(yàn)證在 server 端記錄發(fā)送開(kāi)始和結(jié)束時(shí)間算一下 MB/s。我實(shí)測(cè)在局域網(wǎng) 16KB 分塊下大概能跑到 80 到 120 MB/s取決于磁盤(pán)和網(wǎng)卡。如果你發(fā)現(xiàn)吞吐很低先檢查是不是每發(fā)一塊就 sleep 了。原示例里有個(gè)sleep_for(1000ms)那是調(diào)試用的生產(chǎn)環(huán)境必須去掉否則 1 秒才發(fā)一塊吞吐直接崩。穩(wěn)定性驗(yàn)證連續(xù)傳 100 次同一個(gè)文件觀察內(nèi)存是否增長(zhǎng)??梢杂胻op或任務(wù)管理器看 server 進(jìn)程的 RSS。如果每次傳完內(nèi)存不回落檢查fclose和mg_mgr_free是否被正確調(diào)用。另外故意在傳輸中途斷開(kāi)客戶端看 server 是否在MG_EV_CLOSE里清理了文件句柄。這個(gè)測(cè)試很重要很多內(nèi)存泄漏就是斷連時(shí)沒(méi)清理導(dǎo)致的。還有一個(gè)校驗(yàn)點(diǎn)是 CRC32。如果你在 JSON 頭里聲明了checksum: crc32接收端就應(yīng)該算一遍 CRC32 并比對(duì)。Python 里可以用zlib.crc32。這樣即使 TCP 保證了字節(jié)順序你也能確認(rèn)文件內(nèi)容沒(méi)被中間環(huán)節(jié)改壞。5. 本篇常見(jiàn)錯(cuò)排查401、local proxy failed、reading choices、OAuth這一節(jié)對(duì)照真實(shí)報(bào)錯(cuò)來(lái)講。雖然這些報(bào)錯(cuò)有些來(lái)自模型工具鏈但在你搭建和調(diào)試文件傳輸服務(wù)時(shí)很可能同時(shí)用到模型助手所以一起說(shuō)清楚。401 Unauthorized如果你在調(diào)用模型 API 做代碼解釋時(shí)遇到 401通常是 Key 沒(méi)配或配錯(cuò)。檢查你的請(qǐng)求頭里Authorization: Bearer key是否正確Key 是否過(guò)期。在 TaoToken 的 API Keys 頁(yè)面可以重新生成。注意不要把 Key 硬編碼進(jìn)提交到倉(cāng)庫(kù)的代碼里。local proxy failed這個(gè)報(bào)錯(cuò)一般出現(xiàn)在你本地配了代理但代理沒(méi)起來(lái)或端口不對(duì)。文件傳輸服務(wù)本身不需要代理但如果你用某些工具去訪問(wèn)模型接口工具可能讀了系統(tǒng)代理設(shè)置。解決辦法是檢查環(huán)境變量HTTP_PROXY/HTTPS_PROXY或者在工具配置里顯式關(guān)閉代理。注意這里說(shuō)的是本地開(kāi)發(fā)環(huán)境的代理配置問(wèn)題不是讓你去用什么特殊網(wǎng)絡(luò)手段純粹是配置排查。reading choices 相關(guān)解析錯(cuò)誤這類報(bào)錯(cuò)通常出現(xiàn)在你調(diào)用模型接口后返回的 JSON 結(jié)構(gòu)和你預(yù)期的不一樣。比如你期望choices[0].message.content但實(shí)際返回里choices為空或字段名不同。排查方法是先把原始響應(yīng)打印出來(lái)確認(rèn)結(jié)構(gòu)再改解析代碼。在文件傳輸場(chǎng)景里如果你用模型生成解析代碼一定要讓它先輸出原始 JSON 示例再寫(xiě)解析邏輯。OAuth 相關(guān)報(bào)錯(cuò)如果你用的是 Claude Code 或類似工具可能會(huì)遇到 OAuth 認(rèn)證失敗。這時(shí)候檢查三件套是否齊全Base URL、Key、Model ID。Base URL 用https://taotoken.net/apiKey 用你生成的Model ID 按文檔填。三者缺一不可只填兩個(gè)就會(huì)報(bào)認(rèn)證類錯(cuò)誤。另外文件傳輸本身的常見(jiàn)錯(cuò)報(bào)錯(cuò)/現(xiàn)象可能原因排查動(dòng)作接收端少收字節(jié)發(fā)送端提前 close檢查MG_EV_CLOSE是否在發(fā)完前觸發(fā)文件損壞幀邊界錯(cuò)位打印頭長(zhǎng)度和實(shí)際讀取長(zhǎng)度內(nèi)存持續(xù)增長(zhǎng)文件句柄未關(guān)在 close 事件里 fclose吞吐極低每塊后 sleep去掉 sleep用 send_mbuf 水位控制bind failed端口被占用換端口或查 netstat這些排查動(dòng)作都是可執(zhí)行的不要只停留在“看起來(lái)沒(méi)問(wèn)題”。6. 語(yǔ)義一致 CTA把排障和接入串起來(lái)文件傳輸服務(wù)搭好之后你大概率還會(huì)遇到兩類需求一是繼續(xù)優(yōu)化協(xié)議比如加斷點(diǎn)續(xù)傳、加壓縮二是把模型能力接進(jìn)你的開(kāi)發(fā)流程讓排障更快。這兩件事可以并行。如果你現(xiàn)在卡在某個(gè)報(bào)錯(cuò)上比如local proxy failed或者reading choices解析不對(duì)最直接的辦法是去模型對(duì)話里把報(bào)錯(cuò)和上下文貼進(jìn)去讓它幫你定位。入口在這里模型對(duì)話https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentmodel_chat如果你打算長(zhǎng)期做嵌入式網(wǎng)絡(luò)開(kāi)發(fā)反復(fù)需要生成和改寫(xiě) mongoose 事件處理代碼那 Coding Plan 更合適Coding Planhttps://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentcoding_plan如果你要把能力接進(jìn)自己的腳本或 CI先去 API Keys 生成 Key再對(duì)照接入文檔配 Base URL 和 Model IDAPI Keyshttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentapi_keys接入文檔https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_contentdocAPI 基礎(chǔ)地址是https://taotoken.net/api。配的時(shí)候記住三件套Base URL、Key、Model ID缺一個(gè)都跑不起來(lái)。文件傳輸?shù)尿?yàn)證還是靠你自己的抓包和校驗(yàn)?zāi)P蛶湍慵铀倮斫獾罱K結(jié)果以你的實(shí)測(cè)為準(zhǔn)。