☰
Qt5/Qt6中QFtp缺失?可编译FTP客户端实现与避坑指南
2026/10/29 23:31:10 网站建设 项目流程

简介:这份资源面向使用 Qt5 及以上版本开发桌面应用的开发者,解决高版本 Qt 移除 QFtp 类后无法直接调用 FTP 功能的问题。作者从 Qt4.8.7 源码中提取出 QFtp.h 与 QFtp.cpp 两个文件,并配套编写了完整测试示例,覆盖登录 FTP 服务器、上传文件、删除文件、创建文件、重命名文件等常用操作,适合需要为客户端补充文件传输能力的中级 Qt 学习者参考。压缩包共 17 个文件,约 35KB,包含 5 个 cpp 与 4 个 h 源文件承载核心逻辑,3 个 ui 文件对应主窗口、重命名与新建文件夹对话框,另有 qrc 资源文件、pro 工程文件及图标图片,结构紧凑、便于直接编译运行。目前已有 1319 人学习下载。读者可从中获得可复用的 QFtp 封装源码、清晰的工程组织方式以及各 FTP 功能的调用范例,快速理解信号槽驱动的异步传输流程,并在此基础上扩展断点续传、目录遍历等需求。

1. Qt 里那个被藏起来的 QFtp:一份能直接编译的 FTP 客户端实现

手上有个工控上位机项目,设备端只认 FTP 协议做固件升级包和日志回传,界面用 Qt 写,结果翻遍 Qt5 文档发现 QFtp 类没了——它在 Qt4 时代属于 QtNetwork 模块,Qt5 之后被移出主库,官方说法是功能不完整、维护成本高。这时候要么自己拿 QTcpSocket 从零拼 FTP 命令,要么找一份现成的 QFtp 源码塞进工程。这份资源就是后者:一套可编译的 QFtp 实现,把 FTP 的控制连接、被动模式数据通道、命令队列、状态机都封装好了,接口风格贴近 Qt4 原生 QFtp,connectToHost、login、list、get、put、remove这些槽函数照旧能用。适合正在做 Qt5/Qt6 桌面端、需要跟老设备或老服务端对接的开发者,也适合想搞清楚 FTP 协议在 Qt 事件循环里怎么落地的人。下面按「先跑通、再改参数、最后避坑」的顺序拆。

2. 把 QFtp 塞进 Qt5/Qt6 工程:模块依赖与编译链路

2.1 先搞清楚它依赖哪些 Qt 模块

QFtp 本质是 QTcpSocket 之上的一层协议封装,控制连接走 21 端口,数据连接在被动模式下由服务端返回端口号后另开一条 TCP。所以它只依赖 QtCore 和 QtNetwork,不碰 GUI,也不依赖 QWidget。这一点很关键:意味着你可以把它编成静态库或直接以源码形式加入子项目,在无界面的服务端程序里也能用。

工程里常见的组织方式是把 QFtp 的.h/.cpp放进一个qftp/子目录,主工程.pro里加一行include(qftp/qftp.pri),pri 文件里写头文件路径和源文件列表。用 CMake 的话就是add_library(qftp STATIC ...)再target_link_libraries挂到主目标上。我一般倾向静态库,原因是 QFtp 内部有QFtpPrivate这种 PIMPL 结构,源码直接混进主工程容易和别的同名符号打架,独立成库边界清楚。

需要确认的编译开关:Qt5 下.pro里必须有QT += network,Qt6 下 CMake 里find_package(Qt6 COMPONENTS Network REQUIRED)。如果工程同时支持 Qt5 和 Qt6,QFtp 源码里对QRegExp的使用要留意——Qt6 把QRegExp挪到了 Qt5Compat 模块,源码里若用了它,要么改QRegularExpression,要么在 Qt6 构建时额外链接 Core5Compat。这是移植时第一个会翻车的地方。

2.2 最小可编译示例:连上、登录、列目录

