Qt5/6工程集成QFTP源码:命令队列、信号槽与断点续传排坑

发布时间:2026/10/24 3:21:35
Qt5/6工程集成QFTP源码:命令队列、信号槽与断点续传排坑

简介:QFTP 源码是一份面向 C++/Qt 开发者与网络编程学习者的跨平台 FTP 客户端实现,已在 Linux 与 Windows 平台完成测试,可用于理解文件上传下载流程,或作为自定义 FTP 功能的集成基础。压缩包共 4 个文件,由 2 个 .h 头文件和 2 个 .cpp 实现文件组成,体积约 24KB,体量轻便,接口声明与具体逻辑分离,便于对照阅读和二次开发。已有 297 人学习或下载。源码覆盖连接管理、FTP 命令发送与响应处理、数据传输以及 URL 信息解析等关键环节,读者可观察从建立连接到目录列举、文件获取、上传存储等操作的组织方式,并理解响应码判断、异常分支和状态维护思路。对于需要把 FTP 能力嵌入文件管理器、编辑器或自研工具的开发者,这份源码可作为裁剪、扩展和跨平台适配的参考底稿,也有助于积累网络模块与 Qt 通信组件的实践经验。

1. 一份 QFTP 源码,为什么在 Qt 5/6 的工程里还得自己留着

#include <QFtp>编译不过,是很多人第一次碰到这个资源的原因——从 Qt 5 起 FTP 模块不再随主库一起分发,而存量代码里那一串list、get、put、rename又没办法一夜之间换成别的库。这份 QFTP 源码把QFtp和QUrlInfo两个类重新还原成可以直接拖进工程编译的.h/.cpp,并且标注了 Linux 与 Windows 双平台测试通过,这意味着你在两种工具链下编出来的静态库行为是一致的,而不是在一台机器上撞运气。

它适合三类人:手里有 Qt 4 时代的 FTP 逻辑要往新版本迁的;要在嵌入式 Linux 设备和内网 Windows 客户端之间做文件收发的;以及嫌 libcurl 的回调式接口不好接信号槽、想让传输进度直接刷在界面上的。作为软件/插件形态的源码资源,它的价值不在“新”,而在于把一段被官方摘掉的实现完整留了下来。

2. 选型与源码结构:QFtp 到底补的是哪块能力的缺口

先把一件事说清楚:QFtp 不是“另一种网络库”,它是一段缺失的模块实现。你拿到的源码包本质上是四个文件——qftp.h、qftp.cpp、qurlinfo.h、qurlinfo.cpp,前者定义QFtp这个QObject子类和它的命令枚举,后者定义QUrlInfo,用来承载目录列表里每一条记录的名字、大小、权限、修改时间。编译它得到的不是某个可执行程序,而是一个能被主工程链接的静态库,或者干脆把源码内联进工程。

2.1 QFtp 和 QNetworkAccessManager 的职责分界

不少人拿到源码第一反应是“我都用 QNetworkAccessManager 了,还要它干嘛”。这个疑问值得正面回答,因为两者覆盖的 FTP 能力差距很大。QNetworkAccessManager 确实能处理ftp://开头的 URL,但它只做到“按已知完整路径取一个文件”这一层,控制通道上的 LIST、NLST、DELE、MKD、RMD、RNFR/RNTO 全都没有暴露出来。也就是说,你没法用它列目录、没法判断远端文件是否存在、没法在传输前先探一下大小。

QFtp 走的是完整控制通道 + 数据通道的实现路线:控制连接上跑 USER/PASS/CWD/PASV,数据连接上跑 RETR/STOR/LIST,所有命令按队列串行下发。对比一下就清楚了:

能力QFtpQNetworkAccessManagerlibcurl
下载已知路径文件支持支持支持
上传文件支持不支持支持
列目录 / 判存在支持(list + QUrlInfo)不支持支持
删除 / 改名 / 建目录支持不支持支持
进度信号直接接 Qt 槽原生原生(仅下载)需自行封装
事件循环侵入性无(Qt 原生异步)无需自己做 multi 或线程

所以选型判断很直接:只做“下载一个已知 URL 的文件”,QNetworkAccessManager 够用,还能省掉一份源码;只要出现列目录、上传、远端文件管理中的任意一项,就得回到 QFtp 或者 libcurl。而在一个已经用 Qt 写界面的工程里,QFtp 的dataTransferProgress(qint64, qint64)直接连到进度条上就能用,省掉了一层适配代码,这是它相对 libcurl 最实际的优势。

