news 2026/10/20 12:55:40

Java带进度条的FTP上传实例:Commons Net与Swing协同实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java带进度条的FTP上传实例:Commons Net与Swing协同实战

简介:面向C#/.NET开发者的FTP上传功能示例工程,聚焦带进度条的文件传输场景。工程完整演示FTP连接、身份验证、目录切换、STOR上传流程,并通过已上传字节数与总字节数的比值计算进度,以可视化进度条改善用户体验。上传过程采用分块发送方式,借助回调同步界面状态,兼顾传输性能与进度反馈实时性。资源包共35个文件,以cs源码为主,涵盖窗体设计、上传逻辑与程序入口;另有config配置文件、resx资源文件、exe可执行程序及pdb调试文件等,压缩包仅78KB,整体结构清晰,适合直接阅读和二次复用。包内提供封装好的FTP帮助类及上传界面示例,能帮助开发者理解分块发送、进度回调与异常处理等关键细节,减少重复造轮子。目前已有868人学习下载,适合初涉FTP客户端编程的开发者快速上手,也值得需要可落地进度条实现方案的工程师参考。

1. 先从看不到进度的上传说起:带进度条的 FTP 上传实例

做桌面工具时最怕什么?文件传着传着,界面既没进度条也没提示,只能盯着卡死的窗口猜它是在传还是已经掉了。这个项目就是用 Java 写的一个带进度条的 FTP 上传实例,解决的就是“FTP 文件上传下载看不到进度”这个典型问题。核心思路并不复杂:FTP 协议本身不提供进度回调,所以项目自己包了一层输出流,把每次写入的字节数实时上报给界面。适合两类人,一类是刚接触桌面开发的初学者,想看看网络 IO 和 UI 线程怎么协同;另一类是要做内网分发、后台管理小工具的老手,需要一个能直接抄的进度上传样板。整份代码就三个类,没有框架依赖,跑起来也就两分钟的事。

2. 技术选型与工程结构:Commons Net + Swing 的合理性与落地

2.1 语言与库选型:为什么是 Java 而不是 Python 或 C#

带进度条的上传工具,语言选择其实很多,但每个选型背后都有取舍。用 Python 写,ftplib 天然支持 FTP 操作,tkinter 加一个进度条也很简单,开发速度最快;但问题在于目标机器上不一定有 Python 环境,打包成 exe 又会让体积膨胀。用 C# 的话,FtpWebRequest 和 WinForms 配合也不错,但只能跑 Windows,换到内网一堆 Linux 服务器做管理端就很尴尬。

这个项目选 Java 加 commons-net 库,核心原因是两头兼顾。commons-net 把 FTP 协议细节封装得足够干净,连接、登录、被动模式、二进制传输这些操作都有现成 API,不需要自己去拼 Socket 报文;另一方面 Swing 的 JProgressBar 配合 SwingWorker,后台线程和 UI 线程的切换是标准写法,不需要引入额外的并发框架。对比一下各方案的适用场景:

方案开发成本跨平台分发难度适合场景
Java + Commons Net + Swing中好打 jar 即可内网工具、跨平台管理端
Python + ftplib + tkinter低好需目标机装 Python快速脚本、个人工具
C# + FtpWebRequest + WinForms中差需 .NET 环境Windows 专用环境

选型这件事没有绝对答案,关键是知道自己要什么。如果只在本机跑一跑,Python 完全够用;如果要分发给几十台 Windows 机器还想要零配置,那 C# 反而更顺手。这里选 Java,是因为这个项目定位是“一个可复用的上传模块”,跨平台和可移植性优先级更高。

2.2 项目目录结构:三个类撑起一个完整上传工具

整个工程只拆了三个类,职责划分非常明确,一眼就能看懂每部分在干什么:

ftp-upload-demo/ ├── pom.xml └── src/main/java/com/example/ftp/ ├── FtpUploader.java ├── FtpProgressOutputStream.java └── UploadFrame.java

FtpUploader 负责 FTP 连接、登录、上传的完整流程,是纯粹的业务逻辑,不掺任何界面代码。FtpProgressOutputStream 继承自 OutputStream,在 write 方法里统计字节数并触发回调,是进度数据的源头。UploadFrame 是 Swing 界面,包含文件选择按钮、进度条、状态标签,同时负责把后台线程的进度事件刷新到界面上。

