
簡(jiǎn)介面向Qt初學(xué)者與桌面應(yīng)用開發(fā)者的HTTP網(wǎng)絡(luò)通信示例演示如何借助QNetworkAccessManager發(fā)起POST請(qǐng)求將QJsonDocument、QJsonObject構(gòu)造的JSON數(shù)據(jù)轉(zhuǎn)為字符串再經(jīng)toUtf8()編碼后作為請(qǐng)求體發(fā)送至服務(wù)器并正確處理響應(yīng)。工程配有簡(jiǎn)潔界面便于通過(guò)按鈕觸發(fā)請(qǐng)求并在界面上查看返回結(jié)果適合快速理解Qt網(wǎng)絡(luò)模塊、JSON序列化與異步信號(hào)槽的配合用法。資源包僅5個(gè)文件包含2個(gè)cpp、1個(gè)h、1個(gè)ui、1個(gè)procpp分別負(fù)責(zé)入口和主窗口邏輯h聲明網(wǎng)絡(luò)請(qǐng)求相關(guān)成員與槽函數(shù)ui定義可視布局pro聲明network、widgets等模塊依賴。整體壓縮包大小僅3KB結(jié)構(gòu)緊湊、無(wú)冗余資源可直接用Qt Creator打開并編譯運(yùn)行適合作為輕量級(jí)參考工程。目前已有5380人學(xué)習(xí)。研讀源碼可掌握設(shè)置application/json請(qǐng)求頭、調(diào)用post()發(fā)送數(shù)據(jù)、通過(guò)信號(hào)槽接收服務(wù)端回包等關(guān)鍵寫法也可作為向既有桌面程序集成網(wǎng)絡(luò)功能的起步模板。1. Qt 里 POST JSON 的坑十有八九不在網(wǎng)絡(luò)庫(kù)做 Qt 開發(fā)用 Qthttp 發(fā) POST 請(qǐng)求、帶上 JSON body是再常見(jiàn)不過(guò)的需求登錄鑒權(quán)、狀態(tài)上報(bào)、調(diào)用開放的接口服務(wù)幾乎每個(gè)項(xiàng)目都會(huì)碰到。很多朋友從網(wǎng)上找一段 postjson 的示例抄過(guò)來(lái)結(jié)果服務(wù)端要么收到空 body要么返回亂碼要么請(qǐng)求發(fā)出去之后石沉大海連回調(diào)都不觸發(fā)。這時(shí)候最容易懷疑 Qt 的 HTTP 模塊不靠譜但根據(jù)我拆過(guò)的一堆案例來(lái)看問(wèn)題大多出在三個(gè)地方請(qǐng)求頭 Content-Type 沒(méi)和服務(wù)端對(duì)齊、JSON 序列化格式選錯(cuò)、QNetworkReply 的生命周期沒(méi)管住。這篇文章從 QJsonObject 怎么組裝、QNetworkRequest 怎么配置一直講到 QNetworkReply 怎么安全收尾最后給出一套適合工程落地的封裝思路。適合正在用 C/Qt 對(duì)接服務(wù)端接口的開發(fā)者也適合剛看懂示例但一改就翻車的新手。2. POST JSON 最小鏈路從 QJsonObject 到 QNetworkReply 的完整流程2.1 QNetworkAccessManager 的生命周期與選型在 Qt 里發(fā) HTTP 請(qǐng)求核心門面是 QNetworkAccessManager連接管理、請(qǐng)求調(diào)度、代理設(shè)置、SSL session 緩存都由它統(tǒng)一管理。我一般建議一個(gè)程序生命周期里只創(chuàng)建它一次長(zhǎng)期復(fù)用。原因很簡(jiǎn)單它內(nèi)部維護(hù)了 DNS 緩存、SSL session 緩存和底層的連接池如果你每次請(qǐng)求都 new 一個(gè)再 delete這些緩存全部作廢表現(xiàn)就是請(qǐng)求變慢、并發(fā)上來(lái)之后反復(fù)握手。這個(gè)差異在局域網(wǎng)接口上不明顯一旦連公網(wǎng)服務(wù)體感差距就很明顯。QNetworkAccessManager 的線程模型需要說(shuō)一下它是可重入的也就是說(shuō)不同實(shí)例可以分配在不同線程使用但同一個(gè)實(shí)例不建議跨線程調(diào)用。實(shí)際項(xiàng)目里最常見(jiàn)的做法是把 manager 放在主線程所有 post 都在主線程發(fā)起如果必須在工作線程用就讓那個(gè)線程自己創(chuàng)建自己的 manager不要共享。聲明一個(gè)請(qǐng)求用 QNetworkRequest最基礎(chǔ)的配置是這樣QNetworkRequest request; request.setUrl(QUrl(https://api.example.com/v1/user/login)); request.setHeader(QNetworkRequest::ContentTypeHeader, application/json);URL 這塊有個(gè)很容易忽略的細(xì)節(jié)如果地址里帶了中文參數(shù)、空格或者特殊符號(hào)直接丟給 setUrl最終發(fā)送的字節(jié)可能是錯(cuò)的服務(wù)端收到之后要么 404要么參數(shù)解析不對(duì)。常見(jiàn)的做法是先對(duì)查詢參數(shù)做百分號(hào)編碼或者直接用 QUrlQuery 構(gòu)造查詢串后面 3.3 節(jié)會(huì)專門講。2.2 JSON 構(gòu)造與序列化別用手拼字符串很多人喜歡這樣拼 JSONQString({name:%1,age:%2}).arg(name).arg(age)。這種寫法在小字段場(chǎng)景下能用但一旦字段值里出現(xiàn)引號(hào)、反斜杠、換行你就要自己處理轉(zhuǎn)義而服務(wù)端 JSON 解析器是零容忍的只要有一處引號(hào)不對(duì)稱整個(gè)請(qǐng)求直接拒收。正確做法是先用 QJsonObject 組織數(shù)據(jù)再交給 QJsonDocument 序列化QJsonObject body; body.insert(username, demo); body.insert(age, 28); body.insert(active, true); QJsonDocument doc(body); QByteArray payload doc.toJson(QJsonDocument::Compact);toJson 有兩種常用模式Compact 和 Indented。Compact 是緊湊輸出不換行不加空格網(wǎng)絡(luò)傳輸一般用這個(gè)體積最小Indented 是格式化輸出帶換行和縮進(jìn)適合打印日志和寫配置文件。我見(jiàn)過(guò)有人把默認(rèn)的 Indented 格式直接發(fā)出去服務(wù)端也認(rèn)因?yàn)?JSON 規(guī)范本身允許空白字符但少數(shù)嚴(yán)格的服務(wù)端會(huì)拿 Content-Length 和實(shí)際接收體做一致性校驗(yàn)這種場(chǎng)景下 Compact 更穩(wěn)妥。發(fā) POST 請(qǐng)求時(shí)payload 必須先轉(zhuǎn)成 QByteArray 傳進(jìn)去。如果你直接傳 QJsonObjectQt 的重載可能會(huì)把它按表單字段處理服務(wù)端按 JSON 解析時(shí)什么都拿不到。QNetworkAccessManager manager; QNetworkReply *reply manager.post(request, payload);2.3 異步接收f(shuō)inished 信號(hào)與數(shù)據(jù)讀取post() 調(diào)用之后請(qǐng)求是異步的底層 socket 的讀寫不阻塞調(diào)用線程響應(yīng)到了之后通過(guò)信號(hào)通知。標(biāo)準(zhǔn)寫法是把回調(diào)接在 QNetworkReply 的 finished 信號(hào)上connect(reply, QNetworkReply::finished, this, [reply]() { if (reply-error() ! QNetworkReply::NoError) { qWarning() HTTP error: reply-errorString(); reply-deleteLater(); return; } QByteArray respData reply-readAll(); qDebug() raw response: respData; QJsonParseError parseErr; QJsonDocument respDoc QJsonDocument::fromJson(respData, parseErr); if (parseErr.error ! QJsonParseError::NoError) { qWarning() JSON parse failed: parseErr.errorString(); } reply-deleteLater(); });readAll() 的調(diào)用時(shí)機(jī)必須在 finished 之后在那之前讀可能讀到空數(shù)據(jù)或半截請(qǐng)求體。另外一個(gè)容易踩的細(xì)節(jié)是同一個(gè) reply 上的數(shù)據(jù)只能讀一次readAll() 之后內(nèi)部緩沖區(qū)就空了連續(xù)調(diào)第二次拿到的是空字節(jié)。不管請(qǐng)求成功還是失敗reply 最終都要 deleteLater() 釋放否則每次請(qǐng)求泄漏一塊內(nèi)存惡劣場(chǎng)景下跑一天內(nèi)存占用漲幾百兆。connect 時(shí)把 this 作為上下文對(duì)象傳進(jìn)去是一個(gè)值得堅(jiān)持的習(xí)慣。這樣當(dāng) this 指向的對(duì)象先被銷毀時(shí)lambda 不會(huì)再被觸發(fā)避免在回調(diào)里訪問(wèn)懸垂對(duì)象導(dǎo)致崩潰。2.4 請(qǐng)求頭參數(shù)明細(xì)與常見(jiàn)誤設(shè)POST JSON 場(chǎng)景下請(qǐng)求頭直接影響服務(wù)端怎么解釋你的請(qǐng)求體這里集中列一張表Header 名推薦值說(shuō)明Content-Typeapplication/json服務(wù)端按這個(gè)決定走 JSON 解析分支部分框架要求寫成 application/json; charsetutf-8Content-Lengthpayload.size()Qt 會(huì)自動(dòng)計(jì)算手動(dòng)設(shè)置更明確Acceptapplication/json期望服務(wù)端返回 JSONUser-Agent自定義字符串個(gè)別網(wǎng)關(guān)會(huì)攔空 UA 請(qǐng)求Connection不手動(dòng)設(shè)Qt 默認(rèn)支持 keep-alive手動(dòng)改反而多事兩個(gè)容易迷糊的點(diǎn)。第一所有 header 必須在 post() 之前設(shè)置完成先發(fā)請(qǐng)求再補(bǔ) header很可能不生效抓包看到的還是老請(qǐng)求頭。第二setHeader 和 setRawHeader 不要混著設(shè)置同一個(gè)字段繞來(lái)繞去容易產(chǎn)生兩個(gè)同名 header部分服務(wù)端看到重復(fù)頭直接報(bào)錯(cuò)。我在實(shí)際對(duì)接里就遇到過(guò)網(wǎng)關(guān)打印出兩個(gè) Content-Type 的情況排查半天發(fā)現(xiàn)是代碼里兩處都設(shè)了。3. JSON 組裝與解析嵌套結(jié)構(gòu)、數(shù)組與類型陷阱3.1 嵌套構(gòu)造對(duì)象套數(shù)組數(shù)組套對(duì)象真實(shí)業(yè)務(wù)里的接口很少只有三五個(gè)平鋪?zhàn)侄?。登錄接口可能帶一個(gè) user 對(duì)象上報(bào)接口通常是一個(gè) events 數(shù)組數(shù)組里每個(gè)元素又是一個(gè)獨(dú)立對(duì)象。QJsonObject 和 QJsonArray 可以互相嵌套寫法上先構(gòu)造內(nèi)層再組裝外層代碼會(huì)清晰很多QJsonObject userInfo; userInfo[uid] 10086; userInfo[nickname] dev; QJsonObject loginBody; loginBody[user] userInfo; loginBody[device] PC; QJsonArray events; QJsonObject ev1; ev1[type] click; ev1[page] /home; events.append(ev1); QJsonObject ev2; ev2[type] scroll; ev2[page] /list; events.append(ev2); loginBody[events] events;這段代碼最終生成的 JSON 結(jié)構(gòu)是{ user: {uid: 10086, nickname: dev}, device: PC, events: [ {type: click, page: /home}, {type: scroll, page: /list} ] }構(gòu)造時(shí)有幾個(gè)需要注意的行為。QJsonObject 的 operator[] 用于寫入時(shí)如果 key 不存在會(huì)自動(dòng)創(chuàng)建這對(duì)構(gòu)造是方便的但如果你想先判斷 key 是否存在再?zèng)Q定是否寫入要用 contains()。另外QJsonObject 內(nèi)部的組織順序在不同 Qt 版本之間并不一致官方也沒(méi)有承諾保持插入順序。如果你的業(yè)務(wù)依賴 JSON 字段順序來(lái)生成簽名比如某些支付接口要求把所有字段按字典序排序再拼串那就不要依賴 QJsonObject 的遍歷順序應(yīng)該拿到鍵名之后自己排序。3.2 防御性解析先判類型再取值服務(wù)端返回的 JSON 往往比請(qǐng)求要復(fù)雜得多。有的接口成功時(shí)返回 object失敗時(shí)返回 {code: 400, message: ...}還有的接口干脆返回?cái)?shù)組。解析端我強(qiáng)烈建議養(yǎng)成“類型先判斷、取值有兜底”的寫法QJsonParseError parseErr; QJsonDocument respDoc QJsonDocument::fromJson(respData, parseErr); if (parseErr.error ! QJsonParseError::NoError) { qWarning() parse error: parseErr.errorString(); return; } if (!respDoc.isObject()) { qWarning() response is not an object; return; } QJsonObject root respDoc.object(); if (root.contains(code) root[code].isDouble()) { int code root[code].toInt(); if (code ! 0) { qWarning() business error: root[message].toString(); return; } } if (root.contains(data) root[data].isObject()) { QJsonObject dataObj root[data].toObject(); QString token dataObj[token].toString(); } if (root.contains(list) root[list].isArray()) { QJsonArray list root[list].toArray(); for (const QJsonValue val : list) { if (!val.isObject()) continue; QJsonObject item val.toObject(); QString name item[name].toString(); } }這樣寫的好處是字段缺失和類型不符合都會(huì)被擋在門外不會(huì)因?yàn)橐粋€(gè)字段解析失敗導(dǎo)致整個(gè)響應(yīng)白拿。有一個(gè)容易誤判的地方value(code).toInt(-1) 這種寫法缺 key 時(shí)返回 -1但 key 存在且類型是 string 時(shí)toInt 可能返回 0 而不是 -1所以更穩(wěn)妥的判斷方式是先 contains 再取值。3.3 細(xì)節(jié)三件套緊湊格式、URL 編碼與小數(shù)精度第一件發(fā)送和打印用不同格式。發(fā)送時(shí)用 Compact 緊湊序列化日志里打印請(qǐng)求體時(shí)改用 Indented人眼更容易核對(duì)嵌套關(guān)系。同一個(gè) QJsonObject 可以隨時(shí)轉(zhuǎn)換兩種字節(jié)流不沖突。第二件URL 查詢參數(shù)編碼。很多接口要求在 URL 里帶簽名和時(shí)間戳如果簽名值里出現(xiàn) 、、 這類字符直接拼進(jìn) URL 會(huì)導(dǎo)致參數(shù)被拆分。正確做法是用 QUrlQuery 構(gòu)造QUrlQuery query; query.addQueryItem(app_id, demo); query.addQueryItem(sign, signStr); QUrl url(https://api.example.com/v1/order/create); url.setQuery(query);QUrlQuery 會(huì)自動(dòng)完成百分號(hào)編碼服務(wù)端解碼后拿到的值和本地計(jì)算一致。第三件小數(shù)精度。QJsonValue 對(duì)數(shù)字只區(qū)分 double 和整數(shù)如果你從 double 填進(jìn) QJsonObject接收端拿到的就是帶小數(shù)的值。涉及金額計(jì)算時(shí)浮點(diǎn)誤差會(huì)累積建議約定以“分”為單位的整數(shù)來(lái)傳避免兩個(gè)端對(duì)不上賬。4. 同步等待與超時(shí)控制既要結(jié)果同步也要死等可控4.1 QEventLoop 把異步請(qǐng)求改成本地等待Qt 的 HTTP 請(qǐng)求本質(zhì)是異步的但很多業(yè)務(wù)邏輯是線性的先登錄拿 token再帶 token 請(qǐng)求數(shù)據(jù)。拆成信號(hào)槽一層層回調(diào)也能寫但回調(diào)嵌套多了以后可讀性和排錯(cuò)成本都會(huì)上升。一個(gè)常見(jiàn)的做法是借用 QEventLoop 把“異步請(qǐng)求 等待完成”包裝成同步函數(shù)。這個(gè)寫法適合命令行工具、后臺(tái)任務(wù)線程這類場(chǎng)景。注意如果跑在 UI 線程進(jìn)入事件循環(huán)會(huì)阻塞界面刷新用戶看到的就是界面卡死所以 GUI 程序里不要在主線程做這種同步等待。QByteArray postJson(const QJsonObject body) { QNetworkAccessManager manager; QNetworkRequest request; request.setUrl(QUrl(https://api.example.com/v1/rpc)); request.setHeader(QNetworkRequest::ContentTypeHeader, application/json); QByteArray payload QJsonDocument(body).toJson(QJsonDocument::Compact); QNetworkReply *reply manager.post(request, payload); QEventLoop loop; QObject::connect(reply, QNetworkReply::finished, loop, QEventLoop::quit); QTimer timer; timer.setSingleShot(true); QObject::connect(timer, QTimer::timeout, reply, QNetworkReply::abort); timer.start(3000); loop.exec(); QByteArray result; if (reply-error() QNetworkReply::NoError) { result reply-readAll(); } reply-deleteLater(); return result; }這里的關(guān)鍵設(shè)計(jì)是超時(shí)觸發(fā)時(shí) abort 中斷請(qǐng)求隨后 reply 的 finished 信號(hào)依然會(huì)發(fā)出來(lái)把 loop 的 quit 兜住不會(huì)出現(xiàn)死等。如果只等 finished 而不管超時(shí)遇到服務(wù)端遲遲不響應(yīng)線程就真的掛死了。deleteLater 放在 loop.exec() 返回之后執(zhí)行是安全的因?yàn)榇藭r(shí)請(qǐng)求已經(jīng)結(jié)束或者被 abort。4.2 超時(shí)設(shè)置setTransferTimeout 與單請(qǐng)求兜底如果你用的是 Qt 5.15 及以上版本QNetworkAccessManager 自帶 transfer timeout按毫秒設(shè)置manager.setTransferTimeout(5000);這個(gè)超時(shí)對(duì)該 manager 后續(xù)所有請(qǐng)求生效。需要注意它的語(yǔ)義它是指“無(wú)數(shù)據(jù)活動(dòng)超時(shí)”不是“整個(gè)請(qǐng)求的總時(shí)限”。也就是說(shuō)一個(gè)請(qǐng)求持續(xù)在傳數(shù)據(jù)哪怕已經(jīng)跑了 30 秒它也不會(huì)被掐斷。這和很多服務(wù)端框架里的“總超時(shí)”概念不完全一樣對(duì)接時(shí)要心里有數(shù)。Qt 5.15 之前的版本沒(méi)有這個(gè)接口更早版本的兼容做法就是第 4.1 節(jié)里那樣用單次 QTimer 配合 reply-abort()。有一個(gè)常見(jiàn)的誤用在 QTimer 的槽里直接調(diào) loop.quit()而不是先 abort。表面上循環(huán)退出了但請(qǐng)求還在后臺(tái)繼續(xù)跑后續(xù) finished 信號(hào)仍然會(huì)觸發(fā)線程退出時(shí)可能帶出一堆“connection closed”之類的噪音。還有一個(gè)值得避免的寫法不要在一個(gè) QEventLoop 里再嵌套另一個(gè) QEventLoop 去等新的 HTTP 請(qǐng)求。我見(jiàn)過(guò)有人在 finished 回調(diào)里又起一個(gè)新的 loop 去等下一個(gè)管理器的請(qǐng)求理論上能跑但其中一個(gè)超時(shí)沒(méi)兜住整個(gè)線程就僵住不動(dòng)。連續(xù)多段請(qǐng)求的正確做法是把它們串成隊(duì)列或者像第 2 章那樣用異步回調(diào)鏈。5. 避坑與排查POST JSON 最常翻車的五個(gè)現(xiàn)場(chǎng)5.1 服務(wù)端收到空 bodyContent-Type 與序列化格式?jīng)]對(duì)上現(xiàn)象接口返回 HTTP 200但服務(wù)端業(yè)務(wù)日志顯示收到的 body 是空的或者字段全是 null。原因最常見(jiàn)的有三種。第一post() 時(shí)直接傳了 QJsonObject沒(méi)有先 toJsonQt 按表單編碼處理服務(wù)端走 JSON 解析自然拿不到。第二Content-Type 沒(méi)設(shè)或者設(shè)成了 text/plain服務(wù)端沒(méi)走 JSON 解析分支。第三URL 拼錯(cuò)導(dǎo)致請(qǐng)求打到了另一個(gè)接口上這個(gè)在 5.5 節(jié)單獨(dú)說(shuō)。解決把 payload 先用 qDebug() 打印出來(lái)確認(rèn)它是一段合法 JSON再確認(rèn)請(qǐng)求頭里的 Content-Type 是 application/json最后在調(diào)試代理里看實(shí)際發(fā)出的 body 內(nèi)容判斷丟在哪一環(huán)。5.2 返回中文亂碼或 \uXXXX編碼和解碼不在同一條鏈上現(xiàn)象響應(yīng)里的中文變成“錕斤拷”之類的亂碼或者所有中文都變成 \uXXXX 轉(zhuǎn)義串。原因HTTP 響應(yīng)頭沒(méi)有聲明 charset 時(shí)Qt 默認(rèn)按 UTF-8 讀取如果服務(wù)端返回的是 GBK讀出來(lái)就是亂碼。\uXXXX 其實(shí)是 JSON 規(guī)范里的標(biāo)準(zhǔn)轉(zhuǎn)義不代表數(shù)據(jù)錯(cuò)誤只是還沒(méi)解碼顯示。解決先看原始字節(jié)。遇到亂碼先不要急著接 QJsonDocument先打印原始響應(yīng)或按十六進(jìn)制看字節(jié)確定服務(wù)端實(shí)際編碼。如果是 GBKQt 5 里可以用 QString::fromLocal8Bit 或 QTextCodec 的 GBK codec 轉(zhuǎn)成 Unicode再喂給 fromJson。Qt 6 里 QTextCodec 挪到了兼容模塊接口對(duì)接文檔里一般會(huì)注明。5.3 HTTPS 證書驗(yàn)證失敗本地調(diào)試與正式環(huán)境要分開對(duì)待現(xiàn)象請(qǐng)求本地開發(fā)環(huán)境的 HTTPS 接口時(shí)報(bào) SSL handshake failed 或者 certificate signed by unknown authority。原因本地聯(lián)調(diào)的 HTTPS 服務(wù)大多使用自簽證書Qt 默認(rèn)的證書鏈校驗(yàn)不認(rèn)這個(gè)證書。解決開發(fā)環(huán)境可以臨時(shí)放行。比較穩(wěn)妥的做法是連接 reply 的 sslErrors 信號(hào)在回調(diào)里打印出錯(cuò)信息然后調(diào)用帶參數(shù)版本的 ignoreSslErrors只放行當(dāng)前這幾條錯(cuò)誤不要無(wú)腦調(diào)用無(wú)參版本把未來(lái)所有證書錯(cuò)誤全部忽略。這個(gè)妥協(xié)手段只適合本地調(diào)試測(cè)試和正式環(huán)境必須走正規(guī)證書鏈否則中間人改數(shù)據(jù)沒(méi)有任何提醒安全邊界形同虛設(shè)。5.4 reply 泄漏與隨機(jī)崩潰生命周期沒(méi)理順現(xiàn)象程序運(yùn)行一段時(shí)間后隨機(jī)崩潰崩潰棧指向 QNetworkReply 相關(guān)或者內(nèi)存占用持續(xù)上漲。原因reply 沒(méi)有按規(guī)矩 deleteLater每次請(qǐng)求泄漏一點(diǎn)內(nèi)存另一種隱蔽情況是在兩次事件循環(huán)里對(duì)同一個(gè) reply 調(diào)了多次 deleteLater第二次就是懸垂操作。解決統(tǒng)一在 finished 回調(diào)里先讀數(shù)據(jù)、再 deleteLater絕不在 finished 之前刪除 replylambda 捕獲的 reply 指針一定要在連接銷毀前保持有效。connect 時(shí)帶上上下文對(duì)象比如 connect(reply, QNetworkReply::finished, this, lambda)一旦 this 對(duì)象先銷毀回調(diào)自動(dòng)斷開能規(guī)避一大批懸垂崩潰。5.5 請(qǐng)求沒(méi)有響應(yīng)先查 URL 編碼和 JSON 合法性再查事件循環(huán)現(xiàn)象POST 發(fā)出去之后什么回調(diào)都不觸發(fā)程序不報(bào)錯(cuò)也不崩潰就像請(qǐng)求被黑洞吞了。原因常見(jiàn)兩種。一是 URL 帶了未編碼的中文或特殊字符QNetworkAccessManager 對(duì)非法 URL 的處理很安靜沒(méi)有顯式錯(cuò)誤信號(hào)表現(xiàn)就是沒(méi)任何回調(diào)二是代碼跑在一個(gè)沒(méi)有事件循環(huán)的線程里QNetworkAccessManager 的信號(hào)派發(fā)依賴事件循環(huán)循環(huán)不跑finished 永遠(yuǎn)不會(huì)觸發(fā)。解決第一步把 URL 打印出來(lái)確認(rèn) setUrl 前已經(jīng)做了百分號(hào)編碼第二步檢查當(dāng)前線程有沒(méi)有事件循環(huán)沒(méi)有就換到有循環(huán)的線程或者用第 4 章的同步等待方式第三步給請(qǐng)求加兜底超時(shí)確保任何情況下都不會(huì)無(wú)限等下去。6. 收尾建議把 POST JSON 封裝成公共調(diào)用工具6.1 封裝一個(gè)帶回調(diào)的 postJson最后一個(gè)建議把項(xiàng)目里所有 POST JSON 的調(diào)用收斂到一個(gè)公共位置不要散落在各個(gè)界面類里。通常我會(huì)封裝一個(gè) HttpManager 類內(nèi)部持有唯一的 QNetworkAccessManager對(duì)外只暴露一個(gè) postJson 接口加上完成回調(diào)void HttpManager::postJson(const QUrl url, const QJsonObject body, std::functionvoid(QJsonObject, int) done) { QNetworkRequest req(url); req.setHeader(QNetworkRequest::ContentTypeHeader, application/json); req.setRawHeader(Accept, application/json); QByteArray payload QJsonDocument(body).toJson(QJsonDocument::Compact); QNetworkReply *reply m_manager.post(req, payload); connect(reply, QNetworkReply::finished, this, [reply, done]() { QJsonObject respObj; int status reply-attribute( QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (reply-error() QNetworkReply::NoError) { QJsonParseError err; QJsonDocument doc QJsonDocument::fromJson(reply-readAll(), err); if (err.error QJsonParseError::NoError doc.isObject()) { respObj doc.object(); } } done(respObj, status); reply-deleteLater(); }); }這樣外部調(diào)用只需要關(guān)心數(shù)據(jù)組裝和結(jié)果回調(diào)網(wǎng)絡(luò)細(xì)節(jié)全部收進(jìn)類里。6.2 日志、狀態(tài)碼與簡(jiǎn)單重試在封裝基礎(chǔ)上我會(huì)再加兩條習(xí)慣。第一請(qǐng)求發(fā)出時(shí)打一行“方法 URL payload”日志響應(yīng)收到后打一行“狀態(tài)碼 錯(cuò)誤串 耗時(shí)”排查問(wèn)題不再需要臨時(shí)加調(diào)試語(yǔ)句。第二加一個(gè)簡(jiǎn)單重試失敗且狀態(tài)碼是 5xx 或網(wǎng)絡(luò)錯(cuò)誤時(shí)重新調(diào)用 postJson最多重試兩次每次間隔 300 毫秒覆蓋大多數(shù)偶發(fā)超時(shí)和瞬時(shí)抖動(dòng)。另外如果是 Qt 5.15 以上給 manager 設(shè)一次 setTransferTimeout(8000) 做全局兜底比每個(gè)請(qǐng)求單獨(dú)開定時(shí)器省心GUI 程序優(yōu)先走異步回調(diào)腳本和后臺(tái)線程再考慮 QEventLoop 同步等待。我從做第一個(gè) Qt 網(wǎng)絡(luò)模塊的模擬項(xiàng)目開始每次接口對(duì)接都強(qiáng)制走一遍自檢鏈先打印請(qǐng)求報(bào)文再核對(duì) Content-Type然后確認(rèn) reply 生命周期最后看響應(yīng)原始字節(jié)。這套流程讓我少踩了無(wú)數(shù)坑現(xiàn)在新項(xiàng)目第一天就把 HttpClient 封裝好后面每個(gè)接口只改 JSON 結(jié)構(gòu)和回調(diào)邏輯翻車率低了很多。希望幫你把 POST JSON 這條路走順。本文還有配套的精品資源點(diǎn)擊獲取