简介一套基于Qt的FTP上传功能演示工程面向需要快速掌握QNetworkAccessManager网络编程的Qt开发者帮助解决在桌面应用中集成FTP文件上传的实际需求。压缩包内共13个文件核心代码由4个cpp源文件与3个h头文件构成配合2个ui界面文件、工程文件及翻译配置文件整体仅47KB结构简洁便于直接阅读与复用。已有446人学习下载适用于理解FTP上传的基本流程。通过该Demo读者可重点参考QNetworkRequest构造、QNetworkAccessManager::put调用及finished/uploadProgress信号槽处理等关键环节并在此基础上扩展多文件上传、错误处理、进度展示等功能。工程代码量小、依赖少是学习Qt网络通信与FTP协议交互的实用入门范例。1. Qt实现FTP上传Demo内网传文件为什么还得绕回FTP这条路在设备调试和内部系统对接的现场你经常面对一台系统停留在老版本的内网工控机没有Web服务、没有对象存储唯一的文件通道就是FTP。用Qt实现FTP上传Demo是这类场景里落地最快的方案它不依赖服务器端额外开发只要对方开一个FTP账号就能传。很多刚接触Qt网络编程的开发者会在这里卡住——新版Qt对FTP的支持一改再改老模块需要自己编译上传又是异步流程信号槽套着命令队列很容易一头雾水。这篇笔记会直接讲清楚怎么做从模块选型、编译接入到连接、登录、put上传、进度回调以及被动模式、中文编码、超时重试这些必调参数最后把断点续传和上传后校验补齐。读完你可以照着把Demo跑起来也知道哪些坑必须提前绕开。2. 从协议到选型QFtp、QNetworkAccessManager和手写TcpSocket该选谁2.1 FTP上传的底层逻辑控制连接与数据连接FTP和HTTP有个本质区别它用两条连接。控制连接走21端口负责传命令和应答码数据连接另开通道负责传文件内容。一次完整的上传从客户端视角看是这么一串命令CONNECT → USER/PASS 登录 → CWD 切换目录 → STOR 存储文件 → QUIT。服务器在每条命令后都会回一个三位数应答码比如230表示登录成功、150表示准备接收数据、226表示传输完成。这个流程天然是异步的。你不可能发一条命令就阻塞等待几十秒所以Qt里做FTP上传核心就是对命令队列和信号槽的处理。QFtp这个模块把上面这套状态机封装好了你只管调用connectToHost、login、cd、put它在内部维护一个待执行命令队列每条命令结束后发commandFinished信号数据传输出进度时发dataTransferProgress信号。理解了这两条连接和一个队列后面写代码就不容易乱。2.2 三个可选实现QFtp、QNetworkAccessManager、QTcpSocket手写先说结论Qt 6里QNetworkAccessManager已经把FTP支持移除还在用QNAM写FTP上传的代码基本是陈年项目。即便在Qt 5里它对FTP的支持也很有限没有主动/被动模式控制、没有断点续传接口对很多实际场景来说不够用。手写QTcpSocket当然可控性最高但FTP应答码解析、数据连接建立、LIST解析这些工作量大一个Demo阶段完全没必要。QFtp是官方老模块虽然早已不在标准发行包里但源码仍然以独立形式维护。它保留了完整的FTP命令支持包括cd、rename、remove、mkdir、rawCommand这些。对做上传Demo来说QFtp是唯一一个既有高层封装、又留了底层口子的选择。下面的对比表应该能帮你快速做决定实现方式控制力断点续传主动/被动模式代码量结论QFtp 独立模块高可用rawCommand实现固定被动模式少首选QNetworkAccessManager低不支持不可控最少Qt 6已移除放弃QTcpSocket 手写最高完全可控完全可控非常大仅特殊场景我一般会这样判断如果你只是往一个固定FTP服务器上传文件QFtp足够如果你要处理复杂的主动/被动切换或者服务器对FTP命令兼容性极差那再考虑手写协议层但那时候你写的已经不是Demo了。2.3 把QFtp编译进Qt工程最小接入步骤QFtp不在Qt安装包里需要额外拿到源码放进自己的工程。这步不复杂但很多人第一次会栽在moc处理上。常见做法是把qftp的源文件目录拷到工程里让.qmake自动参与编译。先看目录准备这一步# 把qtftp模块源码里的 qftp 目录拷贝到工程第三目录 mkdir -p 3rdparty/qftp cp -r qtftp源码路径/src/qftp/* 3rdparty/qftp/然后修改工程文件把qftp.cpp和qftp.h加进编译列表QT network INCLUDEPATH $$PWD/3rdparty/qftp SOURCES \ main.cpp \ ftpuploader.cpp \ $$PWD/3rdparty/qftp/qftp.cpp HEADERS \ ftpuploader.h \ $$PWD/3rdparty/qftp/qftp.h注意QFtp是个QObject子类头文件里带了Q_OBJECT宏所以它必须经过moc。直接放在SOURCES和HEADERS里qmake会自动为它跑一遍moc不需要额外处理。如果你的工程是CMake记得把qftp.cpp加入add_executable的源文件列表并且包含qftp头文件目录。接入完成后写一行#include QFtp能编译通过就算成功了。3. 跑通上传主流程连接、登录、put与进度回调的槽编排3.1 建立FTP连接connectToHost与login的串联QFtp是异步的所以不能像普通函数调用那样一行接一行写。我会用一个状态标记m_next来记录当前命令完成后下一步该做什么在commandFinished信号里驱动状态机。先看连接部分// ftpuploader.h #include QFtp #include QFile class FtpUploader : public QObject { Q_OBJECT public: enum NextAction { None, Connect, Login, Cd, Put, Close }; void upload(const QString host, quint16 port, const QString user, const QString pass, const QString remotePath, const QString localPath); private slots: void onCommandFinished(int id, bool error); void onProgress(qint64 done, qint64 total); private: QFtp m_ftp; // QFtp 作为成员绝对不能是 upload() 内的局部变量 NextAction m_next None; QFile *m_file nullptr; QString m_remoteName; };连接和登录的串联逻辑在commandFinished里完成。每条命令完成后根据m_next的值决定下一步动作void FtpUploader::upload(const QString host, quint16 port, const QString user, const QString pass, const QString remotePath, const QString localPath) { connect(m_ftp, SIGNAL(commandFinished(int,bool)), this, SLOT(onCommandFinished(int,bool))); connect(m_ftp, SIGNAL(dataTransferProgress(qint64,qint64)), this, SLOT(onProgress(qint64,qint64))); m_next Connect; m_ftp.connectToHost(host, port); // 命令入队立刻返回 // 登录参数暂存为成员等 Connected 状态完成再使用 m_user user; m_pass pass; m_remotePath remotePath; } void FtpUploader::onCommandFinished(int id, bool error) { if (error) { qWarning() FTP命令失败: m_ftp.errorString(); m_ftp.clearPendingCommands(); // 出错后清空队列避免连锁失败 return; } switch (m_next) { case Connect: m_next Login; m_ftp.login(m_user, m_pass); break; case Login: m_next Cd; m_ftp.cd(m_remotePath); break; case Cd: m_next Put; m_file new QFile(m_localPath, this); if (!m_file-open(QIODevice::ReadOnly)) { qWarning() 本地文件打开失败 m_file-errorString(); return; } m_ftp.put(m_file, m_remoteName, QFtp::Binary); break; default: break; } }这段代码里最关键的是m_ftp必须是成员变量。如果写成局部变量upload函数一返回QFtp对象就被析构信号槽连接全部失效你会看到命令发出去了但槽完全不触发。connectToHost返回的是一个int类型的命令id可以用来精确匹配某一条命令的完成响应但用m_next这个状态标记在串行上传场景里更直观。3.2 put上传与进度回调从本地文件到服务器的数据流put这一步是体力活但细节不少。QFtp的put有两个重载一个接受QByteArray一个接受QIODevice指针。上传大文件必须用QIODevice版本否则整个文件被读进内存。文件open方式必须是ReadOnly并且最好把类型显式传成Binary避免某些服务器对文本模式做换行符转换case Put: { m_file new QFile(m_localPath, this); if (!m_file-open(QIODevice::ReadOnly)) { qWarning() 本地文件打开失败: m_file-errorString(); return; } m_totalBytes m_file-size(); m_ftp.put(m_file, m_remoteName, QFtp::Binary); m_next Close; break; }进度回调连接的是dataTransferProgress信号这个信号反映的是数据连接上实际传输的字节数。注意有个坑有些FTP服务器在被动模式下不会预先报告文件总大小total参数可能是0。这时候进度条会变成不确定状态我一般自己拿本地文件大小兜底void FtpUploader::onProgress(qint64 done, qint64 total) { if (total 0) { total m_totalBytes; // 服务器没给大小用本地文件大小填充 } int percent (total 0) ? (int)(done * 100 / total) : 0; emit uploadProgress(percent); }记住done和total是qint64计算百分比时先乘再除避免整数溢出。文件超过2GB时尤其要注意类型int会直接翻车。3.3 上传完成后的收尾关闭文件、切换目录与命名put命令发出后服务器在收到完整数据后返回226应答这时QFtp发出put对应命令id的commandFinished。不要在数据还没传完时就关闭文件。我的习惯是put命令完成后再做三个收尾动作关闭本地文件、确认远程文件大小、按需把临时文件名改回正式名。临时文件名的做法在现场非常实用先传成目标名.tmp全部完成后再rename成正式名。这样即使上传中途断掉服务器上也不会留下一个半截的正式文件。QFtp里rename是现成接口直接放到命令队列里case Put: { m_next Rename; m_ftp.rename(m_remoteNameTmp, m_remoteName); // 上传用临时名完成后改名 break; } case Rename: { m_file-close(); m_file-deleteLater(); m_ftp.close(); break; }这个顺序保证了一件事服务器上永远只有完整文件或空文件名不会有半截内容被下游任务消费。如果你不需要临时名那至少要在put完成后关闭文件否则在Windows上文件句柄不释放下次上传同名文件会报权限错误。4. 上传参数的四个必调项被动模式、中文编码、超时与重试策略4.1 被动模式不是玄学QFtp默认只有一条路QFtp实现的基本是FTP被动模式也就是客户端主动去连服务器返回的数据端口。对大多数内网环境这没问题但跨网段时容易出状况服务器在PASV应答里返回的是它自己视角的内网地址客户端如果不在同一网段根本连不上这个地址。现象就是登录正常一put就卡在等待数据连接。遇到这种情况先确认服务器配置里有没有被动模式返回外网地址这类开关很多FTP服务端是有的。如果服务器不可调那Qftp这边没有公开接口切主动模式——这是它的硬边界。真要做主动模式只能改QFtp源码或者自己用QTcpSocket写协议层。所以选型时要提前问清楚服务器部署在哪个网段客户端能不能直连它的数据端口。这个问题越早确认越好别等功能联调时才发现。4.2 中文文件名乱码用QTextCodec把本地编码对齐服务器QFtp发送命令时内部把QString转成本地8位编码。在中文Windows上这意味着命令以GBK发出去。如果服务器端用的是UTF-8中文文件名就会变成乱码或者干脆报550找不到路径。反过来也是同理。解决办法是全局统一编码让QFtp发出的命令编码和服务器一致。Qt的QTextCodec提供了setCodecForLocale它会影响QString的toLocal8Bit行为而QFtp底层用的正是toLocal8Bit#include QTextCodec // 在main()里、创建任何QFtp对象之前设置 QTextCodec::setCodecForLocale(QTextCodec::codecForName(UTF-8));如果服务器是GBK就换成codecForName(GBK)。注意这个设置是进程级的全局影响改了之后所有本地编码转换都会跟着变所以要么在程序启动时一次性设好要么就保持默认再手动转码。我最常用的验证方式是先传一个带中文名的空文件到服务器用另一个FTP客户端看一眼文件名是否正常再决定要不要加这一行。4.3 超时控制QFtp没有自带超时用QTimer加中止QFtp另一个让人头疼的点是没有超时设置。网络断了、服务器假死命令队列会一直挂着界面就像卡死。我一般用单发QTimer给每一条关键命令做超时守卫QTimer *timer new QTimer(this); timer-setSingleShot(true); timer-setInterval(15000); // 15秒没响应就中止 connect(timer, QTimer::timeout, this, [this, timer]() { m_ftp.abort(); // 中止当前正在执行的命令 m_ftp.clearPendingCommands(); // 清掉后面排队的命令 timer-deleteLater(); }); // 每发一条命令时重启定时器 int id m_ftp.cd(/upload); timer-start();然后在commandFinished里把定时器停掉。这样把单条命令卡死控制在15秒内配合重试逻辑比让用户干等强得多。abort之后还要记得clearPendingCommands否则队列里后面的命令会继续执行可能把状态搞乱。4.4 重试用例设计哪些错误值得重试不是所有错误都值得重试。登录失败、目录不存在这类属于永久性错误重试一百次也没用直接抛给用户。而超时、数据连接被断开、服务器临时返回4xx这些属于临时性错误适合做两到三次重试。我的做法是给命令id加一个重试计数器超过3次就放弃并在界面上给出上传失败请检查网络和服务器状态而不是一句冷冰冰的errorString。重试时要注意命令队列状态。出现错误后队列可能还有残留命令必须先clearPendingCommands再重新发起connectToHost之前的流程。这也是为什么很多Demo会把上传文件提炼成一个transient操作从connect开始状态机从头跑重试只是把入口重新调用一次。5. FTP上传Demo避坑与排查5个典型翻车点5.1 槽函数全部不触发命令却正常发出现象connectToHost返回了命令id但onCommandFinished、onProgress这些槽一次都没被调用。原因QFtp对象被声明成了upload函数的局部变量函数返回后对象析构信号连带着没了接收者。这是Qt里最常见的黑匣子问题——代码看着没错实际对象已经死了。解决把QFtp声明成类成员或在堆上new出来并指定parent为this。用成员时注意upload函数里不要再重新赋值覆盖已有连接connect一次就够了。5.2 上传成功但远程出现0字节文件现象命令流全部走完没有任何error但服务器上的文件大小是0。原因QFile没有成功打开或者打开后没有检查error状态。很多时候是本地路径不存在但代码里QFile构造函数不报错直到open失败才返回false这里被忽略后put就把一个空设备传上去了。解决open之后立刻判断返回值并输出errorStringput传入前确认m_file-isOpen()为true。文件很大时还要确认m_file-size()不为0这两个检查能挡住九成问题。5.3 put一直卡在等待数据连接进度始终为0现象登录成功切换到目录也成功但put命令发出去后没有任何进度回调界面上一直转圈。原因被动模式下客户端连接PASV返回的数据端口失败。常见于客户端和FTP服务器跨网段或者中间防火墙只放行了21端口没有放行数据端口范围。解决先确认服务器是否配置了正确的被动模式地址然后用同网段一台机器试试能否直连数据端口。QFtp固定走被动模式如果服务器无法配合就得考虑换实现方案而不是继续调Qftp代码。5.4 上传成功但文件损坏或Windows和Unix下文件大小不一致现象二进制文件上传后无法解压、图片打不开或者从Windows传到Unix后换行符变了。原因put用了默认的文本传输类型服务器在做ASCII模式转换。同时put返回后立刻关闭QFile也可能导致数据没完全写入网络缓冲。解决put时显式传QFtp::Binaryput命令的commandFinished触发后再close文件。这两步配合才能保证文件字节级一致。做Demo时我总会在本地放一个已知MD5的文件传上去后用服务器端MD5比对一次性确认传输类型没有坑。5.5 断点续传后文件多出一块或两边长度对不上现象用REST命令续传后服务器上的文件比本地大内容错位。原因最常见的是本地文件seek的偏移和REST命令的偏移不一致。REST让服务器从某个字节开始写但本地文件没同步seek到同一个位置两边就错开了。解决续传前先取本地文件残留大小同时把REST偏移设置成同一个值顺序是seek先做再发REST最后put。另外有些服务器对REST的支持并不标准续传前最好先做一次传1KB测试文件断点续传的验证确认服务端行为正常再正式传。6. 给Demo装上断点续传REST命令与上传后的自校验6.1 用REST命令实现断点续传的最小代码QFtp没有直接封装断点续传但它留了rawCommand这个口子。续传时让文件从本地偏移处继续读同时发REST告诉服务器从对应偏移开始写m_file-seek(localOffset); // 本地文件跳到断点 m_ftp.rawCommand(REST QString::number(localOffset)); m_ftp.put(m_file, m_remoteName, QFtp::Binary); // 入队等REST应答后执行rawCommand和put共用同一个命令队列所以顺序有保证。你不需要等REST的应答手动放行QFtp会一条条执行。续传成功后把localOffset更新成当前进度下次断线继续用。6.2 上传完不是结束用服务端LIST做一次自校验真正成熟的Demo会给自己留一条验证路径。上传完成后发一条list命令在listInfo信号里找到目标文件的size和本地文件大小比对如果服务器支持MD5校验再用原始FTP命令拉取校验值。大小不一致时直接标记为失败并清理掉远程半截文件这样下游拿到的一定是完整文件。我现在做FTP上传功能都会保留一个只读的测试账号专门用来跑传上去、拉回来、比对大小、断点续传、主动取消这五个用例。这个习惯救过我好几次——有一次就因为服务器默认开了ASCII模式让一批二进制包全部损坏靠自校验才在发版前拦下来。把Demo留在能复现、能验证的状态比多写几个功能更值得希望帮到你。本文还有配套的精品资源点击获取