有一点边界要提前认:QFtp 不实现 FTPS(FTP over TLS),也不实现 SFTP。源码里没有 TLS 握手逻辑,connectToHost就是明文控制连接。如果你的对端要求加密,这个资源帮不上忙,别在上面硬改。

2.2 源码包里那几个文件各自承担什么

打开qftp.h,能看到的公开接口大致分四组。第一组是连接管理:connectToHost(host, port)、login(user, password)、close()、abort()、state()。第二组是目录操作:list(path)、cd(dir)、mkdir、rmdir、remove、rename。第三组是传输:get(remote, QIODevice*)、put(QIODevice*, remote, type)。第四组是旁路:rawCommand(cmd)直接往控制连接塞一条原始命令,setTransferMode(QFtp::Active | QFtp::Passive)切换主被动模式。

所有这几组接口都有一个共同点:它们全部返回int,这个返回值是命令 id,不是执行结果。命令被压进内部队列后立刻返回,真正的结果要靠信号回传。这一点是理解整个模块的钥匙——QFtp内部维护一个QQueue<QFtpCommand>,每次调用只是入队,_q_startNextCommand()在前一条命令收到完成应答后才取下一条发出去。

qurlinfo.h里的QUrlInfo则是对 LIST 应答的解析产物。服务端返回的目录列表是一段纯文本,Unix 风格长这样:-rw-r--r-- 1 ftp ftp 10240 Jan 01 12:00 a.bin,QUrlInfo把它拆成name()、size()、lastModified()、isDir()、permissions()。解析器对 Unix 风格支持最好,遇到非标准格式会解析失败,listInfo信号就不发——这是后面排查章节要展开的一个坑。

2.3 什么场景该用它,什么场景该绕开

我把判断收敛成三条。远端目录结构在运行时会变、需要先列出来再决定传哪个文件的,用 QFtp;需要在同一台设备上既当客户端又当服务端的,别用它,它只有客户端实现;传输量大且需要断点续传的,可以用,但得自己拼REST命令,源码里没有封装续传接口。

还有一类场景容易被忽略:老旧设备上的 FTP 服务端实现往往很“将就”,主被动模式行为不一致、LIST 返回格式不标准、对OPTS UTF8不作应答。QFtp 在这类环境下的容错不如 curl 那么皮实,但它的源码是开放的,解析不出来的格式你能直接改进qurlinfo.cpp里那段parseDirEntry逻辑,这比在二进制库里干等要强。

3. 两平台编译落地:qmake 与 CMake 两条集成路线

源码拿到手,第一件事是确认它能在你的工具链下独立编译通过,而不是直接塞进主工程边编边改。独立编译的好处是错误定位干净:编不过就是源码和 Qt 版本的问题,编过了再谈集成。

3.1 Linux 下用 qmake 编出静态库并验符号

给源码目录写一个.pro,把它当独立库对待:

# qftp.pro —— 独立编成静态库,不产出可执行文件 QT += core network QT -= gui # 这个模块不依赖界面,去掉能少一层链接 TARGET = qftp TEMPLATE = lib CONFIG += staticlib c++11 CONFIG -= app_bundle DESTDIR = $$PWD/lib INCLUDEPATH += $$PWD HEADERS += qftp.h qurlinfo.h SOURCES += qftp.cpp qurlinfo.cpp

关键在两个地方:QT += network必须加,QFtp内部用QTcpSocket走控制连接、用QTcpServer/QTcpSocket走数据连接,缺了它整片符号都链接不上;staticlib让 qmake 走ar而不是链接成.so,方便后面内联进主工程。HEADERS里列出qftp.h不是为了编译,而是为了让 qmake 认出里面的Q_OBJECT宏并生成 moc 规则——这一步漏了,链接期会报一堆vtable for QFtp未定义。

编译与验收:

qmake qftp.pro CONFIG+=release make -j$(nproc) # 产物确认 ls -l lib/libqftp.a # 符号确认:能看到 QFtp 的构造、connectToHost、get 等方法才算编对 nm -C lib/libqftp.a | grep "QFtp::" | head -20

