
简介面向需要在Qt应用中实现FTP文件传输的开发者这份示例工程演示了如何调用Qt自带的FTP模块完成从登录到批量上传的完整流程。项目以本机搭建的FTP服务器为连接对象覆盖登录验证、选择本地文件夹批量上传、获取服务端全部文件列表以及实时显示每个文件的上传进度等关键环节适合希望快速上手Qt网络编程的中初级开发者参考。整套资源共包含46个文件压缩包大小约1.69MB文件类型以C源码、Qt工程配置和界面文件为主兼顾可直接运行的演示程序与截图方便在阅读代码的同时对照实际运行效果。目前已有2414人学习下载适合需要借助可运行示例理解FTP交互细节的读者。通过梳理工程内各模块的调用关系不仅能掌握登录、上传、列表、进度反馈这些常用接口的写法还能根据QFtp提供的同类接口举一反三迁移到目录下载、断点续传等更多FTP应用场景。1. 当文件数量上到几百个QFileDialog 逐个选文件就成了笑话QFtp 批量上传的开局与选型批量文件上传这种需求凡是拿 Qt 写过工具的人迟早会撞上。点开界面、一次选几十个文件、点上传听起来不复杂但真到实现层面你会发现 Qt 自带的网络模块里处理 FTP 场景最顺手的仍然是 QFtp 这套老牌 API。它从 Qt4 一路服役到 Qt5 初期被标记废弃但大批存量项目和内部工具至今还在用它原因不外乎三点接口直白、信号槽齐全、对目录遍历和断点续传的支持比你自己拼 QNetworkAccessManager 要省太多事。这篇文章打算顺着一个场景讲——本地一个文件夹里散着几百个文件子目录嵌套三层目标是全部扔到 FTP 服务器对应路径下怎么用 QFtp 把这件事做成一个能交付、敢让同事双击打开就能用的工具。适合读这篇文章的人很明确正在维护 Qt4/Qt5 老项目的客户端工程师或者需要在内部工具里快速实现「选中目录→自动递归上传」的桌面端开发者。看完你会拿到一套可运行的批量上传方案包括登录、目录穿透、队列控制、失败重试以及几个光靠读文档根本躲不开的坑。先说结论QFtp 的 API 不复杂但批量上传的复杂度全在队列编排和信号时序上这两点才是本文真正要解决的东西。2. 先把环境砸实QFtp 的获取方式与最小连接代码2.1 Qt5 里怎么把已经废弃的 QFtp 拉回来用QFtp 在 Qt4 是默认模块到了 Qt5官方把它从核心模块里移了出去代码挪到 qtftp 这个独立仓库里。但好消息是它仍然是开源的而且在新版编译器下能编过。我一般习惯的做法是直接下载 qtftp 源码把它作为一个 pri 文件挂到自己的工程里而不是去改 Qt 的安装目录这样换机器编译不用动环境。# 工程文件里挂载 qtftp 源码以 pri 方式引入 include($$PWD/3rdparty/qtftp/src/qftp.pri) QT network CONFIG c11 SOURCES main.cpp \ uploadworker.cpp HEADERS uploadworker.h这段配置里include会把 qftp.pri 里定义的源文件qftp.cpp、qurlinfo.cpp 等直接编进你的工程省掉单独编译库的步骤。QT network是必须的QFtp 底层依赖 QtNetwork 的套接字和地址解析。如果你是在 Qt4 环境里开发那不需要这一步直接#include QFtp就能用。2.2 登录连接的十行代码connectToHost、login 与状态机的概念QFtp 是异步模型几乎所有命令执行完都通过信号返回结果这跟同步阻塞一套 connect 到底的写法完全不同。第一次用的人最容易栽在「发完命令立刻拿数据」的惯性思维上。#include QFtp QFtp *ftp new QFtp(this); // 连接服务器信号槽驱动后续流程 ftp-connectToHost(192.168.1.100, 21); ftp-login(ftpuser, ftpPass123); connect(ftp, QFtp::commandFinished, this, UploadWorker::onCommandFinished); connect(ftp, QFtp::listInfo, this, UploadWorker::onListInfo);connectToHost的第二个参数是端口默认 21但如果服务器开在别的端口上必须显式传。login如果不带用户名密码QFtp 会按匿名登录处理很多内网 FTP 是允许匿名的但为了权限可控我建议一律显式传账号。commandFinished是这批逻辑的主动脉它会在每个命令结束时触发一次参数里带上命令 ID 和错误码后面批量上传的队列就是靠它一格一格往前推。有一点要提前建立认知QFtp 同一时间只能跑一个命令。它内部确实有命令排队机制但队列是串行的上一条没结束下一条不会发出。这不是缺陷反而是做批量上传时最省心的保证——你不需要考虑并发同步的问题。3. 单文件上传的完整闭环put() 的参数陷阱与进度信号3.1 put 的四种常见调用形态批量上传本质上就是把单个 put() 封装成一个可控的任务单元。所以先把单文件的路子走通再谈循环和队列。QFtp 的 put 接口有两个重载一个接收 QIODevice 指针一个接收本地文件名后者内部会帮你打开文件省去文件句柄管理。// 从本地路径直接上传QFtp 内部负责打开和读取 qint64 id ftp-put(localFilePath, remoteFilePath); // 或者用 QFile 设备自己控制文件生命周期 QFile *file new QFile(localFilePath); file-open(QIODevice::ReadOnly); qint64 id2 ftp-put(file, remoteFilePath);第一种写法最省事适合文件数量多、单个文件不算太大的场景。第二种写法适合你需要在上传前对文件做处理比如加密、加头的场景因为你拿到的是一个已经打开的 QIODevice 接口塞什么进去它都认。put()的返回值是当前命令的 ID这个 ID 会随 commandFinished 信号一起回来是你在回调里识别「哪条命令结束了」的唯一凭证。关于远端路径的写法一个容易忽略的点是QFtp 内部会用cd()语义来解析远端路径。如果你传的远端路径是upload/2025/report.pdf它会自动逐级进入子目录。但前提是这些目录在服务器上已经存在QFtp 不会帮你创建目录。3.2 进度信号要这样接dataTransferProgress 的三个参数进度条是上传工具的脸面QFtp 专门提供了 dataTransferProgress 信号但它的行为跟直觉略有出入。connect(ftp, QFtp::dataTransferProgress, this, [](qint64 done, qint64 total) { // done 是本次命令已传输字节数total 是本次要传的总字节数 int percent (total 0) ? 0 : int(done * 100 / total); qDebug() transfer: percent %; });参数解释done是从当前传输命令开始累计的字节数total是本次命令需要的总字节数。这里有一个经典误解total 并非文件总大小而是「本次命令要传的数据量」在断点续传场景下它会等于剩余字节数而不是文件完整大小。所以在实现进度条时不要拿 total 和 QFileInfo::size() 做比较去判断文件有没有传完要以commandFinished为准。数据转移信号里出现 done 等于 total 不意味着上传结束只意味着当前底层传输任务转完了。4. 批量上传的实现骨架递归遍历目录与串行任务队列4.1 先把本地目录结构拍平QDirIterator 递归收集文件清单批量上传的第一步不是连服务器而是先把本地要传的文件清单完整地捞出来。这一步如果等上传到一半再去补文件队列逻辑就会变得非常脏。我习惯先用 QDirIterator 把目录下所有文件一次性收进一个列表连同相对路径一起记下来。#include QDirIterator QVectorQPairQString, QString collectFiles(const QString rootDir) { QVectorQPairQString, QString result; QDirIterator it(rootDir, QDir::Files | QDir::NoSymLinks, QDirIterator::Subdirectories); while (it.hasNext()) { it.next(); QFileInfo info it.fileInfo(); QString localPath info.absoluteFilePath(); QString remotePath info.absoluteFilePath(); // 关键把本地前缀裁掉保留相对路径 remotePath.remove(0, QDir(rootDir).absolutePath().length() 1); // 统一分隔符为 / 以便 FTP 使用 remotePath.replace(\\, /); result.append({localPath, remotePath}); } return result; }QDirIterator的第三个参数传入QDirIterator::Subdirectories就开启了子树递归。收集时有两个细节值得记住第一remotePath必须以相对路径存储不要带本地盘符或绝对路径前缀否则传到服务器上目录结构会莫名其妙多出几层第二Windows 下路径分隔符要统一替换成/FTP 协议里目录分隔符就这一种。QDir::Files会过滤掉目录条目所以收集结果里不会混入文件夹。但这里有个隐性问题服务器上对应的目录层级可能不存在你后面需要先 mkdir 再 put。4.2 任务队列的两种实现信号槽链式推进与 while 循环陷阱队列推进方式有两种主流做法先说第一种信号槽链式推进——每次 commandFinished 信号触发时检查当前有没有待上传文件有就继续发命令没有就进入收尾。这套机制天然契合 QFtp 的串行命令模型实现起来最稳。// 成员变量中记录待上传列表 QVectorQPairQString, QString m_pending; int m_uploadedCount 0; void UploadWorker::onCommandFinished(int id, bool error) { Q_UNUSED(id); if (error) { qCritical() 命令失败停止上传; emit uploadFailed(); return; } if (m_pending.isEmpty()) { if (m_uploadedCount 0) { emit uploadFinished(m_uploadedCount); } else { emit uploadFailed(); } return; } auto next m_pending.takeFirst(); // 按目录层级逐层创建远端目录 ensureRemoteDir(QFileInfo(next.second).path()); // 发出上传命令 m_ftp-put(next.first, next.second); }这个回调里最容易被忽略的判断是m_pending为空时要区分「全部成功」和「刚开始就失败」两种状态。如果不加m_uploadedCount的判空一个空文件夹被拖进来时也会走 emit uploadFinished看起来功能正常但调用方根本无法分辨「没东西可传」和「传完了」。信号槽链式推进的优势在于不需要复杂的循环跳转每次回调天然就是队列的下一格。第二种做法是新手容易踩进去的 while 写法——在一个槽函数里连续调用 put 多次。这种写法在批量文件数量少时能跑文件一多就出问题QFtp 的命令队列内部有缓冲区限制塞太快会丢命令、乱序。我建议一律走信号槽推进宁可慢一点不要把命令一股脑灌给 QFtp 内核。4.3 远端目录穿透mkdir 的返回码与 QFtp::UnknownError目录不一致是批量上传里最烦人的拦路虎。本地有localA/2025/1月/report.pdf服务器上只有upload/此时直接 put 上去 QFtp 会报 UnknownError。要确保远端目录存在就得在 put 之前逐级 mkdir。void UploadWorker::ensureRemoteDir(const QString remoteDirPath) { // 把路径拆成各级逐层检查并创建 QStringList parts remoteDirPath.split(/, Qt::SkipEmptyParts); QString current; for (const QString part : parts) { current / part; // 尝试进入目录——进入失败说明不存在需要创建 int id m_ftp-mkdir(current); m_pendingMkdirs.append({id, current}); } }但这里有个信号时序问题mkdir 也是异步命令你调用 ensureRemoteDir 之后立刻调 putput 会比 mkdir 先执行或穿插执行导致目录还是不存在。解决思路是把 mkdir 也纳入命令队列串行化——确保 put 之前它依赖的那条 mkdir 已经收到了 commandFinished 且无错误。我常用的做法是mkdirm 之后先不立即 put等到那批 mkdir 命令都跑完再在最后一个 mkdir 的 commandFinished 里发出第一个 put 命令。简单的标志位就能做到比硬等要干净也比在循环里疯狂 sleep 要稳妥。记住一个原则批量上传的任何命令时序都不要靠 sleep 对齐全部交给 commandFinished 推进否则在慢速网络上必坑。5. 批量上传避坑记QFtp 在真实项目中的五个翻车点与对策5.1 被动模式还是主动模式连上了却传不动文件的玄学根源QFtp 内部默认使用主动模式PORT但很多 NAT 环境和企业防火墙下主动模式的回连端口会被拦死表现是登录成功、列目录正常一旦开始 put 就卡住超时。对治手段是切换到被动模式PASVQFtp 没有直接暴露 PASV 开关但可以通过调用ftp-rawCommand(PASV)来切换。// 登录成功后切换被动模式 ftp-login(user, pass); ftp-rawCommand(PASV);注意rawCommand也是异步命令需要等它的 commandFinished 触发后再执行上传。被动模式也不是万能的——某些 FTP 服务器上 PASV 的端口范围受限遇到连不上要从服务器端放行端口段。这个坑在当前 Kubernetes 化部署的内网 FTP 服务上尤其常见建议把 FTP 服务暴露成 NodePort 时同步放行被动模式端口段否则所有客户端都会卡在「能列目录、传不动文件」这个诡异状态。5.2 中文文件名和路径QString 编码与服务器字符集不一致QFtp 默认按 UTF-8 编码发送路径但很多 Windows 下的 FTP 服务器尤其是用 IIS 搭的默认用 GBK 解析路径。表现为中文文件名上传成功后变成乱码或者在 listInfo 后对比本地文件名时对不上。// 如果服务器是 GBK需要在上传前做一次编码转换 QByteArray remotePathGb QString::fromUtf8(remotePath.toUtf8()) .toLocal8Bit();但这里有个更实用的经验不要尝试在代码里硬编码转换成服务器字符集因为不同服务器的默认字符集不一样。正确的做法是在界面里做配置项提供 Automatic / UTF-8 / GBK 三种选项让使用者自行选择。碰到乱码就让用户手动切到 GBK 再试这样你的工具适配性会宽很多不用在代码里猜服务器环境。5.3 commandFinished 丢 ID长任务中断后重复上传put 一个很大的文件时底层网络闪断导致命令失败commandFinished 会携带 errortrue 返回。此时如果队列推进逻辑直接 takeFirst 继续上传下一个文件那个大文件就会永远缺席。正确的处理是在 put 失败时不要丢弃这个任务把它重新插回待上传队列头再做重试计数。void UploadWorker::onCommandFinished(int id, bool error) { if (error) { // 重试计数同一文件失败超过3次才放弃 if (m_retryCount 3) { m_retryCount; // 把失败任务放回队列头 m_pending.prepend(m_currentTask); emit uploadRetrying(m_currentTask.first, m_retryCount); } else { m_failedList.append(m_currentTask); emit uploadFailedFile(m_currentTask.first); } } }prepend是关键重新放回头部而不是尾部避免失败任务被无限延后。重试次数上限设为 3 比较合理再多就说明服务器或网络存在更严重的问题不应让工具卡死在某个文件上。重试之间建议加 1~2 秒延时给网络留恢复时间使用QTimer::singleShot常见做法如下QTimer::singleShot(1500, this, [this]() { m_ftp-put(m_currentTask.first, m_currentTask.second); });5.4 空目录和仅含子目录的目录QDirIterator 不会收集到目录条目QDirIterator 设置QDir::Files后空的子目录不会被收录。如果你需要保留空目录结构比如项目里有个空的 logs 目录就必须先手动遍历目录树去建立远端目录列表。实现办法把QDir::Files换成QDir::Dirs | QDir::NoDotAndDotDot再走一遍收集对每个目录执行 mkdir再对文件执行 put。如果不做这一步最终服务器上的目录结构和本地会不一致——文件都在但空目录消失。QDirIterator dirIt(rootDir, QDir::Dirs | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (dirIt.hasNext()) { dirIt.next(); QString remoteDir dirIt.fileInfo().absoluteFilePath(); remoteDir.remove(0, rootDir.length() 1); m_dirList.append(remoteDir.replace(\\, /)); }5.5 大文件中断续传QFtp 没有直接支持如何用底层接口兜底QFtp 的 put 接口不直接支持断点续传文件传了一半断了要重头开始。做工具时可以接受这个限制但如果你遇到的是几十 GB 的日志包或数据库备份就必须做断点续传。常见做法是先用QFtp::list()查远端文件大小然后手工构造 REST 命令定位偏移量再用 QFile::seek 跳过本地已传字节数最后用 put(QFile*) 从当前位置上传。这一套是底层的功夫活但确实能实现我在第六部分会展开讲一个方案。6. 断点续传与路径映射QFtp 批量上传工具的最后一公里6.1 断点续传的设三个判别条件远端不存在、远端比本地小、远端与本地相等在批量上传的工具里续传判定需要前置条件。每次上传前先发一list命令查远端文件是否存在及大小然后按三种情况分别处理远端状态本地状态处理方式不存在任意完整上传存在大小小于本地大文件断点续传存在大小等于本地任意跳过qFtp 库自身不带这个逻辑需要你在命令队列里增加一个「检查阶段」。通常实现是利用listInfo信号去比对文件大小然后决定后续走 skip 还是从头传还是续传。这个能力对批量上传工具的价值很大——如果前一次传到一半崩了重新运行工具时能瞬间跳过几百个已完成文件。6.2 续传命令组合REST put(QIODevice*) 实现从断点接着传qint64 remoteSize ...; // 从 listInfo 获取 QFile *file new QFile(localPath); file-open(QIODevice::ReadOnly); file-seek(remoteSize); // 跳过已传部分 // 发送 REST 指令告诉服务器接下来从偏移量接收 ftp-rawCommand(QString(REST %1).arg(remoteSize).toLatin1()); // 紧接 rawCommand 完成后再执行 put这段逻辑最折腾的点在于REST是 FTP 协议指令它需要在 put 命令之前立刻发送并且要等rawCommand返回完成信号后紧接着推 put。因为 QFtp 串行命令队列的特性只要你把 REST 放在 put 前调用队列天然保证顺序。真正要小心的是两个细节一是 QFtp 在调用 put(QIODevice*) 时不关心设备当前的读取偏移它会从头读所以你必须手动seek(remoteSize)二是REST命令不是所有 FTP 服务器都支持遇到老的服务器会返回 502那时只能回退到整体重传。6.3 路径映射规则本地盘符、中文目录、分隔符统一处理的配置化技巧批量上传对路径映射必须提供配置口子这个口子是整个工具在上手体验和后期维护上最关键的设计。配置项常见为[upload] local_rootD:/project_data/export remote_root/upload/data/2025 filter_ext.jpg;.png;.pdf skip_existingtrue charsetautolocal_root与remote_root的映射关系是剔除本地前缀、拼接远端前缀。这幅映射在代码里就是一个字符串拼接函数远不是难点真正难的是用户把local_root填错、填成D:/根目录你的采集和上传路径就会瞬间爆炸。所以工具界面里至少要在「开始上传」前给用户展示一行映射预览本地路径样例→远端路径样例。看着对不上号用户自然会去改配置而不是等上传完才发现目录结构乱了。结尾我想说一个已经养成的习惯QFtp 批量上传工具上线前一定要拿三组数据做验证——单文件小文件、单文件大文件超过 2GB、上百文件的嵌套目录。每组数据跑两轮一轮全量传一轮传一半断掉再重跑验证续传和跳过能力。这三组能过这个工具才敢说能交付。路径映射错乱这类问题不是代码崩了才叫 bug是「传完了才发现传错了地方」那时候的后悔药约等于没有。希望这篇拆解能帮你少走这些弯路。本文还有配套的精品资源点击获取