☰
QT之FTP上传下载工具源码解析:选型、实现与避坑指南
2026/10/15 12:41:12 网站建设 项目流程

简介:这是一份基于QT5框架的FTP客户端功能工具源码,面向需要在Windows、Linux及嵌入式Linux等平台实现FTP文件上传与下载的C++开发者。项目通过QNetworkAccessManager与QFtp类的配合,封装了FTP服务器连接、用户登录、目录浏览、文件传输等核心流程,免去开发者单独下载编译QFtp类的繁琐步骤,可直接集成到现有QT工程中快速使用,适用于工业控制、嵌入式设备数据同步及局域网文件管理等多种场景。 资源压缩包共包含12个文件,以cpp源码、h头文件和ui界面文件为主,另有工程配置信息及两张目录/文件图标示意图片,整体包体仅32KB,结构紧凑清晰。核心代码覆盖FTP下载与上传调用、URL地址解析处理和界面布局设计,便于对照学习完整QT项目的模块划分、信号槽衔接与界面交互组织方式。 目前已有1097人浏览学习。对研究QT网络编程或需快速搭建FTP工具的开发者而言,该源码提供了可运行的基础框架,涵盖下载、上传的完整示例,并支持传输进度监控与状态实时反馈。借助QT的信号与槽机制,可深入理解FTP会话管理、异常处理与界面更新思路,并能在其基础上扩展FTPS或SFTP加密传输,掌握跨平台网络应用的实际开发模式。

1. 这个QT之FTP上传下载工具源码,解决的是谁的痛点

拿到一份“QT之FTP上传下载等功能工具源码”时,我第一反应不是看界面长什么样,而是看它用的是哪条FTP技术路线。因为Qt官方对FTP的支持在版本更迭中被砍过、移过、改过,路线选错了,后面所有二次开发都是给自己挖坑。这类工具解决的是典型的内网文件流转问题:设备固件批量分发、配置文件远程更新、产测工位上传测试结果。对这些场景来说,现成的FTP客户端软件能完成任务,但没法嵌入自家业务流程,而“源码”的价值就在于可以改造成带进度条、带任务队列、带断点续传的定制化工具。

读者通常分两类:一类是刚接触Qt,想找个能跑的FTP例子改改界面;另一类是手头已经有一份老源码,编译不过、乱码、大文件崩溃,需要知道坑在哪。这篇笔记按后者的视角写,但前一条路也覆盖:从选型到骨架再到四个核心功能,最后给断点续传和自测技巧。如果你只是要一个能用的内网小工具,看完第二、三章基本就能决定值不值得动手。如果你想改造源码,第四章和第五章是重点。

2. 实现路线选型:QFtp、QNetworkAccessManager与libcurl怎么选

FTP协议本身不复杂,控制连接走命令,数据连接走文件内容,难点在于两个连接怎么协同、被动模式和主动模式怎么切、断点续传怎么发偏移命令。Qt生态里做FTP有三条常见路线,每条路线的维护状态、坑的数量、二次开发成本完全不一样。拿到源码先翻它的include和网络相关目录,确认它是哪条路线,再谈怎么改。

2.1 QFtp老源码的最大门槛是它已经不在官方包里

如果你看到的源码里有#include <QFtp>或者QFtp *ftp = new QFtp(this);,恭喜你拿到的是Qt4时代的代码。QFtp这个类在Qt4年代确实是最好用的FTP接口:login、list、get、put、removeDir都有对应的槽和信号,写起来像在操作本地文件系统。但从Qt5开始,官方包不再包含QFtp,它被挪到社区维护的独立模块里,编译时需要自己把模块源码拷进工程或者修改.pro文件去引用。

更麻烦的是,Qt6之后官方连单独的模块都不再跟进,QFtp的信号槽机制在新版Qt上编译会碰到moc和元类型注册的兼容问题。所以我在判断一个“QT之FTP上传下载等功能工具源码”值不值得用时,第一眼只看一件事:QFtp出现则谨慎,QNetworkAccessManager出现则可以继续看。老源码如果业务逻辑不复杂,把QFtp调用换成QNetworkAccessManager的请求不是很难,但前提是你愿意动网络层代码,而不是只改几个按钮。

2.2 QNetworkAccessManager:Qt5时代的默认FTP答案

