1. 一個 header 撐起 HTTP/HTTPS:httplib 的定位與選型邏輯
1.1 為什麼 C++ 社區會頻繁搜到 httplib
先說個真實場景:你手頭有個 C++ 項目,需要向某個服務端發一個 JSON POST,或者要拉取一個遠程配置。老老實實用系統 API?Windows 的 WinHTTP 和 Linux 的 libcurl 寫法完全不一樣,光是處理回調函數就能讓新手血壓拉滿。翻一下編譯器自帶庫,標準庫裡至今沒有 HTTP 客戶端,只有一堆底層 socket。這種時候,業界基本都會搜到同一個名字:cpp-httplib。
這個庫最狠的地方在於:整個庫只有一個頭文件,聲稱支持 HTTP/HTTPS,由 yhirose 維護,GitHub 上 star 數量非常高。項目裡只需要#include "httplib.h",不需要鏈接亂七八糟的庫,不需要 CMake 引入一大堆依賴,複製一個頭文件就能跑。對很多被 CMake 折磨過的人來說,這簡直是拯救級體驗。
我最早是在公司內部一個工具鏈接庫裡看到它的。當時發現一個只改了兩三個文件的小工具,居然能直接發 HTTPS 請求,一查才發現是 httplib 的功勞。後來我自己在三個不同項目裡都引了它,包含嵌入式 Linux 交叉編譯環境、Windows Qt 程序、以及一個純 C++ 的命令行工具,全部一次通過。
1.2 httplib 和 libcurl、Boost.Beast 的差異
很多人在選型時會糾結:既然 libcurl 是事實標準,為什麼還要用 httplib?我的看法是,要看你的項目體量。
libcurl 功能確實全面,FTP、SMTP、代理、mTLS、異步都支持,但它的 API 是純 C 風格,用起來對 C++ 工程師不太友好。請求完要手動管理 curl_slist、curl_easy_setopt 的各種選項,響應數據要靠回調一點一點往外摳。Boost.Beast 則是另一個極端:功能強大但抽象層級太高,需要對 Asio 的異步模型有一定理解才能上手,編譯時間也感人,顯然不是為了“快速解決問題”設計的。
httplib 的定位非常清晰:Client類和Server類一對一對應 HTTP 的兩個角色,API 設計貼近現代 C++。寫一個 GET 請求就是cli.Get("/path"),註冊一個路由就是srv.Get("/hi", handler),沒有任何多餘概念。
它和 curl 的關係更像是“順手用”和“專業工具”的區別。如果你的需求就是調接口、跑服務、拉數據,httplib 能以最小的心智負擔搞定。如果要做完整的下載管理器、需要斷點續傳、多協議支持,那還是老實用 libcurl。
另外,httplib 的性能其實不差。底層走的是系統平台的原生 socket,HTTP/1.1 keep-alive 也支持。雖說和 Beast 這種能壓到極致的庫沒法比,但對 90% 的業務場景來說綽綽有餘。
2. 接入項目的正確姿勢:頭文件庫也有依賴陷阱
2.1 下載與版本選擇
在 GitHub 搜索yhirose/cpp-httplib,Release 頁面會看到一個巨大的httplib.h。很多人直接點 Download 下載最新版,然後往項目裡一扔,編譯報錯就懵了。
這裡要說一個關鍵點:httplib 的版本迭代很快,而且部分 API 在不同版本間不兼容。比如Server::set_error_handler的回調簽名改過,Client::set_connection_timeout也是後來才加的。如果你在網上抄了一段用法,卻下了最新版,很可能編譯不過。
我的建議是:不要盲目追新。去 Release 頁面選一個發布時間比較久、star 數比較多的穩定版,比如 v0.9.x 系列就相對保守。同時看一下 Release Notes,如果某個版本是為了修 bug 才發的,而你不需要那個 bug fix,那用舊版完全沒問題。另外,如果你使用的是 vcpkg 或 Conan,可以通過包管理器鎖定版本,這對團隊協作特別重要。
下載之後,建議把頭文件放到一個單獨的third_party目錄裡,不要和項目源碼混在一起。這個庫沒有 .cpp 文件,所有實現都在頭文件裡,但這也意味著它會影響整個項目的編譯時間。放到獨立目錄後,可以用-I或 CMaketarget_include_directories精確控制引用範圍。
2.2 編譯選項與依賴
純 HTTP 模式下,httplib 不依賴任何第三方庫,只需要 C++11 或更高標準。我一般用-std=c++14,兼容性最好。如果要用 HTTPS,則需要 OpenSSL,編譯前必須定義宏:
#define CPPHTTPLIB_OPENSSL_SUPPORT #include "httplib.h"注意,這個宏必須在#include "httplib.h"之前定義,否則 OpenSSL 支持不會被啟用。而且,如果有很多源文件都要用 HTTPS 功能,要麼每個文件都先定義這個宏,要麼在編譯參數裡統一加上-DCPPHTTPLIB_OPENSSL_SUPPORT。我見過有人在一個文件裡能連 HTTPS,換個文件就不行,最後發現是宏沒統一。
如果是在 Windows 上用 MSVC,還需要在項目屬性裡鏈接 OpenSSL 的 lib,常見的是libssl.lib和libcrypto.lib。Linux 上則需要-lssl -lcrypto。這裡有個容易踩的坑:如果你是用 vcpkg 安裝的 OpenSSL,庫文件路徑和名稱可能和手動編譯的不一樣,最好在 CMake 裡通過find_package(OpenSSL)統一處理,別手寫路徑。
2.3 第一個可運行例子的編譯命令
先寫一個最小的測試文件main.cpp:
#define CPPHTTPLIB_OPENSSL_SUPPORT #include "httplib.h" int main() { httplib::Client cli("http://www.example.com"); auto res = cli.Get("/"); if (res) { printf("status: %d\n", res->status); } return 0; }Linux 下的編譯命令:
g++ main.cpp -std=c++14 -I./third_party -lssl -lcrypto -o test_http如果是純 HTTP,後面的-lssl -lcrypto可以去掉。Windows 下用 MinGW 也類似,但庫名可能是libssl-3-x64.dll這種,需要用 dll 導入庫。
這裡要強調:如果你只需要 HTTP,就別定義CPPHTTPLIB_OPENSSL_SUPPORT,這樣能少兩個依賴,編譯時間也短一些。很多項目根本不需要 HTTPS,卻莫名其妙鏈了一堆 OpenSSL,純屬給自己找麻煩。
3. HTTP 客戶端實戰:GET、POST、超時與連接複用
3.1 一次標準的 GET 請求
HTTP 客戶端是 httplib 最常用的部分。基本用法非常直白:
#include "httplib.h" int main() { httplib::Client cli("https://api.github.com"); cli.set_connection_timeout(5, 0); // 5 秒連接超時 cli.set_read_timeout(10, 0); // 10 秒讀超時 auto res = cli.Get("/repos/yhirose/cpp-httplib"); if (res && res->status == 200) { printf("%s\n", res->body.c_str()); } else { auto err = res.error(); // httplib::Error 枚舉,可轉成字符串 printf("error: %s\n", httplib::to_string(err).c_str()); } return 0; }幾個細節值得注意:
第一,res是一個shared_ptr<Response>,所以判斷成功要用res != nullptr或直接if (res)。有些新手會先判斷res->status == 200,結果res是空指針直接崩了。判斷順序務必反過來。
第二,res.error()只有在請求失敗時才有意義。它是httplib::Error枚舉,可用httplib::to_string轉成可讀字符串。常見的有Success、Connection、Timeout、SSL等。通過錯誤類型可以快速定位是網絡問題還是服務端問題。
第三,超時設置一定要做。默認情況下,httplib 的讀超時可能很長,一旦服務端掛了半死不活,客戶端會一直卡住。我把連接超時設為 5 秒、讀超時設為 10 秒,是多年調試接口養成的習慣。如果對端只是偶爾慢,可以適度放寬,但絕對不要用默認值。
3.2 帶參數的 POST 與 Content-Type
POST 請求的常見場景是提交 JSON 或表單。httplib 的Post方法有很多重載,我經常用的是這兩個:
#include "httplib.h" #include <string> int main() { httplib::Client cli("https://httpbin.org"); // 表單格式 httplib::Params params = { {"name", "test"}, {"age", "18"} }; auto res1 = cli.Post("/post", params); // JSON 格式 std::string json_body = R"({"name":"test","age":18})"; auto res2 = cli.Post("/post", json_body, "application/json"); if (res1 && res1->status == 200) { printf("res1: %s\n", res1->body.c_str()); } if (res2 && res2->status == 200) { printf("res2: %s\n", res2->body.c_str()); } return 0; }這裡有個很關鍵的坑:httplib 的Post重載非常多,如果你直接傳一個std::string進去,它默認的 Content-Type 是application/octet-stream。如果服務端嚴格校驗 Content-Type,就會返回 415。務必顯式指定application/json或application/x-www-form-urlencoded。
還有就是中文編碼問題。表單參數裡的std::string一般按 UTF-8 發送,但有些老服務端預期 GBK。這種情況不是 httplib 能解決的,需要你先在業務層把字符串轉好編碼再傳。我自己碰到過一次,服務端是某個 Windows 老服務,參數裡含中文就亂碼,調了半天才發現是編碼問題,和 HTTP 庫無關。
3.3 連接複用與 keep-alive 的實際效果
“HTTP 連接複用”這個關鍵詞被搜得多,說明大家都被性能問題困擾過。HTTP/1.1 默認開啟 keep-alive,也就是說,同一個httplib::Client對象多次發請求時,底層不會反覆建立新 TCP 連接,而是復用已有的連接。
這帶來的性能提升非常明顯。我實際測過,對同一個本機服務連續發 1000 次請求,開啟 keep-alive 和不開啟(每次新建 Client)的耗時差距可以到 5 倍以上。原因很簡單:建立 TCP 連接需要握手,如果走 HTTPS 還有 TLS 握手,這個開銷比傳輸幾十個字節的數據大得多。
httplib 的使用方式反過來也決定了是否需要手動管理連接:
// 錯誤示範:每次請求都新建 Client,完全沒有複用 for (int i = 0; i < 1000; i++) { httplib::Client cli("http://127.0.0.1:8080"); auto res = cli.Get("/data"); } // 正確示範:複用同一個 Client httplib::Client cli("http://127.0.0.1:8080"); for (int i = 0; i < 1000; i++) { auto res = cli.Get("/data"); }這裡要注意,httplib 默認會嘗試複用連接,但如果服務端主動關閉了連接,比如 keep-alive 超時,httplib 會在下次請求時自動重新建連。從用戶角度看是無感的。
另外,如果你需要並發請求,建議每個線程持有自己的Client實例。雖然 httplib 的Client內部有鎖,多線程共用也可以,但頻繁的鎖競爭會讓性能大打折扣。我一般會用線程局部存儲或線程池裡每個任務創建一個短生命週期的 Client,配合連接複用效果最好。
3.4 響應處理與錯誤分類
拿到Response後,除了status和body,還需要看headers。httplib 給出的響應頭是Headers類型,本質上是std::multimap<std::string, std::string>。對大小寫不敏感,因為 httplib 內部做了統一轉小寫處理。這點設計很好,不用擔心服務端返回Content-Type還是content-type。
常見的響應處理需求還有重定向。httplib 默認不跟隨重定向,需要手動設置:
cli.set_follow_location(true);這個開關對 https 跳轉到 http 的場景尤其好用。但要注意,set_follow_location(true)之後,res->status返回的是最終響應的狀態碼,原始 301/302 被吞掉了。如果你需要記錄重定向鏈路,就得自己解析Location。
錯誤分類方面,httplib 定義了Error枚舉,我列幾個常見的:
| 錯誤枚舉 | 含義 |
|---|---|
Success | 請求成功 |
Connection | 連接失敗,比如目標端口沒開 |
Bind | 本地綁定地址失敗 |
Read | 讀取響應失敗,可能是連接被中斷 |
Write | 寫入請求失敗 |
Timeout | 超時 |
SSL | SSL/TLS 相關錯誤 |
Canceled | 請求被取消 |
實戰中,Connection和Timeout佔了絕大多數。通過錯誤類型可以快速決定重試策略:連接失敗可以重試,超時要看情況重試,SSL 錯誤重試大概率還是一樣。
4. 用 httplib 快速搭一個本地 HTTP 服務端
4.1 最小可用的服務端
不要以為 httplib 只是客戶端庫,它的服務端能力也足夠支撐中小型內部工具。
最小服務端代碼長這樣:
#include "httplib.h" int main() { httplib::Server svr; svr.Get("/hi", [](const httplib::Request& req, httplib::Response& res) { res.set_content("Hello World!", "text/plain"); }); svr.listen("0.0.0.0", 8080); return 0; }編譯後運行,訪問http://127.0.0.1:8080/hi就能看到響應。這裡有兩個關鍵點:
listen("0.0.0.0", 8080)表示監聽所有網卡地址。如果你的服務只給本機用,可以改成"127.0.0.1",這樣外部機器無法訪問,更安全。有些雲服務器上無論如何都連不上,先檢查是不是監聽在了 127.0.0.1 上。
路由處理函數的簽名是std::function<void(const Request&, Response&)>,Request裡有method、path、headers、body、params等字段,Response則用set_content、set_header、status等成員來構造響應。
4.2 路由參數、查詢參數與靜態文件
實際項目不會只有一個固定路徑,httplib 支持正則匹配和路徑參數。例如:
svr.Get(R"(/users/(\d+))", [](const httplib::Request& req, httplib::Response& res) { auto user_id = req.matches[1]; res.set_content("user id is " + user_id, "text/plain"); });這裡的req.matches是std::smatch類型,matches[0]是整個匹配串,matches[1]是第一個捕獲組。正則用的是std::regex,所以編譯時要注意轉義字符,我習慣用原始字符串R"(...)"來寫,省得被轉義搞暈。
查詢參數處理很簡單:
svr.Get("/search", [](const httplib::Request& req, httplib::Response& res) { auto it = req.params.find("q"); if (it != req.params.end()) { res.set_content("search: " + it->second, "text/plain"); } else { res.set_content("no query", "text/plain"); } });req.params是std::unordered_map<std::string, std::string>,查詢字符串已自動解碼。如果參數重複,只有第一個會被保留。絕大多數場景夠用了。
靜態文件服務也很方便,set_mount_point可以指定目錄映射:
svr.set_mount_point("/", "./public");這樣訪問/style.css就會從./public/style.css讀取文件。要注意的是,如果同時設置了路由和掛載點,路由優先。所以你可以先掛載靜態目錄,再用精確路由覆蓋某些動態接口。
4.3 服務端線程模型與性能調優
httplib 的服務端默認是每請求一線程模型,通過svr.new_task_queue = ...可以自定義線程池。新版提供了ThreadPool實現,可以這樣用:
httplib::Server svr; svr.new_task_queue = [] { return new httplib::ThreadPool(8); // 8 個線程 };如果沒有設置,默認是每個連接創建一個線程。這在請求量小的時候沒問題,一旦有幾百個並發連接,線程數量會爆炸。內部工具的請求量一般不大,但如果你要做壓力測試,肯定要換線程池。
這裡有一個容易被忽略的點:Response的數據在處理函數返回後就被釋放,所以不要在函數裡把req.body或res.body的指針保存到外部。非同步處理沒問題,但如果你想異步處理請求,比如把請求內容塞進隊列,讓工作線程慢慢加工,就必須拷貝一份數據出來,否則就是懸空指針。
我實際用 httplib 寫過一個日誌收集服務,幾十個客戶端同時 POST 日誌,服務端收到後把數據寫入消息隊列。穩定跑了幾個月,沒出過問題。這個庫的服務端能力對內部工具來說完全夠用,但如果你要做超高並發的生產環境服務,還是要換更專業的框架。
5. HTTPS 接入:證書路徑與 OpenSSL 的配合
5.1 證書從哪裡來
HTTPS 和 HTTP 的本質區別在於 TLS 加密層。httplib 在定義了CPPHTTPLIB_OPENSSL_SUPPORT後,Client可以接收https://開頭的地址,Server也可以加載證書提供 HTTPS 服務。
用客戶端訪問 HTTPS 接口時,默認會驗證服務端證書。這需要 CA 證書鏈。在 Windows 上,httplib 會用系統證書庫;在 Linux 上,則需要顯式指定 CA 證書路徑:
httplib::Client cli("https://api.github.com"); cli.set_ca_cert_path("/etc/ssl/certs/ca-certificates.crt");不同 Linux 發行版的 CA 路徑不一樣。Ubuntu 是/etc/ssl/certs/ca-certificates.crt,CentOS 可能是/etc/pki/tls/certs/ca-bundle.crt。如果路徑不對,會報 SSL 證書驗證失敗,很多人會誤以為是 httplib 的問題,其實是 CA 路徑沒設置對。
另外,如果服務端用的是自簽名證書,有兩個選擇:
// 方式一:指定服務端證書,用固定證書驗證 cli.set_ca_cert_path("./server.crt"); // 方式二:關閉證書驗證(僅限測試環境,強烈不建議生產使用) cli.enable_server_certificate_verification(false);我在調試內網環境時偶爾會用方式二,但僅限於自己寫的測試代碼。安全起見,生產環境必須保留證書驗證。
5.2 SSL 選項與常見坑
httplib 支持set_ssl_options來配置 SSL 上下文。一個常見需求是設置最小 TLS 版本,避開老舊的 TLSv1.0/1.1:
cli.set_ssl_options(httplib::SSLServer::SSLOptions{}); // 或者用 cli.set_ssl_options([]{ httplib::SSLServer::SSLOptions opt; return opt; }());但是這個 API 在不同版本裡變動過,如果你編譯報錯,大概率是版本差異。更穩妥的方法是直接設置 OpenSSL 的上下文選項,等後面具體講。
服務端啟用 HTTPS 也很直接:
httplib::SSLServer svr("./server.crt", "./server.key"); svr.Get("/secure", [](const httplib::Request&, httplib::Response& res) { res.set_content("secure area", "text/plain"); }); svr.listen("0.0.0.0", 443);這裡有兩個坑:第一,證書文件必須是 PEM 格式,如果是 PFX/P12 格式,要用 OpenSSL 工具轉換;第二,server.key如果是加密的,會在啟動時要求輸入密碼,這在無人值守環境會卡住。解決辦法是啟動時用-passin pass:你的密碼或者預先去密碼。
還要注意,SSLServer和Server是獨立的類,不能混用。監聽 443 端口需要 root 權限,建議用 8443 這種高級端口做測試。
5.3 httplib 中 http 和 https 的切換方式
一個很容易被忽視的點是:httplib 的 Client 類只有一個,區分 HTTP 和 HTTPS 靠的是構造函數裡的 URL 前綴。
httplib::Client http_cli("http://127.0.0.1:8080"); httplib::Client https_cli("https://127.0.0.1:8443");如果你在構造時寫了https://,但編譯時沒定義CPPHTTPLIB_OPENSSL_SUPPORT,編譯不過。反過來,定義了宏但請求時寫了http://,也完全沒問題。所以項目裡如果同時需要兩種協議,宏定義一次,URL 自己控制。
因此,在寫通用工具時,我通常會把 URL 前綴提取出來作為配置項。使用者傳什麼協議,我就用什麼協議訪問,代碼裡不需要做分支。有一點要注意:如果同一段代碼裡同時有 HTTP 和 HTTPS 兩個 Client,要確保證書驗證邏輯不會誤傷 HTTP 請求。set_ca_cert_path只對 HTTPS 生效,HTTP 請求會忽略它。
關於“http 和 https 的區別”,用最直白的話說:HTTPS = HTTP + TLS 加密層。http 是明文傳輸,很多抓包工具能直接看到請求和響應內容;https 會先做 TLS 握手協商密鑰,之後的數據全是密文。在調試階段,有些人會用“HTTPS 明文捕獲”這類工具,這其實是中間人代理的思路,需要客戶端信任代理的 CA 證書。如果你是服務端開發,遇到這類需求,用 httplib 起一個 HTTP 服務端做代理轉發是不錯的練習項目。
6. 實戰排錯:502 Bad Gateway 背後的連接真相
6.1 一次 502 的複現過程
搜“unexpected status 502 bad gateway: unknown error, url: http://127.0.0.1:1572”的人很多,這種報錯通常出現在反向代理場景裡,但用 httplib 做客戶端時也可能碰到。
我之前做過一個中間層服務,上游是另一個團隊維護的 HTTP 接口。某天客戶端頻繁報 502,日誌裡只有一句“unexpected status 502 bad gateway”。起初我以為是 httplib 的問題,因為報錯信息裡有 “unknown error”。後來冷靜下來,把請求鏈路拆成三段:我的客戶端 → 我的服務 → 上游服務。
第一段先直接用curl訪問我的服務,正常返回。第二段用 httplib 寫個最小客戶端訪問我的服務,也正常。第三段在我的服務裡直接請求上游,發現偶爾返回 502,而且上游的網關日誌顯示“connection reset by peer”。到這裡才明白,502 只是表象,真正的根因是上游服務所在的網關因為某種原因中斷了連接。
這個排查過程說明一件事:502 是網關給的響應,不是源站直接返回的。當你用 httplib 請求一個經過 Nginx 或雲負載均衡的服務時,如果源站進程崩了、超時了、或者連接被複用後服務端口被關閉,網關就會產生 502。
6.2 用 httplib 日誌定位上游錯誤
httplib 本身有日誌能力,可以在構造 Client 之前設置日誌回調:
httplib::Client cli("http://127.0.0.1:8080"); cli.set_logger([](const httplib::Request& req, const httplib::Response& res) { fprintf(stderr, "REQ: %s %s\n", req.method.c_str(), req.path.c_str()); fprintf(stderr, "RES: status=%d body=%s\n", res.status, res.body.c_str()); });這樣每次請求都會打印請求和響應信息。遇到 502 時,重點看兩件事:第一,請求頭裡的 Host 是否正確;第二,響應頭裡的Server字段是源站返回的還是網關返回的。如果Server字段顯示 nginx,說明請求到達了網關但網關沒能轉發成功;如果顯示源站框架名,說明源站乾脆返回了 502 狀態碼。
另外,在代碼裡不要把 502 當成普通錯誤吞掉。我習慣把非 2xx 的所有狀態碼都記錄下來,包括響應體。因為很多網關在返回 502 時,響應體裡其實包含了調試信息,比如 “connect() failed (111: Connection refused) while connecting to upstream”。這些信息對定位問題非常有價值。
“unknown error” 這個詞很誤導人。httplib 的Error枚舉裡Unknown表示的是一個未分類的錯誤,通常伴隨着響應體裡的調試信息。所以看到這個報錯,不要急着去查 httplib 的 issue,先把res->body打印出來看看,八成能找到線索。
6.3 代理與防火牆帶來的偽 502
還有一類 502 和源站無關,而是中間設備乾擾。比如有些內網環境要求必須走代理,如果你在 httplib 客戶端裡沒配置代理,請求被防火牆攔截,可能返回 502 或連接超時。
httplib 支持代理設置:
httplib::Client cli("http://127.0.0.1:8080"); cli.set_proxy("http://proxy.example.com", 3128);配置代理後,連目標地址的請求會先發到代理服務器,由代理轉發。如果代理本身不穩定,或者代理對目標地址訪問受限,也會出現 502。
還有一種情況是“連接複用”引發的偽 502。前面我說 keep-alive 能提升性能,但它有個副作用:如果源站服務重新部署,連接池裡舊連接已經無效,而客戶端以為還能復用,就會發一份請求過去,結果源站一無所知,網關直接返回 502。解決辦法是對 502 做一次重試,重試前關閉舊連接。httplib 裡可以這樣:
auto res = cli.Get("/path"); if (res && res->status == 502) { cli.close(); // 強制關閉所有連接 res = cli.Get("/path"); // 重試 }這種“一次 502 就重建連接”的策略,我在實際項目裡用下來,成功率非常高。重試時要注意冪等性:如果請求是 POST 且會改動數據,必須和業務方確認可以安全重試。
7. 系列預告與個人踩坑清單
寫到這裡,httplib 的基礎用法差不多覆蓋完整了。這個庫單看文檔很容易上手,但真正到了項目裡,總會遇到一些文檔裡沒寫的邊角問題。
我整理了一份個人踩坑清單,給初學者參考:
- 編譯時忘了定義
CPPHTTPLIB_OPENSSL_SUPPORT,結果 https 請求編譯不過。 - 不同發行版 CA 證書路徑不一樣,導致 HTTPS 請求在測試環境通過、生產環境報 SSL 錯誤。
- 把 5 秒超時設得太短,服務端稍微慢一點就誤報故障,後來改成可配置才解決。
- 同一個
Client在多線程共用,導致連接池鎖競爭嚴重,改成每線程一個Client後性能明顯提升。 - 用
std::regex寫路由時忘了用原始字符串,被反斜杠轉義折磨了一個下午。
下一期我打算深入講 httplib 的進階用法,包括自定義Request和Response的序列化、文件上傳下載、Chunked 傳輸、基於 httplib 的 WebSocket 支持,以及如何把它嵌入 Qt 的事件循環裡做異步 HTTP 通信。如果你在項目裡用 httplib 遇到過什麼奇怪的問題,歡迎在評論區分享,我踩過的坑可能正好能幫你省下幾個小時的排查時間。