尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
带进度条的FTP上传下载实例:断点续传与失败重试封装
简介面向需要为文件传输加入可视化反馈的开发者这份 FTP上传实例带进度条以 C# 窗体项目形式提供可运行的参考实现重点解决连接服务器、登录验证、切换目录、分块上传以及按已传字节数实时刷新进度条等常见需求。压缩包共35个文件以12个 cs 源码为主体搭配 resx 界面资源、config 配置、sln 解决方案、csproj 项目文件及编译输出整体约78KB体量轻便打开即可对照项目结构。代码中可见上传帮助类、主窗体与子窗体、程序入口等模块适合拆解 FTP 连接、传输回调与进度显示之间的协作方式也可将封装后的操作类迁移到自有工具或管理系统中复用。已有869人学习适合具备一定 C# 基础、希望补齐 FTP 上传进度反馈与异常处理思路的开发者参考。1. 从一张卡死的上传界面说起带进度条的FTP实例定位上个月同事丢过来一个需求把本地一个 1.8GB 的日志备份包传到远端 FTP 服务器界面点完“开始上传”就只剩转圈半小时后用户以为程序死了直接强杀进程结果远端留下一个半截文件。问题不在 FTP 本身而在缺少可视反馈——这就是 FTP 上传实例带进度条要解决的核心问题。它把文件上传下载的字节流切成可观测的块每传完一块回调一次进度让用户看得见“还在动”。这套东西适合三类人需要给运维工具加传输模块的后端开发、要做桌面端文件同步的客户端开发以及刚接触网络编程、想搞明白“文件是怎么一块一块发出去”的新手。技术栈上这个实例绕不开 FTP 协议本身常见做法是用标准库自带的 FTP 客户端能力配合一个进度回调包装器不依赖重型框架几十行核心代码就能跑通上传、下载、断点续传三件事。2. FTP 传输的底层机制控制连接、数据连接与进度回调2.1 控制连接与数据连接为什么是两条FTP 和普通 HTTP 最大的区别是它把“发指令”和“搬数据”拆成了两条独立的 TCP 连接。控制连接负责登录、切换目录、发送 STOR/RETR 这类命令服务端回 220、331、150、226 这些状态码数据连接专门用来传输文件内容传完就关。这个设计在带进度条的场景里其实是个好事——控制连接上的状态码能告诉我们“传输已开始”“传输已结束”数据连接上的字节流则用来数进度。如果只盯着控制连接等 226中间过程是黑匣子用户看不到任何进展反过来如果把进度挂在数据连接上每读一个块就回调一次界面就能平滑地动起来。理解这一点很关键因为后面所有的进度计算都发生在数据连接这一侧。控制连接只负责“开始”和“结束”两个锚点中间过程全靠数据连接的块回调来填补。我见过有人把进度条挂在控制连接的回调上结果发现它只会在 0 和 100 之间跳变原因就是控制连接的响应粒度太粗根本拿不到字节级的进展。2.2 进度回调的本质每次读完一个块就报一次数文件传输在底层就是一个循环从本地文件读固定大小的块写进数据连接或者从数据连接读块写进本地文件。进度条要做的是在每次循环结束时把“已传字节数”和“总字节数”算成百分比推给 UI。这件事听起来简单但有两个参数会直接决定体验块大小和回调频率。块大小太小比如 1KB一次 1GB 文件要循环一百万次回调频率过高UI 线程会被刷屏拖慢块大小太大比如 8MB进度条就会一格一格地跳用户觉得卡顿。常见做法是 64KB 到 256KB 之间既能让进度条每秒更新几十次又不至于让回调成为瓶颈。回调函数本身也别做重活只负责把数字丢给 UI 层真正的进度条绘制交给主线程去合并刷新。2.3 ftplib、ftputil 还是 SFTP选型上的取舍在 Python 生态里标准库的 ftplib 是最轻的选择没有额外依赖storbinary 和 retrbinary 都提供了 callback 参数虽然这个 callback 拿到的是数据块而不是进度数字但足够我们包装出想要的回调。ftputil 在 ftplib 上做了一层文件对象抽象用起来更像操作本地文件但它的进度回调需要自己魔改反而多一层不确定性。至于 SFTP那是 SSH 协议族的东西和 FTP 不是一回事如果服务端只开了 21 端口选型上就没得挑。我一般会先确认服务端的协议和端口再决定用哪套库。如果只是内网文件搬运ftplib 足够如果要做目录递归同步、权限保留才考虑上 ftputil 或者自己封装一层。这个实例选择 ftplib是因为它的行为最透明——你能清楚看到每一次 socket 读写发生在哪里出问题也容易定位不会被上层抽象遮住。3. 上传实现分块传输、进度计算与断点续传3.1 用包装器给 storbinary 加上进度ftplib 的 storbinary 签名是storbinary(cmd, fp, blocksize8192, callbackNone, restNone)其中 fp 只需要提供read(blocksize)方法。它的内部循环是读一块、发一块、调一次 callback。注意这个 callback 收到的是原始数据块不是累计进度所以直接拿它更新进度条会显示成“当前块大小”不是百分比。解决办法是包一层文件对象在 read 返回时累计字节数再调用我们自己的进度回调。# progress_ftp.py import os from ftplib import FTP class ProgressFile: 包装本地文件对象在每次 read 时累计已传字节并回调 def __init__(self, fp, total_size, callbackNone): self.fp fp self.total_size total_size self.callback callback self.uploaded 0 def read(self, blocksize): buf self.fp.read(blocksize) self.uploaded len(buf) if self.callback: # 回调只传数字UI 层自己去决定怎么画 self.callback(self.uploaded, self.total_size) return buf class FtpUploader: def __init__(self, host, user, passwd, port21, timeout30): self.host host self.port port self.user user self.passwd passwd self.timeout timeout self.ftp None def connect(self): self.ftp FTP() self.ftp.connect(self.host, self.port, timeoutself.timeout) self.ftp.login(self.user, self.passwd) self.ftp.encoding utf-8 # 服务端如果是 latin-1需要按实际情况改 self.ftp.set_pasv(True) # 绝大多数场景走被动模式 def upload(self, local_path, remote_path, callbackNone, blocksize64 * 1024): total_size os.path.getsize(local_path) with open(local_path, rb) as f: wrapped ProgressFile(f, total_size, callback) # storbinary 内部会循环调用 wrapped.read(blocksize) self.ftp.storbinary( fSTOR {remote_path}, wrapped, blocksizeblocksize )这段代码的关键在ProgressFile.read每次 storbinary 向它要一块数据它就去真实文件里读读多少累计多少然后回调(uploaded, total_size)。blocksize 传 64KB 是一个折中值进度条大约每 16 次回调走完 1MB视觉上足够连贯。self.ftp.encoding utf-8这行要留意很多老 FTP 服务端默认用 latin-1 处理文件名传中文路径时如果不匹配远端会出现乱码目录后面避坑章会细说。3.2 断点续传SIZE、REST 与本地 seek 的三方对齐断点续传的流程比完整上传多三步先问服务端“这个文件现在多大”再让本地文件指针跳到同样位置最后在 STOR 命令前发一个 REST 命令告诉服务端“从第 N 字节开始接收”。ftplib 的 storbinary 支持 rest 参数它会在传输前自动发 REST。注意包装器的起始累计数要从远端已有大小开始算否则进度条会从 0 跳到 40% 之类的位置。def resume_upload(self, local_path, remote_path, callbackNone, blocksize64 * 1024): total_size os.path.getsize(local_path) remote_size 0 try: size self.ftp.size(remote_path) remote_size size if size else 0 except Exception: remote_size 0 # 远端文件不存在size 返回 None 或抛错 if remote_size total_size: # 远端已经完整直接返回避免重复传 if callback: callback(total_size, total_size) return with open(local_path, rb) as f: f.seek(remote_size) # 本地跳到远端已有位置 wrapped ProgressFile(f, total_size, callback) wrapped.uploaded remote_size # 进度起点对齐 self.ftp.storbinary( fSTOR {remote_path}, wrapped, blocksizeblocksize, restremote_size, # 告诉服务端从偏移量开始 )这里有个容易翻车的细节ftp.size()在部分服务端上对不存在的文件会抛异常而不是返回 None所以要用 try/except 兜住。另外restremote_size传的是字节偏移不是块序号别搞混。如果服务端不支持 REST 命令storbinary 会直接报错这时候只能退回完整上传判断依据是捕获异常后清空远端文件重传。3.3 缓冲区大小与回调频率的平衡blocksize 的选择会直接影响进度条观感和 CPU 占用。太小比如 8KB一次 1GB 文件要回调十几万次如果回调里还带 UI 刷新主线程会被拖慢太大比如 1MB进度条一次跳 0.1%用户会觉得“怎么半天不动”。我一般会把默认值设成 64KB再留一个参数让调用方按文件大小调整小文件用 16KB 让进度更细腻大文件用 256KB 减少回调次数。还有一点回调里不要做文件写盘或网络请求只更新内存里的计数器。真正画进度条的地方用一个定时器每 100ms 拉一次最新值这样即使回调很密集UI 也不会被刷爆。这个模式在桌面端和 Web 后端都适用本质上就是把“高频计数”和“低频渲染”解耦。4. 下载实现retrbinary、进度条与断点恢复4.1 retrbinary 的块回调如何驱动进度条下载侧的 API 是retrbinary(cmd, callback, blocksize8192, restNone)它的 callback 每收到一块数据就调用一次拿到的是 bytes 块。和上传不同这里不需要包装文件对象直接在回调里写盘并累计字节数就行。总大小通过ftp.size(remote_path)提前问服务端要拿不到就传 0进度条退化成“已下载多少 MB”的计数模式。def download(self, remote_path, local_path, callbackNone, blocksize64 * 1024): total_size self.ftp.size(remote_path) or 0 downloaded 0 with open(local_path, wb) as f: def _write_block(block): nonlocal downloaded f.write(block) downloaded len(block) if callback: callback(downloaded, total_size) self.ftp.retrbinary( fRETR {remote_path}, _write_block, blocksizeblocksize )nonlocal downloaded是 Python 闭包里修改外层变量的写法别忘了。total_size用or 0兜底是因为有些服务端对 SIZE 命令返回 None直接参与除法会抛异常。回调里同步写盘是安全的因为 retrbinary 本身就在当前线程顺序读 socket写盘耗时会被算进块间隔里只要磁盘不是特别慢不会明显拖慢下载速度。4.2 下载断点续传的两处偏移下载的断点续传需要对齐两个偏移本地文件已有大小、服务端 REST 的起始位置。逻辑是先看本地文件多大如果比远端总大小还大说明本地文件是坏的直接清空重下否则以追加模式打开把本地大小作为 rest 传给 retrbinary同时在回调累计时从本地大小起算。def resume_download(self, remote_path, local_path, callbackNone, blocksize64 * 1024): total_size self.ftp.size(remote_path) or 0 local_size os.path.getsize(local_path) if os.path.exists(local_path) else 0 if total_size and local_size total_size: local_size 0 open(local_path, wb).close() # 本地比远端大判定为脏文件 if total_size and local_size total_size: if callback: callback(total_size, total_size) return downloaded local_size with open(local_path, ab) as f: def _write_block(block): nonlocal downloaded f.write(block) downloaded len(block) if callback: callback(downloaded, total_size) self.ftp.retrbinary( fRETR {remote_path}, _write_block, blocksizeblocksize, restlocal_size, )注意文件打开模式是ab而不是wb否则会把已下载的部分清掉。restlocal_size只在 local_size 大于 0 时才真正生效传 0 等价于从头下载不会报错。有一个边界情况如果服务端不支持 RESTretrbinary 会抛异常这时候捕获后把 local_size 重置为 0 再调一次完整下载别让用户卡在半截文件上。4.3 大文件场景的流式落盘所谓流式落盘就是不要等整个文件下载完再写而是每收到一块就写一块。retrbinary 的回调模式天然支持这一点上面的代码已经是流式的。但有一个坑如果用wb打开文件后又因为网络中断抛异常文件会留下一个不完整的状态下次续传时 local_size 就是实际收到的字节数正好可以用来续传。所以不要在异常处理里删文件保留半截文件反而是断点恢复的基础。代价是磁盘上会短暂存在不完整文件如果业务上不能接受可以下载到.part临时文件完成后再重命名。这个细节在文件同步工具里很常见我在实际项目里也会默认用.part后缀避免下游程序误读半成品。5. 避坑与排查FTP 上传下载最容易翻车的几个点5.1 现象进度条瞬间到 100% 但远端文件为空这是最典型的一种翻车。现象是上传回调一路涨到 100%但去服务端一看文件大小是 0或者只有几 KB。原因通常有两个一是控制连接上的 STOR 命令返回了 550 权限拒绝但代码没有检查ftp.storbinary的返回值回调仍然在本地读文件看起来像传完了二是数据连接实际上没建立成功块被写进了错误的通道。解决方式是每次传输后调用ftp.voidresp()或者检查 storbinary 的返回字符串确认服务端回了 226。更稳妥的做法是在上传前先ftp.cwd(remote_dir)确认目录存在再用ftp.size(remote_path)验证文件是否真的落地。如果返回 None 或异常说明传输没生效别被本地回调骗了。5.2 现象中文文件名乱码或无法创建远端出现???.txt或者文件.txt这种名字基本是编码不匹配。ftplib 默认 encoding 是 utf-8但很多老服务端用 latin-1 或者 GBK 处理文件名。现象是本地叫“测试文件.txt”传上去变成乱码下载回来也对不上。解决方式是先确认服务端的字符集把ftp.encoding设成对应值。如果服务端混用可以在连接后发OPTS UTF8 ON尝试开启 UTF-8 模式再用一个中文名文件做小流量测试。测试通过再传正式文件别拿生产数据去试编码。5.3 现象大文件传到一半断连传几百 MB 以上文件时连接在几分钟后断开报EOFError或ConnectionResetError。原因可能是服务端有空闲超时也可能是 NAT 或防火墙掐掉了长连接。被动模式下数据连接是服务端发起的如果客户端在 NAT 后面端口映射不稳定也会导致中断。解决方式分两层应用层加心跳传输大文件时定期发NOOP命令保持控制连接活跃传输层设置合理的 socket 超时并在断连后从已传字节数继续续传而不是从头再来。这就是前面断点续传代码的价值——中断不可怕能接着传就行。5.4 现象断点续传后文件校验失败续传完成后文件大小对得上但 MD5 不一致。常见原因是本地文件在续传前被其他进程改过或者远端文件在两次传输之间被替换导致 REST 的偏移量和实际内容错位。还有一种情况是上传时用了文本模式而不是二进制模式换行符被转换字节数对不上。解决方式是传输前记录文件大小和修改时间续传时先比对远端 SIZE 和本地偏移是否一致上传下载一律走二进制模式storbinary 和 retrbinary 本身就是二进制不要用 storlines。传完后做一次 MD5 或 SHA256 校验校验失败就删掉远端文件重传别在错位上继续追加。6. 二次开发把示例封装成可复用的 FtpClient把上面的代码收进一个类之后二次开发的空间主要在三个方向。第一是连接池化FTP 控制连接是有状态的不能像 HTTP 那样随便并发但可以维护一个小池子每个上传任务独占一条连接用完归还。第二是进度回调的适配层把callback(loaded, total)这种原始数字转成 WebSocket 消息、Qt 信号或者命令行进度条让上层不关心底层是 FTP 还是别的协议。第三是失败重试策略。我一般会把上传下载包在一个带指数退避的重试循环里重试前先走一遍resume_upload或resume_download利用已有的断点信息接着传而不是无脑重来。下面这个骨架把连接、重试和进度串在一起import time class FtpClient: def __init__(self, host, user, passwd, port21, retries3): self.uploader FtpUploader(host, user, passwd, port) self.retries retries def upload_with_retry(self, local_path, remote_path, callbackNone): last_err None for attempt in range(self.retries): try: if self.uploader.ftp is None: self.uploader.connect() self.uploader.resume_upload(local_path, remote_path, callback) return True except Exception as e: last_err e self.uploader.ftp None # 连接已坏下次重建 time.sleep(2 ** attempt) # 指数退避1s, 2s, 4s raise last_err这段骨架里self.uploader.ftp None是血泪经验——FTP 连接一旦抛异常基本就不可用了必须重建不能在上面继续发命令。time.sleep(2 ** attempt)做指数退避避免服务端刚重启就被重试打满。回调函数透传给resume_upload续传时进度会从已有偏移量接着走用户看到的是“重连后从 60% 继续”而不是“又回到 0%”。验证方法我一般分三组小文件几 KB验证路径和权限大文件几百 MB验证进度和断点断网场景验证重试和续传。断网测试不用真拔网线在传输中途手动把self.uploader.ftp.sock.close()模拟异常即可。三组都过了这套封装才敢放进生产工具里。从那以后我每次接文件传输需求都会先把断点续传和失败重试跑一遍再谈 UI因为进度条再好看传断了接不上也是白搭。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