先给一段能直接跑的最小验证代码,目的是确认库编进来了、信号槽通、能拿到目录列表。放在main.cpp里,控制台程序即可,不需要界面。

#include <QCoreApplication> #include <QDebug> #include "qftp.h" int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QFtp ftp; // 连接状态变化,用于观察握手过程 QObject::connect(&ftp, &QFtp::stateChanged, [](int state){ qDebug() << "state:" << state; }); // 命令执行完一条就回调一次,code 是 FTP 应答码 QObject::connect(&ftp, &QFtp::commandFinished, [](int, bool error){ qDebug() << "cmd done, error =" << error; }); // 目录列表逐条回来,这里只打印名字 QObject::connect(&ftp, &QFtp::listInfo, [](const QUrlInfo &info){ qDebug() << "entry:" << info.name(); }); ftp.connectToHost("192.168.1.50", 21); // 换成你的服务端地址 ftp.login("anonymous", "anonymous"); // 匿名登录,正式环境换账号 ftp.list(); // 列当前目录 ftp.close(); // 命令队列执行完后关闭 return app.exec(); }

逻辑说明:QFtp 内部维护一个命令队列,connectToHost、login、list、close依次入队,前一条的commandFinished触发后才发下一条,所以这里连续调用不会乱序。stateChanged的整型值对应QFtp::Unconnected/HostLookup/Connecting/Connected/LoggedIn/Closing,调试时看它走到LoggedIn就说明账号密码没问题。listInfo是逐条推送的,目录大时不要在里面做重活,攒到commandFinished再统一处理。

参数说明:connectToHost第二个参数是端口,默认 21;login两个参数分别是用户名和密码,匿名服务端通常接受anonymous/anonymous;list()不带参数列当前目录,带路径参数列指定目录。注意close()也是入队的,它会在前面所有命令完成后才真正断开,别在close()之后又调get(),那条命令会被丢弃。

2.3 主动模式还是被动模式:默认走被动,别乱改

FTP 有 PORT(主动)和 PASV(被动)两种数据连接建立方式。QFtp 默认用被动模式,也就是客户端发PASV,服务端回一个 IP 和端口,客户端主动去连。这个默认值在绝大多数内网和云主机场景下是对的,因为主动模式要求服务端反连客户端,客户端在 NAT 或防火墙后面时基本连不通。

源码里控制这个行为的是QFtp::setTransferMode,参数QFtp::Passive或QFtp::Active。我一般不动它,除非明确知道服务端只支持主动模式(一些老式嵌入式 FTP 服务端会这样)。改主动模式后,客户端要监听一个本地端口并把地址告诉服务端,防火墙得放行入站,调试成本高,血泪经验是能不改就不改。

被动模式下还有个细节:服务端PASV应答里返回的 IP 有时是内网地址(比如服务端在 NAT 后),客户端拿这个 IP 去连会失败。常见做法是忽略应答里的 IP,只用端口,连控制连接同一个主机地址。QFtp 的部分实现版本已经这么处理了,如果你的版本没处理,需要在QFtpPrivate::_q_pasvReply里把解析出的地址替换成控制连接的对端地址。这是对接云 FTP 时最容易踩的坑之一。

3. 上传下载与断点续传:命令队列、数据通道与进度回调

3.1 get/put 的调用姿势与文件句柄归属

QFtp 的get和put有两组重载:一组直接给本地文件路径,内部自己开 QFile;另一组给QIODevice*,由调用方管理生命周期。前者省事,后者灵活。批量传日志时我用后者,因为可以把多个文件塞进一个QBuffer或者自定义的加密 IODevice,传之前先做一层处理。

QFile *local = new QFile("/data/logs/app.log"); if (!local->open(QIODevice::ReadOnly)) { qWarning() << "open failed"; return; } // 上传:本地文件 -> 远端 /upload/app.log int id = ftp.put(local, "/upload/app.log"); // 传完由 QFtp 发 commandFinished,这里手动删掉 QFile QObject::connect(&ftp, &QFtp::commandFinished, [local](int, bool){ local->close(); local->deleteLater(); });