QNetworkAccessManager是Qt5里默认的网络入口,虽然大家平时拿它发HTTP请求,但它在Qt5中确实保留了FTP协议支持。用起来和HTTP几乎一样:把URL设为ftp://协议,登录信息放进URL里,然后发get或put请求。它内部自己管理控制连接和数据连接,会话层面的PASV、PORT、RETR、STOR都由后端处理,使用者不用碰协议细节。这是大多数“QT之FTP上传下载等功能工具源码”选择的路线,也是我通常会优先推荐的路线。

这条路线有一个明确边界:只有Qt5适用。Qt6版本的QNetworkAccessManager已经不再支持ftp协议,如果项目已经升级到Qt6,就不能指望这条路线,需要转第三方库。另外QNetworkAccessManager对FTP的控制粒度比较弱,比如主动/被动模式它不会给你单独开关,断点续传要靠请求头的偏移设置,这些细节后面单独讲。整体判断就是:源码基于Qt5、场景是内网工具、不需要FTPS加密,直接用这条路线二次开发是最省事的。

2.3 libcurl路线:什么场景才值得引入第三方库

当需求里出现“要支持FTPS”“要兼容SFTP”“要同时对接多个FTP服务器且各自被动端口范围不同”,QNetworkAccessManager就有点扛不住了。我一般会选libcurl封装路线。libcurl是成熟的命令行工具库,FTP、FTPS、SFTP都支持,CURLOPT_* 选项几百个,从被动模式开关到传输模式都能精确控制。Qt代码里封装libcurl,通常做法是写一个FtpTransfer类,内部持有CURL句柄,通过信号把进度和完成状态抛给界面。

代价也很明显:需要处理第三方库的编译和分发。libcurl依赖OpenSSL或者系统TLS库,换一台机器可能缺DLL或so文件。这就是为什么我在多数情况下不会轻易引入libcurl,除非需求里确实需要加密传输或跨协议支持。对一份以“上传下载工具”为目标的源码来说,先确认它有没有引入libcurl相关头文件,如果有,还要看它的依赖项是否随项目一起提供了,否则编译通过也只能在自己机器上跑。

2.4 三种方案对比与我的选型习惯

用一个表来收束选型会清晰很多。这里不列空洞的优缺点,只写对二次开发有实际影响的维度。

路线API易用性断点续传支持FTPS/SFTP支持Qt6兼容二次开发成本
QFtp / QtFtp模块高,信号槽直观需要自己处理REST偏移不支持编译困难源码改造工作量大
QNetworkAccessManager中,按HTTP风格调部分支持,看后端翻译不支持不支持ftp协议适合多数内网工具
libcurl封装低,需管理CURL句柄完整支持支持兼容良好需要解决依赖分发

我的选型习惯很简单:源码本身是QNetworkAccessManager路线,需求又不超过内网和明文FTP,就坚持用下去,不要因为某个功能实现费劲就想着换库。源码本身是QFtp老代码,先评估QFtp调用数量,Qt5项目直接把网络层替换成QNetworkAccessManager,Qt6项目就直接跳到libcurl封装。真正决定要不要换路线的,不是好不好写,而是目标机器上能不能跑起来。FTP工具最终要丢到甲方内网或者产线机器上,依赖越少越可靠。

提示:拿到源码先别急着编译,花十分钟确认网络层用的是哪条路线,能省下之后大部分排错时间。

3. 源码骨架搭建:FtpClient类怎么拆分UI与网络逻辑

确认路线之后,下一步是看源码的工程结构。实用性强的FTP工具源码,界面和网络逻辑一定是分开的,而不是把上传下载代码直接写在按钮的槽函数里。原因很简单:一个工具后面会有上传任务、下载任务、目录刷新三个动作同时发生,如果把QNetworkReply的生命周期散落在各个界面方法里,几乎无法管理取消和重试。

3.1 界面拆法:目录树、任务列表与日志区

我习惯把这类工具界面拆成四块:左上连接参数区,填主机、端口、用户名、密码;左侧目录区用QTreeView展示远程目录和文件;右侧任务区用QTableWidget显示当前上传下载任务、进度、状态;底部是日志区,用QPlainTextEdit记录协议层消息。界面代码和网络代码只通过信号槽通信,界面完全不碰QNetworkReply对象。这样源码给别人二次开发时,改界面不会动网络逻辑,换认证方式也不会动界面。