这三个类各管一段,互相之间通过接口通信,而不是直接引用具体实现。FtpUploader 不关心进度数据最终是显示在进度条还是写在日志里,它只负责把事件抛给回调接口;UploadFrame 也不关心 FTP 命令怎么拼,它只实现回调接口拿进度更新 UI。这种拆分方式让每一部分都能单独测试,后面想改成控制台版本或者加日志输出,不需要动其他两个类。

2.3 依赖引入与连接参数:动手前的准备工作

项目是 Maven 工程,pom.xml 里只需要加一个依赖:

<dependency> <groupId>commons-net</groupId> <artifactId>commons-net</artifactId> <version>3.9.0</version> </dependency>

commons-net 3.9.0 是目前比较稳定的版本,支持 FTP、FTPS、SFTP 等协议,这里只用到了 FTPClient 这个类。引入依赖后,先把连接参数抽成一个配置类,方便后面修改:

public class FtpConfig { private String host = "192.168.1.100"; // FTP 服务器地址 private int port = 21; // 默认端口 private String username = "ftpuser"; // 登录用户名 private String password = "ftpPass123"; // 登录密码 private String remoteDir = "/upload"; // 远程目录 private boolean passiveMode = true; // 是否使用被动模式 private String controlEncoding = "UTF-8"; // 控制连接编码 private int bufferSize = 64 * 1024; // 缓冲区大小(64KB) private int connectTimeout = 10000; // 连接超时(毫秒) private int soTimeout = 60000; // 数据超时(毫秒) // 省略 getter / setter }

这些参数里有几个值得单独说。passiveMode 默认设为 true,是因为大多数内网环境里客户端在防火墙后面,主动模式的数据连接会建立失败,被动模式让服务器来连客户端,穿透性更好。controlEncoding 设成 UTF-8,是为了避免中文文件名变成乱码,这个坑后面会展开讲。bufferSize 设 64KB 是折中方案——太小会导致频繁触发进度回调,太大则进度条刷新滞后,64KB 对大多数网络环境足够顺畅。

3. 核心模块拆解:进度回调、上传线程与 UI 刷新的完整链路

3.1 自定义 OutputStream:进度回调从哪来

FTP 上传的原理是向远程服务器写入一个输出流,commons-net 的 storeFileStream 方法会返回这个流。但默认情况下,这个流是黑匣子——数据写进去了,你不知道写了多少,也没法获取进度。要拿到进度,就必须自己实现一个输出流,在数据写入时统计字节数并通知外部。

public class FtpProgressOutputStream extends OutputStream { public interface ProgressListener { void onProgress(long written, long total); } private final OutputStream delegate; private final long totalBytes; private long writtenBytes; private final ProgressListener listener; public FtpProgressOutputStream(OutputStream delegate, long totalBytes, ProgressListener listener) { this.delegate = delegate; this.totalBytes = totalBytes; this.listener = listener; } @Override public void write(int b) throws IOException { delegate.write(b); writtenBytes++; listener.onProgress(writtenBytes, totalBytes); } @Override public void write(byte[] b, int off, int len) throws IOException { delegate.write(b, off, len); writtenBytes += len; listener.onProgress(writtenBytes, totalBytes); } @Override public void flush() throws IOException { delegate.flush(); } @Override public void close() throws IOException { delegate.close(); } }

逻辑说明:包装了一个 FeedOutputStream 模式的委托流,所有写操作都先传给底层流,同时更新计数器并触发回调。注意 write(byte[], int, int) 这个重载才是关键——FTP 上传时底层是批量写入数据块,如果只重写 write(int) 的单字节版本,那进度条刷新频率会非常低,几乎等于不动。每次写入 len 字节后,把累计值和总量通过 listener 抛出去,界面那边拿到这个事件去刷新进度条。

参数说明:totalBytes 是文件总大小,务必在开始上传前通过 File.length() 拿到,不能等流关闭后再回过头来算;listener 是回调接口,由 UI 层实现,回调方法里的逻辑要尽量轻量,只做数值更新和界面刷新,不要在这里面做文件 IO 或日志写盘。实际使用中还有一个细节:如果不做节流,小文件可能瞬间触发几十次回调,界面频繁刷新反而拖慢速度,可以加一个阈值判断,只有进度变化超过 1% 时才触发回调。

3.2 SwingWorker:让上传跑在后台线程

有了进度回调的源头,接下来要解决的是线程问题。Swing 的 UI 操作必须在事件分发线程(EDT)中执行,而文件读取和网络传输如果也放在 EDT 里,界面就会卡死——这是桌面开发最常见的翻车现场。SwingWorker 是 JDK 自带的方案,专门解决“后台任务 + 进度上报 + UI 刷新”这个组合。

class UploadTask extends SwingWorker<Void, ProgressEvent> { private final File file; private final FtpConfig config; @Override protected Void doInBackground() throws Exception { FtpUploader uploader = new FtpUploader(config); uploader.upload(file, new FtpProgressOutputStream.ProgressListener() { @Override public void onProgress(long written, long total) { publish(new ProgressEvent(written, total)); } }); return null; } @Override protected void process(List<ProgressEvent> chunks) { // 这个方法在 EDT 线程中执行,可以安全地操作界面组件 ProgressEvent latest = chunks.get(chunks.size() - 1); int percent = (int) (latest.written * 100 / latest.total); progressBar.setValue(percent); statusLabel.setText("已上传 " + latest.written + " / " + latest.total + " 字节"); } @Override protected void done() { try { get(); // 如果 doInBackground 抛出异常,这里会重新抛出 statusLabel.setText("上传完成"); } catch (Exception e) { statusLabel.setText("上传失败: " + e.getMessage()); e.printStackTrace(); } } }

逻辑说明:doInBackground 是后台线程的入口,在这里做真正的网络传输。publish 方法把进度事件发送到 UI 队列,SwingWorker 会自动调度 process 方法,并且保证 process 在 EDT 中执行。process 接收的是一个事件列表而不是单个事件,这是因为 SwingWorker 可能会合并连续多次 publish 调用,处理时直接取列表中最后一个事件即可,避免重复刷新。

参数说明:SwingWorker 有两个泛型参数,第一个是 doInBackground 的返回值类型,第二个是 publish 传递的事件类型。这里的 ProgressEvent 是一个简单的数据类,只包含 written 和 total 两个 long 字段。需要注意 done 方法里 get() 的作用——它负责把 doInBackground 里抛出的异常重新抛出来,否则上传失败时你只看到结果,不知道失败原因到底是什么。

3.3 完整上传流程:从本地文件到远程服务器的链路

把前面的输出流和线程串起来,就形成了完整的上传流程。connect、login、传输数据、等待服务器确认,每个环节都不能省。

public boolean upload(File file, FtpProgressOutputStream.ProgressListener listener) throws IOException { FTPClient client = new FTPClient(); try { client.connect(config.getHost(), config.getPort()); client.login(config.getUsername(), config.getPassword()); // 控制连接编码必须在 login 之前设置 client.setControlEncoding(config.getControlEncoding()); if (config.isPassiveMode()) { client.enterLocalPassiveMode(); } client.setFileType(FTP.BINARY_FILE_TYPE); client.setBufferSize(config.getBufferSize()); client.setConnectTimeout(config.getConnectTimeout()); client.setSoTimeout(config.getSoTimeout()); String remotePath = config.getRemoteDir() + "/" + file.getName(); OutputStream remote = client.storeFileStream(remotePath); if (remote == null) { // storeFileStream 返回 null 表示无法建立数据连接 throw new IOException("无法建立上传流,服务器响应: " + client.getReplyString()); } long totalBytes = file.length(); try (InputStream local = new FileInputStream(file); OutputStream progressStream = new FtpProgressOutputStream(remote, totalBytes, listener)) { byte[] buffer = new byte[8192]; int len; while ((len = local.read(buffer)) != -1) { progressStream.write(buffer, 0, len); } progressStream.flush(); } // 关键步骤:必须等待服务器的最终响应 return client.completePendingCommand(); } finally { try { client.logout(); } catch (IOException ignored) { } try { client.disconnect(); } catch (IOException ignored) { } } }

逻辑说明:上传流程分四步。第一步是建立控制连接并登录,控制连接负责发送 FTP 命令。第二步是配置传输模式,被动模式配合二进制类型,这里有先后顺序——setControlEncoding 必须在 login 之前,因为登录时发送的用户名密码已经走控制连接了。第三步是建立数据连接,storeFileStream 会发送 STOR 命令,如果返回 null,说明服务器拒绝了数据连接,此时需要读取 getReplyString 拿到具体错误码,这是排查问题的重要线索。第四步是最后的 completePendingCommand,这一步最容易遗漏。

参数说明:buffer 数组大小设为 8192 字节,是本地读写缓冲区,它决定了一次 read 循环读取多少数据进内存。缓冲区太小会导致频繁 IO 调用,太大则占用内存。completePendingCommand 会阻塞等待服务器返回 226 或 250 响应码,它的返回值才是上传成功的最终判定标准。很多人写完 try-with-resources 就结束,少了这一步,文件可能还没落盘就断开了连接,服务端留下一个不完整的文件。

到这里,一个带进度条的上传功能已经完整跑通了。从自定义输出流拿到字节数,到 SwingWorker 保证线程安全,再到完整上传流程串起来,核心链路其实就这么长。

4. 避坑指南:四个让进度条项目翻车的真实问题

4.1 坑一:进度条卡住不动,最后一下子跳满

现象:点了上传,进度条纹丝不动,文件传完以后瞬间从 0% 跳到 100%,整个过程完全没有中间状态。

原因:FTPClient 底层有缓冲区机制,而且我这里之前把 setBufferSize 设成了 1MB。数据在本地缓冲区积攒,写完才真正发送到服务器,进度回调只在底层 flush 时触发,所以你在界面上看到的只有“开始”和“结束”两个状态。

解决:把缓冲区大小调到 64KB,同时在自定义 OutputStream 里做节流,只有进度变化超过 1% 才触发回调,避免小文件上传时 UI 频繁刷新。如果缓冲区太大,进度条反而会失真,传大文件的时候特别明显。

4.2 坑二:中文文件名变成乱码

现象:本地文件叫“项目说明.pdf”,传到服务器后变成了“????.pdf”或者“杩欐槸涓�枃.pdf”这类乱码。

原因:FTP 协议早期设计时没有统一字符编码,commons-net 的 FTPClient 默认使用 ISO-8859-1 编码发送命令。中文字符在这个编码下拆成两个单字节字符,服务器再按自己的编码解析,就彻底花了。

解决:在连接之后、登录之前加一行 client.setControlEncoding("UTF-8")。注意顺序不能错,因为登录阶段发送的用户名密码也走控制连接,编码必须在所有命令发送之前设置好。如果服务器端没有开启 UTF-8 支持,还需要在服务器配置里加 utf8 on(vsftpd)或在全局配置里开启对应选项。

4.3 坑三:大文件传到一半连接断开

现象:传 2GB 的安装包,跑到 68% 左右进度条不动了,过一会儿报连接超时,服务器上留下一个残缺文件。

原因:FTP 是双通道协议,控制连接和数据连接分开。整个传输过程中控制连接一直存在,但长时间没有数据流动时,防火墙或服务器端超时策略会把控制连接判定为死连接并剪断。一旦控制连接断开,即使数据连接还在,传输也无法继续。

解决:设置 setControlKeepAliveTimeout(300),让 commons-net 每 300 秒发送一次 NOOP 空命令保持控制连接活跃。同时加上 setConnectTimeout(10000) 和 setSoTimeout(60000),防止网络异常时客户端无限阻塞。这两种设置并不冲突,一个是保活,一个是超时判死。

4.4 坑四:上传时整个界面卡死,连取消都点不了

现象:点击上传按钮后,窗口立刻变白,转圈图标不动,点任何按钮都没反应,只能强制结束进程。

原因:把网络传输直接写进了按钮点击事件的代码里。Swing 的事件分发线程只有一个,你的上传逻辑在这个线程里做文件读取和网络 IO,这个线程就被占死了,所有界面事件都在排队等待,表现就是“整个程序死掉了”。

解决:严格使用 SwingWorker,把网络传输放进 doInBackground。还有一个附加要求——process 回调里只能做 setValue、setText 这类轻量 UI 更新,不要在里面做文件解析、字符串拼接大对象、写日志这些操作,否则一样会拖慢 EDT 线程。用线程转储(jstack)观察的话,如果 UI 卡死时能看见自己的代码在 EDT 线程里跑网络 IO,那基本就是踩了这个坑。

5. 二次开发方向:从单任务到多任务队列的升级思路

单文件上传跑通后,下一步自然是批量场景。这里给出一个可落地的改造方向:多任务队列加连接池,把单连接串行上传升级成并发可控的批量上传。

核心问题是:文件多了以后,逐一新建 FTPClient 成本很高,每次都要 TCP 握手、登录认证、退出登录,而且串行上传总量不变的话,总耗时会线性增长。改造方案是维护一个连接池:

public class FtpConnectionPool { private final FtpConfig config; private final Deque<FTPClient> pool = new ArrayDeque<>(); private final int maxSize; public FtpConnectionPool(FtpConfig config, int maxSize) { this.config = config; this.maxSize = maxSize; } public synchronized FTPClient borrow() throws IOException { FTPClient client = pool.pollLast(); if (client == null && pool.size() < maxSize) { client = createNewClient(); } if (client != null && !client.isConnected()) { client = createNewClient(); } return client; } public synchronized void returnConnection(FTPClient client) { if (client != null && client.isConnected() && pool.size() < maxSize) { pool.addLast(client); } else { try { client.disconnect(); } catch (IOException ignored) { } } } }

连接池的 borrow 方法负责取出可用连接,如果池空了就新建;returnConnection 负责归还或销毁。改造后配合一个简单的线程池,每个文件丢给一个任务,进度条改为列表形式,每个任务一行独立进度。这个方向还能继续延伸:上传成功后校验文件大小是否一致、失败自动重试三次、断点续传——这些在现有结构上加都不难,核心还是把“连接”和“任务”解耦。

我自己第一次跑通这个项目时,就是栽在这个批量改造上:文件少了没问题,文件一多,连接没复用,服务器那边报“421 Too many connections”,直接把 FTP 服务搞崩了。从那以后我每次写这种传输工具,都强制先画一张连接生命周期图——哪里借、哪里还、什么时候断开,全理清楚了再动手。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 12:55:16

故障记录不是文档,而是可追溯的系统健康日志体系

1. 为什么“项目故障问题记录汇总”不是文档&#xff0c;而是一份活的系统健康日志“项目故障问题记录汇总”这八个字&#xff0c;乍看平平无奇&#xff0c;像极了某次周会后被随手丢进共享文件夹、再也没人点开的Excel表格。但在我带过的十几个跨平台系统交付项目里&#xff0…

作者头像 李华
网站建设 2026/10/20 12:53:55

仓库扫码验货实操:把库存差异率降80%的改造记录

做仓库管理的人&#xff0c;多半都经历过这种场景&#xff1a;月底大盘点&#xff0c;账上明明躺着300件货&#xff0c;实物怎么数只有298&#xff0c;翻出当天的到货验收单仔细查&#xff0c;才发现收货的时候把规格看差了、把数量数漏了。这种问题在人工验货环节太常见了&…

作者头像 李华
网站建设 2026/10/20 12:53:10

语法:词法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/20 12:52:39

音频驱动的数字人分身:静态照片秒变口播视频的技术拆解

简介&#xff1a;数字人分身系统源码是一套面向短视频创作者、自媒体运营者及企业内容团队的虚拟人物视频生成工具&#xff0c;基于真人声音、表情与动作的克隆技术&#xff0c;让用户输入文案即可驱动数字分身完成口播视频制作&#xff0c;有效解决出镜难、忘词、表现力不足等…

作者头像 李华
网站建设 2026/10/20 12:51:25

九宫格判定如何避坑?三阶幻方校验的数学原理与C语言实现

大家看到“L1-104 九宫格”这个题号时&#xff0c;第一反应多半是“这有什么好讲的&#xff0c;不就是三行三列九个格子嘛”。说实话&#xff0c;我第一次拿到这道题也是这么想的&#xff0c;结果在判题平台上连续交了三次才过。九宫格这道题本身不难&#xff0c;但它把所有入门…

作者头像 李华