逻辑说明:put返回一个命令 id,可以用它跟commandFinished(int id, bool error)里的 id 对上,做多文件并发时的区分。文件句柄在传输期间必须保持打开,QFtp 不会替你关,传完在commandFinished里关并释放,否则文件句柄泄漏,Windows 上还会导致文件被占用删不掉。

参数说明:put(QIODevice*, const QString &remotePath)第二个参数是远端完整路径,不是目录;get同理,第一个参数是远端路径,第二个是本地 QIODevice 或路径。远端路径里的中文和空格要确认服务端编码,FTP 协议本身不规定文件名编码,UTF-8 服务端和 GBK 服务端混用时会乱码,这个后面避坑章细说。

3.2 用 bytesAvailable 和 dataTransferProgress 做进度条

传大文件不给进度,用户会以为卡死。QFtp 提供dataTransferProgress(qint64 done, qint64 total)信号,total 来自服务端返回的文件大小(SIZE命令或LIST解析),done 是已传字节数。接上就能驱动 QProgressBar。

QObject::connect(&ftp, &QFtp::dataTransferProgress, [progressBar](qint64 done, qint64 total){ if (total > 0) progressBar->setValue(int(done * 100 / total)); });

要注意 total 可能为 0:服务端不支持SIZE命令,或者LIST解析失败时拿不到文件大小,这时进度条只能显示已传字节数,不能算百分比。稳妥做法是 total 为 0 时切到不确定模式(setRange(0,0)),别硬算除零。

另一个信号readyRead在下载时触发,表示有数据到达,可以配合read()做流式处理,比如边下边校验 MD5。但 QFtp 内部已经把数据写进你给的 QIODevice 了,再调read()会抢数据,除非你用的是不带 QIODevice 的重载。这个边界要分清,混用会导致文件内容缺失。

3.3 断点续传:REST 命令与本地偏移

FTP 的断点续传靠REST命令,告诉服务端从文件的第 N 字节开始传。QFtp 原生接口没有直接暴露REST,需要看实现里有没有rawCommand之类的口子,或者自己在QFtpPrivate里加一个方法发REST n再发RETR。

常见做法是:下载前先看本地文件已有多大,QFile::size()拿到偏移,发REST offset,再get到同一个文件的追加模式。上传同理,先SIZE远端文件拿已传大小,REST后put时本地文件seek到对应位置。这里有个坑:不是所有服务端都支持REST,发过去可能回502,要判断应答码再决定是续传还是从头传。

// 伪代码:续传下载的核心顺序 qint64 offset = localFile.size(); if (offset > 0) { ftp.rawCommand(QString("REST %1").arg(offset)); // 需实现 rawCommand } ftp.get(remotePath, &localFile); // localFile 以 Append 打开

参数说明:REST的参数是字节偏移,不是行号;偏移必须落在文件边界内,超过远端文件大小会报错。续传前最好比对一下远端文件大小和本地大小,远端比本地还小说明文件被替换过,续传会得到损坏文件,这种情况直接删本地重下。

4. 避坑与排查:编码、超时、队列卡死这几件事

4.1 中文文件名乱码:现象是列表里一堆问号

现象:listInfo拿到的文件名是乱码,或者上传后服务端显示的文件名不对。原因:FTP 协议没有规定文件名编码,服务端可能用 UTF-8,也可能用 GBK,QFtp 默认按 Latin-1 或本地编码解析LIST应答。解决:先确认服务端编码,在 QFtp 解析LIST的地方用QTextCodec转一道;Qt6 没有QTextCodec了,用QStringDecoder。上传时把远端路径按服务端编码转成字节再拼命令。稳妥做法是服务端和客户端统一 UTF-8,并在连接后发OPTS UTF8 ON(如果服务端支持)。

