资讯动态

QFTP源码集成与实战:Qt跨平台FTP客户端异步队列避坑

发布时间:2026/10/24 3:09:19 来源:尧图企业网站定制
简介QFTP源码是一份面向Qt/C开发者与网络编程学习者的跨平台FTP客户端实现适合用于理解FTP连接管理、命令收发、URL信息解析及文件上传下载流程也可作为自定义传输模块集成到桌面工具或编辑器中。压缩包共4个文件由2个.h头文件与2个.cpp实现文件组成整体约24KB体量轻便核心接口声明与协议逻辑分离便于按需阅读和二次开发。目前已有297人学习下载说明其在Qt网络编程与跨平台适配话题中具有一定参考价值。读者可从中了解Linux与Windows下的编译运行差异、FTP响应与错误处理思路以及将源码嵌入实际项目的基本方式适合作为课程实验、小型工具开发或协议学习的参考材料。1. QFTP 源码补的是 Qt 网络栈里缺的那块 FTP 客户端手上有个设备管理工具要跟跑在板子上的 FTP 服务端对接拉日志、推配置、翻目录。用 QNetworkAccessManager 试了一圈才发现它对 FTP 只保留了最基础的下载能力上传、建目录、改名字基本没有落点。QFTP 源码就是在这个位置上补位的它把 Qt4 时代的 QFtp 类单独抽了出来纯 QtCore 加 QtNetwork不引第三方库Linux 和 Windows 两端都完成了测试。它适合谁需要在同一套 Qt 工程里同时做上传下载、目录遍历、断点续传又不想自己套一层 socket 实现 FTP 协议命令的开发者。这份源码的价值不在“能用”而在“命令队列和信号时序都给你摆好了”你只需盯着自己那几个业务场景改。2. 把 QFTP 源码接进工程Linux 与 Windows 两套编译链路拿到源码包第一步永远不是读代码而是先让它在你机器上编出一个能跑的最小可执行文件。这一步过不了后面看什么都是空的。QFTP 源码包的结构很朴素两个头文件、两个实现文件外加一份构建脚本。文件数量少不代表没有依赖关系里面 qurlinfo 是给 LIST 命令的返回结果做解析用的qftp 才是协议状态机主体两者的耦合点在于 QFtp::listInfo 信号抛出来的那个对象类型。2.1 源码里到底有哪几个文件谁依赖谁先认清这四个文件各自的职责后面改代码时才知道该动哪一个文件职责改动频率qftp.hQFtp 类声明、状态与命令枚举、信号槽定义低除非要加接口qftp.cppFTP 连接状态机、命令队列、PASV 解析、数据通道收发中编码和超时逻辑常改这里qurlinfo.hLIST 结果的数据载体文件名、大小、权限、时间中Qt6 下要重写qurlinfo.cpp解析 Unix ls 风格和 DOS 风格的目录列表文本高服务端格式一换就得调qftp.h 里带了 Q_OBJECT 宏也就是说不管走 qmake 还是 CMakemoc 这一步必须开着。很多人在 CMake 里手写 add_executable 时忘了 set(CMAKE_AUTOMOC ON)编译期报一堆 undefined reference to vtable排查半天以为是源码缺东西其实是元对象没生成。这个问题在 Linux 上尤其容易忽略因为 gcc 的报错信息不会直接告诉你“缺 moc”。qurlinfo.cpp 是整份源码里最“脏”的一块。FTP 协议本身没有规定 LIST 命令的输出格式各家服务端返回的文本各不相同Unix 系一般是-rw-r--r-- 1 user group 1024 Jan 01 12:00 nameWindows 系的 FTP 服务端常见的是01-01-24 12:00PM DIR name。解析器要同时吃下这两种正则写漏一个分支表现就是 list 命令返回成功但一个条目都没有。2.2 qmake 路线.pro 里三行决定成败如果主工程本身就是 qmake 体系最省事的做法是把 QFTP 编成静态库再链进去。新建一个子目录放源码配一份这样的 .proQT core network QT - gui TARGET qftp TEMPLATE lib CONFIG staticlib c17 CONFIG - app_bundle # 头文件对外可见主工程 include 时不用写全路径 INCLUDEPATH $$PWD/src HEADERS \ src/qftp.h \ src/qurlinfo.h SOURCES \ src/qftp.cpp \ src/qurlinfo.cpp # Linux 下编静态库也要 -fPIC否则被共享库链接时报 relocation 错误 unix: QMAKE_CXXFLAGS -fPIC关键就三行TEMPLATE lib决定产物形态CONFIG staticlib决定它是静态库而不是动态库QT network决定 socket 那套东西能不能链上。少写 network 这一行编译阶段一切正常链接阶段才炸报 undefined reference to QFtp::connectToHost新手很容易在这里卡住。QT - gui是我习惯加的。QFTP 本身不碰任何界面类去掉 gui 依赖之后这个库可以被纯控制台程序、后台服务甚至无显示的嵌入式程序复用。如果哪天要把这套上传下载逻辑搬到无界面的守护进程里这一步提前做好能省不少拆依赖的功夫。QMAKE_CXXFLAGS -fPIC只有在你要把静态库再链进一个 .so 时才必需。只出可执行文件的话不加也行但加了不会有副作用我一般默认写上省得以后改产物类型时又回来补。2.3 CMake 路线Qt5/Qt6 下 find_package 的写法主工程用 CMake 的话把 QFTP 做成一个静态库 target写起来比 qmake 更清楚cmake_minimum_required(VERSION 3.16) project(qftp LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) # qftp.h 里有 Q_OBJECT这行不能省 find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Network) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Network) add_library(qftp STATIC src/qftp.cpp src/qftp.h src/qurlinfo.cpp src/qurlinfo.h ) # PUBLIC 让下游 target 自动继承这个 include 路径 target_include_directories(qftp PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src) target_link_libraries(qftp PUBLIC Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Network )find_package(QT NAMES Qt6 Qt5 ...)这个写法是官方推荐的兼容套路先找 Qt6 再回退到 Qt5找到哪个就把版本号塞进变量里后面所有 target 名都用Qt${QT_VERSION_MAJOR}::拼出来。这样同一份 CMakeLists 在装了 Qt5 的旧机器和装了 Qt6 的新机器上都能跑。target_include_directories必须用 PUBLIC 而不是 PRIVATE。用 PRIVATE 的话库自己能编过但主工程 include qftp.h 时会报找不到文件因为头文件路径没有往上传。这是 CMake 里最常见的权限误用之一。2.4 第一次编译验证写一个只连不传的最小 main库编出来了先别急着写业务。写一个只做连接和登录的最小程序把整条链路跑通#include QCoreApplication #include QTimer #include QDebug #include qftp.h int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QFtp ftp; // 状态机变化排查连接问题时第一手信息都在这里 QObject::connect(ftp, QFtp::stateChanged, [](QFtp::State s){ qDebug() state - int(s); }); // 每条命令结束都会回调id 是命令编号 QObject::connect(ftp, QFtp::commandFinished, [](int id, bool error){ qDebug() cmd id finished, error error; }); QObject::connect(ftp, QFtp::done, [](bool error){ qDebug() all done, error error ftp.errorString(); ftp.close(); }); ftp.connectToHost(127.0.0.1, 21); ftp.login(anonymous, anonymousexample.com); // 兜底15 秒还没走完流程就退出避免卡在事件循环里 QTimer::singleShot(15000, app, QCoreApplication::quit); return app.exec(); }这里有个容易被忽略的点connectToHost和login是连着调用的中间没有等待连接成功。QFTP 内部有命令队列后面的命令会自动排在前面命令完成之后再发所以同步写法是对的不需要在 stateChanged 里手动补 login。反过来说如果谁在 connectToHost 之后手动 sleep 等连接反而是错的会把事件循环堵死连接永远建立不起来。QTimer::singleShot那个兜底别嫌丑。调试阶段如果地址写错或者对端没开程序会安静地挂在事件循环里什么输出都没有你会怀疑是不是信号没连上。加个超时退出至少能确认“程序跑到了 exec 之后”。2.5 静态库还是源码直入工程两种方式各有适用面。静态库适合多工程复用编一次到处链缺点是改了源码要重新编库调试时断点进不去没有调试符号的话。源码直入适合只有一两个工程用、并且需要频繁改解析逻辑的场景改完直接编断点随便下。我一般的判断标准是如果这个项目里 QFTP 的代码三个月内不会动第二次就做成静态库如果刚接手、还在摸服务端返回格式就先源码直入等逻辑稳定了再抽成库。别一上来就追求工程结构漂亮解析器没调通之前任何抽象都是负担。3. 命令队列与信号时序QFTP 的异步模型怎么用才不出错QFTP 是彻底异步的每个 API 都立即返回一个 int 编号真正的结果通过信号回调。这套模型的好处是不阻塞界面坏处是很多人拿同步思维去写结果就是数据读了一半、文件写空了、进度条归零。这一章把信号顺序和读取时机讲透剩下的就是照着填业务逻辑。3.1 每个 API 返回的 int 是队列句柄不是返回值connectToHost、login、list、get、put、mkdir、remove这些方法全部返回 int。这个 int 不是成功与否的标志而是这条命令在队列里的编号。命令失败与否要看commandFinished(int id, bool error)里的 error 参数。为什么要区分因为你可以连着发五条命令int idMkdir ftp.mkdir(/log); // 先建目录 int idPut ftp.put(buf, /log/a.txt); // 再传文件 int idList ftp.list(/log); // 最后列一下确认三条命令会按顺序执行每条结束时都会触发一次 commandFinished带着各自的 id。你在槽函数里用 id 做分支就知道是哪条命令回来了。如果只看 error 不看 id目录建失败和文件传失败你会分不清。currentId()和currentCommand()可以在任何时刻查当前正在执行的是哪条命令调试时打一行日志比猜快得多。3.2 一次下载的完整信号序列下载一个文件信号是按固定顺序来的顺序信号该做什么1commandStarted(int id)记下 id启动进度计时2dataTransferProgress(qint64, qint64)更新进度注意 total 可能为 -13readyRead()读数据必须读干净4commandFinished(int id, bool error)关文件、释放缓冲、判断成败5done(bool error)队列清空可以关闭连接dataTransferProgress的第二个参数 total 在服务端没给出文件大小时会是 -1直接拿来做除法会得到负数百分比。正确写法是先判断total 0再算比例否则进度条会显示成负值或者直接崩。readyRead可能触发很多次也可能一次都不触发文件为空时。它不是“数据到齐了”的通知而是“现在缓冲区里有东西可读”的通知。这一点理解错代码就会写成“等 readyRead 来了再一次性 readAll”大文件下必然出问题。3.3 readyRead 里必须把缓冲区读干净标准写法是这样QFile *local new QFile(localPath); if (!local-open(QIODevice::WriteOnly)) { qWarning() open failed localPath; delete local; return; } int getCmdId -1; QObject::connect(ftp, QFtp::readyRead, [](){ // 循环读到空为止一次 readyRead 可能对应多段数据 while (ftp.bytesAvailable() 0) { local-write(ftp.readAll()); } }); QObject::connect(ftp, QFtp::commandFinished, [](int id, bool error){ if (id ! getCmdId) return; local-flush(); local-close(); delete local; local nullptr; if (error) qWarning() download failed: ftp.errorString(); }); getCmdId ftp.get(remoteName, local, QFtp::Binary);这里有两个细节值得盯。第一while (ftp.bytesAvailable() 0)必须循环因为 QFTP 内部缓冲可能一次塞进来几十 KBreadAll()一次读不完的情况虽然少见但不是没有尤其是在弱网或者对端限速时。第二文件对象的生命周期要跟着 commandFinished 走不能在 done 里删——done 是队列空了才触发中间如果还有别的命令排在后面文件句柄会早于数据传输结束被释放写盘就写到野指针上去了。getCmdId初值给 -1然后赋值语句放在 connect 之后这个顺序无所谓因为命令是异步的赋值一定发生在任何信号回调之前。但如果你把 connect 写在 get 之后理论上存在极小概率错过立即回调本地回环地址下所以我还是习惯先连信号再发命令。3.4 三条命令连着发时别在 commandFinished 里手动串行新手常写的一种结构是在 commandFinished 里判断“如果这条是 login就发 list如果这条是 list就发 get”。能跑但没必要。QFTP 的队列本来就是串行的直接连着发就行ftp.connectToHost(host, 21); ftp.login(user, pass); ftp.cd(/data); ftp.get(config.ini, file, QFtp::Binary); ftp.close();这五行会严格按顺序执行。唯一要注意的是 close 也会排队如果前面某条命令失败后面的命令仍然会尝试执行失败会累积到 done 的 error 参数上。想要“一失败就中止”得在 commandFinished 里判断 error 然后调ftp.abort()。abort()会把当前命令和队列里所有未执行的命令一起清掉这个行为在某些场景下是优点快速放弃在另一些场景下是坑你以为只取消了当前一条。用它之前先想清楚队列里还有没有必须执行的清理命令比如 close。3.5 对象析构顺序是最隐蔽的崩溃源QFTP 对象如果是在栈上创建的而它还有命令在执行时程序退出析构函数里会去访问已经失效的 socket表现就是退出时随机崩溃gdb 里看到的是 Qt 内部的 socket notifier。这类问题在 Windows 上尤其明显因为 Windows 的 socket 关闭行为和 Linux 不太一样。我一般把 QFtp 放在堆上用 QScopedPointer 或者父子对象托管退出流程固定成三步先abort()清队列再close()关连接最后等 done 信号触发后再 delete。如果业务上允许粗暴退出至少保证 delete 发生在事件循环退出之前。4. 中文路径、被动模式与大文件三个必须落到参数上的点前面两章解决的是“能不能跑”这一章解决的是“跑得对不对”。QFTP 的默认行为在英文环境和干净网络下没问题一旦碰到中文目录、NAT 后面、超过 2GB 的文件就得手动干预。这三个点每一个都能让一次传输静默失败而且报错信息都很模糊。4.1 传输模式Active 还是 Passive取决于中间有没有 NATFTP 有两种数据连接建立方式。Active 是服务端主动连回客户端的一个端口Passive 是客户端去连服务端开的一个临时端口。前者在客户端有防火墙或者处在 NAT 后面时基本必失败因为服务端连不进来。企业内网、云主机、家用路由器后面一律用 Passive。QFTP 里通过 setTransferMode 切换// 连接建立后立刻设置不要等出错再切 ftp.connectToHost(host, port); ftp.setTransferMode(QFtp::Passive); ftp.login(user, pass);我一般显式写这一行不去赌默认值。不同版本的源码在这一点上行为不完全一致显式设置能保证换机器、换服务端之后行为一致。判断是不是模式问题有个快速办法看 stateChanged 走到 LoggedIn 之后list 命令是否卡住不返回。如果控制连接21 端口正常登录成功但所有数据传输命令都超时九成是数据连接方向不对。这时候把模式切一下立刻见分晓。还有一种情况是服务端只允许 Active。这种多见于一些嵌入式设备的精简 FTP 服务为了省资源不开 PASV。碰到这种服务端只能切回 Active同时确保客户端机器上对应的端口段没被防火墙拦。两边都要放行缺一边都不行。4.2 中文目录乱码命令编码和 LIST 解析编码是两条独立链路中文乱码在 QFTP 上分两个完全不同的来源很多人混为一谈改了半天没效果。第一条链路是“发出去的命令”。QFTP 内部把 QString 转成字节流时用的是固定的 8-bit 转换服务端如果不是 UTF-8 模式收到的就是乱码。表现是 mkdir 建出来的目录名是问号或者 cd 到一个中文目录直接报 550。第二条链路是“收回来的 LIST 结果”。qurlinfo 解析的是原始字节转 QString 时如果编码和服务器发出来的一致文件名就对不一致就是乱码。表现是 list 成功但文件名显示成方块。第一条链路的稳妥解法是绕开自动转换自己拼字节// Qt5 用 QTextCodecQt6 里换成 QStringEncoder static QByteArray encodeArg(const QString text, const char *codecName) { #if QT_VERSION QT_VERSION_CHECK(6, 0, 0) QTextCodec *codec QTextCodec::codecForName(codecName); return codec ? codec-fromUnicode(text) : text.toUtf8(); #else QStringEncoder encoder(codecName); return encoder.encode(text); #endif } // 自己拼命令再走 rawCommand编码完全可控 QByteArray cmd MKD encodeArg(QString::fromUtf8(测试目录), GBK); ftp.rawCommand(cmd);rawCommand发的命令服务端的回复通过 rawCommandReply 信号拿到返回码是三位数字257 表示目录创建成功550 表示失败。用 rawCommand 的代价是要自己处理返回码好处是编码、命令格式、参数顺序完全由你决定。至于用哪个编码得看服务端配置。常见的约定是服务端开了 UTF-8 支持就用 UTF-8没开就用本地代码页。判断方法很简单先用一个纯 ASCII 目录名跑通再换成中文试看乱码出现在哪一侧——如果服务端的管理界面上显示的名字就已经是乱的说明是发送侧编码错了如果服务端显示正常但客户端解析出来是乱的那是 LIST 解析侧的问题。第二条链路要改 qurlinfo.cpp 里的解析逻辑把字节转字符串的地方换成固定编码。改之前先抓一份原始 LIST 输出存成文本对着调比盲改快十倍。4.3 大文件进度回调的节流和写盘缓冲传一个几百 MB 的文件dataTransferProgress 会触发得非常频繁如果槽函数里做了重活比如更新界面、写日志、算哈希整个传输会被拖慢。我的做法是在槽里只更新一个原子变量界面那边用定时器每 200 毫秒读一次。// 进度只记数不做任何耗时操作 QObject::connect(ftp, QFtp::dataTransferProgress, [](qint64 done, qint64 total){ m_done.store(done); m_total.store(total); }); // 界面侧定时刷新和传输解耦 QTimer *uiTimer new QTimer(this); uiTimer-setInterval(200); connect(uiTimer, QTimer::timeout, this, [this](){ qint64 d m_done.load(), t m_total.load(); if (t 0) progressBar-setValue(int(d * 100 / t)); }); uiTimer-start();另一个和大小有关的是写盘方式。QFile 默认带缓冲正常写没问题但如果每次 readyRead 都调一次 flush性能会掉得厉害。flush 只在 commandFinished 里做一次就够中间让 Qt 自己管缓冲。超过 2GB 的文件在 32 位进程里还有额外的坑qint64类型本身没问题但某些旧的 QFile 重载在 32 位下会截断偏移量。如果目标环境是 32 位传输前先用 QFileInfo 判断大小超过 2GB 的文件考虑分片或者确认编译选项里开了大文件支持。4.4 ASCII 和 Binary 选错文件内容会被悄悄改写QFTP 的 get 和 put 都有一个 TransferType 参数取值是 QFtp::Binary 或 QFtp::Ascii。选 Ascii 时协议层会做换行符转换传输过程中把 LF 转成 CRLF接收时再转回来。问题在于这个转换是无条件的不管文件本身是什么类型。传一个二进制文件用 Ascii 模式里面的 0x0A 字节会被改成 0x0D 0x0A文件直接损坏而且不报错传输进度显示 100%只有打开文件才发现内容不对。血泪经验除了明确知道是纯文本并且需要跨平台换行统一之外一律用 Binary。这个参数在每一次 get 和 put 里都要显式写别依赖默认值。5. 双平台实测的避坑清单五个现象、五个原因、五个解法下面这五条都是 Linux 和 Windows 双端跑下来真实撞上的现象描述按“你看到什么”写方便对号入座。5.1 现象Linux 能连上同一份代码 Windows 连不上原因通常不是代码而是 Windows 上默认开启了主动模式探测或者本机防火墙拦了数据连接的回连端口。另一种可能是服务端地址填的是主机名Windows 下 DNS 解析走了另一条路径解析到了 IPv6 地址而服务端只监听 IPv4。解决分两步先固定传输模式为 Passive再在 connectToHost 之前手动把主机名解析成 IPv4 地址传进去。如果还不行用 telnet 或 curl 从 Windows 机器上直连 21 端口确认是网络层问题还是代码问题。这一步能把排查范围砍掉一半。5.2 现象list 返回成功但一条记录都没有原因基本锁定在 qurlinfo.cpp 的解析分支没覆盖当前服务端的输出格式。Unix ls 风格和 DOS 风格的正则不一样有些精简 FTP 服务端还会返回 MLSD 格式三种格式混在一起就会漏。解决先用 rawCommand 发一条 LIST把原始文本打到日志里看格式再决定改哪个分支。改解析器之前先备份一份原始输出样本以后换服务端可以直接拿来回归测试。如果服务端支持 MLSD优先用 MLSD它的格式是标准化的比 LIST 好解析得多。5.3 现象小文件正常大文件传到一半断掉原因有三个层次。最浅的是网络超时QFTP 自身没有命令级超时卡住就一直卡着。中间层是服务端设置了空闲断开时间传输过程中如果客户端长时间没有读取比如主线程被别的活堵住服务端会踢连接。最深的是磁盘写入失败本地盘满了但代码没检查 write 的返回值。解决先加一个命令级看门狗下一章给具体实现再检查 readyRead 的读取逻辑是不是在主线程里被阻塞最后把本地 write 的返回值纳入判断写失败立即 abort 并清理半截文件。5.4 现象程序退出时偶发崩溃堆栈指向 socket 层原因是 QFtp 对象析构时还有命令在队列里析构函数访问了已经关闭的 socket。Windows 上这个崩溃比 Linux 更频繁因为 socket 句柄的回收时机不同。解决把退出流程固定下来——先 abort 清队列再 close等 done 信号最后 delete。如果 QFtp 是在某个窗口类里创建的注意父子关系不要形成循环父对象析构时会先删子对象如果子对象的槽函数里又引用了父对象的成员就是悬空引用。5.5 现象中文文件名上传后变成问号原因在 4.2 节讲过发送侧编码不对。但还有一个容易忽略的分支文件名是对的只是路径分隔符用了 Windows 的反斜杠。FTP 协议里路径分隔符永远是正斜杠反斜杠会被当成文件名字符的一部分服务端上就会出现一个名字里带反斜杠的怪文件。解决所有发给服务端的路径统一用正斜杠拼本地路径转远端路径时手动 replace 一次。这个转换写成一个函数全局只在这一处做别散落在各个调用点。6. 断点续传与超时看门狗两个能让 QFTP 进生产的技巧断点续传的关键是 REST 命令。它告诉服务端“下一条传输命令从第 N 个字节开始”但这个状态只对紧接着的那一条命令有效中间插任何别的命令都会让偏移量失效。所以顺序必须是先 REST再立刻 get中间不能有 list 之类的操作。QFileInfo fi(localPath); qint64 offset fi.exists() ? fi.size() : 0; QFile *f new QFile(localPath); // 有本地残留就追加写没有就新建 f-open(offset 0 ? (QIODevice::WriteOnly | QIODevice::Append) : QIODevice::WriteOnly); if (offset 0) { // REST 之后必须紧跟 RETR否则偏移量作废 ftp.rawCommand(REST QByteArray::number(offset)); } ftp.get(remoteName, f, QFtp::Binary);上传方向的续传要用 APPE它表示追加写入而不是覆盖。前提是服务端支持 APPE有些精简服务端不认这个命令返回 500那就只能整传。第二个技巧是看门狗。QFTP 没有内置超时网络一断命令队列会永远停在那一条上done 永远不来。用一个独立的 QTimer 监控 commandStarted 和 commandFinished 之间的时间差超时就 abort。这个类要带 Q_OBJECT别忘记 mocclass FtpWatchdog : public QObject { Q_OBJECT public: FtpWatchdog(QFtp *ftp, int timeoutMs, QObject *parent nullptr) : QObject(parent), m_ftp(ftp) { m_timer.setSingleShot(true); m_timer.setInterval(timeoutMs); connect(m_timer, QTimer::timeout, this, [this](){ m_ftp-abort(); // 会清空整个命令队列 m_ftp-close(); emit stalled(); }); connect(ftp, QFtp::commandStarted, this, [this](int){ m_timer.start(); }); connect(ftp, QFtp::commandFinished, this, [this](int, bool){ m_timer.stop(); }); } signals: void stalled(); private: QFtp *m_ftp; QTimer m_timer; };注意 abort 的副作用是把队列里所有待执行命令一起清掉所以看门狗触发之后不要指望队列还能自动恢复得在 stalled 槽里重建连接、重发剩余任务。超时时间我一般给单条命令 30 秒大文件传输时按文件大小动态放宽否则一个 500MB 的文件在中途就会被误杀。第一人称教训收尾从那以后我每次接一个新的 FTP 服务端都强制先跑一遍三件事——list 看解析、传一个带中文名的空文件看编码、传一个 100MB 的伪随机文件看续传和超时三关都过再写业务代码。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