简介:一份面向QT开发者的FTP客户端工具源码,基于QT5与C++编写,具备良好的跨平台能力,可运行于Windows及Linux(含嵌入式Linux)系统。项目直接利用QT库内建的QNetworkAccessManager与QFtp类实现FTP协议交互,开发者无需单独编译QFtp模块即可完成服务器连接、用户登录、文件下载与上传等操作,特别适合需要快速集成FTP能力或学习QT网络编程的开发者参考。压缩包共12个文件,其中4个cpp、3个h为项目核心源代码,另有ui界面设计文件、pro工程配置文件以及png图标资源,整体包体大小仅32KB,代码量精简而结构完整。目前已有1097人学习下载。源码基于信号与槽机制实现对传输进度和状态的实时监控,调用get()方法下载文件、put()方法上传文件,并预留了清晰的扩展点,便于继续加入错误处理、断点续传或FTPS加密传输等增强功能,是一份简洁实用的C++跨平台网络编程案例。
1. QT5 FTP上传下载工具源码:为什么不推荐直接从零封装QtFtp
做QT5应用开发的人迟早会遇到一个尴尬事:想在项目里塞个FTP上传下载功能,打开帮助文档一查,QFtp类不在核心模块里了,网上找的Demo要么是QT4时代的古董,要么是依赖不明确的半成品。于是很多人撸起袖子自己用QTcpSocket写FTP协议,结果被控制连接和数据连接的来回切换、被动模式的PASV应答解析、断点续传的REST命令顺序折磨到怀疑人生。这份QT5 FTP上传下载等功能工具源码,解决的就是这个痛点:它基于QT5的QFtp库封装了一套可直接落地的上传、下载、列表、删除、重命名工具,带进度信号和错误回调,而且代码里保留了嵌入式Linux交叉编译的痕迹——对于要在arm板子上跑FTP同步任务的开发者来说,这是比翻官方文档更省时间的参考资料,也是理解QT5网络模块的活教材。下面拆一下这份源码的底细、用法和坑。
2. 先看底子:QFtp在QT5里的位置与这份源码的模块拆分逻辑
读一份源码前,先搞明白依赖的是什么库、为什么QT5要把FTP相关类挪出去。把这个背景理清,后面交叉编译遇到找不到头文件时才不会抓瞎。
2.1 QFtp为什么被移出核心库:QT5的网络模块策略变化
QT4时代,QFtp是QtNetwork模块的一部分,工程里直接#include <QFtp>就能用。QT5开始,官方对网络模块做了瘦身,把FTP这种偏应用层协议的实现从核心网络库里剥离出去,原因不难理解:FTP在移动端和嵌入式场景的使用频率大幅下降,HTTPS才是主流,官方不想为FTP的被动连接状态机长期背兼容性包袱。于是QFtp被移到了一个独立维护的仓库,QT5下需要额外构建qtftp库,QT6里甚至一度彻底移除,再之后的版本又通过新方案收回。
这个变动带来的直接影响是:你在QT5里写#include <QFtp>时,如果工程pro文件里没有加QT += network ftp这行,编译器会直接报"QFtp: No such file or directory"。很多刚开始用QT5的开发者在这里就放弃了,转而求助第三方库比如libcurl、libftp,反而引入一堆不熟悉的依赖。这份工具源码的一大价值在于,它把qtftp源码也整理进了工程目录,pro文件里通过include子工程的方式把编译依赖一并解决了,等于给你省掉了自己去git拉取、配版本、编库的步骤。
2.2 它和QNetworkAccessManager的关系:两种FTP实现路径的选型
QT5里其实有两条FTP实现路径。一条是低层一点的QFtp类,直接封装了FTP协议的命令交互,登录之后手动cd、list、get、put,每一步都有对应的信号回调,适合做精细控制——比如手动解析列表内容、自定义超时、控制断点续传的REST命令。另一条是QNetworkAccessManager的ftp协议支持,用QNetworkRequest指定ftp://开头的URL,像HTTP一样异步收发,但它的封装粒度比较粗,上传下载时对进度回调的控制没有QFtp细,而且它依赖QtFtp组件的plugin模块,环境配置比QFtp更繁琐。
这份工具源码选的是QFtp路径,理由很现实:它在嵌入式Linux上更好裁剪。QFtp只依赖Qt5Network和Qt5Core,而QNetworkAccessManager的FTP方案还需要一个独立的FTP reply handler插件,交叉编译时多一个插件要往rootfs里扔。对于做产品固件的人来说,能少一个.so就少一份出问题的风险。另外一个隐含原因是,QFtp的API设计是命令-信号模式,异步操作的结果通过commandStarted、commandFinished、listInfo等信号通知,这种结构天然适合做UI里的进度条和日志输出,商业项目里给用户显示"正在上传xxx文件"这种反馈时,QFtp比QNetworkAccessManager更像是一个有状态的操作序列,而不是一次性的请求。
2.3 源码包的模块清单:上传、下载、列表、删除与登录回调
打开这份源码的目录结构,可以看到它没有把所有逻辑塞进mainwindow.cpp里,而是拆成了几个明确的类。我大致列一下核心文件的功能划分,这个划分结构也值得借鉴到自己的项目里:
| 文件(逻辑模块) | 对应功能点 | 底层API调用 |
|---|---|---|
| 登录模块 | 服务器地址、端口、用户名、密码的配置与连接 | QFtp::connectToHost, QFtp::login |
| 文件列表模块 | 获取当前目录下的文件列表,提取文件名和大小 | QFtp::list, listInfo信号 |
| 下载模块 | 远程文件下载到本地路径,带进度百分比 | QFtp::get, dataTransferProgress信号 |
| 上传模块 | 本地文件上传到远程目录,支持覆盖 | QFtp::put, dataTransferProgress信号 |
| 删除与重命名模块 | 远程文件删除、目录切换 | QFtp::remove, QFtp::rename |
每个模块对应QFtp的一个操作序列,比如登录不是打电话一样一次性成功,而是先connectToHost,等连接建立后再发login命令。这种状态机般的调用模式,初学者最容易搞错的地方是:在connectToHost还没完成时就去调list,命令会被QFtp排在队列里,但一旦登录失败,队列里的命令全部作废,而且不会有明确的错误提示告诉你"因为还没登录所以list失败"。
2.4 工程结构速览:pro文件与信号槽的连接方式
pro文件是这份源码另一个值得看的部分。它除了常规的QT += core gui network之外,还引入了qtftp模块的源码路径,用类似下面这样的配置把QFtp库一起编译进来:
QT += core gui network greaterThan(QT_MAJOR_VERSION, 4): QT += widgets include($$PWD/3rdparty/qtftp/qtftp.pri) SOURCES += \ main.cpp \ MainWindow.cpp \ FtpWorker.cpp HEADERS += \ MainWindow.h \ FtpWorker.h这里的include($$PWD/3rdparty/qtftp/qtftp.pri)是让qmake把qtftp的源码作为一个子工程编进你的可执行文件里,省去了单独编译libQt5Ftp.so再链接的麻烦,对于嵌入式移植来说也非常友好——直接整包拷到arm开发板上交叉编译即可。
信号槽连接方面,源码里的习惯是把QFtp的每个信号单独接一个槽函数,而不是统一用lambda把所有信号包起来。这样做的好处是排查问题时,可以用断点直接定位到onFtpCommandFinished,看看具体是哪个命令ID执行失败。命令ID的传递是通过QFtp::CommandType枚举,比如登录是Login、切换目录是Cd、下载是Get,你在槽函数里打印command的值,就能知道是哪一步卡住了。
3. 把上传下载跑通:QT5编译配置、依赖安装与三个核心调用段
看完骨架之后,把代码编译跑起来才是正经事。这一章我按从零开始的角度,把依赖、编译、调用顺序拆开讲清楚。
3.1 编译之前:qtftp库、组件安装与pro文件核对
如果你拿到的是不带私有库的版本,先要编译qtftp。常见做法是把qtftp源码克隆到第三方目录,然后qmake&&make,得到libQt5Ftp.so和对应的头文件。需要注意qtftp的版本必须与QT5大版本匹配,比如QT5.15就用对应的5.15分支,否则符号链接可能出现undefined reference to QFtpPrivate::...的链接错误。
cd /path/to/3rdparty git clone https://github.com/qt/qtftp.git cd qtftp git checkout v5.15.0 qmake make -j4 sudo make install编译完成后,在工程pro文件里加上QT += network和LIBS += -lQt5Ftp,并在代码里#include <QFtp>。如果是嵌入式交叉编译,不要急着sudo make install,直接把源码以.pri文件方式include进主工程更省心——独立so文件在arm板上还需要拷贝到开发板的/usr/lib目录,多一步就多一个出错的环节。
3.2 登录与目录切换:connectToHost/login/cd的调用顺序
登录在QFtp里是一个典型的异步三步走:先连主机,再发用户名密码,成功后才能操作目录。下面这段是源码里核心连接部分的提炼,列出了关键信号的处理逻辑。
#include <QFtp> #include <QFile> #include <QDebug> QFtp *ftp = new QFtp(this); // 连接到服务器,端口默认21 ftp->connectToHost("192.0.2.10", 21); // 这里不能立即调用login,必须等连接成功 // QFtp内部有命令队列,但connect失败时队列会被清空 connect(ftp, &QFtp::stateChanged, this, [=](int state) { if (state == QFtp::LoggedIn) { qDebug() << "登录成功"; ftp->list(); // 登录成功之后再拉取文件列表 } else if (state == QFtp::Unconnected) { qDebug() << "连接断开"; } }); connect(ftp, &QFtp::commandFinished, this, [=](int id, bool error) { if (error) { qDebug() << "命令执行失败, 错误码:" << ftp->error() << ftp->errorString(); } }); ftp->login("anonymous", "test@example.com");这段代码最常见的运行结果不是直接出错,而是"没反应"——因为stateChanged里的判断条件没覆盖到Connecting和Connected两个中间状态。QFtp的连接状态机依次是Unconnected、HostLookup、Connecting、Connected、LoggedIn,如果你只在LoggedIn时触发list,那前几个状态变化时什么都不会发生,直到登录成功才去list,这种写法本身没问题,但要注意QFtp::commandFinished的信号里传的id对应的是每次调用的返回值,你需要保存login和list的id才能区分回调是哪条命令触发的。
commandFinished的error参数是关键,它只在当前命令执行失败时为true,而且一旦某条命令失败,后续排队的命令不会继续执行。所以实际开发中,建议在commandFinished里判断id,比如id == loginId时检查error,如果失败就不再执行任何FTP操作,直接提示用户并断开。
3.3 上传与下载:get/put的参数与进度信号
上传和下载在QFtp里的API是对称的:get从远程取文件到本地QIODevice,put把本地QIODevice的数据写到远程。源码里为了让UI和业务分离,把QFile对象和缓冲区绑定得比较清晰,核心调用如下:
// 下载文件到本地 QFile *localFile = new QFile("/tmp/downloaded.dat"); localFile->open(QIODevice::WriteOnly | QIODevice::Truncate); int getId = ftp->get("/remote/path/file.dat", localFile); connect(ftp, &QFtp::dataTransferProgress, this, [=](qint64 done, qint64 total) { qDebug() << "下载进度:" << done << "/" << total << "百分比:" << (total > 0 ? done * 100 / total : 0); }); connect(ftp, &QFtp::commandFinished, this, [=](int id, bool error) { if (id == getId) { localFile->close(); if (!error) { qDebug() << "下载完成"; } } });注意:get的第二个参数如果传了QIODevice指针,QFtp会把数据直接写入这个设备;如果不传,需要你在downloadProgress为例的槽里自己读取QByteArray缓冲区。进度信号里有total这个参数,但FTP服务器如果不返回文件大小,total可能是-1,UI上就不能显示百分比,只能显示速率。
put上传时有一个容易忽略的点:QFile打开后,写入数据前需要把文件指针seek到文件末尾,否则QFtp内部会从当前位置开始读,读出来的数据不完整。源码里对此的处理是:
QFile *uploadFile = new QFile("local.bin"); if (!uploadFile->open(QIODevice::ReadOnly)) { qWarning() << "无法打开本地文件"; return; } // 关键:确保从文件头开始读 uploadFile->seek(0); int putId = ftp->put(uploadFile, "/remote/upload.bin");这里还有个隐藏逻辑:put传输完成后,QFtp会关闭这个QIODevice吗?答案是会。所以不要在外面再手动close,避免二次关闭导致崩溃。如果你在put之后还需要读这个文件,就应该复制一份或者重新打开。
3.4 列表解析:list信号中读取数据与文件名提取
FTP的list命令返回的是一行行文本,格式因服务器而异,windows的IIS跟linux的vsftpd返回格式完全不同。QFtp把原始文本通过listInfo信号给出,但这个信号只有在调用list()时带QIODevice参数的情况下才会发出结构化数据,否则只发出一个ListInfo列表,里面每一行是一个QUrlInfo对象,包含文件名、大小、权限等属性。
connect(ftp, &QFtp::listInfo, this, [=](const QUrlInfo &info) { QString name = info.name(); qint64 size = info.size(); bool isDir = info.isDir(); qDebug() << "文件名:" << name << "大小:" << size << "是目录:" << isDir; });如果服务端返回的列表编码非UTF-8,比如Windows中文版默认GBK,这里的name就会直接显示乱码。这种问题绕不开,QFtp不会帮你做转码,因为FTP协议本身没有强制规定控制连接的字符集,所有文件名都是字节流,QFtp只是原样透传。看到乱码时,先确认服务端是Windows还是Linux,再决定是在服务端改编码,还是在客户端做编码转换。这个细节第五章会展开讲。
4. 嵌入式Linux怎么落地:交叉编译qtftp与无界面运行改造
这份源码的第二个应用场景是嵌入式Linux,关键词里也有"嵌入式linux",说明开发者要把它挪到arm板子上。嵌入式的坑跟桌面端完全不同,主要是交叉编译链、openssl依赖、显示环境这三座山。
4.1 交叉编译前的openssl检查:本地编译和arm端编译的区别
qtftp在编译时如果检测到系统有openssl头文件,会默认开启FTPS支持(隐式TLS),这在本地PC上没问题,但交叉编译时arm板子的根文件系统里很可能没有openssl库,或者版本不对,结果就是链接时直接报错找不到libcrypto.so。编译前先确认以下几点:
| 检查项 | 本地PC | 嵌入式arm |
|---|---|---|
| openssl头文件 | /usr/include/openssl | 交叉编译工具链sysroot下要有 |
| libssl.so | /usr/lib/x86_64-linux-gnu | 需要提前交叉编译openssl |
| qtftp是否强制开启ssl | configure时自动检测 | 可手动禁用 |
# 交叉编译qtftp时,强制不启用SSL # 在qtftp源码目录下创建qmake.conf cat > qmake.conf << EOF CONFIG -= ftps DEFINES += QT_NO_SSL EOF # 或者直接修改qtftp.pro # 注释掉 contains(QT_CONFIG, openssl) 相关的EnableOpenSSL逻辑如果你确实需要FTPS,那就得先交叉编译openssl,再把openssl的头文件和库路径通过LIBS变量传给qtftp的qmake。这个链路比较长,如果项目里只是局域网内同步文件,建议直接关掉SSL,省掉一大串依赖,等整体跑通了再加也不迟。
4.2 用QCommandLineParser改造:让工具支持命令行参数
嵌入式场景通常没有显示器,FTP工具被设计成由脚本或系统定时任务触发的同步工具,而不是交互式窗口。因此源码需要一个命令行入口,用QCommandLineParser解析服务器地址、用户名密码、上传下载方向等参数,这样开发板的shell脚本里可以这样调用:
./ftp_tool --server 192.0.2.10 --user sync --passwd 123456 --download /tmp/data --remote /opt/files核心改造代码大致如下:
#include <QCommandLineParser> QCommandLineParser parser; parser.setApplicationDescription("FTP sync tool for embedded linux"); parser.addHelpOption(); QCommandLineOption serverOpt("server", "FTP server address", "address"); parser.addOption(serverOpt); QCommandLineOption userOpt("user", "login username", "user"); parser.addOption(userOpt); QCommandLineOption passwdOpt("passwd", "login password", "passwd"); parser.addOption(passwdOpt); QCommandLineOption downloadOpt("download", "local download path", "path"); parser.addOption(downloadOpt); parser.process(QCoreApplication::arguments()); QString server = parser.value(serverOpt); QString user = parser.value(userOpt); QString passwd = parser.value(passwdOpt); QString localPath = parser.value(downloadOpt); if (server.isEmpty() || user.isEmpty() || passwd.isEmpty()) { parser.showHelp(1); return 1; }注意这款工具源码原本是带界面的,改造为命令行工具时,需要把MainWindow里的信号槽调用逻辑抽到一个不依赖QWidget的FtpWorker类中。具体做法是保留原工程的FtpWorker类,让main.cpp里构造QCoreApplication(而非QApplication),然后直接new FtpWorker并传参。这一步是嵌入式改造的关键,很多人在这一步翻车,多半是因为界面代码和逻辑代码耦合太深,拆不开,那这份源码的模块划分反而帮了你忙,因为它的业务逻辑本来就在独立的worker类里。
4.3 linuxfb或offscreen插件:不带显示器的FTP工具怎么验证
嵌入式板子上即使没有屏幕,QT程序启动时也会去找显示插件,找不到qt5的platform plugin就会直接崩溃退出。方法有两种:一种是用-platform offscreen参数让程序在无显示环境下运行,适合纯业务验证;另一种是交叉编译时带linuxfb插件,通过framebuffer输出到终端屏,适合板子带了小屏幕的场景。
# 无显示环境下运行FTP工具 ./ftp_tool -platform offscreen --server 192.0.2.10 --user test --passwd 123456 --download /tmp # 或者使用linuxfb直接输出到framebuffer ./ftp_tool -platform linuxfb --server 192.0.2.10 --user test --passwd 123456 --download /tmp如果你的QT交叉编译套件没编译offscreen插件,qtbase的configure里需要加上-qpa offscreen。还有一种稳一点的做法:在程序启动时检查QT_QPA_PLATFORM环境变量,没有设置的话自动选择offscreen:
#include <QProcessEnvironment> QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); if (!env.contains("QT_QPA_PLATFORM")) { qputenv("QT_QPA_PLATFORM", QByteArray("offscreen")); }这段代码要放在创建QApplication之前执行,否则插件加载时机已经过了。实际开发中我一般建议写在 main.cpp 的第一行,虽然土但管用。
5. 避坑与常见问题:超时、乱码、被动模式与SSL握手失败
QFtp用得久了你会发现,真正的问题不是它功能少,而是它行为遵守FTP协议,FTP服务器们却不总是遵守FTP协议。这一章我把高频踩坑记录集中整理,每条都给原因和解决路径。
5.1 登录成功但list无响应:超时定时器要自己加
现象:登录正常,但执行list()之后UI一直转圈,没有任何信号触发。
原因:FTP默认端口21,但服务器如果配置了被动模式数据端口范围,而你的客户端没在规定时间内建立数据连接,服务器就卡在等待状态,QFtp本身没有超时机制,命令队列挂起后所有后续命令全部停滞。
解决:给QFtp的每次命令启动一个QTimer,超过15秒没有commandFinished就调用abort()并重新连接。源码里可以在commandStarted信号里重启定时器,commandFinished里停止定时器。
QTimer *ftpTimer = new QTimer(this); ftpTimer->setInterval(15000); ftpTimer->setSingleShot(true); connect(ftp, &QFtp::commandStarted, this, [=](int) { ftpTimer->start(); }); connect(ftp, &QFtp::commandFinished, this, [=](int, bool) { ftpTimer->stop(); }); connect(ftpTimer, &QTimer::timeout, this, [=]() { ftp->abort(); qWarning() << "FTP命令超时,已中止"; });5.2 下载大文件卡在99%:TYPE I二进制模式与缓冲刷新
现象:下载几百兆文件,进度条到99%就不动了,文件大小也对不上,再等几分钟才完成,或者直接失败。
原因:FTP传输默认可能是ASCII模式,ASCII模式下遇到0x0A这种换行符,服务端会做转换,导致实际传输字节数与文件原始字节数不一致,而且QT的QFile只是写到本地缓冲,数据没fsync到磁盘。如果接收数据时正好缓存满了,QIODevice::write的阻塞时间会长得很离谱。
解决:在登录成功后立即发TYPE I命令(二进制模式),QFtp没有直接暴露这个命令的API,但可以通过rawCommand方式发送:
// QT5的QFtp并没有公开rawCommand方法 // 需要通过QFtp私有实现的CommandType枚举发送二进制模式命令 // 常见做法是在登录后调用: ftp->put(Q_NULLPTR, QString(), QFtp::Binary);准确说,QT5的QFtp源码中确实没有公开的SetTransferType方法,但协议栈的私有部分对put和get的第三个参数做了区分:QFtp::Binary和QFtp::Ascii。所以正确的做法是每次调用put/get时,第三个参数都显式传QFtp::Binary,不要用默认值。很多人在默认参数上栽过跟头,特此强调。
5.3 中文文件名的乱码:FTP字符集没有标准答案
现象:Windows IISH服务端的文件列表在Linux客户端显示乱码,上传的中文文件名在Windows上下载时又变成乱码。
原因:FTP协议规定文件名是字节序列,但没规定编码。Windows中文系统默认GBK,Linux和macOS默认UTF-8。QFtp原样透传字节,不做任何转换,谁拿到字节以谁的编码显示必然有一边是乱的。
解决:判断服务器类型后,手动从GBK转成UTF-8,反向时从UTF-8转成GBK。用QTextCodec转换:
#include <QTextCodec> QByteArray rawName = info.name().toUtf8(); QTextCodec *gbkCodec = QTextCodec::codecForName("GBK"); if (gbkCodec) { QString decodedName = gbkCodec->toUnicode(rawName); qDebug() << "转换后的文件名:" << decodedName; }注意QT5.15之后QTextCodec虽然是兼容包,但UTF-8和GBK转换基本都可用,嵌入式环境下记得在pro文件里QT += core5compat。如果服务器支持UTF-8,还可以在登录后发送OPTS UTF8 ON命令,能解决一部分服务器的列表乱码,但不是所有服务器都识别这个命令,识别不了就静默返回500错误,这时就要靠客户端转码兜底。
5.4 嵌入式arm上SSL握手失败:qtftp依赖openssl的版本指纹
现象:LD_LIBRARY_PATH里明明有libssl.so,QFtp连接FTPS服务器时依然The TLS/SSL connection has been established之后立即断开,日志提示ssl handshake failed。
原因:qtftp在引用openssl时是动态加载的,如果开发板上的openssl版本和编译时不一致,尤其当openssl 1.0.2和1.1.1对TLS扩展的处理行为不同时,握手就失败。更隐蔽的问题是openssl的engine路径不同,导致客户端证书加载失败。
解决:最稳的办法是交叉编译qtftp时彻底禁用openssl,用不加密的纯FTP跑通业务,再在应用层做数据校验;如果业务必须传输敏感数据,就不要依赖qtftp自带的FTPS,改为应用层加密(比如预共享密钥的AES),把文件加密后再走FTP明文传输。这两个方案背后的逻辑是一致的:qtftp的TLS支持是轻量实现,没有QT内置的QSslSocket那一整套证书管理工具链,出现问题排查成本极高。
5.5 主动模式连不通:QFtp默认被动模式的边界
现象:两台设备在不同局域网,QFtp能正常登录、list,但下载时卡住,或者提示Connection refused。
原因:QFtp默认使用被动模式(PASV),客户端等待服务器开放数据端口。但服务器端防火墙如果禁用了被动端口范围,数据连接建不起来;反过来,某些老式服务器只支持主动模式(PORT),需要客户端监听数据端口,而QFtp没有公开切换主动被动模式的API。
解决:如果确认服务器只支持主动模式,需要改qtftp源码,在QFtpPrivate的构造函数中把transferMode初始化为Active:
文件位置:3rdparty/qtftp/qftp.cpp
QFtpPrivate::QFtpPrivate() : QObject(), state(QFtp::Unconnected) { transferMode = QFtp::Passive; // 改成 Active // 其他初始化... }注意这里的Active转移模式在NAT网关环境下又会有新问题——客户端监听的端口无法从外部访问。所以实际项目里,我一般建议优先和后端确认能不能开放被动模式端口范围,主动模式是万不得已的兜底方案。
6. 进阶:给这份源码加断点续传的改造思路
QFtp没有公开的断点续传接口,但FTP协议本身支持REST命令,续传的本质就是发完REST offset之后再发RETR文件名,让服务端从指定偏移量开始传。QFtp的底层协议栈其实处理了REST命令,只是没暴露到public API,需要动一点手术。
6.1 REST命令与QFtp源码改造
找到qtftp源码里的qftp.h,在QFtp类的public方法区加一个方法声明,同时在qftp.cpp里实现它,方法是构建一条REST命令让协议栈发送,并把内部状态机切换到期望接收数据的状态。简化实现如下:
// qftp.h 中增加 void restartTransfer(qint64 offset); // qftp.cpp 中实现 void QFtp::restartTransfer(qint64 offset) { Q_D(QFtp); d->restartOffset = offset; d->sendCommand(QFtp::Restart, QString::number(offset)); }qtftp的私有状态枚举里已经有Restart这个命令类型,在FtpCommand定义里能找到它。关键是Restart命令得到350应答后,紧跟着的RETR命令要带着这个offset去请求数据。这需要在QFtpPrivate的processReply里补一段逻辑,把restartOffset在发送RETR/STOR时传给数据引擎。这块代码属于源码的私有逻辑,正常渠道不会给你打包,需要自己在QFtp源码的processReply中找350应答的分支,把偏移量保存下来,再在startTransfer方法里准备数据时读取。
改造完成后,调用时序变成:登录成功后,发一个restartTransfer(offset),确认命令完成后,再发get(file, dev, QFtp::Binary)。这样服务端就只传输offset之后的内容。
6.2 断点续传的验证方法
验证续传功能最直接的办法是用本地文件做对照实验。准备一个100MB的文件,先正常下载到30%的时候手动断网,记录已下载字节数;恢复网络后,用restartTransfer(已下载字节数)续传。注意FTP续传的字节数是按服务端RETR返回的流从头计数的,而客户端已保存的字节数也是从头计的,两者理论上相等。
qint64 localFileSize = localFile->size(); ftp->restartTransfer(localFileSize); conn = QObject::connect(ftp, &QFtp::commandFinished, this, [=](int id, bool err) { if (id == restartId && !err) { // 确认REST成功后,继续执行get int getFileId = ftp->get(remotePath, localFile, QFtp::Binary); // 把getFileId保存下来,在commandFinished里判断是下载完成 } });验证完成后对比本地文件和远程文件的大小是否完全一致,再对比MD5。如果有MD5不一致的情况,八成是REST命令没有先于RETR执行,服务端仍然从0开始传了一遍,但客户端没清空原有文件造成拼接问题。从那以后我再做断点续传,每次都要确认登录后第一条命令是REST,且收到350应答才允许发RETR,这个顺序写过一次就不会再错了。希望帮到你。
本文还有配套的精品资源,点击获取