简介:面向需要在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 和 QTcpSocket,QFtp 头文件并不存在,原因是它经历了一条完整的"弃用—移出—独立维护"路径:
| Qt 版本 | QFtp 状态 | 使用方式 |
|---|---|---|
| Qt 4 | QtNetwork 模块内置 | #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.cpp;CMake 则依赖AUTOMOC,配置不对会直接卡在链接阶段报一堆vtable for QFtp相关错误。下面两种工程写法都经过实际编译验证。
2.2 qmake 工程:SOURCES 写全,QT 加上 network
用 Qt Creator 新建一个纯 C++ 控制台工程,把五个文件拷贝到源码目录,然后在.pro里这样写:
QT += core network CONFIG += c++11 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 的最大特征:每个命令要么成功要么失败,且结果与命令严格一一对应。核心信号有三个:
| 信号 | 参数 | 何时触发 |
|---|---|---|
| stateChanged | int state | 连接状态变化,对应 State 枚举 |
| commandStarted | int id | 某个命令开始执行 |
| commandFinished | int 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 I,QFtp::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::function<int()>; // 返回 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; QQueue<Task> 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 += core5compat(CMake 对应find_package(Qt6 COMPONENTS Core5Compat)并链接Qt6::Core5Compat);想零依赖就手动把qurlinfo.cpp里的 QRegExp 改成 QRegularExpression,注意转义字符规则略有差异,改完跑一次列目录用例验证解析结果。即使当前用 Qt 5,也建议提前把 SkipEmptyParts 改干净,把编译期告警清零。
5.5 中文文件名全变乱码:FTP 控制连接的编码潜规则
现象:listInfo返回的QUrlInfo::name()是"é??ä¸è½½.txt"之类的乱码,下载下来文件名完全对不上。
原因:QFtp 解析控制连接字节流时按 Latin-1(8 位编码)转成 QString,这是老 FTP 协议的默认行为。服务器把中文名按 UTF-8 或 GBK 编码的原始字节发过来,直接按 Latin-1 解码自然全乱。
解决:先确认服务器编码。支持 UTF-8 的服务器(RFC 2640,现代服务器基本都支持)可以在连接后先发一条原始命令开启 UTF-8:
ftp.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 加新功能,都强制先把"命令谁发、设备谁开、失败谁收尾"三行写在注释里,再动手写代码。希望帮到你。
本文还有配套的精品资源,点击获取