做过 Qt 客户端的早晚都会撞上这样一个需求:把数据或者文件通过 HTTP 传到服务器。有人觉得这事简单,QNetworkAccessManager发个 POST 不就完了?真等你把代码写完,传 JSON、传表单、传小文件、传大文件一路测下来,就会发现里面全是坑——进度、超时、取消、中文文件名、分片续传、服务端解析格式,任何一个细节没处理好,线上就会出问题。这篇文章就按我在实际项目里的处理思路,把 Qt 通过 HTTP 上传数据或文件的完整做法捋一遍。看完你可以直接照着搭一个带进度、能取消、不会中文乱码、方便二次扩展的上传模块;如果你是第一次碰 Qt 网络编程,跟着步骤也能把基本流程跑通。
1. 先拆需求:上传数据的几种形态和它们之间的本质区别
1.1 上传需求分两种,服务端靠 Content-Type 区分
很多初学者会把"上传"当成一件事,实际上日常开发里碰到的上传至少分两类:结构化数据上传和文件上传。
结构化数据是指 JSON、键值对这类有固定格式的文本内容,比如提交订单信息、上报设备状态、发送用户反馈。这类请求一般比较小,几 KB 以内,一次性拼成请求体发出去就行。文件上传就完全不同了:图片、日志、安装包、视频切片,单个文件动辄几十 MB 甚至几个 GB,不可能全部塞进字符串里,而且传输过程中要考虑进度反馈、网络中断、断点续传等问题。
在 HTTP 协议层面,服务端区分这两种请求靠的是请求头里的Content-Type:
application/json:请求体是 JSON 字符串,RESTful 接口里最常见。application/x-www-form-urlencoded:请求体是key=value&key2=value2这种 URL 编码后的文本,传统表单提交方式。multipart/form-data:用于文件上传。请求体被切分成多个 part,每个 part 可以带自己的Content-Disposition头,既可以放表单字段,也可以放文件内容。
明白了这三种类型的差别,你先要做的不是写代码,而是找服务端同事确认接口类型。因为 Qt 侧只是发送请求,真正怎么解析取决于服务端框架,两边但凡有一个理解不一致,就会产生"我这里明明发出去了,你那里怎么收到空串"的经典问题。
1.2 Qt 网络模块负责什么,不管什么
Qt 的网络模块封装了 TCP、HTTP/HTTPS、SSL 等一系列底层协议,QNetworkAccessManager是你的入口对象,负责创建请求、发送请求、接收响应。它帮你处理了连接复用、重定向、SSL 握手、cookie 管理等繁琐的东西。
但有一个边界必须认清:Qt 只是站在客户端的角度把 HTTP 报文发出去,服务端接口怎么设计、multipart 的字段规则是什么、分片协议长什么样,都不在 Qt 的管辖范围内。也就是说,你要先把接口规则搞清楚,再决定 Qt 侧怎么写。
我见过不少同事一上来就问"Qt 怎么上传文件",结果连对方接口要求的是form-data还是raw body都没确认,最后自然对接不上。所以这一节的主要任务是:拿到需求后,先判断属于哪种数据类型,然后确认Content-Type,这一步错了后面全部白做。
2. 基础操作:用 QNetworkAccessManager 发 POST,把 JSON 和表单数据传上去
2.1 QNetworkAccessManager 的生命周期建议全局复用
先看一个最基本的数据上传请求。QNetworkAccessManager这个对象建议在应用启动时创建,并且尽量复用,不要每次发请求都 new 一个。它内部维护了连接池、SSL 会话缓存等资源,复用能显著减少握手开销,尤其是在需要频繁上传的场景下。
线程方面有一个容易踩的规则:QNetworkAccessManager必须运行在有事件循环的线程里,而且它和槽函数所在线程的关系要理清。最简单可靠的做法是:在主线程(UI 线程)创建并使用它,请求结果通过信号槽机制回到主线程更新界面。后面的跨线程问题我会专门展开讲,这里先记住一条——不要随随便便丢到子线程里 new。
2.2 提交 JSON 数据:标准三步
提交 JSON 的核心是设置Content-Type为application/json,然后把序列化后的QByteArray作为 POST 请求体发出去。下面是一个可以直接跑的最小示例:
#include <QNetworkAccessManager> #include <QNetworkRequest> #include <QNetworkReply> #include <QJsonDocument> #include <QJsonObject> // manager 通常作为类成员或全局唯一实例 QNetworkAccessManager* manager = new QNetworkAccessManager(this); QNetworkRequest request; request.setUrl(QUrl("https://your-server.example.com/api/data")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); request.setHeader(QNetworkRequest::UserAgentHeader, "MyQtClient/1.0"); QJsonObject obj; obj["name"] = "demo"; obj["count"] = 42; QByteArray payload = QJsonDocument(obj).toJson(QJsonDocument::Compact); QNetworkReply* reply = manager->post(request, payload); connect(reply, &QNetworkReply::finished, this, [this, reply]() { if (reply->error() != QNetworkReply::NoError) { qWarning() << "上传失败:" << reply->errorString(); } else { int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); QByteArray body = reply->readAll(); qDebug() << "服务端返回" << status << body; } reply->deleteLater(); });这里有两个容易出错的地方,值得提醒。
第一,QJsonDocument::toJson输出的是 UTF-8 编码的字节流。如果你的 JSON 里包含中文,QJsonObject内部本身就是 QString 的 Unicode 表示,序列化成字节流时 Qt 会正确转成 UTF-8。真正容易出错的是反过来解析时用了错误的编码假设,这也是为什么我建议服务端和客户端都约定 UTF-8。
第二,finished信号到达时,reply->error()不能代表一切。HTTP 状态码为 4xx、5xx 时,error()会返回错误,但同时服务端错误详情往往就在响应体里。所以我在代码里即使走了错误分支,也会去读HttpStatusCodeAttribute,方便排查。
2.3 提交表单键值对:QUrlQuery 帮你完成转义
如果服务端接口要的是application/x-www-form-urlencoded,Qt 侧最方便的工具是QUrlQuery。它会自动处理键值的 URL 编码,避免你手写字符串时漏掉特殊字符:
QUrlQuery params; params.addQueryItem("username", "demo"); params.addQueryItem("source", "qt-client"); params.addQueryItem("tags", "key1,key2"); QNetworkRequest request; request.setUrl(QUrl("https://your-server.example.com/api/form")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); QByteArray body = params.toString(QUrl::FullyEncoded).toUtf8(); QNetworkReply* reply = manager->post(request, body);注意params.toString()的编码模式参数。默认的QUrl::PrettyDecoded在某些场景下不会对空格、引号做严格转义,HTTP 表单场景下建议用QUrl::FullyEncoded,保证所有特殊字符都被正确编码。
还有一个细节:如果你同时需要带请求头,比如鉴权的Authorization头,用request.setRawHeader("Authorization", "Bearer xxx")。setRawHeader接受QByteArray,可以避免 QString 到 Latin1 的隐式转换问题——这个问题在文件上传的中文场景里还会再次出现,后面会细说。
3. 重头戏:QHttpMultiPart 实现 multipart/form-data 文件上传
3.1 为什么文件上传必须要用 multipart
有人会问:为什么不能把文件内容直接拼进 JSON 或者 URL 编码表单里?原因有两个:
- 二进制数据无法放进 URL 编码文本。图片、压缩包里的字节可能包含任意值,一旦直接放进 URL 编码字符串,效率低且容易出错。
- multipart 协议天生支持"多个部分"。一个请求体里可以同时携带文本字段和文件字段,每个字段有独立的
Content-Disposition头,能表达字段名、文件名、MIME 类型等信息。
multipart 的机制不难理解:客户端生成一个随机字符串作为boundary(分隔边界),然后把请求体切成若干段,每一段用--boundary开始,字段间的分隔线也是它,最后用--boundary--收尾。注意,这个 boundary 一般由客户端生成,服务器靠它来切分请求体。在 Qt 里,这些底层拼接工作被封装进了QHttpMultiPart,你不需要手工拼 boundary。
3.2 用 QHttpMultiPart 组装请求体的完整步骤
Qt 官方提供的标准做法是用QHttpMultiPart配合QHttpPart。看下面的示例,这是一个典型的"表单字段 + 文件字段"组合上传:
// 1. 创建 multipart 对象,指定类型为表单 QHttpMultiPart* multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType); // 2. 添加一个普通文本字段 QHttpPart textPart; textPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"note\"")); textPart.setBody("this is a text field"); // 3. 准备文件字段 QString filePath = "/path/to/pic.png"; QFile* file = new QFile(filePath); if (!file->open(QIODevice::ReadOnly)) { qWarning() << "无法打开文件" << file->errorString(); delete file; return; } QHttpPart filePart; filePart.setHeader( QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name=\"file\"; filename=\"pic.png\"")); filePart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); filePart.setBodyDevice(file); // 关键点:直接把设备作为 body,流式发送 multiPart->append(textPart); multiPart->append(filePart); // 4. 发送请求 QNetworkRequest request; request.setUrl(QUrl("https://your-server.example.com/api/upload")); QNetworkReply* reply = manager->post(request, multiPart); // 5. 释放时机:两个对象都跟着 finished 信号回收 connect(reply, &QNetworkReply::finished, multiPart, &QHttpMultiPart::deleteLater); connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);这里最关键的一步是filePart.setBodyDevice(file)。
setBodyDevice意味着 Qt 会在发送时流式读取文件,而不是先把整个文件读进QByteArray再发送。这对大文件上传来说是决定性的差异:一个 2GB 的文件,如果走setBody+QByteArray,内存直接撑爆;走setBodyDevice,内存占用基本恒定,Qt 内部用事件循环配合底层 socket 按块读取数据。
3.3 一个很容易翻车的点:边界 boundary 和对象所有权
先说 boundary。很多初学者查阅旧资料时会手动给请求头设置Content-Type: multipart/form-data; boundary=xxx,然后在 Qt 里这么做反而可能翻车。因为manager->post(request, multiPart)这个重载在内部会自动帮你设置 Content-Type,并生成与 multipart 对象匹配的 boundary。如果你手动写死了另一个 boundary,两者不一致,服务端按请求头里的 boundary 切分请求体时会直接解析失败,返回 400 或者文件为空。
再说对象所有权。QHttpMultiPart是通过指针传给post()的,文档写得很清楚:QNetworkAccessManager 不会接管它的所有权。所以你必须自己保证它活到请求结束,并且在结束后手动delete。最常见的做法就是上面示例里的:
connect(reply, &QNetworkReply::finished, multiPart, &QHttpMultiPart::deleteLater);这个 connectivity 的思路是:QHttpMultiPart::deleteLater()在事件循环里安全销毁对象,reply结束时正是归还内存的最稳妥时机。如果没有这一步,你会发现关闭窗体或连续上传几次后内存一直涨,因为每个 multipart 对象都没释放。
还有一个QFile的生命周期问题。如果你在函数栈里创建QFile,函数一退出文件对象就没了,但请求还在异步发送,设备读取会崩溃。推荐做法是把QFile的 parent 设为multiPart:
file->setParent(multiPart);这样文件会跟着 multipart 一起释放,不需要手动管理,顺序正好合理。
4. 体验兜底:进度反馈、取消上传、超时处理一整套细节
4.1 uploadProgress 信号:从拿到字节数到算速度、估算剩余时间
文件上传没进度条,用户在 UI 上就只能干等着,体验非常糟糕。Qt 提供了QNetworkReply::uploadProgress(qint64 bytesSent, qint64 bytesTotal)信号,会在发送过程中持续触发。基本用法:
connect(reply, &QNetworkReply::uploadProgress, this, [this, reply](qint64 bytesSent, qint64 bytesTotal) { if (bytesTotal <= 0) { // 总大小未知时,给个无限进度即可 ui->progressBar->setRange(0, 0); return; } ui->progressBar->setRange(0, int(bytesTotal)); ui->progressBar->setValue(int(bytesSent)); // 顺便算一下瞬时速度和剩余时间 qint64 now = QDateTime::currentMSecsSinceEpoch(); static qint64 lastTs = now; static qint64 lastSent = bytesSent; double speed = 0.0; if (now > lastTs) { speed = (bytesSent - lastSent) * 1000.0 / (now - lastTs); // 字节/秒 } lastTs = now; lastSent = bytesSent; if (speed > 0.0) { double remainSec = (bytesTotal - bytesSent) / speed; ui->labelSpeed->setText(QString("%1 KB/s,剩余约 %2 s") .arg(speed / 1024.0, 0, 'f', 1) .arg(remainSec, 0, 'f', 0)); } });这里有两个注意点:
bytesTotal 可能是 -1。当某些服务器返回 chunked 传输且未声明总大小时,bytesTotal可能是 -1,你的进度条区间会变成非法值。判断bytesTotal <= 0时应该切换到忙碌状态,而不是硬算百分比。
uploadProgress 信号不一定只在发送中触发。请求结束前最后会补发一次bytesSent == bytesTotal,这通常可以作为"上传完成"的辅助判断,但不要完全依赖它,还是以finished信号为准。
4.2 取消上传和超时:abort 与 setTransferTimeout 的正确姿势
用户想取消上传,最简单的是调用reply->abort()。调用后reply会收到OperationCanceledError,然后触发finished信号。所以在你的错误处理分支里,应该单独把这个情况拎出来,不和真正的网络错误混为一谈:
auto handleError = [this, reply](QNetworkReply::NetworkError code) { if (code == QNetworkReply::OperationCanceledError) { // 用户主动取消,不提示错误 return; } // 真正的错误处理 };超时方面,Qt 5.15 开始提供了便利方法QNetworkRequest::setTransferTimeout(int timeout),单位毫秒,这个超时同时控制上传和下载的无数据时长:
request.setTransferTimeout(30000); // 30 秒内没有数据传输就超时如果你的项目还在用 5.15 之前的 Qt,只能自己用QTimer模拟:
QTimer* timer = new QTimer(reply); timer->setSingleShot(true); connect(timer, &QTimer::timeout, reply, &QNetworkReply::abort); timer->start(30000); connect(reply, &QNetworkReply::finished, timer, &QTimer::stop);注意setTransferTimeout的文档语义是"超过这个时间没有连上或没有数据流动就超时",不是总耗时上限。一个 2GB 的文件只要持续有数据在传,就不会触发超时;但如果网络断了且迟迟没有收到任何底层数据,就会超时并触发 abort。这个语义正好符合我们的需求。
4.3 断线场景:怎么区分"正常结束"和"服务端主动断开"
文件上传途中,尤其是弱网环境,经常遇到"发到一半连接没了"的情况。QNetworkReply::error信号会给你一个错误码,常见的有:
ConnectionClosedError:底层连接被关闭,服务端或中间设备断开。RemoteHostClosedError:远端主机主动关闭了连接。ProtocolFailure:协议层错误,比如收到非法响应。TimeoutError:如果走了自定义 timer 的 abort 路径,则表现为OperationCanceledError。
实际开发中,如果错误码是连接类错误,可以提示用户"网络中断,可重试",并且把已经发送的进度保存下来,为断点续传做准备。这里也呼应了后面要讲的续传设计——客户端先判断是哪类失败,再决定是原地重发当前分片,还是记录进度后整体续传。具体逻辑我会在下一节展开。
5. 大文件上传:内存、分片、断点续传和 MD5 校验
5.1 大文件的两座大山:内存峰值和传输稳定性
当文件超过几百 MB,问题就不再是简单的"把文件发给服务器"了。
第一座山是内存。前面说过setBodyDevice已经解决了内存拷贝问题,但如果你用的是同步阻塞方式、或者在发送前把整个文件readAll()到QByteArray里做 pre-check,那些习惯会把内存峰值顶上去。判断标准:内存占用不该随文件大小线性增长。一个 1GB 的文件上传,客户端内存占用控制在几十 MB 以内是合理的。
第二座山是稳定性。一次 HTTP 请求传输 1GB 文件,经历的任何一次网络抖动都可能中断整个传输。而 HTTP 协议本身并没有"断点续传"的内置机制——它只是尽最大努力把请求体发完。要提升稳定性,必须自己设计分片策略。
5.2 分片上传的接口约定与客户端实现思路
先说一个前提:分片上传一定要服务端支持,否则只是客户端自己在瞎切。常规约定一般是这样:
- 把大文件按固定大小切分,比如每片 1MB 或 4MB。
- 每片一个请求,请求里带
offset(分片在文件中的起始偏移)、total(文件总大小)、md5(分片或整体校验值)。 - 服务端逐个接收分片,最后一片到达后再合并,并返回成功。
Qt 侧可以写一个简单状态机:发当前分片,收到成功响应后再发下一片,失败则重试当前片若干次。伪代码结构如下:
class ChunkUploader : public QObject { Q_OBJECT public: void start(const QString& filePath, qint64 chunkSize = 1024 * 1024) { m_file.setFileName(filePath); if (!m_file.open(QIODevice::ReadOnly)) return; m_total = m_file.size(); m_offset = 0; m_chunkSize = chunkSize; sendNextChunk(); } private: void sendNextChunk() { if (m_offset >= m_total) { qDebug() << "所有分片上传完成"; return; } m_file.seek(m_offset); QByteArray chunk = m_file.read(qMin(m_chunkSize, m_total - m_offset)); QNetworkRequest request; request.setUrl(QUrl("https://your-server.example.com/api/chunk")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QJsonObject meta; meta["offset"] = m_offset; meta["size"] = chunk.size(); meta["total"] = m_total; meta["md5"] = QCryptographicHash::hash(chunk, QCryptographicHash::Md5).toHex(); QByteArray body = QJsonDocument(meta).toJson(QJsonDocument::Compact); QNetworkReply* reply = m_manager->post(request, body); connect(reply, &QNetworkReply::finished, this, [this, reply, chunk]() { if (reply->error() == QNetworkReply::NoError) { m_offset += chunk.size(); reply->deleteLater(); sendNextChunk(); // 成功才发下一片 } else { // 重试逻辑:指数退避 + 上限次数 qWarning() << "分片失败" << m_offset << reply->errorString(); reply->deleteLater(); } }); } QNetworkAccessManager m_manager; QFile m_file; qint64 m_offset = 0; qint64 m_total = 0; qint64 m_chunkSize = 0; };这个示例没有处理重试的退避策略,生产上建议每片重试 3 次,失败后指数级增加间隔,比如 1 秒、2 秒、4 秒。另外要注意,上传进度条在分片模式下应该显示"总文件进度",而不是单片的进度。计算逻辑很简单:已经成功上传的字节数加上当前片的 bytesSent,除以总文件大小。
5.3 断点续传与整体校验
断点续传的本质就是"记录上次成功的位置"。客户端在每次发送分片成功后,把m_offset持久化到本地配置文件,下次启动时读取它,直接从该偏移继续。这样一个几小时的大文件任务,中途退出后也能接着传,不用从头再来。
但续传有一个隐含条件:服务端必须保证它已成功保存了之前所有分片。如果服务器合成分片时发现少了一片,客户端从某个中间 offset 续传就会失败。所以更稳妥的方案是:客户端每次启动任务时先询问服务端"这个文件目前收到了多少字节",然后从服务器确认的 offset 开始续传。这个设计看似多了一次请求,实际上能避开非常多"两边状态不一致"的坑。
最后是 MD5 校验。建议同时在两个层面做:
- 分片级 MD5:每片发送前计算该分片的 MD5,随分片请求带给服务端,服务端比对后存储,能及时发现传输过程中的数据损坏。
- 整体文件 MD5:整个文件发送完后,客户端再发一个"合并完成"请求,把完整文件的 MD5 交给服务端。服务端合成后再算一次 MD5,两边一致才算任务成功。
Qt 里计算整体 MD5 不要一次性读入内存,逐块累加即可:
QCryptographicHash hash(QCryptographicHash::Md5); QFile f("/path/to/bigfile.bin"); if (f.open(QIODevice::ReadOnly)) { QByteArray buf; while (!f.atEnd()) { buf = f.read(1 << 16); // 每次读 64KB hash.addData(buf); } } QByteArray md5Hex = hash.result().toHex();6. 实战排坑:中文文件名、SSL 配置和跨线程问题
6.1 中文文件名的编码深坑,这是被问得最多的一个
如果你的上传文件名是中文或者带空格的特殊字符,很容易出现服务端收到文件名乱码,甚至直接解析 multipart 失败。原因在于QNetworkRequest::setHeader在内部将 QVariant 中的字符串转成 HTTP 头时,默认走的是 Latin1 编码,而中文在 Latin1 里会被替换成问号,或者变成完全错乱的字节。
解决办法是不直接传原始 QString,而是把文件名做 URL 百分号编码后再放进 header 字符串:
QString fileName = "测试报告.png"; QByteArray encodedName = QUrl::toPercentEncoding(fileName); QString disposition = QStringLiteral("form-data; name=\"file\"; filename=\"%1\"") .arg(QString::fromLatin1(encodedName)); filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant(disposition));这样发送出去后,filename字段里是%E6%B5%8B%E8%AF%95...这样的百分号编码内容,服务端拿到后按 URL 解码就能还原中文。如果服务端框架比较新,也可以直接用filename*=UTF-8''...的 RFC 5987 格式,但这个格式更依赖服务端实现,保险起见我一般默认用百分号编码方案。
还要提醒一点:文件路径里带中文也要注意。QFile打开路径时,Qt 内部处理没问题,但如果你把完整路径直接拼到 filename 字段里,服务端拿到的是C:\Users\...\测试.png这种带有目录结构的字符串,很多服务端会把路径分隔符当非法字符拒绝。正确做法永远是只取QFileInfo(filePath).fileName(),把目录信息剥离掉。
6.2 SSL 配置:自签名证书和测试环境的处理方式
走 HTTPS 上传时,如果服务端用的是自签名证书或内部私有 CA,Qt 默认的 SSL 校验会直接拒绝连接。这时候你需要手动修改QSslConfiguration。
只建议在测试或内网环境这样干:
QSslConfiguration sslConfig = request.sslConfiguration(); sslConfig.setPeerVerifyMode(QSslSocket::VerifyNone); request.setSslConfiguration(sslConfig);生产环境千万别把VerifyNone写死,那等于放弃了证书校验,中间人攻击风险极大。正规做法是把服务端证书链安装到测试机器的系统信任区,或者把公钥证书打包进客户端,用QSslConfiguration::setCaCertificates加载。网上很多教程直接让你关掉校验,那是对安全极不负责的写法。
6.3 跨线程与并发请求:三个容易忽略的约束
最后一个高频坑是线程。QNetworkAccessManager不能在非主线程里随便 new,因为它的异步机制依赖于线程的事件循环。如果你确实需要把上传任务放到工作线程,正确姿势是:在工作线程的对象里创建 manager,同时确保该线程有QEventLoop在跑(比如通过QThread的run()或者moveToThread后正常启动)。但结果要回传 UI 时,再通过信号槽跨线程丢回来,不要在工作线程里直接操作QProgressBar。
并发请求本身倒是安全的。同一个QNetworkAccessManager同时发出多个上传请求完全没问题,每个QNetworkReply是独立对象,你用 lambda 捕获对应的reply指针,或者在 finished 槽里通过sender()区分,就能准确知道哪个请求完成了。常见错误是把多个请求的响应混在一起,尤其不要在 lambda 里捕获一个循环变量做索引而不加拷贝。
另外,上传接入了系统代理环境时,Qt 在大多数平台会自动读取系统代理配置,但如果你的服务和代理有跳板冲突,可以手动设置:
QNetworkProxy proxy; proxy.setType(QNetworkProxy::HttpProxy); proxy.setHostName("proxy.example.local"); proxy.setPort(8080); manager->setProxy(proxy);这里的重点是不盲目信任默认设置,也要搞清楚自己的网络环境到底需不需要代理。
6.4 调试建议:日志记录和抓包验证
上传功能写完,第一步不是连真实服务器,而是在本地搭一个能接收 multipart 的模拟服务端,把自己当服务器看看 Qt 到底是把什么内容发出去的。具体做法:用任意后端框架起一个接口,打印收到的Content-Type、boundary、每个 part 的 header 和 body 大小,核对文件名编码是否正确。这样一轮验证下来,能排除掉大部分"服务端解析不了"的问题。
客户端侧也要做好日志:
- 记录请求 URL 和 Content-Type。
- 记录每次 uploadProgress 的字节数和总字节数。
- 记录 finished 时的 HTTP 状态码和响应体摘要。
- 如果是分片上传,记录每个分片的 offset、size、重试次数。
排查问题时,把客户端日志和服务端日志一对,问题基本就现形了。我经历过最诡异的一次是服务端反馈"上传到一半总差几个字节",最后发现是客户端在setBodyDevice之后又提前读了一次文件指针,把文件设备的位置给改变了。这种问题不看日志根本猜不到。
关于 Qt 通过 HTTP 上传数据或文件,我能想到的坑差不多就这些。如果你只是传小 JSON,照第二节的代码就够用;如果是传文件,把第三节的 multipart 写法抄下来,再补上进度和超时;遇到大文件,直接按第五节做分片。每次写上传前多问一句"服务器到底要我发什么格式",能帮你省下大量对接时间。