动态规划实战:摆动序列与最大子数组和算法解析

动态规划实战:摆动序列与最大子数组和算法解析

1. 摆动序列与最大子数组和问题概述在算法领域,摆动序列和最大子数组和是两个经典问题,经常出现在编程面试和算法竞赛中。2026年2月2日这个日期可能暗示着某个特定算法竞赛或编程挑战的举办时间。这两个问题虽然看似独立,但实际上都涉及数组处…

📅 2026/11/2 3:32:59
2026科研检索工具排行榜:10个文献检索工具与AI自动化工作流实战

2026科研检索工具排行榜:10个文献检索工具与AI自动化工作流实战

1. 科研检索这件事,为什么2026年值得重新聊一遍我做了十多年科研工具相关的工作,从最早在图书馆里翻纸质索引,到后来用各种数据库的高级检索语法,再到现在看着一批批年轻研究者用AI Agent自动跑文献综述,这个领域的变化…

📅 2026/11/2 3:32:59
如何让ASR-LLM-TTS语音链路具备‘活人感’?

如何让ASR-LLM-TTS语音链路具备‘活人感’?

1. 为什么“ASR→LLM→TTS”这条链路在真实对话中总像在跟录音机聊天?你有没有试过用市面上那些标榜“超自然语音交互”的AI产品?刚开口说“今天天气怎么样”,它秒回“根据最新气象数据,北京城区今日晴转多云,最高气温…

