戰(zhàn):用 stream::Get 與 open_stream 實(shí)現(xiàn)逐塊讀取、SSE 與反向代理)
后端網(wǎng)絡(luò)【免費(fèi)下載鏈接】cpp-httplibA C header-only HTTP/HTTPS server and client library項(xiàng)目地址https://gitcode.com/GitHub_Trending/cp/cpp-httplib點(diǎn)擊查看免費(fèi)下載本篇文章以 cpp-httplib 官方文檔 README-stream.md 為核心系統(tǒng)講解該庫(kù)新增的Streaming API基于迭代器的stream::Get()/stream::Result高層接口以及直接操作 socket 的open_stream()/StreamHandle底層接口。讀完本文你將掌握如何用極低內(nèi)存代價(jià)逐塊消費(fèi) HTTP 響應(yīng)體落地 LLM 流式輸出、SSEServer-Sent Events、大文件下載與反向代理等真實(shí)場(chǎng)景并理解這些 API 在 httplib.h 中的底層實(shí)現(xiàn)原理。一、什么是 cpp-httplib 流式 API傳統(tǒng)上Client::Get()會(huì)等待整個(gè)響應(yīng)體完整接收并緩沖到內(nèi)存中這對(duì)小響應(yīng)和 Keep-Alive 連接復(fù)用很友好但遇到超大文件或無(wú)限流式輸出如大模型逐字生成時(shí)內(nèi)存與首字節(jié)延遲都不可接受。cpp-httplib 為此提供了流式擴(kuò)展數(shù)據(jù)直接從網(wǎng)絡(luò) socket 讀取一次只保留當(dāng)前一個(gè)數(shù)據(jù)塊在內(nèi)存中配合迭代器風(fēng)格的循環(huán)逐塊處理實(shí)現(xiàn)真正的 socket 級(jí)流式true socket-level streaming。流式 API 特別適用于以下幾類(lèi)場(chǎng)景LLM / AI 流式響應(yīng)如 ChatGPT、Claude、Ollama 等以 JSON Lines 逐行輸出的接口Server-Sent EventsSSE實(shí)時(shí)推送大文件下載及下載進(jìn)度跟蹤反向代理實(shí)現(xiàn)把上游響應(yīng)體原樣轉(zhuǎn)發(fā)給下游。使用前請(qǐng)先記住三條重要約束無(wú) Keep-Alive每次stream::Get()都會(huì)使用一條專(zhuān)用連接響應(yīng)體讀完后連接即關(guān)閉需要連接復(fù)用時(shí)請(qǐng)改用Client::Get()。只能迭代一次next()方法只能從頭到尾遍歷響應(yīng)體一遍。Result 非線(xiàn)程安全stream::Get()可以在多個(gè)線(xiàn)程同時(shí)被調(diào)用但返回的stream::Result只能由單個(gè)線(xiàn)程使用。二、Quick Start第一個(gè)流式客戶(hù)端在項(xiàng)目目錄中引入頭文件后即可用與Client::Get()幾乎相同的寫(xiě)法發(fā)起流式請(qǐng)求#include httplib.h int main() { httplib::Client cli(http://localhost:8080); // Get streaming response auto result httplib::stream::Get(cli, /stream); if (result) { // Process response body in chunks while (result.next()) { std::cout.write(result.data(), result.size()); } } return 0; }核心循環(huán)是while (result.next())next()每次從 socket 讀入一塊數(shù)據(jù)并返回true讀到流結(jié)束返回falseresult.data()指向當(dāng)前塊起始位置result.size()給出當(dāng)前塊字節(jié)數(shù)。整個(gè)過(guò)程中內(nèi)存中只保留一個(gè)塊配合std::cout.write()即可把響應(yīng)原樣輸出。需要說(shuō)明的是stream::Get只是open_stream(GET, ...)的便捷封裝源碼見(jiàn) httplib.h默認(rèn)塊大小chunk_size 8192字節(jié)。除了 GETstream命名空間還提供Post/Put/Patch/Delete/Head/Options等對(duì)應(yīng)方法均支持傳Headers、Params與請(qǐng)求體見(jiàn) httplib.h。三、API 分層從高層到低層四層接口cpp-httplib 針對(duì)不同使用訴求提供了四層 API從上到下抽象層級(jí)遞減、控制粒度遞增┌─────────────────────────────────────────────┐ │ SSEClient │ ← SSE-specific, parsed events │ - on_message(), on_event() │ │ - Auto-reconnect, Last-Event-ID │ ├─────────────────────────────────────────────┤ │ stream::Get() / stream::Result │ ← Iterator-based streaming │ - while (result.next()) { ... } │ ├─────────────────────────────────────────────┤ │ open_stream() / StreamHandle │ ← General-purpose streaming │ - handle.read(buf, len) │ ├─────────────────────────────────────────────┤ │ Client::Get() │ ← Traditional, full buffering └─────────────────────────────────────────────┘選擇建議如下使用場(chǎng)景推薦 API需要自動(dòng)重連的 SSESSEClient見(jiàn) README-sse.mdLLM 流式輸出JSON Linesstream::Get()大文件下載stream::Get()或open_stream()反向代理open_stream()小響應(yīng) Keep-AliveClient::Get()其中SSEClient命名空間sse實(shí)現(xiàn)于 httplib.h 起的SSEMessage/SSEClient會(huì)把流按 SSE 規(guī)范解析成帶event/data/id字段的消息對(duì)象并內(nèi)置自動(dòng)重連與Last-Event-ID續(xù)傳邏輯是 SSE 場(chǎng)景下的最高層選擇。四、低層 API 參考StreamHandle 與 open_streamStreamHandle是流式 API 的底層句柄接管 socket 連接的所有權(quán)數(shù)據(jù)直接從網(wǎng)絡(luò)讀取。聲明位于 httplib.h。// Open a stream (takes ownership of socket) httplib::Client cli(http://localhost:8080); auto handle cli.open_stream(GET, /path); // Check validity if (handle.is_valid()) { // Access response headers immediately int status handle.response-status; auto content_type handle.response-get_header_value(Content-Type); // Read body incrementally char buf[4096]; ssize_t n; while ((n handle.read(buf, sizeof(buf))) 0) { process(buf, n); } }注意使用open_stream()時(shí)連接專(zhuān)用于流式傳輸不支持 Keep-Alive需要連接復(fù)用的場(chǎng)景請(qǐng)改用client.Get()。StreamHandle 成員一覽成員類(lèi)型說(shuō)明responsestd::unique_ptrResponse含響應(yīng)頭的 HTTP 響應(yīng)對(duì)象errorError請(qǐng)求失敗時(shí)的錯(cuò)誤碼is_valid()bool響應(yīng)有效時(shí)返回 trueread(buf, len)ssize_t直接從 socket 讀取最多l(xiāng)en字節(jié)get_read_error()Error獲取最近一次讀錯(cuò)誤has_read_error()bool檢查是否發(fā)生讀錯(cuò)誤open_stream 的底層實(shí)現(xiàn)細(xì)節(jié)從 httplib.h 的ClientImpl::open_stream()實(shí)現(xiàn)可以看到它做了完整的事前準(zhǔn)備目標(biāo)編碼與緩沖發(fā)送路徑保持一致空Params時(shí)直接用path否則調(diào)用append_query_params()追加查詢(xún)參數(shù)再經(jīng)detail::encode_request_target()編碼保證無(wú)論走哪個(gè) API同樣的path產(chǎn)生同樣的請(qǐng)求行連接準(zhǔn)備在socket_mutex_保護(hù)下檢查現(xiàn)有 socket 是否存活is_socket_aliveSSL 下還會(huì)檢查對(duì)端是否關(guān)閉失效則斷開(kāi)重建并通過(guò)setup_proxy_connection()處理代理所有權(quán)轉(zhuǎn)移transfer_socket_ownership_to_handle()httplib.h把 socket 描述符及 SSL 會(huì)話(huà)從Client移交給StreamHandle客戶(hù)端自身的socket_置為INVALID_SOCKET從此連接生命周期完全由句柄掌控請(qǐng)求寫(xiě)出先在內(nèi)存BufferStream中組裝請(qǐng)求行與請(qǐng)求頭write_request_linecheck_and_write_headers校驗(yàn)通過(guò)后再一次性寫(xiě)入網(wǎng)絡(luò)隨后寫(xiě)入請(qǐng)求體若有避免被拒絕的頭污染線(xiàn)上數(shù)據(jù)響應(yīng)解析讀取響應(yīng)行與響應(yīng)頭并做與普通路徑相同的 framing 檢查HEAD、204、304 可合法攜帶無(wú)體 framing 頭Content-Length沖突視為Error::Read傳輸語(yǔ)義識(shí)別根據(jù)響應(yīng)頭設(shè)置BodyReader的content_length/chunked標(biāo)志is_chunked_transfer_encoding()判定分塊傳輸并對(duì)Content-Encoding創(chuàng)建對(duì)應(yīng)的解壓器見(jiàn)下節(jié)。StreamHandle::read()在存在解壓器時(shí)走read_with_decompression()路徑httplib.h否則直接調(diào)用detail::read_body_content()按 Content-Length / chunked 語(yǔ)義讀取當(dāng)分塊流讀完后還會(huì)解析 trailerparse_trailers_if_needed()httplib.h。測(cè)試 test/test.cc 中的StreamHandleTest驗(yàn)證了is_valid()對(duì)response與error的組合判定邏輯。五、高層 API 參考stream::Get() 與 stream::Resultstream::Result是對(duì)StreamHandle的迭代器式封裝聲明于 httplib.h實(shí)現(xiàn)于 httplib.h使用起來(lái)更符合直覺(jué)#include httplib.h httplib::Client cli(http://localhost:8080); cli.set_follow_location(true); // ... // Simple GET auto result httplib::stream::Get(cli, /path); // GET with custom headers httplib::Headers headers {{Authorization, Bearer token}}; auto result httplib::stream::Get(cli, /path, headers); // Process the response if (result) { while (result.next()) { process(result.data(), result.size()); } } // Or read entire body at once auto result2 httplib::stream::Get(cli, /path); if (result2) { std::string body result2.read_all(); }stream::Result 成員一覽成員類(lèi)型說(shuō)明operator bool()bool響應(yīng)有效時(shí)返回 trueis_valid()bool與operator bool()等價(jià)status()intHTTP 狀態(tài)碼headers()const Headers響應(yīng)頭get_header_value(key, def)std::string獲取響應(yīng)頭值可帶默認(rèn)值has_header(key)bool檢查響應(yīng)頭是否存在next()bool讀取下一塊數(shù)據(jù)讀完返回 falsedata()const char*當(dāng)前塊數(shù)據(jù)指針size()size_t當(dāng)前塊大小read_all()std::string把剩余響應(yīng)體全部讀入字符串error()Error獲取連接/請(qǐng)求錯(cuò)誤read_error()Error獲取最近一次讀錯(cuò)誤has_read_error()bool檢查是否發(fā)生讀錯(cuò)誤實(shí)現(xiàn)細(xì)節(jié)上next()httplib.h在句柄無(wú)效或已結(jié)束時(shí)直接返回false否則確保內(nèi)部緩沖區(qū)不小于chunk_size_后調(diào)用handle_.read()n 0則更新current_size_并返回true讀到 0 或負(fù)值時(shí)置finished_ true并返回false。read_all()不過(guò)是反復(fù)next()并把每塊append到字符串——這解釋了只能迭代一次的約束一旦讀完finished_即被置位。此外stream::Result是**僅移動(dòng)move-only**類(lèi)型拷貝構(gòu)造與拷貝賦值被刪除移動(dòng)構(gòu)造/賦值可用httplib.h因此把它放入 lambda 捕獲列表或返回時(shí)需使用std::move。六、實(shí)戰(zhàn)示例示例 1SSEServer-Sent Events客戶(hù)端用stream::Get()逐塊讀取并實(shí)時(shí)輸出事件流每讀一塊立即flush#include httplib.h #include iostream int main() { httplib::Client cli(http://localhost:1234); auto result httplib::stream::Get(cli, /events); if (!result) { return 1; } while (result.next()) { std::cout.write(result.data(), result.size()); std::cout.flush(); } return 0; }需要自動(dòng)重連、事件解析與Last-Event-ID續(xù)傳的完整 SSE 客戶(hù)端可參考倉(cāng)庫(kù)示例 example/ssecli-stream.cc它在主循環(huán)中不斷調(diào)用httplib::stream::Get(cli, path, headers)連接失敗或讀錯(cuò)誤后按retry_ms間隔重連并通過(guò)parse_sse_line()按 SSE 規(guī)范把緩沖內(nèi)容切分成event/data/id/retry字段遇到空行即完成一個(gè)事件對(duì) 204/404/401/403 等永久性錯(cuò)誤則直接退出不再重連。它還會(huì)校驗(yàn)Content-Type是否為text/event-stream并在流結(jié)束后用result.read_error()區(qū)分正常結(jié)束與連接中斷。示例 2LLM 流式響應(yīng)以本地 Ollama 服務(wù)的/api/generate為例逐塊接收模型生成內(nèi)容并在讀取結(jié)束后檢查錯(cuò)誤#include httplib.h #include iostream int main() { httplib::Client cli(http://localhost:11434); // Ollama auto result httplib::stream::Get(cli, /api/generate); if (result result.status() 200) { while (result.next()) { std::cout.write(result.data(), result.size()); std::cout.flush(); } } // Check for connection errors if (result.read_error() ! httplib::Error::Success) { std::cerr Connection lost\n; } return 0; }result.status()在響應(yīng)頭到達(dá)后即可獲得無(wú)需等待整個(gè)響應(yīng)體配合read_error()可區(qū)分流正常結(jié)束與中途斷開(kāi)兩種收尾形態(tài)。示例 3大文件下載與進(jìn)度顯示邊讀邊寫(xiě)磁盤(pán)并用累計(jì)字節(jié)數(shù)輸出進(jìn)度#include httplib.h #include fstream #include iostream int main() { httplib::Client cli(http://example.com); auto result httplib::stream::Get(cli, /large-file.zip); if (!result || result.status() ! 200) { std::cerr Download failed\n; return 1; } std::ofstream file(download.zip, std::ios::binary); size_t total 0; while (result.next()) { file.write(result.data(), result.size()); total result.size(); std::cout \rDownloaded: (total / 1024) KB std::flush; } std::cout \nComplete!\n; return 0; }由于每個(gè)塊處理完即被覆蓋即使文件達(dá)數(shù) GB內(nèi)存占用也穩(wěn)定在一個(gè)塊默認(rèn) 8 KB附近。倉(cāng)庫(kù)測(cè)試 test/test.cc 中的PostLarge用例即用stream::Post讀取 100 KB 響應(yīng)并斷言累計(jì)字節(jié)數(shù)精確等于100 * 1024驗(yàn)證了塊拼接的完整性。示例 4反向代理流式轉(zhuǎn)發(fā)服務(wù)端 handler 中打開(kāi)到上游的流把狀態(tài)碼、響應(yīng)頭與響應(yīng)體原樣轉(zhuǎn)發(fā)給下游客戶(hù)端#include httplib.h httplib::Server svr; svr.Get(/proxy/(.*), [](const httplib::Request req, httplib::Response res) { httplib::Client upstream(http://backend:8080); auto handle upstream.open_stream(/ req.matches[1].str()); if (!handle.is_valid()) { res.status 502; return; } res.status handle.response-status; res.set_chunked_content_provider( handle.response-get_header_value(Content-Type), handle std::move(handle) mutable { char buf[8192]; auto n handle.read(buf, sizeof(buf)); if (n 0) { sink.write(buf, static_castsize_t(n)); return true; } sink.done(); return true; } ); }); svr.listen(0.0.0.0, 3000);關(guān)鍵在于handle std::move(handle)把StreamHandle移動(dòng)進(jìn)set_chunked_content_provider的 lambda使 socket 生命周期覆蓋整個(gè)響應(yīng)發(fā)送過(guò)程每次回調(diào)從上游讀一塊、經(jīng)sink.write()寫(xiě)到下游直到read()返回非正值時(shí)調(diào)用sink.done()收尾。這正是反向代理場(chǎng)景讀到什么轉(zhuǎn)發(fā)什么的典型實(shí)現(xiàn)。七、與既有 API 的對(duì)比特性Client::Get()open_stream()stream::Get()響應(yīng)頭可用時(shí)機(jī)完整接收后立即可用立即可用響應(yīng)體讀取方式一次性整體緩沖直接從 socket 讀取迭代器式讀取內(nèi)存占用整個(gè)響應(yīng)體在內(nèi)存極小可控極小可控Keep-Alive 支持? 支持? 不支持? 不支持壓縮處理自動(dòng)處理自動(dòng)處理自動(dòng)處理最適合場(chǎng)景小響應(yīng)、連接復(fù)用底層流式控制便捷流式讀取八、流式 API 的特性清單真正的 socket 級(jí)流式數(shù)據(jù)直接從網(wǎng)絡(luò) socket 讀取不經(jīng)整包緩沖低內(nèi)存占用任意時(shí)刻內(nèi)存中只有當(dāng)前一個(gè)數(shù)據(jù)塊壓縮支持gzip、brotli、zstd 自動(dòng)解壓分塊傳輸完整支持 chunked transfer encoding含 trailer 解析SSL/TLS 支持HTTPS 連接同樣可用。關(guān)于壓縮與解壓從源碼看open_stream()讀取響應(yīng)頭中的Content-Encoding后調(diào)用detail::create_decompressor()創(chuàng)建解壓器httplib.h若是已知編碼但當(dāng)前構(gòu)建未啟用對(duì)應(yīng)后端如未開(kāi)啟 brotli返回Error::UnsupportedContentEncoding未知編碼則原樣透?jìng)?。解壓路徑read_with_decompression()內(nèi)部使用 8192 字節(jié)的壓縮緩沖kDecompressionBufferSize分塊喂給解壓器并受payload_max_length即客戶(hù)端的set_payload_max_length約束防止解壓炸彈。分塊傳輸側(cè)detail::ChunkedDecoderhttplib.h按 RFC 9112 解析 chunk-size、chunk-ext 與 trailer并對(duì)畸形分塊返回讀取錯(cuò)誤。測(cè)試 test/test.cc 覆蓋了 open_stream 對(duì) gzip 解壓、未知編碼、默認(rèn)請(qǐng)求頭Host / User-Agent / Accept-Encoding、POST 的 Content-Type 行為、大響應(yīng)與分塊響應(yīng)讀取等場(chǎng)景test/test.cc 則驗(yàn)證了 brotli 壓縮內(nèi)容經(jīng)open_stream自動(dòng)解壓后內(nèi)容一致。StreamApiTestfixturetest/test.cc為所有stream::*測(cè)試統(tǒng)一起了一個(gè)本地 HTTP 服務(wù)覆蓋 Get/Post/Put/Patch 的基本讀寫(xiě)、查詢(xún)參數(shù)、自定義頭與 404 狀態(tài)碼等斷言是理解 API 行為最直觀的參照。九、Keep-Alive 行為與選型提醒流式 APIstream::Get()/open_stream()在流的整個(gè)生命周期內(nèi)接管 socket 所有權(quán)這意味著流式連接不支持 Keep-AliveStreamHandle析構(gòu)時(shí) socket 隨即關(guān)閉StreamHandle的connection_與socket_stream_均由其獨(dú)占持有見(jiàn) httplib.h需要連接復(fù)用的場(chǎng)景請(qǐng)使用標(biāo)準(zhǔn)client.Get()API。// Use for streaming (no Keep-Alive) auto result httplib::stream::Get(cli, /large-stream); while (result.next()) { /* ... */ } // Use for Keep-Alive connections auto res cli.Get(/api/data); // Connection can be reused選型時(shí)把握一條主線(xiàn)小響應(yīng)、高頻短請(qǐng)求選Client::Get()享受連接復(fù)用大響應(yīng)、實(shí)時(shí)流、逐塊處理選stream::Get()需要直接控制 socket 讀取粒度、做代理轉(zhuǎn)發(fā)選open_stream()純 SSE 且有重連訴求直接選SSEClient。十、相關(guān)資源流式 API 是 cpp-httplib 近期新增能力需求源頭對(duì)應(yīng)上游 issue #2269原始功能請(qǐng)求本倉(cāng)庫(kù)中的落地實(shí)現(xiàn)集中在 httplib.h 的stream命名空間與ClientImpl::open_stream。SSE 高層客戶(hù)端文檔見(jiàn) README-sse.md實(shí)現(xiàn)位于 httplib.h。帶自動(dòng)重連的流式 SSE 客戶(hù)端示例example/ssecli-stream.cc。流式 API 的完整測(cè)試test/test.ccopen_stream()測(cè)試與 test/test.ccstream::*測(cè)試。編譯運(yùn)行本倉(cāng)庫(kù)示例時(shí)直接#include httplib.h即可header-only無(wú)需鏈接額外庫(kù)example/目錄下提供Makefile與各示例源碼可參照構(gòu)建。贊分享后端網(wǎng)絡(luò)【免費(fèi)下載鏈接】cpp-httplibA C header-only HTTP/HTTPS server and client library項(xiàng)目地址https://gitcode.com/GitHub_Trending/cp/cpp-httplib點(diǎn)擊查看免費(fèi)下載相關(guān)推薦實(shí)時(shí)數(shù)據(jù)推送新范式cpp-httplib實(shí)現(xiàn)Server-Sent Events(SSE)全指南實(shí)時(shí)數(shù)據(jù)推送新范式cpp httplib實(shí)現(xiàn)Server Sent Events SSE 全指南 你是否還在為Web應(yīng)用的實(shí)時(shí)數(shù)據(jù)更新煩惱傳統(tǒng)輪詢(xún)效率低下后端網(wǎng)絡(luò)cpp-httplib HTTP重定向處理301、302與最佳實(shí)踐cpp httplib HTTP重定向處理301、302與最佳實(shí)踐 你是否在C HTTP服務(wù)開(kāi)發(fā)中遇到過(guò)重定向邏輯混亂、客戶(hù)端兼容性問(wèn)題或性能瓶頸作為c后端網(wǎng)絡(luò)3行代碼搞定cpp-httplib響應(yīng)頭獲取實(shí)戰(zhàn)指南3行代碼搞定cpp httplib響應(yīng)頭獲取實(shí)戰(zhàn)指南 你是否還在為C HTTP客戶(hù)端獲取響應(yīng)頭信息而煩惱本文將用最簡(jiǎn)潔的方式帶你掌握cpp httpl后端網(wǎng)絡(luò)上一篇Liquidsoap與FFmpeg集成指南解鎖高級(jí)媒體處理與HLS流媒體能力下一篇CrowdSec 威脅情報(bào)共享協(xié)議比較TAXII vs STIX vs MISP創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考