任务区的每一行,我会在Qt::UserRole里存一个任务对象指针,任务对象里记录本地路径、远程路径、类型、当前状态、QNetworkReply指针。这样用户点“取消”按钮时,只需要从当前行取出任务对象,然后对它调用cancel方法。界面刷新进度时,也只绑定任务的信号。这种结构看起来比在按钮里临时new一个reply要绕,但到后面加断点续传、加批量队列时,你会发现这个绕是省时间的。

3.2 FtpClient类的头文件:信号、槽与任务映射

网络层我通常会封装成一个FtpClient类,构造函数收主机端口和认证信息,对外暴露listDir、download、upload、cancelAll四个方法。这个类内部持有一个QNetworkAccessManager,以及一个从QNetworkReply到本地文件对象的映射。下面是一个可以直接抄的骨架头文件。

#ifndef FTPCLIENT_H #define FTPCLIENT_H #include <QObject> #include <QNetworkAccessManager> #include <QNetworkReply> #include <QUrl> #include <QHash> class FtpClient : public QObject { Q_OBJECT public: // host: FTP服务器地址或IP // port: 默认21,如果服务器用了非标端口就传实际值 // user/password: 登录凭证,不支持匿名情况下必填 FtpClient(const QString &host, quint16 port, const QString &user, const QString &password, QObject *parent = nullptr); void listDir(const QString &remotePath); void download(const QString &remoteFile, const QString &localFile); void upload(const QString &localFile, const QString &remoteFile); void cancelAll(); signals: // 目录列表返回后一次性抛出,items里是文件名和大小 void listFinished(const QString &path, const QList<QPair<QString, qint64>> &items); void downloadProgress(const QString &remoteFile, qint64 received, qint64 total); void uploadProgress(const QString &remoteFile, qint64 sent, qint64 total); void operationFinished(const QString &remoteFile, int type, bool success); private: QUrl buildUrl(const QString &remotePath); QNetworkAccessManager m_manager; QHash<QNetworkReply*, QFile*> m_fileMap; // 下载中的reply对应本地文件 QString m_host; quint16 m_port; QString m_user; QString m_password; }; #endif // FTPCLIENT_H

头文件的逻辑说明都在注释里,这里补三点容易被忽略的参数约束。端口参数要单独存,不要拼到URL后面再解析,因为后面断点续传时要反复拼URL,存原始值最省事。密码里如果带有@、/、#这类特殊字符,不要手写字符串拼接URL,要用QUrl的setPassword方法,它会自动做百分号编码。m_fileMap这个映射很关键,它保证在多个下载任务并发时,每个reply的数据只落到自己的文件里,不会串数据。

3.3 登录与URL构造:认证信息放进URL的注意点

FTP的登录在QNetworkAccessManager里不单独处理,每次请求的URL里带认证信息,后端会在建立控制连接时自动发USER和PASS。所以buildUrl是FtpClient里最基础也最容易出错的方法。常见错误是把路径直接拼在主机后面,比如ftp://user:pass@host//root/ftp,三个斜杠会让路径解析出问题。正确做法是用QUrl的setPath,把整个远程路径交给它处理。

