简介面向有一定C基础、希望在Qt项目中快速接入网络通信能力的开发者这是一套基于QNetworkAccessManager、QNetworkReply与QNetworkRequest构建的网络请求模块封装资源。资源核心价值在于把网络请求流程从业务代码中抽离拆分为职责单一的独立模块并通过日志保存实现请求过程的可追踪性能显著降低代码耦合度与调试成本。压缩包共22个文件整体大小约925KB以cpp/h源码文件为主同时含有ui界面文件、sln/vcxproj工程配置以及可直接运行的exe示例目录结构直观方便在Qt环境中快速加载查看效果。目前已有1193人学习/下载。具体模块包括CLog日志模块、NetworkHelper网络请求封装、TextStreamHelper字符流请求、PicDownHelper图片下载与NetworkModule UI示例覆盖从底层网络通信到上层业务调用的完整链路既适合学习Qt网络模块化设计思想也适合直接复用或按业务二次改造有效节省重复开发时间。1. 网络请求模块封装先把业务代码从 QNetworkAccessManager 里解放出来几乎每个 Qt 项目活到第二年都会长出两三套风格迥异的网络请求写法有的地方直接 new QNetworkAccessManager有的走静态函数有的把 reply 指针到处传最后在某个业务模块里攒了一堆 lambda 和 disconnect 的烂账。QNetWork 这个网络请求模块封装做的就是把这层乱账收敛成一条统一通道外部只关心「我要发什么请求、带什么参数、等什么结果」不关心底层 QNetworkAccessManager 怎么管理连接、reply 何时被回收、线程间信号怎么转发。这套封装的价值不在于把 QNetworkAccessManager 包一层壳而在于给整个项目的网络层定了一套稳定契约。适合谁用用 C/Qt 写客户端、且受够了在十几个文件里搜 reply-error() 判错的开发者也适合团队里想收拢网络层规范、但不想从零设计接口的工程负责人。下文所有实现都围绕一个目标让业务侧用最短代码拿到结果让公共层把所有脏活拦在自己肚子里。2. 封装的第一层设计接口收敛与生命周期归属2.1 为什么先定接口而不是先写代码拆这个模块时我第一件事不是打开 Qt 文档而是翻了一遍现有项目里所有直接使用 QNetworkAccessManager 的地方。翻完发现共性非常明显90% 的调用都只用了 get 和 post参数来源要么是 QVariantMap要么是拼好的 QString返回值要么是 QByteArray要么是 JSON 序列化后的 QJsonObject。剩下 10% 涉及上传下载、自定义 header、取消请求。这说明封装不需要把 Qt 网络框架的所有能力都暴露出来先把高频路径做稳低频需求留一个逃生舱口就行。接口设计我遵循一个原则调用方传入的是业务意图不是网络细节。拿登录举例业务侧想表达的是「用账号密码换一个 token」不是「帮我发一个 POST 请求到 /auth/loginContent-Type 设成 application/json把 body 序列化成这样」。所以对外接口形如// 业务侧登录调用 void login(const QString account, const QString password) { QVariantMap body; body.insert(account, account); body.insert(password, password); Network::instance().post(/auth/login, body, this, [this](const Network::Response resp) { if (resp.ok()) { // 解析 resp.data() 里的 token QString token resp.json().value(token).toString(); } else { // 统一错误提示 emit loginFailed(resp.errorString()); } }); }这里有几个关键取舍。回调里用了this作上下文对象QNetWork 内部会把这个对象和回调绑定当对象析构时自动丢弃未发出的回调从根上避免「请求还没回来发起者已经没了」的野指针问题。返回值统一包了一层Network::Response里面封装了ok()、data()、json()、errorString()四个方法业务侧不需要自己判断 QNetworkReply::NetworkError 的枚举值。参数方面post的第二个参数支持 QVariantMap 就是为业务侧省事内部做主序列化默认按 JSON 输出Content-Type 自动设置。如果你传的是 QByteArray则会按原始字节流发送Header 需要自己通过第四个参数传入。常见做法是第四个参数放一个QNetworkRequest的配置回调给需要自定义 header 的场景留口子。这套接口的优点是把 90% 的调用收敛到了两个重载函数上新成员看一遍示例就能上手不用去查 QNetworkAccessManager 的整个继承树。2.2 connect 的隐式绑定与信号转发别让 reply 指针到处飞很多自己封过一版的人会倒在同一个坑上把 QNetworkReply 指针存在成员变量里等请求结束再去读。这个做法在并发请求一多就会出问题——前一个请求还没结束后一个已经把成员变量覆盖了。QNetWork 的处理思路是不持有 reply而是让 reply 的生命周期归属在一个请求上下文中请求结束即销毁。class RequestContext : public QObject { Q_OBJECT public: QNetworkReply* reply nullptr; QObject* context nullptr; std::functionvoid(const Response) callback; QTimer timeoutTimer; explicit RequestContext(QObject* parent nullptr) : QObject(parent) { timeoutTimer.setSingleShot(true); // 超时统一走失败回调不等底层网络层慢慢重试 connect(timeoutTimer, QTimer::timeout, this, [this]() { if (reply) { reply-abort(); } Response resp; resp.setError(timeout); if (callback) callback(resp); }); } };核心逻辑就是把每次请求包装成一个独立上下文QTimer 负责兜底超时。这里面值得注意的点timeoutTimer是单次触发超时后先abort()掉底层 reply再主动触发回调。abort 会触发 QNetworkReply 的 finished 信号所以需要在回调触发前保证不会重复执行。做法是在发送请求前 connect finished 信号时把 signal 和 context 绑定connect(reply, QNetworkReply::finished, context, [context]() { // 先停掉超时定时器防止回调触发两次 context-timeoutTimer.stop(); Response resp; QNetworkReply* rep context-reply; if (rep-error() QNetworkReply::NoError) { resp.setData(rep-readAll()); } else { resp.setError(rep-errorString()); } rep-deleteLater(); if (context-callback) { context-callback(resp); } }, Qt::QueuedConnection);这个实现的读取方式有个细节容易被忽略readAll 必须在 finished 信号里读不要拖到下一次事件循环。因为底层缓冲区在 reply 销毁后就不存在了deleteLater 是安全的但如果你的代码路径里提前用了 rep-close() 或者直接 delete缓冲区就没了。QTimer 的超时时间建议做成每个请求可配默认给业务侧传一个timeoutMs参数QNetWork 内部把 defaultTimeoutMs 设为 15 秒。实测经验内网接口 15 秒够用上公网或者是弱网环境建议调到 30 秒。调太大的问题在于底层 TCP 重传已经把连接折腾得半死了超时兜底失去意义。2.3 封装层的线程策略主线程调用回调回主线程Qt 的 QNetworkAccessManager 官方说明是可以在任意线程使用但 reply 的信号要和对应线程的事件循环绑定。具体到封装层我的建议很直接不要在非 GUI 线程直接发请求。常见的翻车姿势是在 worker 线程里 new 一个 QNetworkAccessManager然后在 lambda 里更新 UI触发 QObject::connect: Cannot queue arguments 之类的告警甚至直接崩溃。QNetWork 的做法是 Network 类本身就是 QObject 的单例构造在 main thread所有公开方法内部通过 QMetaObject::invokeMethod 把实际发送动作投递到 Network 对象所在线程执行回调则利用 Qt::QueuedConnection 自动回到调用方线程。核心代码体现为void Network::post(const QString path, const QVariantMap body, QObject* context, std::functionvoid(const Response) cb, int timeoutMs) { // 组装请求但不直接发送 QNetworkRequest req; req.setUrl(QUrl(m_baseUrl path)); req.setHeader(QNetworkRequest::ContentTypeHeader, application/json); auto ctx new RequestContext(this); ctx-context context; ctx-callback cb; ctx-timeoutTimer.start(timeoutMs 0 ? timeoutMs : m_defaultTimeoutMs); // 把实际网络动作投递到 Network 线程 QMetaObject::invokeMethod(this, [this, req, body, ctx]() { QNetworkReply* reply m_manager-post(req, QJsonDocument::fromVariant(body).toJson()); ctx-reply reply; connect(reply, QNetworkReply::finished, ctx, [ctx]() { /* 前面那段处理逻辑 */ }, Qt::QueuedConnection); }); }这里有一个几乎所有第一次封装的人都会踩的坑QMetaObject::invokeMethod的 lambda 版本只有在 Qt 5.10 以上才能配合Qt::QueuedConnection跨线程用如果你的项目还锁在 Qt 5.9 或更早就得改用QMetaObject::invokeMethod(this, doPost, Qt::QueuedConnection, Q_ARG(QNetworkRequest, req))配合槽函数的方式。另一个坑是 lambda 捕获了ctx、req、body其中ctx是裸指针如果 Network 在 lambda 执行前被销毁ctx就成了野指针。解决的常见做法是让 Network 的析构里主动 abort 掉所有未完成请求的 reply同时把 m_manager 的父对象置为 Network保证子对象先于 Network 析构。我个人的习惯是不在析构里等请求而是直接清空所有 RequestContext 并 abort 底层 reply代价是会丢掉未完成请求的回调。3. 超时、重试与取消把网络层的玄学变成可预测行为3.1 QNetworkRequest 的超时属性为什么不能直接用QNetworkRequest 有一个 setTransferTimeout 方法Qt 5.15 开始支持但它只能兜底整个传输过程的总超时没法做「连接超时」和「首字节超时」的区分。更麻烦的是如果你要支持 Qt 5.12 或更早的长期支持版本这个方法根本不存在只能在业务侧自己用 QTimer 包。QNetWork 的默认策略是用 QTimer 做总超时原因很简单跨版本一致性好行为可控。超时时间设置多少合理我拆过的项目里有按接口类型区分的先例但落到实际维护最简单可靠的是全局默认 15 秒个别接口通过参数覆盖。给两个具体数值做参考登录类接口建议 10 秒以内用户感知到了卡顿就会重试批量下载图片或文件给到 60 秒也不过分因为大文件的 readAll 耗时不在网络层而在 IO。如果你的场景要区分连接超时和读取超时常见做法是分两个 QTimer一个从发送开始计时到 connected 信号触发时停止另一个在收到第一个字节后启动。实现也不复杂在 ReplyContext 里加一个QTimer connectTimerconnect 到 QNetworkReply 的 connected 信号收到后换成 dataTimer。3.2 重试策略哪个层级该重试哪个层级不该重试网络请求失败后要不要自动重试这是封装层最容易做过头的地方。我的结论是只有满足两个条件才自动重试——请求是幂等的GET、HEAD、PUT 一般认为幂等并且错误类型属于网络层故障ConnectionRefused、Timeout、HostNotFound、RemoteHostClosedError。业务层错误HTTP 400、401、403、500一律不重试重试只会放大问题。int Network::retryPolicy(const QString httpMethod, QNetworkReply::NetworkError err, int retriedCount) { // 非幂等请求不自动重试防止重复扣款/重复提交 if (httpMethod ! GET httpMethod ! HEAD httpMethod ! PUT) { return 0; } // 业务错误不重试 if (err 400) return 0; switch (err) { case QNetworkReply::ConnectionRefusedError: case QNetworkReply::TimeoutError: case QNetworkReply::HostNotFoundError: case QNetworkReply::RemoteHostClosedError: return (retriedCount 2) ? 500 : 0; // 最多重试2次间隔500ms default: return 0; } }重试间隔这里用固定 500ms 是保守做法实际项目中我一般用 500ms、1s、2s 的递增序列配合一点随机抖动500ms rand()%200防止几十个客户端同时重试把服务器打崩。这个抖动参数很重要我在一个模拟项目X 里见过没有抖动的重试逻辑上线后高峰期服务端错误率反而升高的情况因为客户端们像训练好的士兵一样整齐地在同一毫秒重试。重试的实现不要放在回调里递归调用否则每次重试都会重新走一遍请求组装逻辑容易把 header 或 body 弄丢。正确做法是在 RequestContext 里记录原始请求参数重试时直接复用struct RetryInfo { QString method; QNetworkRequest request; QByteArray body; int retriedCount 0; }; // 把 retryInfo 放进 RequestContext // finished 信号处理里如果 retryPolicy 返回 0则用 retryInfo 重新发送3.3 取消请求让析构和切页面不再产生幽灵回调移动端和桌面端的取消场景不太一样但共性是用户点击返回或切换页面后之前发出去的请求如果还挂在网上结果回来时业务侧已经不再关心了。不处理这个问题的代价是内存泄漏 无意义的流量消耗 潜在的野指针崩溃。处理方案分三层第一层调用方传入的 context 对象析构时RequestContext 里的回调要被清空。这要求 Network 能感知 context 的析构事件常见做法是调用QObject::connect(context, QObject::destroyed, requestContext, QObject::deleteLater)。这一层能保证回调不会触发但底层网络请求仍在跑浪费一个连接。第二层真正的取消是把底层 reply abort 掉。在业务侧主动取消的场景直接调用Network::abortByContext(context)内部遍历所有 RequestContext凡 context 匹配的reply-abort() 并清理。这个函数在切换页面时集中调用一次比每个请求单独记录指针再逐个 abort 稳妥得多。第三层区分「取消」和「失败」。abort 会触发 QNetworkReply::OperationCanceledError在回调里业务侧需要区分收到这个错误码时不是真正的服务器失败而是本地主动取消应该直接忽略回调而不是弹错误提示。这一层不处理的典型症状是用户快速切换页面界面弹出「网络请求失败」的 toast体验非常糟糕。QNetWork 的做法是在 Response 里加一个canceled()方法内部把 OperationCanceledError 映射为 true同时给回调一个 cancel 标志位。实际使用中还有一类场景是排队取消比如文件下载列表里用户取消了第 3 个文件但第 1、2 个还在下载这时候 abort 单个 reply 即可不需要中止整个 Network。所以 abort 接口必须支持按 context 精确取消别偷懒做成全局 abort后患无穷。4. 与业务数据对接从 QVariantMap 到类型安全的响应包装4.1 响应统一包装把 QJson 解析的脏活留在封装层网络请求拿到 QByteArray 后业务侧马上要面对的是「这是不是 JSON」「解析失败怎么办」「字段缺失怎么办」的问题。如果每个调用方都自己写一遍 QJsonDocument::fromJson不出三个月就会出现三种不同的错误处理风格。QNetWork 的统一做法是 Response 内部预解析一次业务侧通过json()拿 QJsonObject通过array()拿 QJsonArray用ok()判断整体成功。class Response { public: bool ok() const { return m_ok m_httpStatus 200 m_httpStatus 300; } QJsonObject json() const { if (!m_jsonParsed) { m_json QJsonDocument::fromJson(m_data).object(); m_jsonParsed true; } return m_json; } QJsonArray array() const { if (!m_jsonParsed) { QJsonDocument doc QJsonDocument::fromJson(m_data); m_jsonParsed true; if (doc.isArray()) m_array doc.array(); } return m_array; } QString errorString() const { return m_errorString; } int httpStatus() const { return m_httpStatus; } // canceled() 标记是否被本地取消 bool canceled() const { return m_canceled; } private: bool m_ok false; bool m_canceled false; bool m_jsonParsed false; int m_httpStatus 0; QByteArray m_data; QString m_errorString; QJsonObject m_json; QJsonArray m_array; };这里有一个性能细节json() 和 array() 使用懒解析第一次调用时解析一次之后复用。别小看这个设计如果一个响应体有几百 KB每个字段都调一次 fromJson 会有明显的卡顿。另一个容易被忽略的是ok()的判断条件有的后端即使业务失败HTTP 状态码依然是 200只是 body 里带code ! 0。这种场景 QNetWork 的做法是提供一个setBizValidator回调由业务侧注入自己的业务码判断规则Response 在构造后主动调用一次。不过考虑到通用性这个机制通常做成 optional默认只看 HTTP 状态码。json() 解析失败时QJsonObject 是空的业务侧如果没有判空直接取 key会拿到 QJsonValue::Undefined再 toString 就是空字符串。这种空数据比报错更隐蔽我建议在 json() 内部做一次 document.isObject() 判断不是对象时在 qWarning 里打一条日志方便排查。很多人问要不要在 ok() 里把「解析失败」直接算作失败我的答案是不建议有的接口成功响应是空 bodyHTTP 200 就代表成功强行解析只会误伤。4.2 POST 请求体构造form 表单、JSON、文件上传三种形态封装层最常见的失误是只支持 JSON遇到 form 或文件上传就要业务侧绕过封装层。QNetWork 的接口至少需要支持三种 bodyQVariantMap序列化成application/jsonQUrlQuery序列化成application/x-www-form-urlencoded以及QHttpMultiPart用于文件上传。三种分别对应 90% 的后端接口、老式 PHP/Java 服务、以及附件场景。JSON 和 form 的转换不复杂但有一个参数编码的坑form 表单里如果有中文、emoji、特殊字符必须用QUrl::toPercentEncoding处理直接拼字符串会在后端收到乱码。这个坑甚至可以排进「网络层最常见的五个坑」前三名。文件上传则要小心生命周期void Network::uploadFile(const QString path, const QString filePath, const QString fieldName, QObject* context, std::functionvoid(const Response) cb) { QNetworkRequest req; req.setUrl(QUrl(m_baseUrl path)); QHttpMultiPart* multiPart new QHttpMultiPart(QHttpMultiPart::FormDataType); QFile* file new QFile(filePath); if (!file-open(QIODevice::ReadOnly)) { delete file; delete multiPart; Response resp; resp.setError(cannot open file); cb(resp); return; } QHttpPart filePart; filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QString(form-data; name\%1\; filename\%2\) .arg(fieldName, QFileInfo(filePath).fileName())); filePart.setBodyDevice(file); file-setParent(multiPart); // 关键让 multiPart 接管 file 生命周期 multiPart-append(filePart); // 后续通过 m_manager-post(req, multiPart) 发送 }这里有一个连很多老手都会踩的坑setBodyDevice(file)之后file 和 multiPart 的所有权关系。官方文档要求 multiPart 必须比 reply 活得久QNetworkReply 完成时不会自动帮你 delete multiPart。常见处理方式是把 file 的 parent 设为 multiPart再在 reply 的 finished 信号里multiPart-deleteLater()。如果忘了 deleteLater每一次上传都会泄漏一组 QHttpMultiPart QFile长时间运行后内存涨得悄无声息。另一个细节是filename%2里中文文件名建议做一下 URL 编码部分后端网关有过不了校验的情况。文件下载和上传是对称的但下载要注意的是大文件不应该用 readAll 一把梭。QNetWork 在下载场景建议提供两个信号downloadProgress(qint64, qint64)和finished业务侧拿到字节流后自己写文件不要等全部读完再落盘。真需要断点续传的话封装层至少要把Rangeheader 和文件写入偏移量透传出来不能堵死在统一回调模型里。4.3 Header 管理与 Cookie 同步让公共参数不再散落各处很多项目里的 header 是散着加的登录模块加 Authorization统计模块加 User-Agent日志模块又往里面塞 traceId。时间一长同一个 QNetworkRequest 在不同代码路径里被 setHeader 了好几次有的还互相覆盖。QNetWork 的做法是维护一个全局默认 header 集合每次发请求前自动合并业务侧也可以通过一个 QVariantMap 追加本次请求的临时 header。void Network::setDefaultHeader(const QByteArray key, const QByteArray value) { m_defaultHeaders.insert(key, value); qInfo() Network default header set: key value; } QNetworkRequest Network::buildRequest(const QString path, const QVariantMap extraHeaders) { QNetworkRequest req; req.setUrl(QUrl(m_baseUrl path)); req.setRawHeader(User-Agent, m_userAgent); for (auto it m_defaultHeaders.constBegin(); it ! m_defaultHeaders.constEnd(); it) { req.setRawHeader(it.key(), it.value()); } for (auto it extraHeaders.constBegin(); it ! extraHeader.constEnd(); it) { req.setRawHeader(it.key().toUtf8(), it.value().toByteArray()); } return req; }Common header 的合并顺序要固定先默认后临时临时覆盖默认。这块我吃过一次亏某次排查一个线上问题发现线上请求的 header 里出现了两个 Authorization 字段一个过期一个有效后端取了过期的那个导致大面积登录失效。原因就是某个调用方 setHeader 的时候没有先 remove底层 QNetworkRequest 把同名 header 叠加了。QNetWork 在 setRawHeader 前先req.setRawHeader(key, QByteArray())清掉旧值。关于 CookieQt 的 QNetworkAccessManager 有 setCookieJar 全局管理我的建议是如果你们的后端是 Token 认证就干脆别用 Cookie省去 CSRF 和过期同步的麻烦。如果一定要用QNetWork 应该把 cookieJar 的读写封装到内部不要暴露给业务侧直接改。因为 Cookie 过期策略一旦散落到业务代码里就是一场灾难。5. 避坑与排查封装网络层时最常见的五个翻车现场5.1 现象回调触发两次导致 UI 重复刷新这个问题在封装层出现频率极高。原因基本是两种一是超时 QTimer 触发了回调但后续 reply 的 finished 信号又触发了一次二是 QNetworkReply 的 finished 信号和 errorOccurred 信号都在你两个都 connect 了。解决方式是统一在 RequestContext 里加一个bool finished false的标记进入回调处理函数时先判断已处理则直接 return。if (ctx-handled) return; ctx-handled true; ctx-timeoutTimer.stop();注意这个标记要在发送请求前初始化为 false并且整个生命周期不能重置。不推荐把标记放在 callback 对象里因为这个对象可能被多次构造或拷贝状态位会丢失。我习惯直接放在 RequestContext 类里最可靠。5.2 现象用 QNetworkAccessManager 发 HTTPS 请求报 SSL 错误这个坑在 Qt 5.12 时代尤其多。现象是 sslErrors 信号触发errorString显示 The certificate is expired 或 self-signed certificate。原因大多不是证书真的过期而是系统 CA 证书库路径没配置对。Linux 下 Qt 默认从/etc/ssl/certs读 CA 列表如果你的 Qt 是交叉编译或者自定义安装的CA 路径会失效。解决方式分两个层次// 第一层调试期先打开忽略证书错误排除业务问题 connect(reply, QNetworkReply::sslErrors, this, [](const QListQSslError errors) { qWarning() ssl errors: errors; // 仅测试时使用不要放到生产环境 }); // 第二层生产环境正确配置 CA QSslConfiguration config QSslConfiguration::defaultConfiguration(); config.setCaCertificates(QSslCertificate::fromPath(/etc/ssl/certs/ca-certificates.crt)); config.setProtocol(QSsl::TlsV1_2OrLater); QSslConfiguration::setDefaultConfiguration(config);需要注意的是QSslCertificate::fromPath只加了一个文件如果你的系统是多个证书分散在目录里要遍历目录逐个加载。另外 Qt 5.15 之后 TLS 1.0/1.1 默认禁用如果你的服务器还开 TLS 1.0直接握手失败这不是封装层能解决的问题得让后端升 TLS 版本。生产环境里不要用审查 ssl 错误列表全放行的做法除非你知道自己在做什么。5.3 现象Post 请求带了大 body 后偶发卡死典型场景是上传一个几 MB 的图片connected 信号正常触发但传输到一半卡住。底层原因可能是 QNetworkAccessManager 在不设置传输超时的情况下等待服务器读 body 超时。解决方式通常是把QNetworkRequest::setTransferTimeout设到合理值。但还有一个容易忽略的细节如果你用的 QHttpMultiPart 上传body 不是一次性写入底层 socket 的QNetworkAccessManager 会按 chunk 分块发送如果服务器的 body 读取逻辑有 bug 或者超时时间设得过短服务端提前断开连接客户端就会卡在等待响应状态。此时抓包能看到客户端已经发完数据服务器没有回复任何字节。解决方式是同时设置全局请求超时保证至少能回调失败而不是永久等待。我还踩过一个更隐蔽的姿势某些版本的 Qt 里QNetworkRequest 的 TransferTimeout 是基于整个请求的不是基于 idle 的。也就是说如果服务器持续给你推数据即使传了 10 分钟也是合法的因为 transfer 始终在发生。这并不代表你的请求卡了。排查时可以观察底层 socket 的活动而不是盯着墙上的钟。5.4 现象同一个 QNetworkAccessManager 发大量并发请求后新请求排队有一个隐藏参数QNetworkAccessManager 的连接池大小由网络代理配置和底层协议决定HTTP/1.1 对同一主机的并发连接数一般限制在 6 个左右超过的会排队。如果你们的首页同时触发 10 个图片请求 3 个接口请求超过 6 个并发上限后面的请求会一直排队表现就是资源加载慢。解决方法有两个方向一是业务侧合并请求减少请求数二是使用 HTTP/2。Qt 的 QNetworkAccessManager 对 HTTP/2 的支持需要后端开启 ALPN 协商且 Qt 编译时开启了 feature-http2。如果你的项目是一个连接重用特别频繁的桌面工具型应用比如地图客户端瓦片请求非常多建议单独维护一个专用 QNetworkAccessManager并且在请求头加上Connection, keep-alive。这个 header 通常能减少 TCP 握手开销但在 HTTP/2 下无效。需要留意的是QNetworkAccessManager 的线程亲和性会直接影响连接复用如果在多个线程里分别创建 manager连接池是分裂的达不到复用效果。5.5 现象get 请求参数中文乱码、特殊字符服务端解析不了这是网络层封装里最经典的坑。根本原因是 URL 里带了未编码的中文、、、?等字符后端按分隔符解析时被误导。解决方式是统一走QUrlQuery构造 query而不是字符串拼。QUrlQuery query; query.addQueryItem(keyword, keyword); query.addQueryItem(page, QString::number(page)); query.addQueryItem(size, QString::number(size)); QUrl url(m_baseUrl path); url.setQuery(query);注意QUrlQuery内部默认会做 percent encoding且默认使用的编码格式是PercentDecoded。如果你的服务端是 Java 系的它们默认解码 URL 时用的是 UTF-8基本不会出问题如果服务端是某些老 PHP 环境它们会按 GBK 解码这种问题编码层解决不了只能和后端约定编码格式。我遇到过一个项目前端用QUrl::toPercentEncoding对中文做了处理后端收到的还是乱码最后发现是后端反代服务器在转发请求时对 URL 二次解码了因为转码逻辑被包了两层。这种跨 team 的问题不要自己猜直接抓包看后端拿到的是什么。6. 压测与验证用一组可量化的检查项验收你的封装层封装写完不是上线就完事而是要过一轮能证明「这层壳不会漏水」的验证。我最常做的一套检查项是五个维度并发正确性、超时可靠性、取消无泄漏、重复回调为零、错误码覆盖。逐个说下验证手段。第一项并发正确性模拟同时发起 20 个不同接口的请求其中一半结果立即返回、一半延迟 2 秒业务侧挂 timer 每 500ms 检查一次。如果封装层的 RequestContext 管理有问题20 个请求里必然出现回调错乱或者野指针崩溃。第二项超时验证把 defaultTimeoutMs 临时设成 200ms请求一个必然超过 200ms 才响应的接口确认收到 timeout 回调且只触发一次。第三项取消验证发送一个慢请求后立即调用 abortByContext然后让页面正常析构观察有没有报 QObject::killTimer 之类的警告以及内存是否持续增长。第四项重复回调在回调里加计数跑 500 个请求样本计数不等于请求数的直接排查。第五项错误码覆盖用一个伪服务器分别返回「404、500、空响 应、断连、超时」五类结果逐一确认 Response 的错误信息足够准确业务侧不看日志也能定位到原因。还有一个值得安利的验证技巧给封装层加一个内部统计计数器每次 destoryed 的 RequestContext 数量、完成的请求数量、超时数量、取消数量全部打进日志。上线观察一个礼拜对比数据就能看出哪个接口经常超时、哪个接口被取消次数最多。这个习惯帮我发现了两次线上隐患一次是接口超时集中在某个第三方服务另一次是某页面切走时取消了大量无关请求纯属业务侧误用。从那以后我每接一个新的网络封装层都强制要求先跑通这套检查项再加业务代码统计日志也成为了发版评审的必过项。这组经验希望能帮到正在或准备封装网络层的你少走几个我已经帮你们踩过的坑。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?