Qt QNetworkAccessManager 实现 FTP 上传下载:能力边界与避坑指南 简介这份资源面向需要在 Qt 项目中实现 FTP 上传与下载功能的开发者尤其适合从 Qt4 迁移到 Qt5、发现 QFtp 已被移除而寻找替代方案的中级学习者。由于 Qt5 出于实现质量考虑不再提供 QFtp APIQNetworkAccessManager 成为处理 ftp:// URL 上传下载的官方途径但它并未提供 list()、cd()、remove()、mkdir()、rmdir()、rename() 和 rawCommand() 等目录与命令操作本资源正是围绕这一限制展开的实践示例。压缩包为 rar 格式共 10 个文件约 43KB包含 3 个 cpp 与 2 个 h 源文件承载核心逻辑另有 pro 工程文件、ui 界面文件、qrc 资源文件、user 配置及一张 jpg 图片结构紧凑、便于直接编译运行。目前已有 4351 人学习下载读者可从中获取 QNetworkAccessManager 实现 FTP 传输的完整代码框架、自定义 FtpManager 的封装思路以及界面与网络模块的衔接方式为二次开发与功能扩展提供参考。1. QNetworkAccessManager 做 FTP 上传下载为什么它比你想的更能打很多人第一次听到用 Qt 的 QNetworkAccessManager 走 FTP第一反应是「这玩意儿不是搞 HTTP 的吗」。我当初也这么想直到有个工业现场的项目设备端只允许开一个 FTP 端口做文件交换不允许额外引第三方库我才硬着头皮把 QNetworkAccessManager 的 FTP 能力翻了个底朝天。结论是它确实能上传、能下载、能列目录而且 API 和 HTTP 那套几乎一致学习成本极低。代价是它只支持被动模式、不支持 FTPS 加密、断点续传要自己拼 Range 头这些边界必须先认清楚。这篇笔记就按「能不能用、怎么跑通、参数怎么设、坑在哪」的顺序把 QNetworkAccessManager 实现 FTP 文件上传下载这条路径讲透适合手里有 Qt 5.9 以上环境、需要在不引额外依赖的前提下做文件传输的嵌入式或桌面端开发者。2. QNetworkAccessManager 的 FTP 能力边界先搞清楚它能干什么2.1 它到底封装了 FTP 的哪些动作QNetworkAccessManager 内部通过 QFtp 的替代实现Qt5 之后 QFtp 被移除FTP 支持被合并进 QNetworkAccessManager来发 FTP 命令。你不需要手动拼 USER、PASS、PASV、STOR、RETR 这些命令只要构造一个 QNetworkRequest把 URL 写成ftp://user:passwordhost:port/path的形式剩下的握手、登录、被动模式协商都由它完成。具体支持的动作有这么几类。下载用manager-get(request)返回一个 QNetworkReply数据通过 readyRead 信号流式吐出。上传用manager-put(request, data)data 是 QByteArray也可以传一个 QIODevice 指针做流式上传。列目录用manager-get()请求一个目录 URL返回的是 FTP LIST 的原始文本需要自己解析。删除文件用manager-deleteResource(request)。这些动作和 HTTP 的调用方式完全对称所以如果你写过 HTTP 下载迁移过来几乎没有心智负担。但要注意它不支持主动模式PORT只走被动模式PASV。这在绝大多数 NAT 和防火墙环境下反而是好事因为被动模式是客户端发起数据连接穿透性更好。可如果你面对的是一个只认主动模式的老旧 FTP 服务器那就没辙了。2.2 为什么选它而不是 QFtp 或 libcurlQt5 之后 QFtp 被官方标记为废弃并移出核心模块虽然还能单独编译但维护成本高。libcurl 功能全支持 FTPS、断点续传、主动被动切换但引入它意味着你要处理跨平台编译、证书、依赖打包在一个只想传几个文件的 Qt 项目里属于杀鸡用牛刀。QNetworkAccessManager 的优势在于「零额外依赖」。它就在 QtNetwork 模块里你的工程只要QT network就能用。信号槽机制天然适配 Qt 的事件循环不会阻塞 UI。对于「上传一个配置文件」「下载一个固件包」这类需求它是最省事的选择。代价也要说清楚。第一不支持 FTPSFTP over TLS如果你的服务器强制加密它连不上。第二没有内置断点续传大文件传到一半断了只能重来除非你自己用 Range 头做分片。第三进度反馈要靠 reply 的 downloadProgress / uploadProgress 信号精度取决于底层缓冲区不是每个字节都触发。第四超时控制要自己挂 QTimerQNetworkAccessManager 本身没有 FTP 层面的超时参数。提示如果你的场景要求加密传输或断点续传趁早换 libcurl 或 QFtp 分支别在 QNetworkAccessManager 上硬扛后面会讲到为什么。2.3 最小可跑通的下载示例先看一个能直接编译运行的下载代码把链路跑通再谈优化。// ftpdownloader.h #include QObject #include QNetworkAccessManager #include QNetworkReply #include QFile class FtpDownloader : public QObject { Q_OBJECT public: explicit FtpDownloader(QObject *parent nullptr); void startDownload(const QString ftpUrl, const QString savePath); private slots: void onReadyRead(); void onFinished(); void onError(QNetworkReply::NetworkError code); private: QNetworkAccessManager *m_manager; QNetworkReply *m_reply nullptr; QFile *m_file nullptr; };// ftpdownloader.cpp #include ftpdownloader.h #include QUrl #include QNetworkRequest #include QDebug FtpDownloader::FtpDownloader(QObject *parent) : QObject(parent), m_manager(new QNetworkAccessManager(this)) {} void FtpDownloader::startDownload(const QString ftpUrl, const QString savePath) { m_file new QFile(savePath, this); if (!m_file-open(QIODevice::WriteOnly)) { qWarning() 无法打开本地文件: savePath; return; } QUrl url(ftpUrl); // 形如 ftp://user:pass192.168.1.10:21/firmware.bin QNetworkRequest request(url); request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); m_reply m_manager-get(request); connect(m_reply, QNetworkReply::readyRead, this, FtpDownloader::onReadyRead); connect(m_reply, QNetworkReply::finished, this, FtpDownloader::onFinished); connect(m_reply, QOverloadQNetworkReply::NetworkError::of(QNetworkReply::error), this, FtpDownloader::onError); } void FtpDownloader::onReadyRead() { if (m_file m_reply) { m_file-write(m_reply-readAll()); // 流式写入避免大文件占内存 } } void FtpDownloader::onFinished() { if (m_file) { m_file-close(); m_file-deleteLater(); m_file nullptr; } if (m_reply) { if (m_reply-error() QNetworkReply::NoError) { qDebug() 下载完成; } m_reply-deleteLater(); m_reply nullptr; } } void FtpDownloader::onError(QNetworkReply::NetworkError code) { qWarning() 下载出错: code m_reply-errorString(); }这段代码的逻辑很直白构造 URL发起 getreadyRead 来了就往文件里写finished 来了就收尾。关键参数在 URL 本身——用户名、密码、主机、端口、路径全塞在ftp://后面。FollowRedirectsAttribute对 FTP 其实作用有限但加上无害。要注意的是readyRead的触发频率。底层 TCP 缓冲区有数据就会触发不是按 FTP 数据块边界来的所以readAll()拿到的可能是半个数据块也可能是好几个。对文件写入来说无所谓因为字节流顺序是对的。但如果你要在 readyRead 里做协议解析就得自己维护缓冲区。3. 上传、进度与目录操作把功能补全3.1 用 put 上传文件并拿到进度上传和下载对称把 get 换成 put多传一个数据源。数据源可以是 QByteArray也可以是 QIODevice 指针。大文件建议用 QIODevice避免一次性把整个文件读进内存。void FtpUploader::startUpload(const QString ftpUrl, const QString localPath) { m_file new QFile(localPath, this); if (!m_file-open(QIODevice::ReadOnly)) { qWarning() 无法打开待上传文件: localPath; return; } QUrl url(ftpUrl); // ftp://user:passhost:21/upload/target.bin QNetworkRequest request(url); // 关键告诉服务器这是个二进制文件避免某些服务器做 ASCII 转换 request.setHeader(QNetworkRequest::ContentTypeHeader, application/octet-stream); m_reply m_manager-put(request, m_file); // 传 QIODevice流式上传 connect(m_reply, QNetworkReply::uploadProgress, this, FtpUploader::onProgress); connect(m_reply, QNetworkReply::finished, this, FtpUploader::onFinished); } void FtpUploader::onProgress(qint64 sent, qint64 total) { if (total 0) { int percent static_castint(sent * 100 / total); emit progressChanged(percent); // 发给 UI 更新进度条 } }put的第二个参数传 QIODevice 时QNetworkAccessManager 会在上传过程中不断从设备读数据读完自动关闭吗不会它不会替你关文件finished 里要自己 close 和 deleteLater。ContentTypeHeader设成 octet-stream 是个血泪经验某些 FTP 服务器在 ASCII 模式下会把\n转成\r\n二进制文件传上去就废了显式声明二进制能规避这个问题。uploadProgress的 total 在 FTP 场景下不一定准。如果服务器在 STOR 之前没返回文件大小预期total 可能是 -1这时候进度条只能做「已发送字节数」的显示做不了百分比。这是协议层面的限制不是 Qt 的锅。3.2 列目录与解析 LIST 返回列目录用 get 请求一个以/结尾的 URL返回的是服务器 LIST 命令的原始文本。格式没有统一标准Unix 风格和 Windows 风格差别很大。void FtpClient::listDirectory(const QString ftpDirUrl) { QUrl url(ftpDirUrl); // ftp://user:passhost:21/pub/ QNetworkRequest request(url); m_reply m_manager-get(request); connect(m_reply, QNetworkReply::finished, this, [this]() { QString listing QString::fromUtf8(m_reply-readAll()); // Unix 风格示例: -rw-r--r-- 1 owner group 1024 Jan 1 12:00 file.bin QRegularExpression re(R(^([\-dl])\S\s\d\s\S\s\S\s(\d)\s\S\s\S\s\S\s(.)$)); auto it re.globalMatch(listing); while (it.hasNext()) { auto match it.next(); QString name match.captured(3).trimmed(); qint64 size match.captured(2).toLongLong(); bool isDir match.captured(1) d; qDebug() (isDir ? [DIR] : [FILE]) name size; } m_reply-deleteLater(); }); }这个正则只覆盖 Unix 风格。Windows IIS 的 FTP LIST 返回格式完全不同类似01-01-24 12:00PM DIR folder。实际项目里我一般会先请求一次看返回文本的特征再决定用哪套解析规则或者干脆让服务器管理员统一格式。解析目录这件事没有银弹别指望一个正则打天下。3.3 删除、重命名与超时控制删除用deleteResource重命名 QNetworkAccessManager 没有直接封装得用sendCustomRequest发 RNFR/RNTO比较麻烦一般不建议在它上面做。超时控制是必须补的。QNetworkAccessManager 没有 FTP 超时接口要自己挂 QTimer超时后调用m_reply-abort()。QTimer::singleShot(30000, this, [this]() { if (m_reply m_reply-isRunning()) { m_reply-abort(); // 30 秒没完成就掐断 qWarning() FTP 操作超时; } });30 秒是个经验值局域网可以缩到 10 秒跨公网传大文件要按文件大小和带宽估算别一刀切。abort 之后 reply 会触发 finished错误码是 OperationCanceledError收尾逻辑里要能识别这种情况别当成正常完成。4. 避坑与排查那些让我加班到凌晨的 FTP 问题4.1 上传成功但文件大小是 0现象reply 返回 NoError服务器上文件也创建了但大小是 0 字节。原因put 传的是 QIODevice如果这个设备在 put 调用时读位置不在开头或者设备已经被关闭QNetworkAccessManager 读不到数据就会创建一个空文件。另一个常见原因是 QFile 以 WriteOnly 打开却拿来上传。解决上传前确认m_file-isOpen()且m_file-pos() 0必要时seek(0)。打开模式必须是 ReadOnly。上传完成后在 finished 里检查m_reply-error()和服务器返回的 226 状态别只看有没有崩。4.2 中文文件名乱码或路径 550现象本地文件叫「测试.bin」上传后服务器上变成乱码或者直接返回 550 找不到路径。原因FTP 协议本身对文件名编码没有强制规定服务器可能用 GBKQt 默认用 UTF-8 拼 URL。编码不一致就乱码。路径 550 往往是 URL 里的中文没有正确百分号编码。解决用QUrl::toPercentEncoding对路径段单独编码或者跟服务器管理员确认编码在 URL 里显式用QUrl::fromEncoded。如果服务器是 GBKQt 这边没有直接支持得自己转码后再拼 URL。这个问题没有通用解只能按服务器实际情况调。4.3 大文件传到一半内存暴涨现象上传一个 500MB 的文件进程内存一路涨到 500MB 以上。原因用了put(request, QByteArray)的重载整个文件被读进内存。或者虽然传了 QIODevice但 readyRead 里做了不当的缓存累积。解决一律用 QIODevice 重载让 Qt 按需读取。下载侧同理readyRead 里readAll()后立刻写文件并释放不要往一个全局 QByteArray 里 append。我见过有人在 readyRead 里m_buffer m_reply-readAll()最后再一次性写大文件必炸。4.4 被动模式端口被防火墙拦现象登录成功LIST 也能返回但一传数据就卡死最后超时。原因FTP 被动模式下服务器会开一个随机高端口等客户端连。如果客户端和服务器之间有防火墙这个高端口没放行数据连接就建不起来。控制连接21 端口是通的所以登录和命令都正常只有数据传输失败。解决让服务器管理员把被动模式端口范围固定下来比如 50000-50100然后在防火墙上放行这个范围。客户端这边 QNetworkAccessManager 没法指定被动模式端口只能靠服务器侧配置。这是 FTP 协议的老毛病跟 Qt 无关。4.5 多线程里用同一个 manager 导致 reply 错乱现象两个线程同时用同一个 QNetworkAccessManager 发请求回调串了A 的 finished 里拿到 B 的数据。原因QNetworkAccessManager 不是线程安全的它绑定在创建它的线程事件循环上。跨线程调用会导致信号槽队列混乱。解决每个线程创建自己的 QNetworkAccessManager或者干脆把 FTP 操作集中到一个工作线程用信号槽跟主线程通信。我一般会在工作线程里 new 一个 manager所有 FTP 请求都走这个线程的事件循环主线程只发指令、收进度。5. 进阶分片续传、批量队列与一个验证技巧断点续传是 QNetworkAccessManager 做 FTP 最大的短板但可以用 Range 头绕过。FTP 的 REST 命令对应 HTTP 的 RangeQNetworkRequest 里设setRawHeader(Range, bytes1024-)部分服务器认。下载时先QFile::size()拿到已下载字节数拼进 Range然后以 Append 模式打开文件继续写。上传侧更麻烦FTP 的 APPE 命令 QNetworkAccessManager 没有直接暴露得用sendCustomRequest(request, APPE, device)而且不是所有服务器都支持。我的建议是超过 100MB 的文件别用 QNetworkAccessManager 做续传换 libcurl省下来的调试时间够你写完整个上传模块。批量队列倒是不难。维护一个 QQueue 存待传文件路径一个 reply 的 finished 里 pop 下一个继续传同时限制并发数为 1 或 2。并发数别开太高FTP 服务器对同时连接数通常有限制开多了会被踢。验证传输完整性有个实用技巧上传完成后不要只信 reply 的 NoError再发一次 get 请求把文件大小读回来跟本地QFileInfo::size()比对。如果服务器支持 MD5 命令很多不支持可以进一步校验哈希。我一般会在上传后自动做一次大小比对不一致就重传一次这个后悔药救过我好几次。// 上传后校验大小的片段 void FtpUploader::verifyRemoteSize(const QString ftpUrl, qint64 localSize) { QNetworkRequest request{QUrl(ftpUrl)}; auto *reply m_manager-get(request); connect(reply, QNetworkReply::finished, this, [reply, localSize]() { // 通过 HEAD 或列目录拿远端大小这里简化处理 qint64 remoteSize reply-header(QNetworkRequest::ContentLengthHeader).toLongLong(); if (remoteSize ! localSize) { qWarning() 大小不一致本地 localSize 远端 remoteSize; } reply-deleteLater(); }); }最后说个习惯。每次接 FTP 相关的活我会先在一台干净的测试服务器上把上传、下载、列目录、删文件、超时、断网这六种情况各跑一遍把每种情况的 reply-error() 和 errorString() 记下来做成一张对照表贴在工位上。真到现场出问题对着表一看就知道是认证失败、路径不存在还是数据连接被拦比盲猜快得多。这套流程帮我省下的排查时间远比写代码本身多。希望帮到你。本文还有配套的精品资源点击获取