nm -C里的-C是 demangle,把 C++ 修饰名还原成可读形式。如果只看到 moc 生成的QFtp::staticMetaObject而看不到QFtp::connectToHost,说明qftp.cpp没被真正编进去,回头检查SOURCES路径。另外make之前建议先make clean一次,源码包如果带着别人机器上的Makefile和.o,很容易编出一份半新半旧的库。

3.2 Windows + MSVC 的编译参数与 ABI 对齐

Windows 这边流程一样,只是把make换成nmake或jom:

qmake qftp.pro CONFIG+=release jom # 没有 jom 就用 nmake dir lib\qftp.lib

真正容易翻车的是 ABI 对齐,不是命令本身。三条经验:第一,编库用的 Qt 必须和主工程用的 Qt 是同一套 Kit,同一个版本、同一个编译器、同一个位数,混用 MinGW 编的库给 MSVC 工程链接必然失败;第二,debug 与 release 的 CRT 不能混,主工程是/MDd而库是/MD,链接期会直接甩LNK2038: mismatch detected for 'RuntimeLibrary',这时候不要试图加/NODEFAULTLIB糊过去,重新编一遍库更快;第三,源码文件的编码统一成 UTF-8 无 BOM,某些编辑器保存时会带上 BOM,MSVC 新版能认,老版本会在文件头报一个莫名其妙的语法错误。

如果你不想编库,Windows 下还有一条更省事的路:把四个源码文件直接拖进主工程的.pro,让 qmake 一起编。代价是每次全量重编都要多编两个文件,收益是不存在任何 ABI 问题。

3.3 CMake 把 qftp 源码直接编进主工程

现在不少工程已经切到 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) # qftp.h 里有 Q_OBJECT,必须开 find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Network) add_library(qftp STATIC third_party/qftp/qftp.cpp third_party/qftp/qftp.h # 头文件也要列出来,AUTOMOC 靠它扫 Q_OBJECT third_party/qftp/qurlinfo.cpp third_party/qftp/qurlinfo.h ) target_include_directories(qftp PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/third_party/qftp) target_link_libraries(qftp PUBLIC Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network) add_executable(ftp_demo main.cpp) target_link_libraries(ftp_demo PRIVATE qftp)

这里最容易被忽略的是把.h也写进add_library的源文件列表。CMAKE_AUTOMOC只对“被当作源文件处理”的头文件生成 moc,qftp.h没列进去,编译能过、链接会挂在QFtp::staticMetaObject上,报错信息还指向一个跟你代码无关的位置。另外target_link_libraries用的是PUBLIC,因为qftp.h暴露了QFtp类型,任何链接它的目标都得能看到 Qt Network 的头文件路径。

集成完成后写个二十行的冒烟测试,确认类真的可用:

#include <QCoreApplication> #include <QDebug> #include "qftp.h" int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QFtp ftp; // 只连不发命令,验证对象构造与状态机初值 qDebug() << "initial state:" << ftp.state(); // 期望 QFtp::Unconnected QObject::connect(&ftp, &QFtp::stateChanged, [](int s){ qDebug() << "state ->" << s; }); ftp.connectToHost(QStringLiteral("127.0.0.1"), 21); return app.exec(); }

冒烟测试只做一件事:确认QFtp能被构造、state()返回Unconnected、connectToHost能推进状态机。本地没起 FTP 服务端时会停在Connecting然后转Unconnected,这本身就是正常行为,说明异步链路是通的。

4. 命令队列与信号槽:把一次 FTP 会话写成状态机

QFtp 的用法和“调一个函数拿一个返回值”完全不同,它是队列 + 信号的模型。写成同步阻塞的思维,一定会掉进“命令还没执行完就发下一条”的坑里。

4.1 连接、登录、列目录的最小闭环

先看一段能跑通的最小代码,把连接、登录、列目录串起来:

// FtpLister.h #pragma once #include <QObject> #include <QStringList> #include "qftp.h" #include "qurlinfo.h" class FtpLister : public QObject { Q_OBJECT public: explicit FtpLister(QObject *parent = nullptr) : QObject(parent), m_ftp(new QFtp(this)) { connect(m_ftp, &QFtp::listInfo, this, [this](const QUrlInfo &info){ // 每条目录记录都会进到这里,按需过滤 if (!info.isDir()) m_names << info.name(); }); connect(m_ftp, &QFtp::commandFinished, this, &FtpLister::onCommandFinished); connect(m_ftp, &QFtp::done, this, &FtpLister::onDone); } void start(const QString &host, quint16 port, const QString &user, const QString &pass, const QString &dir) { m_ftp->setTransferMode(QFtp::Passive); // 绝大多数内网场景走被动 m_ftp->connectToHost(host, port); m_ftp->login(user, pass); // 入队,不阻塞 m_ftp->cd(dir); // 入队 m_listId = m_ftp->list(); // 入队,记下 id 供后续比对 } signals: void finished(bool ok, const QStringList &names); private slots: void onCommandFinished(int id, bool error) { if (error) { emit finished(false, m_names); return; } if (id == m_listId) { // 目录列表命令正常结束,此时 m_names 已收集完整 } } void onDone(bool error) { emit finished(!error, m_names); } private: QFtp *m_ftp = nullptr; QStringList m_names; int m_listId = -1; };

这段代码能说明 QFtp 的两个核心设计。第一,connectToHost、login、cd、list四条命令是连着调用的,它们不会并发执行,QFtp 内部串行推进:连上之后才发 USER,登录成功才发 CWD,切换目录成功才发 LIST。这就是命令队列的价值,你不需要自己写状态机去等每一步。第二,结果分两个层级回来:commandFinished(id, error)每完成一条命令就发一次,带命令 id,适合做精细控制;done(error)在整条队列跑空之后发一次,适合做“这一轮做完了”的收尾。

需要注意的是listInfo和commandFinished的先后:目录内容是在 LIST 的数据连接上陆续到达的,listInfo可能发很多次,最后一次之后才是commandFinished(m_listId, false)。所以收集动作必须放在listInfo里,不能在commandFinished里才去读——那时候数据早就到完了,但如果你在commandFinished里才开始处理,反而会漏掉中间状态。

4.2 get/put 的数据落盘与进度回传

下载一个文件,接口是get(remotePath, QIODevice*),第二个参数必须是一个已经以写方式打开的QIODevice:

void FtpDownloader::download(const QString &remote, const QString &local) { m_file.setFileName(local); if (!m_file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { emit finished(false, m_file.errorString()); return; } m_ftp->setTransferMode(QFtp::Passive); m_ftp->connectToHost(m_host, m_port); m_ftp->login(m_user, m_pass); m_getId = m_ftp->get(remote, &m_file); // 文件对象必须活到传输结束 } void FtpDownloader::onDataProgress(qint64 done, qint64 total) { // total 可能为 -1(服务端未在传输前给出大小),此时按未知总数处理 emit progress(done, total); } void FtpDownloader::onCommandFinished(int id, bool error) { if (error) { m_file.close(); emit finished(false, m_ftp->errorString()); // errorString 是最近一次错误 return; } if (id == m_getId) { m_file.close(); // 收尾必须自己关文件 emit finished(true, QString()); } }

参数上有几个细节值得记。get的QIODevice*生命周期由调用方负责,QFtp 只往里写数据,不接管所有权,所以m_file必须是成员变量而不是栈上的局部对象——写成局部变量,函数返回时对象析构,传输线程再往里写就是野指针。dataTransferProgress的第二个参数有时候是-1,表示服务端在传输开始前没告诉大小,进度条得按“不确定模式”处理,别直接拿它做除数。

上传是反过来的:put(QIODevice*, remotePath, type),第一个参数以QIODevice::ReadOnly打开,第三个参数是QFtp::Binary(二进制)或QFtp::Ascii(文本)。二进制传输时一定要显式传QFtp::Binary,走 ASCII 模式服务端会做换行符转换,传压缩包或可执行文件必然损坏,这是血泪经验。

4.3 命令 id 与 commandFinished 的配对纪律

命令 id 是这套异步模型里唯一能把“结果”和“意图”对上的东西,用不好就会出现“下载完成回调里干了上传的收尾”。三条纪律:

第一,每条需要收尾的命令都保存 id。int m_getId = m_ftp->get(...),不要图省事用“最后一条命令的 id”去猜。

第二,不要在一堆命令里靠顺序判断。有人写成“第一次 commandFinished 是登录、第二次是列目录”,这在命令失败重试、或者中间插了一条rawCommand之后立刻失效。

第三,命令失败后队列里剩下的命令通常不会再执行,需要重新排队。所以onCommandFinished一旦收到error == true,正确的做法是收尾退出、把错误抛给上层,而不是继续以为后面的命令会跑完。这一点在弱网环境下尤其明显:登录失败之后list的 id 永远不会出现在commandFinished里,代码如果在等它就会一直挂着。

5. 踩坑排查:从「卡在 list」到「传到一半断流」

这一章都是我实际遇到过、并且有明确现象和解决路径的问题。按“现象 → 原因 → 解决”的顺序写,你可以直接拿去对着排查。

5.1 卡在 list 不返回:被动模式端口与回包地址

现象:connectToHost和login都正常,commandFinished收到了登录成功的回调,但list发出去之后既没有listInfo也没有commandFinished,界面就一直转圈。

原因:FTP 的数据连接是另起一条 TCP,被动模式下服务端在 PASV 应答里回一个 IP 和端口让客户端去连。很多服务端会在这个应答里塞自己的内网地址(比如192.168.x.x),如果客户端和服务端之间隔着 NAT 或者防火墙,这个地址根本连不通,数据连接建立不起来,控制连接就一直在等。另一种情况是服务端只开放了 21 端口,被动模式用的那一段高位端口被防火墙挡了。

解决:三步走。先在代码里确认模式是setTransferMode(QFtp::Passive),内网场景几乎都该用被动;再确认服务端的被动端口段是固定且已放行的,常见做法是把被动端口范围收窄到一段连续的、防火墙已开的口子;最后用抓包或者netstat看数据连接有没有真的建立起来——只看到 21 端口的连接、没有第二个端口的连接,就是被挡了。如果对端是主被动不兼容的老服务端,试着切到QFtp::Active,但要意识到主动模式下是服务端回连客户端,客户端的防火墙同样要放行。

5.2 中文文件名乱码:编码协商没做,或者做了服务端不认

现象:listInfo收到的文件名是乱码,或者get一个中文名文件时服务端回 550 找不到文件,而用别的客户端连同一台服务器是正常的。

原因:FTP 协议的控制通道默认按 ISO-8859-1 解释参数,QFtp内部对路径字符串的处理也是按这个假设来的。服务端如果实际用的是 UTF-8,两边就对不上。 RFC 2640 定义了OPTS UTF8 ON这个协商命令,但很多服务端要么不实现、要么实现了但只在登录后才认。

解决:登录成功后、任何带路径的命令之前,先发一条rawCommand(QStringLiteral("OPTS UTF8 ON"))。如果服务端直接回 500 不支持,那就只能退回到“本地转码再发”的路子:把QString用toLocal8Bit()转成服务端期望的编码再拼进路径。注意rawCommand也是入队的,它和后续的list/get在同一条控制连接上按顺序执行,不需要额外等待。

5.3 退出时崩溃:QFtp 对象析构顺序

现象:程序关闭窗口时偶发崩溃,堆栈落在QTcpSocket相关的位置,或者控制台出现QSocketNotifier: Socket notifiers cannot be enabled or disabled from another thread之类的告警。

原因:QFtp析构时如果命令队列里还有没跑完的命令,内部的QTcpSocket会在半路被销毁,而信号还在投递中。更隐蔽的一种是QFtp作为栈对象,比它依赖的QIODevice(比如上传用的文件对象)先析构,回调里访问了已经销毁的兄弟对象。

解决:析构前先abort()清空队列,并且用deleteLater()而不是delete:

FtpClient::~FtpClient() { if (m_ftp) { m_ftp->abort(); // 清空命令队列,关闭数据连接 m_ftp->deleteLater(); // 交给事件循环,等信号投递完再销毁 } if (m_file.isOpen()) m_file.close(); // 文件对象最后关,别在 QFtp 之前 }

另外,QFtp及其从属的 socket 必须和它的父对象在同一个线程里,跨线程使用要么整体搬过去,要么用信号槽把调用投递回对象所属线程,别直接调。

5.4 大文件传到一半断流:没有看门狗,也没有重连

现象:几百兆的文件传到 60% 左右停住,dataTransferProgress不再发,commandFinished也不来,程序不报错,就是不动了。

原因:中间链路(尤其是内网跨网段)会静默丢弃长时间空闲的连接,而 QFtp 本身没有超时机制——socket 没断,它就一直等。另外服务端可能有单次会话的时长限制或者带宽限制,超限后直接掐数据连接但不发任何应答。

解决:加一个看门狗定时器,每次dataTransferProgress触发就重置,超时就abort()并按断点续传重来:

m_watchdog = new QTimer(this); m_watchdog->setSingleShot(true); m_watchdog->setInterval(30000); // 30 秒没有任何进度就判定卡死 connect(m_watchdog, &QTimer::timeout, this, [this]{ m_ftp->abort(); emit finished(false, QStringLiteral("transfer stalled")); }); connect(m_ftp, &QFtp::dataTransferProgress, this, [this](qint64 d, qint64 t){ m_watchdog->start(); // 有进度就续命 emit progress(d, t); });

超时阈值别设太短,慢速链路上传大文件时单次进度回调之间隔十几秒是正常的,设成 5 秒会频繁误杀。三十秒到一分钟是比较好用的区间。

5.5 链接报 LNK2019 / undefined reference:模块没带 network

现象:主工程编译通过,链接阶段报undefined reference to QFtp::connectToHost(...),或者 MSVC 下LNK2019: unresolved external symbol。

原因:QFtp的实现依赖QTcpSocket、QTcpServer、QUrlInfo里对QDateTime的使用,主工程的.pro或CMakeLists.txt里只写了QT += core,没加network,链接器找不到 Qt Network 里的符号。

解决:qmake 工程加QT += network,CMake 工程加find_package(... COMPONENTS Network)和对应的target_link_libraries。如果静态库和主工程分开编,还要确认静态库是以PUBLIC方式传递依赖的——qmake 的LIBS只传库路径不传依赖关系,CMake 用target_link_libraries(qftp PUBLIC Qt::Network)才能自动带过去。

6. 进阶:断点续传、看门狗复位与一份可复现的回归清单

把基础流程跑通之后,真正决定这套代码能不能上生产的是两件事:断了之后能不能接上,以及每次改完能不能快速验证没退步。

断点续传的实现依赖REST命令,QFtp 没有封装它,得用rawCommand手工拼。顺序很关键:先看本地已下载文件的大小,再用追加方式打开文件,然后发REST <offset>,最后发RETR。REST必须紧挨着RETR,中间插别的命令服务端就会忽略这个偏移量:

void FtpDownloader::resume(const QString &remote, const QString &local) { QFileInfo fi(local); const qint64 offset = fi.exists() ? fi.size() : 0; m_file.setFileName(local); // 有偏移量就追加,没有就新建 const auto mode = offset > 0 ? (QIODevice::WriteOnly | QIODevice::Append) : (QIODevice::WriteOnly | QIODevice::Truncate); if (!m_file.open(mode)) { emit finished(false, m_file.errorString()); return; } m_ftp->setTransferMode(QFtp::Passive); m_ftp->connectToHost(m_host, m_port); m_ftp->login(m_user, m_pass); if (offset > 0) m_ftp->rawCommand(QStringLiteral("REST %1").arg(offset)); // 服务器不支持会回 5xx m_getId = m_ftp->get(remote, &m_file); }

要注意两点:REST不是所有服务端都支持,回 5xx 时得退化成从头传;断点续传的前提是远端文件在两次传输之间没有变化,如果服务端那边的文件被替换了,续传会拼出一个损坏的文件,稳妥做法是续传前先用list拿到远端大小和修改时间做一次比对。

看门狗还有一处要复位:命令之间。登录慢、列目录慢的场景下,光靠dataTransferProgress续命是不够的,因为传输还没开始。我一般会在commandFinished里也重置一次定时器,这样整个会话的任意阶段卡住都能被兜住。

回归验证我固定跑这四个场景,覆盖了绝大部分翻车点:

场景构造方式通过标准
空目录列取远端建一个空目录后listlistInfo不触发,commandFinished正常返回
中文名往返上传含中文名的文件再列目录名字与本地一致,无乱码
大文件中断续传传输中途abort()后重新调用resume最终文件大小与远端一致,校验和相同
服务端不可达指向一个关闭的端口在超时时间内收到finished(false, ...),不挂死

这套东西我现在的习惯是:任何一次改动qftp.cpp或者调整传输参数之后,四条场景必须全过一遍才允许合进主线。有一次图省事只测了下载,结果中文名那条在 Windows 客户端上整片乱码,回滚排查花的时间比跑一遍测试多得多。从那以后我每次改这块都强制走一遍这四条。希望帮到你。

本文还有配套的精品资源,点击获取