资讯详情 QFtp老工程实战:源码集成、API使用、任务链封装与避坑
📅 2026/10/21 6:33:53
简介面向需要在Qt 5.x及以上版本中使用QFtp类的开发者资源从Qt 4.8.7中提取QFtp源码并封装为QFtp.h和QFtp.cpp配套完整可运行的测试工程。解决了Qt 5移除QFtp后FTP客户端功能难以直接复用的问题适合在迁移旧项目或需要纯Qt实现FTP上传、下载、删除、重命名等操作的场景中使用。压缩包共17个文件以cpp、h源码为主包含5个C源文件、4个头文件另有3个界面ui文件、2个png图标、1个qrc资源文件、1个pro工程文件和1个user配置文件整体仅35KB轻量易读。目前已有1318人学习下载。通过该示例读者可快速掌握QFtp类的提取方法直接引用QFtp.h和QFtp.cpp到自己的项目中同时参考测试例子的界面交互与逻辑写法实现登录FTP服务器、上传文件、删除文件、创建目录、重命名文件等完整功能节省自行封装网络协议的时间。1. QFtp被 Qt 官方移出却在老工程里继续服役的 FTP 客户端如果你维护过 2015 年以前的 Qt 工程一定见过 QFtp一个不需要任何第三方库、new 出来就能连 FTP 服务器传文件的客户端。Qt 5 之后官方把它从核心模块移出不再随 Qt 一起发布但大量嵌入式界面、Windows 小工具、内网同步脚本至今还在用它——因为 QFtp 的命令 id 机制、主动/被动模式切换和 rawCommand 原始命令比 QNetworkAccessManager 更贴近 FTP 协议本身。这份资源把 QFtp 完整整理成一套可直接编译的源码包适合维护老工程的开发者也适合要做断点续传或细粒度控制传输流程的人。下面按工程集成、API 实战、任务链封装和避坑的顺序讲最后补三个实际用过的进阶技巧。2. 源码级接入qmake 与 CMake 两种集成 QFtp 的方式2.1 为什么只能源码集成QFtp 的版本漂流史目前 Qt 里的网络模块只有 QNetworkAccessManager 和 QTcpSocketQFtp 头文件并不存在原因是它经历了一条完整的弃用—移出—独立维护路径Qt 版本QFtp 状态使用方式Qt 4QtNetwork 模块内置#include QFtp直接编译Qt 5.0 起从 qtbase 移除官方单独维护分支需要自行下载源码Qt 6仍然不在官方模块列表只能源码级集成所谓官方单独维护本质也是把源码仓库里的几个.h/.cpp文件拷出来塞进你的工程。这套源码构成非常固定核心就是五个文件qftp.h、qftp.cpp是主类qurlinfo.h、qurlinfo.cpp是目录条目数据类qftp_p.h是私有实现头。其中qftp.cpp里有一行#include qftp_p.h私有头一旦缺失编译到一半会莫名其妙报找不到文件这是最常见的接入失败点后面排坑章会单独讲。还有一个容易被忽略的细节QFtp 类声明了Q_OBJECT需要 moc 预处理。qmake 把qftp.cpp写进SOURCES就会自动生成moc_qftp.cppCMake 则依赖AUTOMOC配置不对会直接卡在链接阶段报一堆vtable for QFtp相关错误。下面两种工程写法都经过实际编译验证。2.2 qmake 工程SOURCES 写全QT 加上 network用 Qt Creator 新建一个纯 C 控制台工程把五个文件拷贝到源码目录然后在.pro里这样写QT core network CONFIG c11 TARGET ftp_demo TEMPLATE app SOURCES main.cpp \ qftp.cpp \ qurlinfo.cpp HEADERS qftp.h \ qurlinfo.h \ qftp_p.h这里的关键是qftp.cpp必须出现在SOURCES里这样 qmake 才会自动处理它内部的Q_OBJECT生成对应的 moc 文件。QT network提供 QFtp 运行依赖的 QTcpSocket 和事件循环基础设施漏掉这一行编译大概率在include QTcpSocket处翻车。qftp_p.h放在HEADERS里是让 Qt Creator 能识别并索引它真正起作用的是它和qftp.cpp处于同一目录#include qftp_p.h才能被找到。2.3 CMake 工程AUTOMOC 和 Qt6 链接库如果工程是 CMake 维护的对应写法如下cmake_minimum_required(VERSION 3.16) project(ftp_demo LANGUAGES CXX) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) find_package(Qt6 REQUIRED COMPONENTS Core Network) add_executable(ftp_demo main.cpp qftp.cpp qurlinfo.cpp ) target_include_directories(ftp_demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_link_libraries(ftp_demo PRIVATE Qt6::Core Qt6::Network)CMAKE_AUTOMOC ON是必须项Open 的 CMake 工程不会像 qmake 一样自动发现 Q_OBJECT。用 Qt5 的话把Qt6替换成Qt5其余不用改。QFtp 只依赖 QtCore 和 QtNetwork不碰 Widgets所以QCoreApplication就能跑适合写成后台服务或者命令行同步工具。编译验证时如果系统里有多个 Qt 版本记得显式指定路径mkdir -p build cd build cmake .. -DCMAKE_PREFIX_PATH/opt/Qt/6.8.0/gcc_64 cmake --build . -j4CMAKE_PREFIX_PATH指向你本机 Qt 安装的根目录编译完成后直接运行生成的ftp_demo没有报错就说明 QFtp 源码已经顺利接入工程。这一步做完后面所有 API 示例都在这套工程里跑。3. 核心 API 实战连接登录、列目录与上传下载的完整流程3.1 先看懂异步模型命令 id 是唯一的对账凭证QFtp 所有方法都是异步的调用后立刻返回一个int类型的命令 id真正的执行结果靠信号通知。这套机制是 QFtp 区别于 QNetworkAccessManager 的最大特征每个命令要么成功要么失败且结果与命令严格一一对应。核心信号有三个信号参数何时触发stateChangedint state连接状态变化对应 State 枚举commandStartedint id某个命令开始执行commandFinishedint id, bool error某个命令执行完毕error 为 true 表示失败State 枚举的值从 0 递增Unconnected、HostLookup、Connecting、Connected、LoggedIn登录成功后状态固定在LoggedIn(值为 4)。写 UI 的人一般拿它控制按钮可用性——状态没到 LoggedIn 时下载按钮就该是灰的。我一般在 main 函数里先做一套全局信号对接把命令结果全部打印出来观察整条链路的执行顺序#include QCoreApplication #include QFtp #include QDebug int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QFtp ftp; QObject::connect(ftp, QFtp::commandFinished, app, [](int id, bool error) { if (error) { qWarning() 命令失败, id id 错误码 ftp.errorCode() 描述 ftp.errorString(); return; } qInfo() 命令完成, id id; }); QObject::connect(ftp, QFtp::stateChanged, app, [](int state) { qInfo() 状态变化 - state; }); ftp.connectToHost(192.168.1.100, 21); ftp.login(ftpuser, passwd_123); ftp.cd(/pub); ftp.list(); return app.exec(); }这段逻辑里最重要的认知是connectToHost、login、cd、list这四行代码连续执行QFtp 内部有一个命令队列会按调用顺序依次执行你不需要手动等 Connected 再去调 login。命令 id 是你在信号回调里识别当前哪条命令结束了的唯一凭证尤其是后面做任务链封装时id 比对必须严格。3.2 连接登录与目录切换参数细节和返回 id 的用途connectToHost第二个参数是端口号FTP 默认 21但内网测试环境经常映射到 2121、1021 之类的端口。login不传参数时默认匿名登录底层发的用户名字符串是anonymous需要账号密码时就传两个参数。这两个方法都返回命令 id建议保存下来int connId ftp.connectToHost(192.168.1.100, 2121); int loginId ftp.login(ftpuser, passwd_123); int cdId ftp.cd(/pub/files);保存 id 的意义在于commandFinished回调里可以精确判断是哪一步出的问题。比如loginId对应的 commandFinished 返回 error说明账号密码不对connId失败则多半是 IP/端口不通。日志里把 id 和命令名做成对照表排障时一眼就能定位不用猜。cd的用法值得多说一句它改变的是 QFtp 内部维护的当前远端目录。之后你调用get、put、list时如果不传完整路径都会基于这个当前目录解析。常见误区是以为cd只影响下一次命令实际上它持续作用于后续所有相关操作直到你再次cd回根目录。3.3 列目录与解析list listInfo 的组合list()触发服务器返回目录条目每个条目通过listInfo信号逐个送过来参数是QUrlInfo对象QObject::connect(ftp, QFtp::listInfo, app, [](const QUrlInfo info) { qInfo() 文件名: info.name() 大小: info.size() 目录: info.isDir() 符号链接: info.isSymLink() 修改时间: info.lastModified(); }); ftp.list(); // 列当前目录 // 或者 ftp.list(/pub/download); // 列指定目录QUrlInfo的字段覆盖了 Unix 文件列表的大部分信息name()是文件名size()是字节数isDir()/isFile()/isSymLink()判断类型还有permissions()权限位和lastModified()时间戳。一个关键限制是list只对目录生效如果对单个文件调用list()大多数服务器不会触发任何listInfo只会返回一个空成功的 commandFinished。3.4 上传下载get / put 的设备生命周期管理get和put的签名一眼看过去有些令人困惑因为第二个参数是QIODevice *dev而不是文件路径。QFtp 不负责打开文件你必须自己把文件准备好。下载到本地文件的典型写法// 下载 /pub/package.zip 到本地 D:/backup/ QFile *local new QFile(D:/backup/package.zip, ftp); if (!local-open(QIODevice::WriteOnly | QIODevice::Truncate)) { qWarning() 本地文件打开失败: local-errorString(); return; } int getId ftp.get(/pub/package.zip, local, QFtp::Binary);这里new QFile(...)的父对象指向ftp是一个关键习惯QFtp 不会替你持有或释放这个设备你必须保证它在整个传输期间存活直到对应 id 的 commandFinished 发出。如果文件对象是栈上的函数一退出设备就被析构传输半路读到空数据甚至直接崩溃。上传是对称的QFile *up new QFile(C:/work/log.txt, ftp); if (!up-open(QIODevice::ReadOnly)) { qWarning() 本地文件打开失败; return; } int putId ftp.put(up, /upload/log.txt, QFtp::Ascii);QFtp::Binary对应 FTP 的 TYPE IQFtp::Ascii对应 TYPE A。文本文件用 Ascii 时服务器会做换行符转换二进制用 Ascii 会损坏文件。拿不准的一律 Binary这是我在真实项目里用坏过文件才记住的教训。get也可以不传设备QFtp 会把数据缓冲在内部通过readyRead信号和readAll()取走适合拉小配置文件QObject::connect(ftp, QFtp::readyRead, app, [ftp]() { QByteArray data ftp.readAll(); qInfo() 收到文本: data; // 记得用 data.size() 做边界判断 }); int id ftp.get(/etc/manifest.txt, nullptr, QFtp::Ascii);4. 任务链封装把 QFtp 的异步命令串成可重用的顺序流程4.1 为什么必须自己排队裸 QFtp 只保证执行不保证业务顺序QFtp 内部队列保证命令按调用顺序发出但业务上有个致命缺口它不会因为前一条失败就跳过后续命令。比如连上 → 登录 → cd → 下载四步如果登录失败QFtp 还是会执行 cd 和 get结果返回一堆无用错误你还要逐个 id 去排查是哪一步断的。而且命令之间的依赖关系完全没有表达——get依赖cd成功cd依赖登录成功这种层级逻辑必须由调用方自己维护。另一个问题是 UI 场景下用户点了下载按钮你要在整条链路结束前禁用按钮、显示进度。裸 QFtp 的信号风暴会让按钮状态管理变成一团乱麻。我做过一个同步工具早期直接连发命令结果失败时界面残留了半截状态后来全部收敛成一个任务链执行器问题才彻底消失。4.2 用 std::function 队列做一个最小顺序执行器核心思路把 QFtp 的每个操作封装成一个返回命令 id 的可调用对象按顺序丢进队列每次只有一条命令在跑commandFinished确认前一条结束后才从队首取下一条。这样业务代码只需要按步骤调用不用关心异步时序#include functional #include QQueue #include QFile #include QFtp class FtpWorker : public QObject { Q_OBJECT public: using Task std::functionint(); // 返回 QFtp 命令 id explicit FtpWorker(QObject *parent nullptr) : QObject(parent), m_ftp(new QFtp(this)), m_runningId(-1) { connect(m_ftp, QFtp::commandFinished, this, FtpWorker::onCommandFinished); connect(m_ftp, QFtp::dataTransferProgress, this, FtpWorker::onTransferProgress); } void connectTo(const QString host, quint16 port 21) { enqueue([this, host, port] { return m_ftp-connectToHost(host, port); }); } void login(const QString user, const QString pass) { enqueue([this, user, pass] { return m_ftp-login(user, pass); }); } void cd(const QString dir) { enqueue([this, dir] { return m_ftp-cd(dir); }); } void download(const QString remote, const QString local) { if (m_file) { qWarning() 上一个传输还没结束; return; } m_file new QFile(local, this); if (!m_file-open(QIODevice::WriteOnly | QIODevice::Truncate)) { qWarning() 本地文件打开失败: local; delete m_file; m_file nullptr; return; } enqueue([this, remote] { return m_ftp-get(remote, m_file, QFtp::Binary); }); } signals: void taskFinished(bool ok); void progress(qint64 done, qint64 total); private slots: void onCommandFinished(int id, bool error) { if (id ! m_runningId) return; // 不是当前任务直接忽略 m_runningId -1; if (error) { emit taskFinished(false); m_tasks.clear(); // 失败就放弃后续所有任务 return; } // 队列空了说明当前这条命令是最后一个任务 if (m_file m_tasks.isEmpty()) { m_file-close(); delete m_file; m_file nullptr; } runNext(); } private: void enqueue(Task task) { m_tasks.enqueue(std::move(task)); runNext(); } void runNext() { if (m_runningId ! -1 || m_tasks.isEmpty()) return; Task task m_tasks.dequeue(); m_runningId task(); // 执行 QFtp 命令拿回 id if (m_runningId -1) runNext(); // 命令没发出去直接走下一个 } QFtp *m_ftp; QQueueTask m_tasks; int m_runningId; QFile *m_file nullptr; };m_runningId是这个执行器的核心状态它标识当前在跑的命令 id信号回调里第一件事就是比对 id不匹配直接 return避免把 QFtp 内部其他命令比如握手阶段的子命令误判为当前任务。enqueue把新任务丢进队尾如果当前空闲就立即触发runNext。onCommandFinished里如果是错误直接清空队列——失败的链路没有必要继续跑下去。业务代码的调用姿势就非常清爽了FtpWorker worker; QObject::connect(worker, FtpWorker::taskFinished, [](bool ok) { qInfo() (ok ? 整条任务链成功 : 任务链失败后续已取消); }); worker.connectTo(192.168.1.100, 21); worker.login(ftpuser, passwd_123); worker.cd(/pub/download); worker.download(package.zip, D:/backup/package.zip);注意download的本地文件由 worker 持有任务链走完才关闭释放。这样就不会出现函数退出后文件指针悬空的经典翻车现场。4.3 超时保护用看门狗兜底卡死的连接FTP 在服务器异常、网络黑洞的情况下commandFinished可能永远不来。裸 QFtp 没有内置超时所以任务链执行器里需要配一个 QTimer 看门狗#include QTimer // 在 FtpWorker 构造函数里 m_watchdog new QTimer(this); m_watchdog-setSingleShot(true); int timeoutMs 15 * 1000; connect(m_watchdog, QTimer::timeout, this, [this] { if (m_runningId -1) return; // 当前空闲不需要处理 qWarning() 命令超时, 中止 id m_runningId; m_ftp-abort(m_runningId); m_runningId -1; m_tasks.clear(); emit taskFinished(false); }); // 每条命令发出时启动 m_watchdog-start(timeoutMs); // 在 onCommandFinished 里停止 m_watchdog-stop();超时值的设置有讲究大文件传输时dataTransferProgress会持续触发但commandFinished只有传输完全结束才发。如果只看完成信号5 GB 文件传 30 分钟15 秒的看门狗必然误杀。正确做法是把看门狗设计成任何相关信号到来就重置也就是把dataTransferProgress也接上去重新start(timeoutMs)或者对传输类命令单独放宽到足够覆盖文件大小的时长。我一般对是否仍在活动和总时长上限分开处理前者看进度信号后者卡死极限。5. 排坑手册QFtp 编译失败、传输挂起与中文乱码的五个现场5.1 include 直接翻车Qt5/6 里没有这个头文件现象qftp.h: No such file or directory或者链接阶段报undefined reference toQFtp::xxx 一大串。原因Qt 5 起 QFtp 已从 qtbase 移出官方发行版不再安装对应的头文件和库。Qt Creator 里创建工程时勾选 Qt Network 模块不会带入 QFtp。解决走第 2 章的源码集成路线把qftp.h/qftp.cpp/qurlinfo.h/qurlinfo.cpp/qftp_p.h五个文件拷进工程目录编进 SOURCES。这一条解决后环境差异就彻底从问题清单里消失了。5.2 私有头缺失或导出宏报错qftp_p.h 与 Q_AUTOTEST_EXPORT现象编译报qftp_p.h找不到或者Q_AUTOTEST_EXPORT未定义导致链接失败。原因qftp.cpp内部依赖私有实现类QFtpPrivate源码包如果只给了公开接口文件#include qftp_p.h必然扑空。Q_AUTOTEST_EXPORT是 Qt 内部用于标记导出符号的宏脱离 Qt 源码构建环境后可能没有定义。解决从官方维护分支把qftp_p.h一并拷出放与qftp.cpp同目录。若仍报宏未定义在工程层面手动置空# qmake 写法 DEFINES Q_AUTOTEST_EXPORT# CMake 写法 target_compile_definitions(ftp_demo PRIVATE Q_AUTOTEST_EXPORT)实际经验是把五个文件拷齐后绝大多数环境不需要动宏这一项只有用 Qt 6 源码树里的特殊构建配置才会碰到。5.3 传输永远停在 0%主被动模式选错现象登录、列目录都正常一执行get或put进度信号迟迟不来或者一直报 0/N最后超时中断。原因FTP 数据传输需要额外一条数据连接。被动模式PASV/EPSV是客户端主动连服务器开出的端口适合客户端在 NAT 后面的场景主动模式PORT是服务器回连客户端要求客户端有公网可达的端口。服务器限制、客户端防火墙策略、中间设备拦截任何一个环节不对数据连接就建不起来。解决传输前显式指定模式不要依赖默认值ftp.setTransferMode(QFtp::Passive); // 大多数内网环境首选 // 或 ftp.setTransferMode(QFtp::Active); // 服务器只开主动模式时setTransferMode只对后续的get/put生效所以把它放在每次传输前调用最安全。如果切换后仍然卡 0%抓包看 PASV 返回的地址端口和实际建连目标是否一致——NAT 网关没做端口转发时服务器返回的往往是内网地址客户端自然连不进去。这种情况是网络环境问题QFtp 层面无解只能换服务器或者做转发。5.4 Qt 6 编译报错SkipEmptyParts 与 QRegExp 的退场现象Qt 6 下编译qftp.cpp或qurlinfo.cpp报SkipEmptyParts is not a member of QString或者QRegExp未声明。原因Qt 5.14 开始把QString::SkipEmptyParts迁移到Qt::SkipEmptyParts旧写法标记为弃用Qt 6 则直接把 QRegExp 移出 QtCore改放 Qt5Compat 模块。解决文本替换即可。QString::SkipEmptyParts全部改为Qt::SkipEmptyParts。QRegExp 有两条路愿意引入兼容模块就加QT core5compatCMake 对应find_package(Qt6 COMPONENTS Core5Compat)并链接Qt6::Core5Compat想零依赖就手动把qurlinfo.cpp里的 QRegExp 改成 QRegularExpression注意转义字符规则略有差异改完跑一次列目录用例验证解析结果。即使当前用 Qt 5也建议提前把 SkipEmptyParts 改干净把编译期告警清零。5.5 中文文件名全变乱码FTP 控制连接的编码潜规则现象listInfo返回的QUrlInfo::name()是é??ä¸è½½.txt之类的乱码下载下来文件名完全对不上。原因QFtp 解析控制连接字节流时按 Latin-18 位编码转成 QString这是老 FTP 协议的默认行为。服务器把中文名按 UTF-8 或 GBK 编码的原始字节发过来直接按 Latin-1 解码自然全乱。解决先确认服务器编码。支持 UTF-8 的服务器RFC 2640现代服务器基本都支持可以在连接后先发一条原始命令开启 UTF-8ftp.rawCommand(OPTS UTF-8 ON);如果服务器明确返回 200 支持那么乱码名的修复思路是把 Latin-1 解码结果重新转回字节再按 UTF-8 解一次QString fixUtf8Name(const QString mojibake) { // mojibake 的每个字符都来自原始字节的 Latin-1 映射 // toLatin1() 可以无损还原字节流 return QString::fromUtf8(mojibake.toLatin1()); }老服务器用 GBK/GB18030 时换编码器QTextCodec *gbk QTextCodec::codecForName(GB18030); QString name gbk-toUnicode(info.name().toLatin1());如果不想跟编码搏斗还有一个绕开方案维护一份远端文件名 → 本地文件名的对照表下载时不用name()拼路径。这个坑在中文服务器上最容易翻车建议在正式工具里把编码探测做成可配置项别写死。6. 进阶技巧进度信号、断点续传与设备收尾6.1 进度信号的正确姿势dataTransferProgress(qint64 done, qint64 total)在传输期间持续触发但total有可能是 -1 或 0表示服务器没给文件大小。计算百分比前务必判断QObject::connect(ftp, QFtp::dataTransferProgress, app, [](qint64 done, qint64 total) { if (total 0) { qInfo() 已传输 done 字节总大小未知; return; } qInfo() 进度: done / total QStringLiteral(%1%).arg(done * 100.0 / total, 0, f, 1); });6.2 断点续传REST 与 Append 的组合QFtp 没有专门的断点续传接口但可以借助rawCommand发送 FTP 原始命令REST再配合本地文件追加打开QFile *file new QFile(localPath, this); // Append 保留已下载部分新数据从尾部写入 file-open(QIODevice::WriteOnly | QIODevice::Append); int restId ftp.rawCommand(QString(REST %1).arg(file-size())); int getId ftp.get(remotePath, file, QFtp::Binary);REST 必须紧跟传输命令中间不要插其他操作否则服务器会重置偏移量。续传完成后必须做完整性校验对比本地文件大小与服务端 size 是否一致。我在某客户机器上遇到过忽略 REST 的服务器结果是文件头缺了一段校验不过就整段重传别留半截文件当成功的任务。6.3 收尾谁关闭设备、谁释放指针QFtp 不会替你关闭传入get/put的 QIODevice即使命令被abort对应 id 的 commandFinished 也会触发。所以统一在回调里按 id 收尾void onGetFinished(int id, bool error) { if (id ! m_getId) return; m_file-close(); // 无论成功失败都要关 delete m_file; m_file nullptr; if (error) qWarning() 下载失败: m_ftp-errorString(); }close()同理int closeId ftp.close();之后等 closeId 的 commandFinished 再删除 QFtp 对象。直接 delete 会把未发完的 QUIT 命令截断服务器侧留着半开连接过一段时间端口会被占满。这套 QFtp 源码包连同示例工程已经整理成可直接编译的压缩包按第 2 章的两种工程配置之一接进项目就能跑。这东西我用坏过几个版本最早图省事把 get 的文件指针放在栈上函数一结束设备就被析构传着传着读到的是空数据后来把所有操作收进任务链文件持有者固定为 worker 成员在 commandFinished 里统一释放才再没复现过。从那以后我每次给 QFtp 加新功能都强制先把命令谁发、设备谁开、失败谁收尾三行写在注释里再动手写代码。希望帮到你。本文还有配套的精品资源点击获取