4.2 命令队列卡死:某条命令永远等不到 commandFinished

现象:调了list()之后界面一直转圈,commandFinished不回调。原因:服务端对某条命令的应答格式不符合 QFtp 的解析预期,比如多行应答的结束行没匹配上,状态机停在中间态。解决:打开 QFtp 内部的调试输出(实现里通常有qDebug开关),看最后收到的原始应答是什么;对照 RFC 959 确认多行应答的格式(123-开头,123结尾)。实在不行在QFtpPrivate里给状态机加超时,超过 N 秒没进展就发ABOR并重置状态,别让整个队列僵死。

4.3 被动模式端口被防火墙拦:连得上控制连不上数据

现象:登录成功,list或get卡住,最后超时。原因:PASV返回的数据端口是随机的,服务端防火墙或客户端出口防火墙没放行。解决:服务端侧配置被动模式端口范围(比如 50000-50100)并在防火墙放行;客户端侧确认没有出站限制。排查时用telnet 服务端IP 数据端口试一下通不通,比看日志快。

4.4 大文件传输内存暴涨:QIODevice 用错模式

现象:传几百 MB 的文件,进程内存一路涨到几个 G。原因:用了QBuffer或QByteArray做中转,整个文件读进内存。解决:get/put直接传QFile*,让 QFtp 流式读写;如果必须走内存,分块传,每块几十 MB,传完释放。QFtp 内部是按块读 QIODevice 的,只要 IODevice 本身不缓存全量数据就不会爆。

4.5 连接后立刻 close 导致命令丢失

现象:connectToHost后马上close(),结果什么都没发生。原因:close入队后,前面的命令如果还在连接中,队列处理逻辑可能直接跳到关闭。解决:把close()放到最后一个commandFinished里调,或者用stateChanged等到Unconnected再退出事件循环。这个坑在写一次性脚本时特别容易踩,因为脚本跑完就app.exec()返回了,命令还没发完。

5. 进阶:把 QFtp 包成可复用的传输服务与自检清单

做到这一步,QFtp 能跑能传,但直接散落在业务代码里调用,迟早会变成一团乱麻。我的习惯是包一层FtpService类,对外只暴露uploadFile、downloadFile、listDir三个方法,内部管连接复用、失败重试、超时和编码转换。这样业务层不用关心命令队列,换实现(比如换成 libcurl)也只改一个类。

重试策略上,连接失败重试 3 次,间隔 2 秒递增;传输中断按断点续传逻辑重试 1 次,还失败就上报。超时用QTimer挂在每条命令上,commandFinished到了就停表,超时了发ABOR并重置连接。编码转换集中在一处,服务端编码做成配置项,别散在各处toUtf8()。

自检清单我一般这么过一遍:

检查项通过标准不通过的典型表现
模块依赖只链 QtCore/QtNetwork编译报 QWidget 相关未定义
被动模式PASV 后能连数据端口list 卡住超时
编码中文文件名双向正确列表乱码或上传名错
大文件内存平稳进程内存随文件线性涨
断点续传REST 后文件完整续传后文件损坏
关闭顺序所有命令完成后断开最后一条命令丢失

验证方法很土但有效:拿一个本地起的 FTP 服务端(比如 FileZilla Server 或 vsftpd),造一个含中文名、含大文件、含空目录的测试目录,跑一遍 list/get/put/remove/mkdir,再手动断网模拟中断,看续传和重试是否符合预期。这套流程走下来,基本能把上面列的坑提前暴露。

从那以后我每次把 QFtp 接进新工程,都强制先跑这个最小验证程序再动业务代码——先确认库编进来了、信号槽通了、被动模式能连数据端口,再谈上传下载。很多所谓的「QFtp 有 bug」,其实是依赖没配对或者服务端编码没对齐。希望帮到你。

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

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

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

立即咨询