QFTP源码集成与实战:Qt跨平台FTP客户端异步队列避坑
简介:QFTP源码是一份面向Qt/C++开发者与网络编程学习者的跨平台FTP客户端实现,适合用于理解FTP连接管理、命令收发、URL信息解析及文件上传下载流程,也可作为自定义传输模块集成到桌面工具或编辑器中。压缩包共4个文件,由2个.h头文件与2个.cpp实现文件组成,整体约24KB,体量轻便,核心接口声明与协议逻辑分离,便于按需阅读和二次开发。目前已有297人学习下载,说明其在Qt网络编程与跨平台适配话题中具有一定参考价值。读者可从中了解Linux与Windows下的编译运行差异、FTP响应与错误处理思路,以及将源码嵌入实际项目的基本方式,适合作为课程实验、小型工具开发或协议学习的参考材料。
1. QFTP 源码补的是 Qt 网络栈里缺的那块 FTP 客户端
手上有个设备管理工具,要跟跑在板子上的 FTP 服务端对接:拉日志、推配置、翻目录。用 QNetworkAccessManager 试了一圈才发现,它对 FTP 只保留了最基础的下载能力,上传、建目录、改名字基本没有落点。QFTP 源码就是在这个位置上补位的:它把 Qt4 时代的 QFtp 类单独抽了出来,纯 QtCore 加 QtNetwork,不引第三方库,Linux 和 Windows 两端都完成了测试。它适合谁?需要在同一套 Qt 工程里同时做上传下载、目录遍历、断点续传,又不想自己套一层 socket 实现 FTP 协议命令的开发者。这份源码的价值不在“能用”,而在“命令队列和信号时序都给你摆好了”,你只需盯着自己那几个业务场景改。
2. 把 QFTP 源码接进工程:Linux 与 Windows 两套编译链路
拿到源码包,第一步永远不是读代码,而是先让它在你机器上编出一个能跑的最小可执行文件。这一步过不了,后面看什么都是空的。QFTP 源码包的结构很朴素:两个头文件、两个实现文件,外加一份构建脚本。文件数量少不代表没有依赖关系,里面 qurlinfo 是给 LIST 命令的返回结果做解析用的,qftp 才是协议状态机主体,两者的耦合点在于 QFtp::listInfo 信号抛出来的那个对象类型。
2.1 源码里到底有哪几个文件,谁依赖谁
先认清这四个文件各自的职责,后面改代码时才知道该动哪一个:
| 文件 | 职责 | 改动频率 |
|---|---|---|
| qftp.h | QFtp 类声明、状态与命令枚举、信号槽定义 | 低,除非要加接口 |
| qftp.cpp | FTP 连接状态机、命令队列、PASV 解析、数据通道收发 | 中,编码和超时逻辑常改这里 |
| qurlinfo.h | LIST 结果的数据载体,文件名、大小、权限、时间 | 中,Qt6 下要重写 |
| qurlinfo.cpp | 解析 Unix ls 风格和 DOS 风格的目录列表文本 | 高,服务端格式一换就得调 |
qftp.h 里带了 Q_OBJECT 宏,也就是说不管走 qmake 还是 CMake,moc 这一步必须开着。很多人在 CMake 里手写 add_executable 时忘了 set(CMAKE_AUTOMOC ON),编译期报一堆 undefined reference to vtable,排查半天以为是源码缺东西,其实是元对象没生成。这个问题在 Linux 上尤其容易忽略,因为 gcc 的报错信息不会直接告诉你“缺 moc”。
qurlinfo.cpp 是整份源码里最“脏”的一块。FTP 协议本身没有规定 LIST 命令的输出格式,各家服务端返回的文本各不相同:Unix 系一般是-rw-r--r-- 1 user group 1024 Jan 01 12:00 name,Windows 系的 FTP 服务端常见的是01-01-24 12:00PM <DIR> name。解析器要同时吃下这两种,正则写漏一个分支,表现就是 list 命令返回成功但一个条目都没有。
2.2 qmake 路线:.pro 里三行决定成败
如果主工程本身就是 qmake 体系,最省事的做法是把 QFTP 编成静态库再链进去。新建一个子目录放源码,配一份这样的 .pro:
QT += core network QT -= gui TARGET = qftp TEMPLATE = lib CONFIG += staticlib c++17 CONFIG -= app_bundle # 头文件对外可见,主工程 include 时不用写全路径 INCLUDEPATH += $$PWD/src HEADERS += \ src/qftp.h \ src/qurlinfo.h SOURCES += \ src/qftp.cpp \ src/qurlinfo.cpp # Linux 下编静态库也要 -fPIC,否则被共享库链接时报 relocation 错误 unix: QMAKE_CXXFLAGS += -fPIC关键就三行:TEMPLATE = lib决定产物形态,CONFIG += staticlib决定它是静态库而不是动态库,QT += network决定 socket 那套东西能不能链上。少写 network 这一行,编译阶段一切正常,链接阶段才炸,报 undefined reference to QFtp::connectToHost,新手很容易在这里卡住。
QT -= gui是我习惯加的。QFTP 本身不碰任何界面类,去掉 gui 依赖之后,这个库可以被纯控制台程序、后台服务甚至无显示的嵌入式程序复用。如果哪天要把这套上传下载逻辑搬到无界面的守护进程里,这一步提前做好能省不少拆依赖的功夫。
QMAKE_CXXFLAGS += -fPIC只有在你要把静态库再链进一个 .so 时才必需。只出可执行文件的话不加也行,但加了不会有副作用,我一般默认写上,省得以后改产物类型时又回来补。
2.3 CMake 路线:Qt5/Qt6 下 find_package 的写法
主工程用 CMake 的话,把 QFTP 做成一个静态库 target,写起来比 qmake 更清楚:
cmake_minimum_required(VERSION 3.16) project(qftp LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) 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 src/qftp.cpp src/qftp.h src/qurlinfo.cpp src/qurlinfo.h ) # PUBLIC 让下游 target 自动继承这个 include 路径 target_include_directories(qftp PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) target_link_libraries(qftp PUBLIC Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network )find_package(QT NAMES Qt6 Qt5 ...)这个写法是官方推荐的兼容套路,先找 Qt6 再回退到 Qt5,找到哪个就把版本号塞进变量里,后面所有 target 名都用Qt${QT_VERSION_MAJOR}::拼出来。这样同一份 CMakeLists 在装了 Qt5 的旧机器和装了 Qt6 的新机器上都能跑。
target_include_directories必须用 PUBLIC 而不是 PRIVATE。用 PRIVATE 的话,库自己能编过,但主工程 include "qftp.h" 时会报找不到文件,因为头文件路径没有往上传。这是 CMake 里最常见的权限误用之一。
2.4 第一次编译验证:写一个只连不传的最小 main
库编出来了,先别急着写业务。写一个只做连接和登录的最小程序,把整条链路跑通:
#include <QCoreApplication> #include <QTimer> #include <QDebug> #include "qftp.h" int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QFtp ftp; // 状态机变化,排查连接问题时第一手信息都在这里 QObject::connect(&ftp, &QFtp::stateChanged, [](QFtp::State s){ qDebug() << "state ->" << int(s); }); // 每条命令结束都会回调,id 是命令编号 QObject::connect(&ftp, &QFtp::commandFinished, [](int id, bool error){ qDebug() << "cmd" << id << "finished, error =" << error; }); QObject::connect(&ftp, &QFtp::done, [&](bool error){ qDebug() << "all done, error =" << error << ftp.errorString(); ftp.close(); }); ftp.connectToHost("127.0.0.1", 21); ftp.login("anonymous", "anonymous@example.com"); // 兜底:15 秒还没走完流程就退出,避免卡在事件循环里 QTimer::singleShot(15000, &app, &QCoreApplication::quit); return app.exec(); }这里有个容易被忽略的点:connectToHost和login是连着调用的,中间没有等待连接成功。QFTP 内部有命令队列,后面的命令会自动排在前面命令完成之后再发,所以同步写法是对的,不需要在 stateChanged 里手动补 login。反过来说,如果谁在 connectToHost 之后手动 sleep 等连接,反而是错的,会把事件循环堵死,连接永远建立不起来。
QTimer::singleShot那个兜底别嫌丑。调试阶段如果地址写错或者对端没开,程序会安静地挂在事件循环里,什么输出都没有,你会怀疑是不是信号没连上。加个超时退出,至少能确认“程序跑到了 exec 之后”。
2.5 静态库还是源码直入工程
两种方式各有适用面。静态库适合多工程复用,编一次到处链,缺点是改了源码要重新编库,调试时断点进不去(没有调试符号的话)。源码直入适合只有一两个工程用、并且需要频繁改解析逻辑的场景,改完直接编,断点随便下。
我一般的判断标准是:如果这个项目里 QFTP 的代码三个月内不会动第二次,就做成静态库;如果刚接手、还在摸服务端返回格式,就先源码直入,等逻辑稳定了再抽成库。别一上来就追求工程结构漂亮,解析器没调通之前,任何抽象都是负担。
3. 命令队列与信号时序:QFTP 的异步模型怎么用才不出错
QFTP 是彻底异步的,每个 API 都立即返回一个 int 编号,真正的结果通过信号回调。这套模型的好处是不阻塞界面,坏处是很多人拿同步思维去写,结果就是数据读了一半、文件写空了、进度条归零。这一章把信号顺序和读取时机讲透,剩下的就是照着填业务逻辑。
3.1 每个 API 返回的 int 是队列句柄,不是返回值
connectToHost、login、list、get、put、mkdir、remove这些方法全部返回 int。这个 int 不是成功与否的标志,而是这条命令在队列里的编号。命令失败与否要看commandFinished(int id, bool error)里的 error 参数。
为什么要区分?因为你可以连着发五条命令:
int idMkdir = ftp.mkdir("/log"); // 先建目录 int idPut = ftp.put(&buf, "/log/a.txt"); // 再传文件 int idList = ftp.list("/log"); // 最后列一下确认三条命令会按顺序执行,每条结束时都会触发一次 commandFinished,带着各自的 id。你在槽函数里用 id 做分支,就知道是哪条命令回来了。如果只看 error 不看 id,目录建失败和文件传失败你会分不清。
currentId()和currentCommand()可以在任何时刻查当前正在执行的是哪条命令,调试时打一行日志比猜快得多。
3.2 一次下载的完整信号序列
下载一个文件,信号是按固定顺序来的:
| 顺序 | 信号 | 该做什么 |
|---|---|---|
| 1 | commandStarted(int id) | 记下 id,启动进度计时 |
| 2 | dataTransferProgress(qint64, qint64) | 更新进度,注意 total 可能为 -1 |
| 3 | readyRead() | 读数据,必须读干净 |
| 4 | commandFinished(int id, bool error) | 关文件、释放缓冲、判断成败 |
| 5 | done(bool error) | 队列清空,可以关闭连接 |
dataTransferProgress的第二个参数 total 在服务端没给出文件大小时会是 -1,直接拿来做除法会得到负数百分比。正确写法是先判断total > 0再算比例,否则进度条会显示成负值或者直接崩。
readyRead可能触发很多次,也可能一次都不触发(文件为空时)。它不是“数据到齐了”的通知,而是“现在缓冲区里有东西可读”的通知。这一点理解错,代码就会写成“等 readyRead 来了再一次性 readAll”,大文件下必然出问题。
3.3 readyRead 里必须把缓冲区读干净
标准写法是这样:
QFile *local = new QFile(localPath); if (!local->open(QIODevice::WriteOnly)) { qWarning() << "open failed" << localPath; delete local; return; } int getCmdId = -1; QObject::connect(&ftp, &QFtp::readyRead, [&](){ // 循环读到空为止:一次 readyRead 可能对应多段数据 while (ftp.bytesAvailable() > 0) { local->write(ftp.readAll()); } }); QObject::connect(&ftp, &QFtp::commandFinished, [&](int id, bool error){ if (id != getCmdId) return; local->flush(); local->close(); delete local; local = nullptr; if (error) qWarning() << "download failed:" << ftp.errorString(); }); getCmdId = ftp.get(remoteName, local, QFtp::Binary);这里有两个细节值得盯。第一,while (ftp.bytesAvailable() > 0)必须循环,因为 QFTP 内部缓冲可能一次塞进来几十 KB,readAll()一次读不完的情况虽然少见但不是没有,尤其是在弱网或者对端限速时。第二,文件对象的生命周期要跟着 commandFinished 走,不能在 done 里删——done 是队列空了才触发,中间如果还有别的命令排在后面,文件句柄会早于数据传输结束被释放,写盘就写到野指针上去了。
getCmdId初值给 -1,然后赋值语句放在 connect 之后,这个顺序无所谓,因为命令是异步的,赋值一定发生在任何信号回调之前。但如果你把 connect 写在 get 之后,理论上存在极小概率错过立即回调(本地回环地址下),所以我还是习惯先连信号再发命令。
3.4 三条命令连着发时,别在 commandFinished 里手动串行
新手常写的一种结构是:在 commandFinished 里判断“如果这条是 login,就发 list;如果这条是 list,就发 get”。能跑,但没必要。QFTP 的队列本来就是串行的,直接连着发就行:
ftp.connectToHost(host, 21); ftp.login(user, pass); ftp.cd("/data"); ftp.get("config.ini", file, QFtp::Binary); ftp.close();这五行会严格按顺序执行。唯一要注意的是 close 也会排队,如果前面某条命令失败,后面的命令仍然会尝试执行,失败会累积到 done 的 error 参数上。想要“一失败就中止”,得在 commandFinished 里判断 error 然后调ftp.abort()。
abort()会把当前命令和队列里所有未执行的命令一起清掉,这个行为在某些场景下是优点(快速放弃),在另一些场景下是坑(你以为只取消了当前一条)。用它之前先想清楚队列里还有没有必须执行的清理命令,比如 close。
3.5 对象析构顺序是最隐蔽的崩溃源
QFTP 对象如果是在栈上创建的,而它还有命令在执行时程序退出,析构函数里会去访问已经失效的 socket,表现就是退出时随机崩溃,gdb 里看到的是 Qt 内部的 socket notifier。这类问题在 Windows 上尤其明显,因为 Windows 的 socket 关闭行为和 Linux 不太一样。
我一般把 QFtp 放在堆上,用 QScopedPointer 或者父子对象托管,退出流程固定成三步:先abort()清队列,再close()关连接,最后等 done 信号触发后再 delete。如果业务上允许粗暴退出,至少保证 delete 发生在事件循环退出之前。
4. 中文路径、被动模式与大文件:三个必须落到参数上的点
前面两章解决的是“能不能跑”,这一章解决的是“跑得对不对”。QFTP 的默认行为在英文环境和干净网络下没问题,一旦碰到中文目录、NAT 后面、超过 2GB 的文件,就得手动干预。这三个点每一个都能让一次传输静默失败,而且报错信息都很模糊。
4.1 传输模式:Active 还是 Passive,取决于中间有没有 NAT
FTP 有两种数据连接建立方式。Active 是服务端主动连回客户端的一个端口,Passive 是客户端去连服务端开的一个临时端口。前者在客户端有防火墙或者处在 NAT 后面时基本必失败,因为服务端连不进来。
企业内网、云主机、家用路由器后面,一律用 Passive。QFTP 里通过 setTransferMode 切换:
// 连接建立后立刻设置,不要等出错再切 ftp.connectToHost(host, port); ftp.setTransferMode(QFtp::Passive); ftp.login(user, pass);我一般显式写这一行,不去赌默认值。不同版本的源码在这一点上行为不完全一致,显式设置能保证换机器、换服务端之后行为一致。
判断是不是模式问题有个快速办法:看 stateChanged 走到 LoggedIn 之后,list 命令是否卡住不返回。如果控制连接(21 端口)正常登录成功,但所有数据传输命令都超时,九成是数据连接方向不对。这时候把模式切一下立刻见分晓。
还有一种情况是服务端只允许 Active。这种多见于一些嵌入式设备的精简 FTP 服务,为了省资源不开 PASV。碰到这种服务端只能切回 Active,同时确保客户端机器上对应的端口段没被防火墙拦。两边都要放行,缺一边都不行。
4.2 中文目录乱码:命令编码和 LIST 解析编码是两条独立链路
中文乱码在 QFTP 上分两个完全不同的来源,很多人混为一谈,改了半天没效果。
第一条链路是“发出去的命令”。QFTP 内部把 QString 转成字节流时用的是固定的 8-bit 转换,服务端如果不是 UTF-8 模式,收到的就是乱码。表现是 mkdir 建出来的目录名是问号,或者 cd 到一个中文目录直接报 550。
第二条链路是“收回来的 LIST 结果”。qurlinfo 解析的是原始字节,转 QString 时如果编码和服务器发出来的一致,文件名就对;不一致就是乱码。表现是 list 成功但文件名显示成方块。
第一条链路的稳妥解法是绕开自动转换,自己拼字节:
// Qt5 用 QTextCodec;Qt6 里换成 QStringEncoder static QByteArray encodeArg(const QString &text, const char *codecName) { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) QTextCodec *codec = QTextCodec::codecForName(codecName); return codec ? codec->fromUnicode(text) : text.toUtf8(); #else QStringEncoder encoder(codecName); return encoder.encode(text); #endif } // 自己拼命令再走 rawCommand,编码完全可控 QByteArray cmd = "MKD " + encodeArg(QString::fromUtf8("测试目录"), "GBK"); ftp.rawCommand(cmd);rawCommand发的命令,服务端的回复通过 rawCommandReply 信号拿到,返回码是三位数字(257 表示目录创建成功,550 表示失败)。用 rawCommand 的代价是要自己处理返回码,好处是编码、命令格式、参数顺序完全由你决定。
至于用哪个编码,得看服务端配置。常见的约定是:服务端开了 UTF-8 支持就用 UTF-8,没开就用本地代码页。判断方法很简单,先用一个纯 ASCII 目录名跑通,再换成中文试,看乱码出现在哪一侧——如果服务端的管理界面上显示的名字就已经是乱的,说明是发送侧编码错了;如果服务端显示正常但客户端解析出来是乱的,那是 LIST 解析侧的问题。
第二条链路要改 qurlinfo.cpp 里的解析逻辑,把字节转字符串的地方换成固定编码。改之前先抓一份原始 LIST 输出存成文本,对着调,比盲改快十倍。
4.3 大文件:进度回调的节流和写盘缓冲
传一个几百 MB 的文件,dataTransferProgress 会触发得非常频繁,如果槽函数里做了重活(比如更新界面、写日志、算哈希),整个传输会被拖慢。我的做法是在槽里只更新一个原子变量,界面那边用定时器每 200 毫秒读一次。
// 进度只记数,不做任何耗时操作 QObject::connect(&ftp, &QFtp::dataTransferProgress, [&](qint64 done, qint64 total){ m_done.store(done); m_total.store(total); }); // 界面侧定时刷新,和传输解耦 QTimer *uiTimer = new QTimer(this); uiTimer->setInterval(200); connect(uiTimer, &QTimer::timeout, this, [this](){ qint64 d = m_done.load(), t = m_total.load(); if (t > 0) progressBar->setValue(int(d * 100 / t)); }); uiTimer->start();另一个和大小有关的是写盘方式。QFile 默认带缓冲,正常写没问题,但如果每次 readyRead 都调一次 flush,性能会掉得厉害。flush 只在 commandFinished 里做一次就够,中间让 Qt 自己管缓冲。
超过 2GB 的文件在 32 位进程里还有额外的坑:qint64类型本身没问题,但某些旧的 QFile 重载在 32 位下会截断偏移量。如果目标环境是 32 位,传输前先用 QFileInfo 判断大小,超过 2GB 的文件考虑分片,或者确认编译选项里开了大文件支持。
4.4 ASCII 和 Binary 选错,文件内容会被悄悄改写
QFTP 的 get 和 put 都有一个 TransferType 参数,取值是 QFtp::Binary 或 QFtp::Ascii。选 Ascii 时,协议层会做换行符转换:传输过程中把 LF 转成 CRLF,接收时再转回来。
问题在于这个转换是无条件的,不管文件本身是什么类型。传一个二进制文件用 Ascii 模式,里面的 0x0A 字节会被改成 0x0D 0x0A,文件直接损坏,而且不报错,传输进度显示 100%,只有打开文件才发现内容不对。
血泪经验:除了明确知道是纯文本并且需要跨平台换行统一之外,一律用 Binary。这个参数在每一次 get 和 put 里都要显式写,别依赖默认值。
5. 双平台实测的避坑清单:五个现象、五个原因、五个解法
下面这五条都是 Linux 和 Windows 双端跑下来真实撞上的,现象描述按“你看到什么”写,方便对号入座。
5.1 现象:Linux 能连上,同一份代码 Windows 连不上
原因通常不是代码,而是 Windows 上默认开启了主动模式探测,或者本机防火墙拦了数据连接的回连端口。另一种可能是服务端地址填的是主机名,Windows 下 DNS 解析走了另一条路径,解析到了 IPv6 地址而服务端只监听 IPv4。
解决分两步:先固定传输模式为 Passive,再在 connectToHost 之前手动把主机名解析成 IPv4 地址传进去。如果还不行,用 telnet 或 curl 从 Windows 机器上直连 21 端口,确认是网络层问题还是代码问题。这一步能把排查范围砍掉一半。
5.2 现象:list 返回成功,但一条记录都没有
原因基本锁定在 qurlinfo.cpp 的解析分支没覆盖当前服务端的输出格式。Unix ls 风格和 DOS 风格的正则不一样,有些精简 FTP 服务端还会返回 MLSD 格式,三种格式混在一起就会漏。
解决:先用 rawCommand 发一条 LIST,把原始文本打到日志里看格式,再决定改哪个分支。改解析器之前先备份一份原始输出样本,以后换服务端可以直接拿来回归测试。如果服务端支持 MLSD,优先用 MLSD,它的格式是标准化的,比 LIST 好解析得多。
5.3 现象:小文件正常,大文件传到一半断掉
原因有三个层次。最浅的是网络超时,QFTP 自身没有命令级超时,卡住就一直卡着。中间层是服务端设置了空闲断开时间,传输过程中如果客户端长时间没有读取(比如主线程被别的活堵住),服务端会踢连接。最深的是磁盘写入失败,本地盘满了但代码没检查 write 的返回值。
解决:先加一个命令级看门狗(下一章给具体实现),再检查 readyRead 的读取逻辑是不是在主线程里被阻塞,最后把本地 write 的返回值纳入判断,写失败立即 abort 并清理半截文件。
5.4 现象:程序退出时偶发崩溃,堆栈指向 socket 层
原因是 QFtp 对象析构时还有命令在队列里,析构函数访问了已经关闭的 socket。Windows 上这个崩溃比 Linux 更频繁,因为 socket 句柄的回收时机不同。
解决:把退出流程固定下来——先 abort 清队列,再 close,等 done 信号,最后 delete。如果 QFtp 是在某个窗口类里创建的,注意父子关系不要形成循环,父对象析构时会先删子对象,如果子对象的槽函数里又引用了父对象的成员,就是悬空引用。
5.5 现象:中文文件名上传后变成问号
原因在 4.2 节讲过,发送侧编码不对。但还有一个容易忽略的分支:文件名是对的,只是路径分隔符用了 Windows 的反斜杠。FTP 协议里路径分隔符永远是正斜杠,反斜杠会被当成文件名字符的一部分,服务端上就会出现一个名字里带反斜杠的怪文件。
解决:所有发给服务端的路径统一用正斜杠拼,本地路径转远端路径时手动 replace 一次。这个转换写成一个函数,全局只在这一处做,别散落在各个调用点。
6. 断点续传与超时看门狗:两个能让 QFTP 进生产的技巧
断点续传的关键是 REST 命令。它告诉服务端“下一条传输命令从第 N 个字节开始”,但这个状态只对紧接着的那一条命令有效,中间插任何别的命令都会让偏移量失效。所以顺序必须是:先 REST,再立刻 get,中间不能有 list 之类的操作。
QFileInfo fi(localPath); qint64 offset = fi.exists() ? fi.size() : 0; QFile *f = new QFile(localPath); // 有本地残留就追加写,没有就新建 f->open(offset > 0 ? (QIODevice::WriteOnly | QIODevice::Append) : QIODevice::WriteOnly); if (offset > 0) { // REST 之后必须紧跟 RETR,否则偏移量作废 ftp.rawCommand("REST " + QByteArray::number(offset)); } ftp.get(remoteName, f, QFtp::Binary);上传方向的续传要用 APPE,它表示追加写入而不是覆盖。前提是服务端支持 APPE,有些精简服务端不认这个命令,返回 500,那就只能整传。
第二个技巧是看门狗。QFTP 没有内置超时,网络一断,命令队列会永远停在那一条上,done 永远不来。用一个独立的 QTimer 监控 commandStarted 和 commandFinished 之间的时间差,超时就 abort。这个类要带 Q_OBJECT,别忘记 moc:
class FtpWatchdog : public QObject { Q_OBJECT public: FtpWatchdog(QFtp *ftp, int timeoutMs, QObject *parent = nullptr) : QObject(parent), m_ftp(ftp) { m_timer.setSingleShot(true); m_timer.setInterval(timeoutMs); connect(&m_timer, &QTimer::timeout, this, [this](){ m_ftp->abort(); // 会清空整个命令队列 m_ftp->close(); emit stalled(); }); connect(ftp, &QFtp::commandStarted, this, [this](int){ m_timer.start(); }); connect(ftp, &QFtp::commandFinished, this, [this](int, bool){ m_timer.stop(); }); } signals: void stalled(); private: QFtp *m_ftp; QTimer m_timer; };注意 abort 的副作用是把队列里所有待执行命令一起清掉,所以看门狗触发之后不要指望队列还能自动恢复,得在 stalled 槽里重建连接、重发剩余任务。超时时间我一般给单条命令 30 秒,大文件传输时按文件大小动态放宽,否则一个 500MB 的文件在中途就会被误杀。
第一人称教训收尾:从那以后我每次接一个新的 FTP 服务端,都强制先跑一遍三件事——list 看解析、传一个带中文名的空文件看编码、传一个 100MB 的伪随机文件看续传和超时,三关都过再写业务代码。希望帮到你。
本文还有配套的精品资源,点击获取