简介这份资源面向使用 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 原生 QFtpconnectToHost、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 networkQt6 下 CMake 里find_package(Qt6 COMPONENTS Network REQUIRED)。如果工程同时支持 Qt5 和 Qt6QFtp 源码里对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第二个参数是端口默认 21login两个参数分别是用户名和密码匿名服务端通常接受anonymous/anonymouslist()不带参数列当前目录带路径参数列指定目录。注意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也可能用 GBKQFtp 默认按 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」其实是依赖没配对或者服务端编码没对齐。希望帮到你。本文还有配套的精品资源点击获取