简介:这份资源面向需要实现FTP文件上传功能的开发者,尤其适合正在学习C#网络编程、希望为上传过程加入可视化进度反馈的初中级程序员。压缩包共35个文件,以cs源码、resx与resources资源文件、config配置、exe可执行程序及sln解决方案、csproj项目文件为主,整体约78KB,结构完整,可直接用Visual Studio打开运行调试。资源围绕FTP连接建立、登录验证、切换工作目录、分块上传与进度计算等环节展开,封装了可复用的FTP帮助类,便于在其它项目中直接调用。已有868人学习下载,读者可从中掌握上传进度的计算与回调更新思路,并参考其中的错误处理与重试策略,快速搭建自己的带进度条上传模块。
1. FTP上传实例带进度条:一个被低估的刚需场景
做桌面工具或内部管理系统的开发者,迟早会撞上一个需求:把本地文件传到 FTP 服务器,而且用户要能看到“传到哪了”。听起来简单,真动手就发现坑不少——标准库的 FTP 客户端是同步阻塞的,进度回调没地方挂;大文件传一半断了,重连后从零开始;中文文件名在部分服务器上直接乱码。这份 FTP 上传实例(带进度条)源码包,解决的就是这类“看着不起眼、写起来费劲”的落地问题。它把上传流程拆成可观测的分块读写,每传一段就回调一次进度,适合需要给用户可视化反馈的文件同步工具、批量上传客户端、以及教学演示场景。下面按“能跑起来 → 看懂进度怎么算 → 避开断点与编码的坑”这条线拆开讲。
2. 进度条背后的分块读写:为什么不能直接调一个 upload 就完事
2.1 同步 FTP 客户端的能力边界
多数语言标准库或轻量 FTP 库提供的上传接口,本质是一次性把文件流交给底层 socket,调用方拿不到中间状态。以 Python 的ftplib为例,storbinary接受一个文件对象和块大小,内部循环读块、写 socket,但循环过程对调用方不可见。想拿到进度,只有两条路:一是自己接管读写循环,把storbinary换成手动transfercmd+ 分块sendall;二是用回调钩子,但并非所有库都提供。
这份实例采用的是第一条路,也是兼容性最好的做法。核心思路是:打开本地文件,按固定块大小(常见 8KB 到 64KB)循环读取,每读一块就通过数据连接发送,同时累加已发送字节数,用“已发送 / 总大小”算出百分比,推给 UI 层。这样进度是真实的字节级进度,不是“开始 0%、结束 100%”的假动画。
选块大小有讲究。块太小,循环次数多,回调频繁,UI 线程容易被刷屏拖慢;块太大,进度跳动明显,用户感觉卡顿。我一般用 16KB 或 32KB,局域网内可以拉到 64KB。下面是一段可抄的骨架,语言用 Python,其他语言照这个结构翻译即可。
import ftplib import os def upload_with_progress(host, user, pwd, local_path, remote_path, block_size=32768, on_progress=None): total = os.path.getsize(local_path) sent = 0 ftp = ftplib.FTP(host, timeout=30) ftp.login(user, pwd) # 被动模式在多数 NAT 环境下更稳,先设上 ftp.set_pasv(True) # 二进制模式,避免换行符被改写 with open(local_path, 'rb') as f: conn = ftp.transfercmd(f'STOR {remote_path}') try: while True: chunk = f.read(block_size) if not chunk: break conn.sendall(chunk) sent += len(chunk) if on_progress: on_progress(sent, total) finally: conn.close() ftp.voidresp() # 收尾,确认服务器返回 226 ftp.quit()这段代码的关键点在三个地方。transfercmd('STOR ...')返回的是数据连接,控制连接仍由ftp对象持有,所以发送完必须调voidresp()读取服务器的完成响应,否则下一次操作会读到残留响应导致协议错位。set_pasv(True)是主动防御,很多服务器在 NAT 后面只接受被动模式。on_progress回调里只做数值传递,真正的 UI 更新要切回主线程,这一点在桌面框架里尤其重要,后面避坑章节会展开。
2.2 进度回调的线程安全与节流
进度回调最容易被忽视的是线程模型。上传通常跑在后台线程,而进度条控件属于 UI 线程,直接跨线程改控件在多数框架里会崩或静默失败。常见做法是回调里只把(sent, total)塞进队列或通过信号槽投递,由 UI 线程定时取出刷新。另一个细节是节流:32KB 块传一个 1GB 文件要回调三万多次,没必要每次都刷界面,可以按时间间隔(比如 100ms)或百分比变化(比如每 1%)才更新一次。
import time class ProgressThrottle: def __init__(self, min_interval=0.1): self.min_interval = min_interval self.last = 0.0 def should_emit(self): now = time.monotonic() if now - self.last >= self.min_interval: self.last = now return True return False把should_emit的判断放在回调入口,能显著降低 UI 压力。参数min_interval按界面刷新率调,60Hz 屏幕给 0.016 秒理论上够,但实际给 0.1 秒体感更平滑,因为进度条本身有动画过渡。这个类不依赖具体框架,Qt、WinForms、Tkinter 都能套。
2.3 断点续传的判定条件
带进度条的上传,用户最常问的下一个问题是“断了能不能接着传”。FTP 协议本身支持续传,靠的是APPE(追加)或REST(偏移)命令,但前提是服务器支持并且文件已存在。判定逻辑要分三种情况:远端文件不存在,走全新STOR;远端存在且大小小于本地,用REST指定偏移后STOR;远端大小等于或大于本地,要么跳过要么提示覆盖。这里有个坑:REST之后必须紧跟STOR,中间不能插别的命令,否则偏移失效。
def remote_size(ftp, remote_path): try: return ftp.size(remote_path) except ftplib.error_perm: return None # 文件不存在或无权限查询 def resume_offset(ftp, local_path, remote_path): rsize = remote_size(ftp, remote_path) if rsize is None: return 0 lsize = os.path.getsize(local_path) if rsize < lsize: return rsize return None # 远端不短于本地,交给上层决定覆盖还是跳过拿到偏移后,本地文件seek(offset),发送前调ftp.sendcmd(f'REST {offset}'),再transfercmd('STOR ...')。注意REST的返回值是 350,不是 200,判断成功要看前缀。这套逻辑写进实例后,进度条的起点也要相应改成从偏移量开始算,否则用户会看到进度从 0 跳到 40% 再走,体验很怪。
3. 从源码到可运行:环境、参数与一次完整上传
3.1 运行环境与依赖确认
这份实例是轻量脚本型项目,不依赖重型框架,跑起来只需要目标语言的运行时和标准库里的 FTP 模块。以 Python 版为例,3.7 以上即可,不需要额外pip install。如果实例里带了 GUI 部分,会依赖对应的界面库,按源码里的导入语句装齐就行。部署前先确认三件事:本机到 FTP 服务器的 21 端口控制连接通、被动模式的数据端口段没被防火墙拦、目标目录有写权限。前两条用任意 FTP 客户端手动连一次就能验证,第三条在登录后PWD加MKD试一下。
参数层面,实例暴露的配置项通常包括主机、端口、用户名、密码、本地路径、远端路径、块大小、超时。建议把超时设成 30 秒起步,跨公网上传时给到 60 秒,避免大块传输时被误判断线。块大小按网络质量调,内网 64KB,公网 16KB 到 32KB 更稳。
3.2 一次完整上传的执行顺序
把前面几块拼起来,一次带进度、可续传的上传流程是这样的:
def full_upload(cfg, on_progress=None): ftp = ftplib.FTP(cfg['host'], timeout=cfg.get('timeout', 30)) ftp.login(cfg['user'], cfg['pwd']) ftp.set_pasv(True) offset = resume_offset(ftp, cfg['local'], cfg['remote']) if offset is None: raise RuntimeError('远端文件不短于本地,需确认是否覆盖') total = os.path.getsize(cfg['local']) with open(cfg['local'], 'rb') as f: f.seek(offset) if offset > 0: ftp.sendcmd(f'REST {offset}') conn = ftp.transfercmd(f"STOR {cfg['remote']}") sent = offset try: while True: chunk = f.read(cfg.get('block', 32768)) if not chunk: break conn.sendall(chunk) sent += len(chunk) if on_progress: on_progress(sent, total) finally: conn.close() ftp.voidresp() ftp.quit()执行顺序不能乱:登录 → 设被动 → 查远端大小定偏移 → 本地 seek → 发 REST → 开数据连接 → 循环发送 → 关数据连接 → 读完成响应 → 退出。任何一步颠倒,轻则续传失效,重则控制连接协议错位,表现为下一次命令返回上一次的结果。on_progress的sent从offset起算,保证进度条百分比连续。
3.3 上传结果的校验方式
传完不能只看“没报错”就完事。可靠的校验是比对远端文件大小与本地是否一致,一致再算成功。部分服务器对SIZE命令在二进制模式下的返回值是准确的,直接ftp.size(remote)对比os.path.getsize(local)即可。如果服务器不支持SIZE,退而求其次用LIST解析,但解析格式因服务器而异,不如大小比对稳。校验失败时保留本地文件、记录远端路径,方便人工排查,别自动删源文件。
4. 避坑与排查:中文名、被动模式与半截文件
4.1 中文文件名上传后变乱码
现象:本地叫“报告.pdf”,传上去变成一串问号或乱码。原因:FTP 控制连接的文件名编码取决于服务器实现,部分服务器默认用 Latin-1,而客户端按 UTF-8 发送,两边对不上。解决:登录后先发OPTS UTF8 ON尝试开启 UTF-8,服务器不支持时再退回按服务器实际编码转码文件名。判断服务器能力可以看FEAT命令的返回里有没有UTF8。这一步放在登录之后、任何文件操作之前。
4.2 被动模式连不上数据连接
现象:控制连接登录成功,一到上传就卡住或报超时。原因:客户端默认主动模式,服务器回连客户端的数据端口被防火墙或 NAT 挡住。解决:显式set_pasv(True),并确认服务器被动端口段在防火墙放行。如果服务器返回的被动地址是内网 IP,而客户端在外网,需要在客户端侧忽略服务器返回的地址、改用控制连接的对端地址,部分库提供trust_server_pasv_ipv4_address之类的开关,按所用库的文档处理。
4.3 上传中断留下半截文件
现象:网络抖动后上传失败,远端留下一个不完整文件,下次续传时偏移算错。原因:STOR已经开始写,连接断开时服务器不会自动删除半截文件。解决:续传逻辑必须基于远端实际大小算偏移,而不是本地记录的“已传字节”。另外可以在上传前把远端临时文件名加后缀,传完再RNFR/RNTO重命名,失败时清理临时文件,避免半截文件被误认为完整文件。
4.4 进度条卡在 99% 不动
现象:进度显示接近完成但迟迟不到 100%。原因:最后一块发完后,voidresp()在等服务器返回 226,这段时间没有字节发送,进度自然不动;如果服务器处理慢或网络延迟高,观感就是卡住。解决:把“发送完成”和“服务器确认”分成两个状态,发送到 100% 后进度条切到“校验中”或“等待确认”,别让用户以为卡死。同时给voidresp所在的 socket 设合理超时,超时后明确报错而不是无限等。
4.5 大文件传输内存暴涨
现象:传几百 MB 的文件时进程内存持续上涨。原因:误用了f.read()不带参数,一次性把整个文件读进内存。解决:循环里必须带块大小参数,且块大小别设成文件总大小。检查代码里所有read(调用,确认都有参数。这个坑在照抄示例代码时最容易犯,因为很多示例为了简短写成f.read()。
5. 进阶:把进度回调做成可复用的上传器
把上面的逻辑收成一个类,比散落的函数更好维护,也方便在不同项目里复用。核心是把连接管理、续传判定、进度节流、结果校验四件事分开,每件都能单独替换。下面这个骨架去掉了具体 UI 依赖,只保留可测试的部分。
class FtpUploader: def __init__(self, host, user, pwd, timeout=30, block=32768): self.host, self.user, self.pwd = host, user, pwd self.timeout, self.block = timeout, block self.ftp = None def connect(self): self.ftp = ftplib.FTP(self.host, timeout=self.timeout) self.ftp.login(self.user, self.pwd) self.ftp.set_pasv(True) try: self.ftp.sendcmd('OPTS UTF8 ON') except ftplib.error_perm: pass # 服务器不支持就按默认编码走 def upload(self, local, remote, on_progress=None): offset = resume_offset(self.ftp, local, remote) or 0 total = os.path.getsize(local) with open(local, 'rb') as f: f.seek(offset) if offset: self.ftp.sendcmd(f'REST {offset}') conn = self.ftp.transfercmd(f'STOR {remote}') sent = offset try: while True: chunk = f.read(self.block) if not chunk: break conn.sendall(chunk) sent += len(chunk) if on_progress: on_progress(sent, total) finally: conn.close() self.ftp.voidresp() return self.verify(local, remote) def verify(self, local, remote): try: return self.ftp.size(remote) == os.path.getsize(local) except ftplib.error_perm: return False def close(self): if self.ftp: self.ftp.quit()这个类的好处是每个方法职责单一:connect管连接和编码协商,upload管传输和进度,verify管结果,close管释放。测试时可以用本地搭的 FTP 服务跑一遍,重点验证三个边界:空文件上传、远端已存在且更短时的续传、远端已存在且更长时的拒绝逻辑。空文件容易在read循环里直接退出,voidresp仍要调用;续传的偏移要落在块边界上,否则部分服务器会拒绝REST。
验证方法上,我习惯用“大小比对 + 抽样哈希”两步。大小比对快,能挡住绝大多数截断问题;抽样哈希取文件头尾各 1MB 算摘要,比全文件哈希省时间,又能发现内容错位。这套组合在批量上传场景里比单纯看进度条可靠得多。
从那以后我每次写上传逻辑,都强制先跑一遍“空文件、半截文件、中文名”这三个用例,再谈进度条好不好看。进度条是给用户看的,续传和校验才是给自己留的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取