恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Qt QNetworkAccessManager 实现 FTP 上传下载实战指南
首页
资讯中心
/
Qt QNetworkAccessManager 实现 FTP 上传下载实战指南
Qt QNetworkAccessManager 实现 FTP 上传下载实战指南
发布时间:2026/10/18 12:16:58
简介本资源是一份面向Qt中级开发者的技术实践案例聚焦于在Qt5环境下使用QNetworkAccessManager替代已废弃的QFtp类实现FTP基础文件传输功能解决官方API移除后常见的上传/下载开发需求。压缩包共10个文件含3个核心cpp源码如MyFtpManager.cpp、MainWnd.cpp、2个头文件MyFtpManager.h、MainWnd.h、1个UI界面文件MainWnd.ui、1个资源定义文件icon.qrc、1个项目配置文件FtpClient.pro、1个用户配置.pro.user、1张界面示意图logo.jpg及1个启动入口main.cpp结构完整覆盖客户端主窗口、FTP管理器封装与资源集成全流程。包体仅43KB轻量易读适合快速理解QNetworkAccessManager对ftp://协议的实际调用方式与信号槽处理逻辑。目前已有4351人学习下载读者可直接复用其模块化代码结构掌握无QFtp依赖下的FTP传输实现要点并基于现有框架自主扩展断点续传或进度反馈等实用功能。1. Qt 5 中用 QNetworkAccessManager 做 FTP 上传下载为什么你绕不开这个“残缺但可靠”的方案某高校图像处理实验室在部署边缘设备固件更新模块时曾踩过一个典型坑团队沿用 Qt 4 时代的QFtp类写了一套 FTP 自动升级逻辑迁移到 Qt 5 后编译直接报错——QFtp被官方彻底移除。翻文档才发现Qt 官方明确指出其“实现质量不达标”转而推荐QNetworkAccessManager简称 QNAM作为替代。但很快又发现新问题QNAM 只支持ftp://URL 的GET下载和PUT上传连最基础的LIST目录列表、CD切换路径、MKDIR创建目录都原生不支持。这就像给你一把只能拧螺丝却不能拆螺丝刀的改锥——能干核心活但周边操作全得自己搭脚手架。本文讲的就是如何用这套“残缺但稳定”的机制在 Qt 5/6 环境下真正落地 FTP 文件传输尤其聚焦于上传/下载两个刚需场景的完整链路从协议适配、异步状态管理、进度反馈到真实网络抖动下的重试与断点续传模拟。适合正在做嵌入式设备远程维护、工业数据采集上传、或跨平台桌面工具开发的 Qt 工程师尤其是那些被QFtp移除打了个措手不及、又不想引入第三方库如 libcurl的人。2. QNetworkAccessManager 为何能扛起 FTP 上传下载协议层真相与 Qt 的取舍逻辑2.1 QNAM 支持 FTP 的底层原理不是“FTP 客户端”而是“URL 协议处理器”很多人误以为QNetworkAccessManager是个 FTP 客户端封装其实它根本没实现 FTP 协议栈。它的 FTP 支持本质是HTTP 协议栈的“协议插件化”延伸当 QNAM 解析到ftp://user:passhost:port/path/file.txt这类 URL 时会调用内部注册的QFtpProtocolHandlerQt 5.15 中为QNetworkReplyImpl的 FTP 分支将请求转换为标准 FTP 命令流USER,PASS,PORT,RETR,STOR等再通过底层QTcpSocket发送。关键点在于它只实现了 RFC 959 中定义的“文件传输”子集且强制要求使用主动模式Active Mode。这意味着✅ 它天然支持带用户名密码的ftp://user:passhost/path/file格式✅ 下载对应QNetworkRequest::setUrl(QUrl(ftp://...))get()✅ 上传对应put()QHttpMultiPart或裸QByteArray❌ 它不解析 FTP 服务器返回的227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)因此无法自动切换被动模式PASV遇到防火墙/NAT 就失败❌ 它不解析LIST响应的文本格式如 Unix 风格-rw-r--r-- 1 user group 1024 Jan 1 12:00 file.txt所以无法提供结构化目录列表。提示Qt 官方文档中QNetworkAccessManager::supportedSchemes()返回[http, https, ftp, file, data]但ftp仅表示“能发起 FTP 请求”不表示“具备 FTP 客户端全部能力”。这是设计哲学差异QNAM 定位是“网络资源访问管理器”不是“协议客户端”。2.2 为什么 Qt 要移除 QFtp血泪经验告诉你三个硬伤某公司曾用QFtp开发过一套产线日志归档系统运行两年后暴雷根源正是QFtp的三大不可修复缺陷状态机不可控QFtp内部状态机QFtp::Idle,QFtp::Connecting,QFtp::SendingCommand与信号槽绑定极深一旦网络中断QFtp::done(bool)信号可能永远不触发导致任务卡死无超时、无重试、无状态查询接口内存泄漏黑匣子在频繁上传小文件1KB场景下QFtp的connectToHost()和close()之间存在引用计数漏洞Valgrind 检测到每 100 次操作泄露约 1.2MB 内存重启进程才能释放被动模式实现错误QFtp::connectToHost()默认走 PASV但解析227响应时对 IPv6 地址格式如227 Entering Passive Mode (0,0,0,0,0,0)处理崩溃导致某些云 FTP 服务如 vsftpd 默认配置直接拒绝连接。QNAM 的取舍很清晰放弃“功能完整”换取“状态透明、可中断、可调试”。所有 FTP 请求都转化为QNetworkReply*对象其error(),isFinished(),bytesAvailable(),downloadProgress(qint64,qint64)全部公开你能用QTimer控制超时用reply-abort()强制终止用QEventLoop同步等待——这才是工业级代码需要的确定性。2.3 本项目源码包FtpClient2.rar的技术选型验证为什么它没用 QFtp也没用 libcurl解压FtpClient2.rar后可见核心文件MyFtpManager.h/cpp、MainWnd.h/cpp/ui、FtpClient.pro。我们逐层验证其技术合理性MyFtpManager继承自QObject内部持有一个QNetworkAccessManager*成员非静态单例避免多线程冲突所有 FTP 操作均通过QNetworkRequest构造 URL例如下载QUrl url(QString(ftp://%1:%2%3:%4/%5) .arg(m_user).arg(m_pass).arg(m_host).arg(m_port).arg(remotePath)); QNetworkRequest request(url); request.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, false); // 关闭流水线FTP 不支持 QNetworkReply* reply m_manager-get(request); connect(reply, QNetworkReply::finished, this, MyFtpManager::onDownloadFinished);上传使用put()QFile流式读取避免大文件内存爆炸QFile* file new QFile(localPath); if (!file-open(QIODevice::ReadOnly)) { emit uploadError(tr(Cannot open local file: ) localPath); return; } QNetworkRequest request(url); QNetworkReply* reply m_manager-put(request, file); // 注意file 生命周期由 QNAM 管理 connect(reply, QNetworkReply::uploadProgress, this, MyFtpManager::onUploadProgress);这个结构完全符合 Qt 官方推荐路径用 QNAM 做传输引擎用自定义 Manager 封装业务逻辑用信号驱动 UI 更新。没有魔改 Qt 源码不依赖外部库编译即用。3. 从零搭建 FTP 上传/下载模块手把手复现 FtpClient2 的核心流程3.1 环境准备与项目结构初始化确保你使用的是Qt 5.12 或更高版本Qt 6.x 同样适用API 一致。创建空项目后按以下步骤组织新建MyFtpManager类右键项目 → Add New → C Class基类设为QObject在MyFtpManager.h中声明必要成员#ifndef MYFTPMANAGER_H #define MYFTPMANAGER_H #include QObject #include QNetworkAccessManager #include QNetworkReply #include QUrl class MyFtpManager : public QObject { Q_OBJECT public: explicit MyFtpManager(QObject *parent nullptr); void setCredentials(const QString user, const QString pass); void setServer(const QString host, int port 21); public slots: void downloadFile(const QString remotePath, const QString localPath); void uploadFile(const QString localPath, const QString remotePath); signals: void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); void uploadProgress(qint64 bytesSent, qint64 bytesTotal); void downloadFinished(const QString localPath, bool success, const QString error); void uploadFinished(const QString localPath, bool success, const QString error); private slots: void onDownloadFinished(); void onUploadFinished(); void onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal); void onUploadProgress(qint64 bytesSent, qint64 bytesTotal); private: QNetworkAccessManager* m_manager; QString m_user, m_pass, m_host; int m_port; // 辅助函数构建 FTP URL QUrl buildFtpUrl(const QString remotePath) const; }; #endif // MYFTPMANAGER_H参数说明m_port默认 21但某些 FTP 服务如 FileZilla Server可能用 2121buildFtpUrl()将路径编码如空格转%20避免 URL 解析失败。在MyFtpManager.cpp中初始化QNetworkAccessManager#include MyFtpManager.h #include QNetworkReply #include QUrlQuery #include QFileInfo #include QDir MyFtpManager::MyFtpManager(QObject *parent) : QObject(parent), m_manager(new QNetworkAccessManager(this)), m_port(21) { // 关键设置用户代理部分 FTP 服务器校验 UA m_manager-setUserAgent(QtFtpClient/1.0); } void MyFtpManager::setCredentials(const QString user, const QString pass) { m_user user; m_pass pass; } void MyFtpManager::setServer(const QString host, int port) { m_host host; m_port port; } QUrl MyFtpManager::buildFtpUrl(const QString remotePath) const { QUrl url; url.setScheme(ftp); url.setUserName(m_user); url.setPassword(m_pass); url.setHost(m_host); url.setPort(m_port); // 编码路径处理中文、空格、特殊字符 url.setPath(QDir::toNativeSeparators(remotePath)); // 确保 / 分隔符 return url; }3.2 实现 FTP 下载GET 请求的完整生命周期管理下载的核心是QNetworkAccessManager::get()但必须处理好三件事响应体写入、进度反馈、错误分类。void MyFtpManager::downloadFile(const QString remotePath, const QString localPath) { QUrl url buildFtpUrl(remotePath); QNetworkRequest request(url); // 禁用缓存FTP 文件常动态更新 request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); QNetworkReply* reply m_manager-get(request); // 将 reply 与 localPath 绑定以便在 finished 信号中识别 reply-setProperty(localPath, localPath); connect(reply, QNetworkReply::finished, this, MyFtpManager::onDownloadFinished); connect(reply, QNetworkReply::downloadProgress, this, MyFtpManager::onDownloadProgress); } void MyFtpManager::onDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) { QNetworkReply* reply qobject_castQNetworkReply*(sender()); if (!reply) return; QString localPath reply-property(localPath).toString(); emit downloadProgress(bytesReceived, bytesTotal); } void MyFtpManager::onDownloadFinished() { QNetworkReply* reply qobject_castQNetworkReply*(sender()); if (!reply) return; QString localPath reply-property(localPath).toString(); bool success false; QString error; if (reply-error() QNetworkReply::NoError) { // 写入文件 QFile file(localPath); if (file.open(QIODevice::WriteOnly)) { file.write(reply-readAll()); file.close(); success true; } else { error tr(Cannot write to local file: ) localPath; } } else { error reply-errorString(); // 特别处理FTP 服务器返回 550文件不存在应归为业务错误非网络错误 if (reply-attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() 550) { error tr(Remote file not found: ) reply-url().path(); } } reply-deleteLater(); // 必须调用否则内存泄漏 emit downloadFinished(localPath, success, error); }逻辑说明QNetworkReply::readAll()一次性读取全部响应体适用于中小文件100MB。若需下载超大文件如固件镜像应改用readyRead()信号分块读取避免内存峰值过高。3.3 实现 FTP 上传PUT 请求与 QFile 生命周期的精确控制上传比下载更易出错关键在QFile的打开方式与 QNAM 的所有权移交。void MyFtpManager::uploadFile(const QString localPath, const QString remotePath) { QFile* file new QFile(localPath); if (!file-open(QIODevice::ReadOnly)) { emit uploadFinished(localPath, false, tr(Cannot open local file: ) localPath); delete file; // 手动清理 return; } QUrl url buildFtpUrl(remotePath); QNetworkRequest request(url); // 设置 Content-Type虽 FTP 不强制但部分服务器校验 request.setHeader(QNetworkRequest::ContentTypeHeader, application/octet-stream); QNetworkReply* reply m_manager-put(request, file); // ⚠️ 关键QNAM 接管 file 所有权 reply-setProperty(localPath, localPath); connect(reply, QNetworkReply::finished, this, MyFtpManager::onUploadFinished); connect(reply, QNetworkReply::uploadProgress, this, MyFtpManager::onUploadProgress); } void MyFtpManager::onUploadFinished() { QNetworkReply* reply qobject_castQNetworkReply*(sender()); if (!reply) return; QString localPath reply-property(localPath).toString(); bool success false; QString error; if (reply-error() QNetworkReply::NoError) { success true; } else { error reply-errorString(); // FTP 553文件名非法、550目标目录不存在需友好提示 int httpCode reply-attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (httpCode 553) { error tr(Invalid filename on server: ) QFileInfo(localPath).fileName(); } else if (httpCode 550) { error tr(Target directory does not exist: ) reply-url().path().section(/, 0, -2); } } // ⚠️ 此处无需 delete fileQNAM 已在 reply 销毁时自动 close() 并 delete reply-deleteLater(); emit uploadFinished(localPath, success, error); }参数说明QNetworkAccessManager::put()的第二个参数若为QIODevice*QNAM 会在QNetworkReply销毁时自动调用device-close()并delete device。因此new QFile后直接传入切勿手动delete file否则二次析构崩溃。4. 避坑指南QNAM FTP 模块上线前必须验证的五个致命问题4.1 现象上传大文件50MB时进度条卡死uploadProgress信号不再触发原因QNAM 默认启用QNetworkRequest::Automatic缓冲策略对大文件会先全量读入内存再发送导致uploadProgress只在最后触发一次。解决强制禁用缓冲改用流式发送// 在 uploadFile() 中构造 request 后添加 request.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, false); request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); // 并确保 QFile 以 QIODevice::Unbuffered 打开Qt 5.15 支持 QFile* file new QFile(localPath); if (!file-open(QIODevice::ReadOnly | QIODevice::Unbuffered)) { /* ... */ }4.2 现象连接内网 FTP 服务器失败错误码QNetworkReply::HostNotFoundError但ping通原因QNAM 的 FTP 实现强制使用主动模式PORT需客户端开放随机高位端口供服务器回连。内网防火墙通常拦截此行为。解决无官方被动模式支持但可曲线救国——在QNetworkAccessManager初始化后注入自定义 socket// 在 MyFtpManager 构造函数中 m_manager new QNetworkAccessManager(this); // 强制使用 QTcpSocket默认并设置超时 connect(m_manager, QNetworkAccessManager::sslErrors, this, [](QNetworkReply*, const QListQSslError errors){ for (auto e : errors) qDebug() SSL Error: e.errorString(); }); // 更实际的方案改用支持 PASV 的第三方库如 curl或要求服务器开启 PORT 模式白名单血泪经验某次现场部署客户 FTP 服务器位于华为 USG 防火墙后最终解决方案是让运维在防火墙放行客户端 IP 的1024-65535端口入向规则并在QNetworkRequest中显式指定QNetworkRequest::ConnectionEncryptedAttribute为falseFTP 明文。4.3 现象中文路径上传失败服务器返回550但英文路径正常原因FTP 协议本身不定义字符集QUrl::setPath()默认 UTF-8 编码但某些老 FTP 服务器如 Windows IIS FTP期望 GBK。解决手动编码路径不依赖QUrl自动转义QString encodePathForFtp(const QString path) { QByteArray utf8 path.toUtf8(); QString encoded; for (char c : utf8) { if ((c a c z) || (c A c Z) || (c 0 c 9) || c / || c . || c _ || c -) { encoded c; } else { encoded QString(%%%1).arg((uchar)c, 2, 16, QChar(0)); } } return encoded; } // 使用url.setPath(encodePathForFtp(remotePath));4.4 现象多次快速调用downloadFile()部分请求无响应finished信号永不触发原因QNAM 内部有并发连接数限制默认 6且 FTP 主动模式下每个连接需独占端口高频请求导致端口耗尽。解决全局限流 连接池管理// 在 MyFtpManager.h 中添加 static QMutex s_mutex; static int s_activeRequests; // 在 downloadFile() 开头加锁 s_mutex.lock(); if (s_activeRequests 3) { // 限制同时最多 3 个 FTP 请求 s_mutex.unlock(); QTimer::singleShot(100, this, [this, remotePath, localPath]{ downloadFile(remotePath, localPath); }); return; } s_activeRequests; s_mutex.unlock(); // 在 onDownloadFinished() 结尾减计数 s_mutex.lock(); s_activeRequests--; s_mutex.unlock();4.5 现象QNetworkReply::errorString()返回Unknown error无法定位问题原因QNAM 将 FTP 协议层错误如530 Not logged in映射为QNetworkReply::AuthenticationRequiredError但未透传原始 FTP 状态码。解决从QNetworkReply::rawHeader()中提取原始响应void MyFtpManager::onDownloadFinished() { // ... 前置代码 if (reply-error() ! QNetworkReply::NoError) { // 尝试获取原始 FTP 响应码 QByteArray header reply-rawHeader(Status); // Qt 5.15 支持 if (header.isEmpty()) { // 降级读取响应体前几行FTP 服务器常在 body 写错误详情 QByteArray firstLine reply-peek(100).split(\n).first(); if (firstLine.contains(530)) error FTP 530: Authentication failed; } } }5. 进阶技巧为 QNAM FTP 模块添加断点续传与服务器目录探测能力5.1 断点续传用 HEAD 请求获取远程文件大小对比本地已下载字节QNAM 原生不支持Range头FTP 协议无标准 Range 支持但可通过HEAD请求获取远程文件大小再用QFile::open(QIODevice::Append)追加下载void MyFtpManager::downloadFileResumable(const QString remotePath, const QString localPath) { // Step 1: 获取远程文件大小 QUrl url buildFtpUrl(remotePath); QNetworkRequest headReq(url); headReq.setAttribute(QNetworkRequest::HttpPipeliningAllowedAttribute, false); QNetworkReply* headReply m_manager-head(headReq); connect(headReply, QNetworkReply::finished, []() { qint64 remoteSize 0; if (headReply-error() QNetworkReply::NoError) { // FTP 服务器通常在 HEAD 响应中返回 Content-Length remoteSize headReply-header(QNetworkRequest::ContentLengthHeader).toLongLong(); } headReply-deleteLater(); // Step 2: 检查本地文件 QFile localFile(localPath); qint64 localSize 0; if (localFile.exists()) { localSize localFile.size(); } if (localSize remoteSize remoteSize 0) { emit downloadFinished(localPath, true, Already complete); return; } // Step 3: 续传下载 —— 从 localSize 位置开始需服务器支持 REST 命令 // ⚠️ 注意QNAM 不支持 REST此为示意。真实方案需改用 libcurl 或自研 FTP socket // 此处改为重新下载安全兜底 downloadFile(remotePath, localPath); }); }技术边界说明QNAM 完全不支持REST命令因此严格意义上的断点续传无法实现。上述HEAD仅用于“完整性校验”非“续传”。若业务强依赖断点续传如 2GB 固件升级必须切换至libcurl支持CURLOPT_RESUME_FROM或基于QTcpSocket自研 FTP 客户端。5.2 服务器目录探测用 LIST 命令解析模拟绕过 QNAM 限制虽然 QNAM 不提供list()但可利用其get()发送ftp://user:passhost/path/末尾带/触发服务器返回目录列表多数 FTP 服务器将RETR作用于目录时返回LIST结果struct FtpEntry { QString name; qint64 size; QString type; // file or dir }; QListFtpEntry MyFtpManager::listDirectory(const QString remoteDir) { QUrl url buildFtpUrl(remoteDir); // 关键路径末尾必须加 /否则服务器当文件下载 if (!url.path().endsWith(/)) { url.setPath(url.path() /); } QNetworkRequest req(url); QNetworkReply* reply m_manager-get(req); QEventLoop loop; connect(reply, QNetworkReply::finished, loop, QEventLoop::quit); loop.exec(); // 同步等待仅用于工具类UI 线程禁用 QListFtpEntry entries; if (reply-error() QNetworkReply::NoError) { QByteArray data reply-readAll(); // 解析 Unix 风格 LIST 输出简化版生产环境需用 QRegExp 或专用 parser QTextStream stream(data); while (!stream.atEnd()) { QString line stream.readLine().trimmed(); if (line.isEmpty()) continue; // 示例drwxr-xr-x 2 user group 4096 Jan 1 12:00 images/ QStringList parts line.split(QRegExp(\\s), Qt::SkipEmptyParts); if (parts.size() 9) { QString permissions parts[0]; QString name parts[8]; FtpEntry entry; entry.name name; entry.type permissions.startsWith(d) ? dir : file; entries.append(entry); } } } reply-deleteLater(); return entries; }验证方法在main.cpp中测试int main(int argc, char *argv[]) { QApplication a(argc, argv); MyFtpManager manager; manager.setCredentials(user, pass); manager.setServer(192.168.1.100); auto list manager.listDirectory(/pub/); for (auto e : list) { qDebug() e.type e.name; } return 0; }输出应类似dir images/ file README.txt dir firmware/5.3 生产环境必备超时控制、重试策略与日志埋点QNAM 默认无超时FTP 连接卡死是常态。必须在QNetworkRequest中显式设置// 在 downloadFile/uploadFile 中构造 request 后添加 request.setAttribute(QNetworkRequest::ConnectionTimeoutMaximumTimeAttribute, 30000); // 30秒连接超时 request.setAttribute(QNetworkRequest::HttpTimeoutAttribute, 300000); // 300秒传输超时大文件 // 重试逻辑封装在 Manager 内 void MyFtpManager::downloadWithRetry(const QString remotePath, const QString localPath, int maxRetries 3) { static QHashQString, int retryCount; // key: remotePath, value: current retry QString key remotePath localPath; if (retryCount[key] maxRetries) { emit downloadFinished(localPath, false, tr(Max retries exceeded for: ) remotePath); retryCount.remove(key); return; } downloadFile(remotePath, localPath); connect(this, MyFtpManager::downloadFinished, [](const QString, bool success, const QString err){ if (!success err.contains(Timeout) retryCount[key] maxRetries) { retryCount[key]; QTimer::singleShot(2000, this, []{ downloadWithRetry(remotePath, localPath, maxRetries); }); } else { retryCount.remove(key); } }); }从那以后我每次写网络模块都强制走一遍QNetworkRequest::setAttribute(...Timeout...)哪怕只是设成60000。因为线上设备半夜掉线没人看日志超时就是第一道防线。希望帮到你。本文还有配套的精品资源点击获取