📅 2026/11/2 3:32:59
MORE NEWS

更多资讯

📰

逆向拆解六款开源RAG:从架构共性到自研蓝图与避坑指南

RAG 这个词从 2023 年火到现在,热度一直没降过。但我在实际项目里观察到一个很普遍的现象:很多团队一上来就自己造轮子,从文档解析、切片、向量化到检索排序全部手写一遍,结果做出来的东西召回率惨不忍睹,用户问三句答…

📰

RAG答错别急着换模型:文档预处理才是效果分水岭

1. 为什么模型背了锅:RAG 答错的真实归因RAG 答错的时候,绝大多数人的第一反应是换模型。从 7B 换到 14B,从 14B 换到 70B,再不行就上闭源 API,账单翻了几倍,效果可能只提升了几个百分点。我见过太多团队在…

📰

Formula One 9.0 工业级表格控件深度解析与COM集成实战

简介:Formula One 9.0 是一款面向VTS(视频标题系统或车辆追踪系统)内容编辑场景的免费桌面工具,适用于媒体制作、交通监控系统运维及Java GUI开发学习者,解决模板化内容快速生成与定制化编辑需求。资源包为完整安装版&…

📰

Python写模型,Java扛服务:AI落地的真实分工

前阵子有个写Java后端的朋友问我:AI这波热潮里,Python是不是已经把Java的活全抢了?他焦虑的原因很现实——打开招聘软件,算法岗几乎都写着"熟悉Python",连不少Java岗位的JD里都开始加一句"了解Python优…

📰

RAGFlow企业知识库选型与部署实战:DeepDoc解析、GraphRAG与Dify对比

1. 企业知识库选型的核心矛盾与 RAGFlow 的切入点企业知识库这个赛道,这两年我最大的感受就是:Demo 满天飞,落地一地鸡毛。几乎每一家声称要做“企业大脑”的团队,第一周就能搭出一个能问答的机器人,但真正扔进业务部门…

📰

35岁转行网络安全的挑战与可行路径

1. 大龄转行网安的行业现状剖析35岁转行网络安全这个看似充满诱惑的选择,背后隐藏着许多行业真相。作为一个在安全行业摸爬滚打多年的从业者,我见过太多满怀希望转行而来,最终却黯然离场的案例。网络安全行业确实存在人才缺口,但这…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