QUrl FtpClient::buildUrl(const QString &remotePath) { QUrl url; url.setScheme("ftp"); url.setHost(m_host); url.setPort(m_port); url.setUserName(m_user); url.setPassword(m_password); // remotePath以/开头表示服务器绝对路径,这是FTP服务的惯例 url.setPath(remotePath.isEmpty() ? "/" : remotePath); return url; }

这里有几个细节值得单独说。setUserName和setPassword会自动做百分号编码,所以密码里有特殊字符时安全,编码后的URL显示会比较长,但不要为了好看手动去改。setPath传绝对路径即可,不要在前面再加斜杠,QUrl会处理路径分隔符。如果你访问的是服务器默认目录下的文件,remotePath传相对路径也行,但我会统一用绝对路径,避免登录后默认目录不确定导致文件写错位置。登录失败的表现在后面排查章节再展开,这里只需要理解认证是随请求走的。

4. 上传下载核心实现:从LIST到STOR的落地代码

骨架搭好之后,核心就是四个功能:目录列表、下载、上传、取消。这一章给的是可以直接编译进工程的实现,参数细节放在每段代码后面说。FTP的目录列表用的是LIST命令,下载对应RETR命令,上传对应STOR命令,QNetworkAccessManager把这些命令封装成了普通请求,所以代码读起来不像FTP协议调试器,更像HTTP客户端。

4.1 目录列表:一份LIST响应能有多少种格式

QNetworkAccessManager触发LIST命令有一个关键点:当访问的URL路径以斜杠结尾,或者显式设置了FtpListAttribute属性时,get请求会发送LIST命令,否则发送RETR下载。很多人在这踩坑,明明是列目录,结果把整个目录当文件下载了。下面这段实现同时设置了属性,保证行为可预测。

void FtpClient::listDir(const QString &remotePath) { QNetworkRequest request(buildUrl(remotePath)); // 强制该请求发送LIST命令而不是RETR下载 request.setAttribute(QNetworkRequest::FtpListAttribute, true); QNetworkReply *reply = m_manager.get(request); connect(reply, &QNetworkReply::finished, this, [this, reply, remotePath]() { QByteArray data = reply->readAll(); QList<QPair<QString, qint64>> items; QStringList lines = QString::fromUtf8(data).split('\n'); for (const QString &line : lines) { // Unix风格LIST输出:权限 链接数 属主 属组 大小 日期 文件名 QStringList parts = line.split(QRegularExpression("\\s+")); if (parts.size() >= 9 && parts.at(0).startsWith('-')) { bool ok = false; qint64 size = parts.at(4).toLongLong(&ok); // 文件名可能带空格,取第9列之后的所有字段 QString name = parts.mid(8).join(' '); if (ok && !name.isEmpty()) items.append(qMakePair(name, size)); } } emit listFinished(remotePath, items); reply->deleteLater(); }); }

LIST响应没有统一标准,Unix风格、Windows风格、IIS风格都存在。上面这段只解析Unix风格,是FTP服务端最常见的输出,但对Windows服务器的返回值会解析失败。常见做法是先用reply->url().path()判断服务器类型,或者干脆只取每行最后一个字段当文件名,大小字段另做容错。这里的parts.at(4)在Unix风格里是字节数,如果解析失败就跳过这条记录,保证界面不会因为一条异常行崩溃。QRegularExpression("\s+")按连续空白切分,比按单个空格split更稳,因为服务器可能用制表符对齐列。

4.2 下载落地:QNetworkReply按readyRead写文件

下载的核心逻辑是不要把整个文件读进内存再写盘,而是利用reply的readyRead信号,收到一块数据就往本地文件里写一块。这样下载2GB文件时,内存占用始终维持在一个小范围内。另一个关键点是本地文件的打开时机必须在请求之前,否则reply开始返回数据时文件还没创建,数据就丢了。

void FtpClient::download(const QString &remoteFile, const QString &localFile) { QFile *file = new QFile(localFile); // 下载必须用WriteOnly,不能用Append,除非是断点续传场景 if (!file->open(QIODevice::WriteOnly | QIODevice::Truncate)) { emit operationFinished(remoteFile, TypeDownload, false); delete file; return; } QNetworkReply *reply = m_manager.get(QNetworkRequest(buildUrl(remoteFile))); m_fileMap.insert(reply, file); connect(reply, &QNetworkReply::readyRead, this, [this, reply]() { // m_fileMap保证了多个并发下载时文件与reply一一对应 QFile *f = m_fileMap.value(reply, nullptr); if (f) f->write(reply->readAll()); }); connect(reply, &QNetworkReply::downloadProgress, this, [this, remoteFile](qint64 received, qint64 total) { emit downloadProgress(remoteFile, received, total); }); connect(reply, &QNetworkReply::finished, this, [this, reply, remoteFile]() { QFile *f = m_fileMap.take(reply); if (f) { f->close(); if (reply->error() != QNetworkReply::NoError) f->remove(); // 下载失败就删掉半截文件,避免下次被当成完整文件 delete f; } emit operationFinished(remoteFile, TypeDownload, reply->error() == QNetworkReply::NoError); reply->deleteLater(); }); }

写本地文件用WriteOnly加Truncate,会覆盖同名旧文件。如果是断点续传,这里要改成Append模式并配合偏移,后面第六章讲。readyRead里用m_fileMap.value(reply, nullptr)而不是直接在lambda里捕获file裸指针,是因为finished和readyRead的触发时机不固定,如果任务被cancel导致reply先被销毁,裸指针就成了悬垂指针。这个踩坑点我在后面避坑章还会展开。参数上,remoteFile是服务器上的绝对路径,localFile是本地保存路径,调用前需要确保本地目录已经存在,否则open失败会走错误分支。

4.3 上传文件:直接传QFile给put并看进度

上传比下载简单,因为QNetworkAccessManager的put方法接受一个QIODevice指针,内部会分块读取上传,不需要自己处理数据切分。容易翻车的做法是先把整个文件读进QByteArray再包成QBuffer,大文件一次吃掉几GB内存。正确做法是把打开的QFile直接传给put,让管理器逐步读取。

void FtpClient::upload(const QString &localFile, const QString &remoteFile) { QFile *file = new QFile(localFile); // 上传必须ReadOnly,put内部负责数据读取和进度上报 if (!file->open(QIODevice::ReadOnly)) { emit operationFinished(remoteFile, TypeUpload, false); delete file; return; } QNetworkRequest request(buildUrl(remoteFile)); // put内部会接管file对象,因此这里不要delete file QNetworkReply *reply = m_manager.put(request, file); connect(reply, &QNetworkReply::uploadProgress, this, [this, remoteFile](qint64 sent, qint64 total) { emit uploadProgress(remoteFile, sent, total); }); connect(reply, &QNetworkReply::finished, this, [this, reply, remoteFile]() { emit operationFinished(remoteFile, TypeUpload, reply->error() == QNetworkReply::NoError); reply->deleteLater(); }); }

put接管QFile所有权这一点要注意,reply完成后它自己会delete文件,不要再在自己写的代码里delete。uploadProgress的total参数在很多FTP服务端上可能是-1,因为数据连接建立前总大小未知,界面显示进度条时要做总大小未知的容错,比如显示已上传字节数而不是百分比。另外put默认走STOR命令,如果服务器对同名文件覆盖有特殊策略,比如需要先删除再上传,那就要在put之前先发一个删除请求,这个在源码改造时往往容易被忽略,表现为第二次上传同一文件失败。

4.4 取消与超时:abort之后还要收拾什么

取消操作最容易写漏。用户点击取消,直觉上是调用reply->abort(),但abort之后会触发finished信号,如果finished分支里还在写文件、还在发信号,就会出现“取消成功但界面还报错”的怪现象。同时abort会让reply的error变成OperationCanceledError,不能用NoError去判断成功,否则取消和失败被混在一起。下面是一个取消实现和它配套的超时保护。

void FtpClient::cancelAll() { // 枚举m_manager下所有活动reply,逐个abort const QList<QNetworkReply*> replies = m_manager.findChildren<QNetworkReply*>(); for (QNetworkReply *reply : replies) { reply->abort(); } }

超时保护的做法是用QTimer单发,超时后主动abort,避免服务器无响应时任务栏一直转圈。服务端无响应是FTP工具里很常见的情况,不处理的话会让工具看起来像卡死。超时时间我一般设15秒,控制连接建立和目录列表都比较快,超过15秒通常是网络或服务端出问题了。代码里把timer的父对象设为reply,reply销毁时timer自动清理,不必手动delete。abort之后检查reply->error()是否是OperationCanceledError,在finished槽里区分取消和失败,界面才能显示正确的状态。

5. FTP工具避坑指南:QFtp被移出、中文乱码与防火墙翻车

这一章是实际使用中高频踩坑记录的集合。每一条都有现象、原因、解决办法三部分。这些坑不是理论推导出来的,是翻车之后一步步查出来和拆出来的,直接照着排查能省大把时间。

5.1 编译报错:老源码第一眼先查QFtp

现象:工程文件里#include <QFtp>或者.pro里写了QT += network,编译时报QFtp头文件找不到,或者在链接阶段报一堆未定义符号。原因:QFtp在Qt5之后就不在官方包分发目录里了,.pro即使声明了net work也拿不到这个类。解决办法有两条路径。一是把老工程里的QFtp调用全部替换成QNetworkAccessManager,适合源码体量不大、只有上传下载几个功能的情况。二是找到Qt4兼容模块的源码,把ftp模块的cpp和h文件直接拷进工程一起编译,这条路适合已经写了很多基于QFtp的业务代码时用。我的建议是你只要想做二次开发,就选替换路线,因为QFtp的信号槽机制在新版Qt里迟早还会出兼容问题,晚改不如早改。

5.2 中文文件名乱码:协议编码问题的血泪经验

现象:上传一个叫“测试报告.bin”的文件,服务器上显示成乱码;或者从服务器下载一个中文名文件,本地保存下来的文件名是问号。原因:FTP协议没有统一规定控制连接和路径使用哪种字符编码,服务端用UTF-8,客户端用系统本地编码,一个字节一个字节错位,自然乱码。解决办法分两头处理。发送侧,构造URL时不要直接往路径里放中文,用QUrl::toPercentEncoding把中文转成百分号编码,让服务器按自己的编码解码。接收侧,目录列表拿到的字节流不要直接用fromUtf8,先尝试UTF-8解码,解码结果里出现无法映射的字符就换GBK再试一次。这个启发式方法不能100%覆盖所有服务器,但解决了绝大多数内网场景。

5.3 目录列表卡住:被动模式与防火墙的关系

现象:能登录,公共目录能看到,但某些目录打开后一直转圈,最后超时失败。原因:FTP的数据连接需要额外协商,主动模式是服务器主动连回客户端随机端口,被动模式是客户端去连服务器的数据端口。内网客户端往往有防火墙,而服务端没配好被动端口范围。QNetworkAccessManager在Qt5的FTP后端默认走主动模式还是被动模式取决于实现,但无论哪一种,都有可能因为防火墙丢包而卡死。解决办法是优先确认服务端的被动端口范围是否已放开,以及客户端所在网络是否允许发起非标准端口连接。如果只是内网工具,两端都开放对应端口就能解决问题。排查时看日志,如果卡在list阶段,先telnet一下服务端的数据端口范围,确认端口通不通。

5.4 大文件上传内存暴涨:不要把文件读进QByteArray

现象:上传1GB文件,内存以肉眼可见速度上涨,最后到了2G多崩溃。原因:代码里用QFile::readAll()读成大字节数组再丢给上传接口,或者更常见的是从网上抄了一段把文件转成QByteArray再包QBuffer的代码。解决办法就是把代码改成第四章上传那种写法,直接传打开的QFile给put,让网络层内部分块读取。这里有个判断口诀:凡是代码里出现“先读完再发”的,大文件必爆内存。FTP上传要做到内存占用与文件大小无关,才算是能用的工具。改完之后再看上传大文件,内存曲线应该是一条平稳直线。

5.5 取消下载闪退:QNetworkReply悬垂指针的玄学

现象:用户点下载后立刻点取消,或者下载完成瞬间关闭窗口,程序偶尔闪退,不是必现,但一出现就在指针相关代码里。原因:lambda里捕获了QNetworkReply裸指针,reply在abort或finished后已经销毁,lambda再访问它就成了悬垂指针。解决办法是两条。一是在lambda里改用QPointer包裹的reply对象,QPointer在对象销毁后自动变null,访问前先判空。二是在finished槽里先取走m_fileMap里的文件对象,再deleteLater,避免在销毁流程里访问已释放对象。这类问题凭静态看代码很难发现,跑多线程加取消操作就暴露。源码里如果所有connect都是裸指针捕获,基本可以断定没做过取消场景的完整测试。

5.6 退出程序卡死:线程与reply的清理顺序

现象:下载大文件过程中直接关窗口,程序没有立刻退出,任务管理器里进程还在,CPU却不高。原因:reply还在活动,文件句柄没关,QNetworkAccessManager析构时会在销毁活动请求时等待,或者手动创建的线程没退出。解决办法是在主窗口closeEvent里先cancelAll,把所有reply abort掉,再等待finished信号处理完成,最后关掉文件对象。如果代码里单独开了网络线程,则需要请求线程中断并join。一个稳的退出顺序是:abort所有请求,等所有finished发完,关闭m_fileMap里余留的文件,再退出事件循环。顺序不能反,反了就是偶现假死。

6. 断点续传与自测:把工具变成无人值守也可靠的工具

前面四章做出的是一个能用的FTP工具,但这章讲的是让它从“能用”变成“可靠”的关键技巧。可靠的定义是:断网重连后能继续传、传完能确认文件没问题、改动后能快速回归验证。

6.1 用Range头触发REST实现断点续传

断点续传的原理是FTP协议里的REST命令,它告诉服务端跳到文件的第N个字节继续传。QNetworkAccessManager的FTP后端会把请求里的Range头转成对应的REST偏移命令。实现时,下载前先看本地文件已存在的大小,然后带着偏移发起请求。代码如下。

void FtpClient::downloadResume(const QString &remoteFile, const QString &localFile) { QFileInfo info(localFile); qint64 existSize = info.exists() ? info.size() : 0; // 本地已下载字节数 QFile *file = new QFile(localFile); // 续传必须用Append,且seek到末尾,不能Truncate if (!file->open(QIODevice::WriteOnly | QIODevice::Append)) { emit operationFinished(remoteFile, TypeDownload, false); delete file; return; } file->seek(existSize); QNetworkRequest request(buildUrl(remoteFile)); if (existSize > 0) { // Range头格式是 bytes=起始字节- request.setRawHeader("Range", QString("bytes=%1-").arg(existSize).toLatin1()); } QNetworkReply *reply = m_manager.get(request); m_fileMap.insert(reply, file); connect(reply, &QNetworkReply::finished, this, [this, reply, remoteFile]() { QFile *f = m_fileMap.take(reply); if (f) { f->close(); if (reply->error() != QNetworkReply::NoError) f->remove(); delete f; } emit operationFinished(remoteFile, TypeDownload, reply->error() == QNetworkReply::NoError); reply->deleteLater(); }); }

续传的边界条件比普通下载多一个:如果服务端不支持REST命令,返回错误时本地文件会被我上面的代码删除,这其实是保护行为,清掉半截文件避免它被当成完整结果。另一个要注意的是,续传和覆盖下载是两种模式,界面上要分开按钮,不要自动续传。因为有时候用户就是要重新下载整个文件,自动续传会把旧文件内容留到新文件里,造成两份文件拼接的隐患。

6.2 上传后的校验思路

FTP协议没有标准的文件MD5校验命令,有的服务端支持MFMT或HASH扩展,但兼容性参差。可靠的办法是上传完成后把文件下载回来做比对,但这样耗时翻倍。折中方案是下载目录列表,比对服务器返回的文件大小与本地文件大小。目录列表里的数字是服务端算好的字节数,和本地大小相等基本能说明传完整了。如果对完整性要求苛刻,再考虑两次下载采样校验,取文件头、中间、末尾各几KB做MD5对比,不用整盘下载,就能发现绝大多数传输损坏问题。内网环境下这个方案性价比很高。

6.3 一条自测流程让改动可回归

每次改完FTP源码,我会用一个固定脚本验收,上传、下载、比对三步走。脚本逻辑很简单,但能覆盖协议层、认证层、读写层三个层面的回归。

#!/usr/bin/env bash set -euo pipefail # 前提:本机21端口已有FTP服务,且根目录可写 src=/tmp/ftp_test_$(date +%s).bin dst=/tmp/ftp_test_download.bin # 生成1MB随机文件,避免用全0文件掩盖数据问题 head -c 1048576 /dev/urandom > "$src" md5_before=$(md5sum "$src" | cut -d' ' -f1) # 假设源码编译出来的命令行程序叫 ftp_tool,子命令自行替换 ./ftp_tool upload "$src" /test.bin --host 127.0.0.1 --port 21 ./ftp_tool download /test.bin "$dst" --host 127.0.0.1 --port 21 md5_after=$(md5sum "$dst" | cut -d' ' -f1) if [ "$md5_before" = "$md5_after" ]; then echo PASS else echo FAIL exit 1 fi

这个脚本的用处是把早期上线时遇到过的中文乱码问题挡住。我遇到过一次改完编码逻辑,界面正常了,但下载回的文件和源文件MD5对不上,就是脚本在跑回归时发现的。之后我养成一个习惯:任何一次FTP网络层改动,不管多小,都先跑一遍这个脚本再合入版本。FTP工具源码改动频繁的是编码、认证、任务调度这几块,都直接关系传输结果,没有回归验证的改动就是在赌。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询