简介这是一份专注于FTP上传场景的C#示例工程面向需要实现带进度反馈的桌面工具开发者。工程演示了从建立FTP连接、登录认证、切换目录到分块传输与进度计算的完整流程适合正在学习网络编程或准备移植上传功能的读者参考。压缩包约78KB共35个文件主体为12个C#源码文件辅以窗体设计资源、配置文件、可执行程序及调试符号等结构清晰便于直接查看核心上传逻辑与界面交互。已有866人学习适合作为快速上手FTP上传进度条机制的入门模板。通过阅读代码可以掌握如何借助回调跟踪已发送字节数、按比例刷新进度条并了解异常处理与资源释放等实践细节为后续扩展断点续传或分块上传提供基础。 FTP这技术说起来挺老但在实际项目里一直没退役。近两年我陆续在给企业做内部系统时碰了好几次同样的需求文件得传到远端FTP服务器但上传过程不能干瞪眼用户要看到进度条。有的场景是给复合机做扫描文件回传有的是嵌入式设备上报日志还有的是老系统对接数据得走FTP落库。标题叫“FTP上传实例带进度条”看着简单真做起来坑不少。进度条这事自己一旦上手就会发现它表面上是UI问题实际是整个传输链路的问题。这篇文章我把自己实操中验证过的方案、计算逻辑、代码示例和排坑记录都整理出来覆盖Python脚本、Web前端、文件夹递归上传、断点续传这几个方向。不管你是给内部工具做个文件上传还是做一套带进度反馈的对接程序都值得花几分钟看完。1. FTP上传方案的整体设计思路1.1 为什么FTP还是很多场景下的硬需求前阵子跟人聊天对方觉得FTP活着就是个技术活化石。其实真不是这样FTP在特定场景下反而比HTTP接口更省事。比如内部网络里的文件分发、扫描仪和多功能一体机的扫描到文件夹功能、工业设备的数据上报、Linux服务器之间的备份传输这些场景里FTP至今是最通用的协议。很多设备固件里直接内置了FTP客户端你要改造成HTTP接口反而得动硬件固件完全没必要。另一个现实因素是FTP服务端部署极其简单。Linux上装个vsftpdWindows上开个IIS FTP或FileZilla Server几分钟就能跑起来。权限控制、用户隔离、匿名访问这些基础能力全都现成。所以当项目要求“把文件传到对方指定的FTP目录”时我基本都是直接按FTP来设计不会再绕到HTTP上去。1.2 进度条背后到底在算什么进度条的数学逻辑非常直白已传输字节数除以文件总字节数乘100%。但到了实操里真正的难点在于“怎么拿到这个已传输字节数”还有“传输到哪一步才算真正完成”。FTP上传走的是数据连接而进度反馈是靠上传命令的数据回调。以Python的ftplib为例storbinary方法支持一个callback参数每次写入一块数据到数据连接时这个回调就会被触发回调里的data就是这一块bytes累加它的长度就能得到已传字节。这是进度条的数据来源。这里必须提一个容易误导新手的地方传输进度到100%不代表上传完成。数据连接传完最后一个字节服务端还有响应确认命令连接上还有一层应答两步都走完才算结束。所以进度条不要卡在“发完数据立刻提示完成”要等在quit()或close()收到服务端响应之后再收尾。很多开发遇到“进度条卡在99%”的问题其实就是这里没处理好。1.3 从零造还是用现成库我的选型标准实现FTP上传能用标准库就不用第三方库能用协议库就别自己抡socket。我之前见过有人直接用socket去解析FTP协议传个小文件还行传大文件遇到被动模式数据连接、超时重连、目录切换这些问题代码会迅速膨胀到没法维护。Python方向用ftplib就足够它内置了FTP和FTP_TLS单文件上传、目录切换、文件列表都能做。Java方向用Apache Commons NetAndroid也是用它居多。如果你在Web前端做上传要注意浏览器现在基本已经移除了ftp://协议支持所以网页端是没法直连FTP服务器的只能通过后端中转或者是走HTTP网关再由后端同步到FTP。这里面的进度条分为两段一段是浏览器到后端一段是后端到FTP我后面章节会展开说。2. 核心细节解析与实操要点2.1 主动模式与被动模式选哪个FTP区别于HTTP的地方在于它有两条连接一条命令连接通常是21端口一条数据连接。数据连接的建立方式又分主动模式和被动模式。我做项目时一律默认采用被动模式让客户端发起数据连接。原因是主动模式下服务器会主动去连接客户端的一个随机端口这意味着客户端设备必须开放入站端口在大多数办公网络和企业安全策略下根本做不到。被动模式下客户端去连接服务器的某个数据端口只要服务器放行了21端口和一定范围的被动端口段整条链路就通了。部署时记得在防火墙上把被动端口段一并放行不要只开了21端口然后吐槽“为什么能登录但传不了文件”。2.2 二进制传输与缓冲区设置FTP协议里有“传输方式”的概念ASCII模式和二进制模式。ASCII模式会把换行符按平台转换Windows和Linux的换行本来就不同传个压缩包或者图片过去被改坏问题就出在这。所以做上传时一律用二进制模式。ftplib里的storbinary就是固定二进制比storbinary对应的ASCII方向方法storlines要稳得多。缓冲区的设置我跟大家分享一个经验默认blocksize是8192字节也就是每次读8KB写8KB。传小文件没毛病传大文件时效率能明显感觉到慢。实测把blocksize调到64KB或128KB在局域网环境下传输速度有明显提升同时回调触发频率也不会太高进度条的刷新不至于过密导致UI卡顿。服务器带宽一般调的特别大反而会占内存我给的建议是64KB起步最多128KB。2.3 进度计算与文件名编码的坑进度计算时需要注意一个实际偏差网络传输中块的边界是分片的一个文件分成了1000份这个比例偶尔会细微波动所以进度百分比出现99.9%、100.1%之类的数很正常大多数进度条库会在内部做截断处理。更头疼的是文件名编码。Windows自带的FTP服务默认用的是ANSI编码常见就是GBKLinux上的vsftpd默认UTF-8。如果你的程序是跨平台用的中文文件名传到Linux的FTP上是乱码传到Windows上又是另一套乱码。解决方案是在创建FTP连接后根据服务器系统的编码来做适配。Python里可以用ftp.encoding gbk或utf-8来切换但要注意这个属性在不同版本中的位置第一次设置前最好查一下版本文档。更保险的做法是统一约定服务器端使用UTF-8大多数vsftpd配置支持这样调整。3. 实操过程与核心环节实现3.1 Python ftplib实现单文件上传进度条这里给一个可跑的完整示例核心就是storbinary配合callback做进度累加。为了照顾控制台演示我直接把进度打印出来实际项目里可以替换成回调节点更新UI。import os from ftplib import FTP def upload_file_with_progress(host, port, user, password, local_path, remote_dir, remote_nameNone): file_size os.path.getsize(local_path) uploaded [0] def progress_callback(data): uploaded[0] len(data) percent (uploaded[0] / file_size) * 100 print(f\rProgress: {percent:.2f}% ({uploaded[0]}/{file_size} bytes), end, flushTrue) ftp FTP() ftp.connect(host, port, timeout30) ftp.login(user, password) ftp.set_pasv(True) ftp.cwd(remote_dir) remote_file remote_name or os.path.basename(local_path) with open(local_path, rb) as f: ftp.storbinary(fSTOR {remote_file}, f, blocksize65536, callbackprogress_callback) ftp.quit() print(\nUpload complete.) if __name__ __main__: upload_file_with_progress( host192.168.1.100, port21, userftpuser, passwordftppass, local_path./release.zip, remote_dir/backup )这个例子里我把blocksize设成了64KB。实际操作中你按这个脚本改改参数就能直接跑通。有一点要记得cwd切目录前最好确认目录存在目录不存在时直接STOR会抛异常需要先调用ftp.mkd()创建目录。3.2 递归上传整个文件夹的完整实例很多业务场景不是传一个文件而是要把整个目录整体传上去比如前端的构建产物、日志打包目录。这里给出递归上传的思路核心是先用os.walk遍历本地文件系统在FTP上逐一创建目录再逐个上传文件。import os from ftplib import FTP, error_perm def ensure_remote_dir(ftp, remote_dir): try: ftp.cwd(remote_dir) except error_perm: parts remote_dir.split(/) current for part in parts: if not part: continue current / part try: ftp.cwd(current) except error_perm: ftp.mkd(current) ftp.cwd(current) def upload_directory(ftp, local_dir, remote_dir): ensure_remote_dir(ftp, remote_dir) total_files 0 total_bytes 0 for root, dirs, files in os.walk(local_dir): rel_dir os.path.relpath(root, local_dir) current_remote_dir remote_dir if rel_dir . else f{remote_dir}/{rel_dir.replace(os.sep, /)} ensure_remote_dir(ftp, current_remote_dir) for name in files: local_file os.path.join(root, name) total_bytes os.path.getsize(local_file) total_files 1 processed_bytes [0] processed_files [0] for root, dirs, files in os.walk(local_dir): rel_dir os.path.relpath(root, local_dir) current_remote_dir remote_dir if rel_dir . else f{remote_dir}/{rel_dir.replace(os.sep, /)} ftp.cwd(current_remote_dir) for name in files: local_file os.path.join(root, name) file_size os.path.getsize(local_file) def progress_callback(data): processed_bytes[0] len(data) file_percent (processed_bytes[0] / total_bytes) * 100 print(f\rFiles: {processed_files[0]}/{total_files}, Bytes: {processed_bytes[0]}/{total_bytes} ({file_percent:.2f}%), end, flushTrue) with open(local_file, rb) as f: ftp.storbinary(fSTOR {name}, f, blocksize65536, callbackprogress_callback) processed_files[0] 1 print(\nAll files uploaded.)这段代码有两个值得留意的点。第一我硬性把文件路径里的os.sep替换成/因为FTP协议里目录分隔符统一用斜杠Windows下跑脚本如果不处理会出现路径错乱。第二第一遍先统计总字节和总文件数这样用户才能看到全局进度而不是单个文件内进度。统计阶段会稍微花点时间但换来的进度完整性在传大目录时体验很好。3.3 网页端上传进度条的实现思路前端上传FTP一定要先想清楚链路。现代浏览器不再支持ftp://协议所以网页只能把文件POST给后端接口后端再用FTP协议转发给目标服务器。这里有两个进度节点浏览器到后端后端到FTP。浏览器到后端这段推荐用XMLHttpRequest而不是fetch因为fetch的Upload接口支持度有限而xhr.upload.onprogress事件是标准且成熟的。示例代码如下const input document.getElementById(file-input); input.addEventListener(change, (event) { const file event.target.files[0]; const formData new FormData(); formData.append(file, file); const xhr new XMLHttpRequest(); xhr.open(POST, /api/upload); xhr.upload.onprogress (e) { if (e.lengthComputable) { const percent Math.round((e.loaded / e.total) * 100); document.getElementById(progress-bar).style.width percent %; document.getElementById(progress-text).innerText percent %; } }; xhr.onload () { if (xhr.status 200) { console.log(Upload to backend success); } }; xhr.send(formData); });第二部分后端收到文件后再按上一章的Python或Java方式上传到FTP。如果希望后端到FTP的进度也能体现在网页上可以把FTP上传进度通过WebSocket或SSE实时推给前端。这样一条链路上前端拿到的是两步进度的汇总用户可以直观看到从本地到后端再到FTP服务器的完整过程。这种方案我在做运维后台时用过用户感知明显比“只有一个转圈”好很多。4. 常见问题与排查技巧实录4.1 连接失败、被动模式与防火墙先说最经典的“能登录但传不了文件”。FTP登录走21端口数据连接走另开的端口。如果服务器只放行21端口被动模式下数据连接根本建立不了表现就是进度条刚出来马上报“数据连接超时”或“无法建立数据连接”。解决思路是到FTP服务器上把被动端口范围固定下来比如vsftpd里配置pasv_min_port50000和pasv_max_port50100再把这个范围在防火墙里一并放行。顺便说一句很多云服务器的安全组规则里默认只放行了21这一步特别容易漏。另一个相关报错是FileZilla客户端连的时候提示“不支持的FTP over TLS”或者“服务器不支持FTP over TLS”。这其实不是错误是FileZilla默认用“要求显式FTP over TLS”的加密策略而你的服务器只开了普通FTP。如果在内网可信环境可以在站点管理器里把加密改为“仅使用普通FTP”。不要一看到“不安全”就觉得服务器挂了。4.2 进度条不动、提前满格、卡99%进度条纹丝不动大概率是数据连接压根没通或者是服务端处理慢但连接没断。排查时先看服务器端的日志看有没有数据连接建立记录。如果是局域网传大文件也检查一下客户端和服务器之间的MTU或者限速策略。进度条提前满格但实际没传完这个坑我踩过一次。当时是回调里直接用了len(data)去累加但回调被调用的时机在底层缓冲已发送时。如果把服务器确认当作完成标志就没这个问题。记住一点进度条计算的是“发送到系统缓冲区”的进度不是“服务器落盘”的进度。所以做完storbinary之后必须把quit()成功返回当作真正的完成信号界面上的文案也要设计成收到这个信号才显示“上传完成”。4.3 服务器端vsftpd启动失败与配置检查vsftpd启动失败是Linux部署FTP时的老问题。最常见的几个原因配置文件中存在无效参数、21端口被占用、没有设置被动端口范围但被防火墙拦截。排查命令列一下systemctl status vsftpd journalctl -u vsftpd -n 50 ss -lntp | grep :21如果看到类似“vsftpd: not found”的报错确认一下是否安装了vsftpd没装的话用系统包管理器装一下。配置检查方面我习惯装好之后先把/etc/vsftpd.conf里多余的自定义项全部注释掉只保留最基本的anonymous_enableNO、local_enableYES、write_enableYES跑通后再逐步加配置。这样能把问题范围缩小不会因为一个无关的配置项导致服务起不来。4.4 大文件上传的稳定性与断点续传大文件上传最容易踩的坑有几个网络闪断导致整个文件重传、内存占用过高导致程序被杀。内存这块只要坚持用流式读取像代码里open(...)配合storbinary而不是一次read()全量读进内存基本不会出问题。断点续传这块ftplib其实有transfercmd可以实现但用起来稍微复杂一点。思路是先用rest命令告诉服务器从哪个偏移量继续收然后从本地文件的对应位置继续读。示例片段ftp.transfercmd(fSTOR {remote_file}, restoffset) with open(local_path, rb) as f: f.seek(offset) while True: chunk f.read(65536) if not chunk: break conn.sendall(chunk) conn.close()注意rest命令在FTP协议里是标准命令但极少数老旧的FTP服务端不支持做的时候先验证一下服务端的能力。如果本地已经有上传记录比如你记录过已传字节数断点续传配合进度条显示是非常完美的一种体验也能避免主备切换时出现坏文件。再补一句关于文件上传到一半被覆盖的问题。有些项目为了“保证目录干净”每次上传前会把远端同名文件删掉。这个操作要谨慎一旦传输中断文件就没了。我通常的做法是先传到临时文件名比如xxx.zip.tmp传完再Rename成正式名这样远端永远不会残留半个文件用户也不可能看到上传中的不完整版本。最后再分享一个小经验进度条这种功能做之前最好先确认用户对“进度”的预期是什么。有的场景是单个大文件的传输进度有的场景是成百上千个小文件的批量进度。二者技术实现上完全不同。碰到“上传文件夹”这种需求时建议直接给用户展示“第N个文件共M个”加整体字节比例而不是单纯在一个文件内部做进度否则用户看到进度条长期停在10%体验反而更差。本文还有配套的精品资源点击获取 SEO 优化官网定制响应式建站教育培训建站