首页 / 资讯中心 / 文章详情

带进度条的FTP上传工具:Java Swing与commons-net实战解析

带进度条的FTP上传工具:Java Swing与commons-net实战解析 ★ FEATURED ARTICLE
简介FTP上传实例带进度条是一份面向C#开发者的轻量级示例项目核心目标是解决文件上传过程中无法直观感知进度的问题。项目基于FTP协议演示了从连接服务器、登录验证、目录切换到通过STOR命令上传文件的完整链路并利用已上传数据量与总数据量的比值驱动进度条刷新代码结构清晰适合刚接触网络编程或需要快速集成上传功能的开发者。压缩包共35个文件、约78KB主体为12个C#源文件以及配套的resx/resources资源文件另含项目配置文件、可执行程序与调试符号其中还封装了一个可复用的FTP帮助类便于在其他WinForm项目中直接引用避免重复编写底层连接代码。目前已有868人学习下载资源体积虽小但功能完整既能作为教学示例理解FTP交互原理也能为实际开发中的上传进度展示提供可靠的参考尤其适合桌面工具、内部管理系统的文件传输模块。同时示例对网络延迟、文件大小限制等常见问题也给出了可扩展的思路具备一定的实用价值。1. 一个带进度条的FTP上传工具解决的不止是“能传文件”当你用FTP上传一个几百MB的文件盯着命令行卡在那里你根本不知道它是在传还是已经断线。这个带进度条的上传实例就是把FTP上传从黑盒变成可视化的一个小工具。它基于Java Swing构建窗口使用commons-net库做FTP底层通信通过监听数据流的字节传输来实时刷新进度条。适合刚接触FTP编程的学生、需要快速搭一个文件传输工具的运维人员也适合想搞懂“网络传输回调机制UI更新”三者怎么协作的开发者。没有复杂的业务逻辑精读完就能跑跑完就能动手改。2. 项目结构和技术选型一个单文件也能拆出MVVM的影子2.1 技术栈为什么是Java Swing commons-net我最早写FTP上传时是用JDK自带的Socket手撸控制连接那是真的痛苦FTP命令的应答码、数据连接的主动/被动模式、二进制和ASCII切换每一步都要自己解析。后来换成commons-net库直接用它的FTPClient一下省掉了80%的裸协议处理。选它是因为commons-net已经封装了FTP协议的基本命令流转我们需要关注的就剩连接参数和上传字节流的进度回调。UI我选Swing而不是JavaFX因为Swing自带的JProgressBar和SwingWorker配合得很成熟而且这个Demo不需要炫酷界面但如果你要发给同事用可以后续换JavaFX。无论你是做FTP文件上传还是下载这套“协议层封装进度回调UI线程桥接”的思路都是通用的。编译打包用Maven或Gradle都行只需要引入commons-net一个依赖dependency groupIdcommons-net/groupId artifactIdcommons-net/artifactId version3.9.0/version /dependency版本号我习惯用3.9.0以上因为3.6以后对TLS/SSL支持和字符集处理更稳定。这个Demo用到的几个核心类FTPClient负责连接、登录、切换目录、设置文件类型CopyStreamListener监听复制流的进度回调是进度条的数据源SwingWorker后台线程执行上传完成后回到事件分发线程更新UIJProgressBar显示百分比和已完成字节数选型的核心逻辑是用commons-net处理协议用SwingWorker处理线程用CopyStreamListener处理进度三个组件各干一件事互不干扰。这也是这个Demo值得拆开看的原因你再回头写任何网络上传类工具都可以套这个三层结构。2.2 目录结构和文件职责这个Demo没有拆多模块但为了后续扩展我建议你按下面这个方式组织哪怕只有两个类ftp-upload-progress/ ├── pom.xml ├── src/ │ └── main/ │ └── java/ │ └── com/demo/ftp/ │ ├── App.java // 启动类负责创建窗口 │ ├── FtpUploader.java // 封装FTP连接上传逻辑 │ ├── UploadTask.java // SwingWorker子类后台执行上传 │ └── ProgressListener.java // 实现CopyStreamListener接收进度事件 └── README.mdApp.java只做一件事创建主窗口并显示不掺业务代码。FtpUploader.java核心业务类持有FTPClient实例提供connect、upload、disconnect方法。UploadTask.java继承SwingWorkerInteger, ProgressPayload在doInBackground里调用上传逻辑在process里接收中间进度数据并更新进度条。ProgressListener.java每次数据块传输时触发把FTPClient内部回调转成进度事件。我之所以把FtpUploader和UploadTask拆开是想让FTP逻辑和UI线程解耦。这样以后要把Swing换成命令行或者Web接口只需要改UploadTask的调用方式FtpUploader不用动。2.3 从启动到上传完成主流程梳理整个流程可以分成五个阶段用户选择本地文件输入服务器IP、端口、用户名、密码、远程路径。点击“上传”按钮创建FtpUploader实例并连接服务器。FtpUploader.setFileType(FTP.BINARY_FILE_TYPE)设为二进制避免图片或压缩包传完后损坏。调用FTPClient.storeFile(remotePath, inputStream)底层会自动打开数据连接并传输。传输过程中commons-net会回调ProgressListener我们拿到已传字节数除以文件总字节数算出百分比刷新JProgressBar。这个流程里最容易被忽略的是第3步很多人传纯文本没问题传图片后打不开八成就是没设二进制传输。后面避坑章会再提。主流程非常短但每一步都牵涉到一个技术点连接参数决定能不能连上二进制模式决定文件完不完整线程模型决定UI卡不卡。下一章逐个拆开讲。3. 核心模块实战拆解上传、进度、UI三者怎么串起来3.1 FTP连接与配置被动模式、超时、编码一个都不能少我先放最常用的连接配置代码这段代码基本可以直接抄进你的工具类public class FtpUploader { private FTPClient ftp new FTPClient(); public boolean connect(String host, int port, String user, String pass, String encoding) throws IOException { ftp.connect(host, port); // 1. 建立控制连接 int reply ftp.getReplyCode(); // 2. 拿服务器应答码 if (!FTPReply.isPositiveCompletion(reply)) { ftp.disconnect(); return false; // 非2xx开头说明握手失败 } ftp.login(user, pass); // 3. 登录 ftp.setFileType(FTP.BINARY_FILE_TYPE);// 4. 二进制传输 ftp.enterLocalPassiveMode(); // 5. 被动模式 ftp.setControlEncoding(encoding); // 6. 控制连接编码 ftp.setConnectTimeout(10_000); // 7. 连接超时 ftp.setDataTimeout(30_000); // 8. 数据超时 return true; } }这段代码的每一行都有讲究。enterLocalPassiveMode()就是被动模式现代FTP服务器大多躲在防火墙后面主动模式下服务器去连接客户端端口会失败被动模式由客户端主动连服务器数据端口成功率高得多。遇到“连接上了但一传输就挂”的情况先检查这里是不是写成了主动模式。setControlEncoding解决的是中文目录和文件名乱码问题。FTP协议默认用ISO-8859-1解析响应遇到UTF-8编码的中文文件名会变成问号。我一般传UTF-8但某些老Linux服务器用的是GBK那就传GBK。这个值要和服务器保持一致后面避坑章会展开。setConnectTimeout和setDataTimeout分别针对控制连接和数据连接的超时内网可以调小跨域大文件建议把DataTimeout设成60秒以上否则网络抖动超过30秒就会报socketTimeout。3.2 进度条的核心CopyStreamListener回调机制FTP上传本质上就是客户端把本地文件的字节流复制到FTP服务器的数据连接上。commons-net提供了CopyStreamListener接口每次复制一批字节就会回调一次。实现这个接口等于给上传过程装了一个计数器。我写的进度监听器长这样public class ProgressListener implements CopyStreamListener { private final long totalBytes; private long uploadedBytes 0; private ConsumerDouble onProgress; // 用来向外抛进度 public ProgressListener(long totalBytes, ConsumerDouble onProgress) { this.totalBytes totalBytes; this.onProgress onProgress; } Override public void bytesTransferred(long totalBytesTransferred, int bytesTransferred, long streamSize) { // streamSize有时会返回-1所以文件总大小要在外面传进来 uploadedBytes totalBytesTransferred; double percent totalBytes 0 ? (double) uploadedBytes / totalBytes * 100 : 0; onProgress.accept(percent); } Override public void bytesTransferred(byte[] buffer, int offset, int length, int bytes) { // 这个方法已过时但仍需实现我们通过bytesTransferred重载拿总字节数 } }注意bytesTransferred第一个参数是“本次传输到目前为止的累计字节数”这是commons-net内部维护的所以即使分段传输你也不需要自己累加。streamSize理论上等于剩余字节但实测经常会返回-1所以我不依赖它而是在创建监听器时把文件总长度传进来。ConsumerDouble是这个类向外暴露进度的通道。FtpUploader里的上传方法会接收这个Consumer把百分比的数字抛给UI层。这样做的好处是ProgressListener完全不知道Swing的存在以后换控件或者写控制台输出都能复用。3.3 SwingWorker线程让进度条不卡死的关键如果直接在窗口线程里调用storeFile上传期间窗口会白屏进度条直接失去响应。原因是文件传输是阻塞IO会一直占着事件分发线程。正确的做法是把上传丢到后台线程再通过SwingWorker的process方法把进度数据分批次送回UI线程。我用UploadTask来干这件事public class UploadTask extends SwingWorkerBoolean, Double { private final FtpUploader uploader; private final String localPath; private final String remotePath; private final JProgressBar progressBar; private final JLabel statusLabel; Override protected Boolean doInBackground() throws Exception { // 在这里执行真正的上传 return uploader.upload(localPath, remotePath, percent - publish(percent)); } Override protected void process(ListDouble chunks) { // 批量接收进度数据取最后一个刷新UI double latest chunks.get(chunks.size() - 1); progressBar.setValue((int) Math.round(latest)); statusLabel.setText(已上传 String.format(%.2f, latest) %); } Override protected void done() { try { boolean ok get(); statusLabel.setText(ok ? 上传完成 : 上传失败); } catch (InterruptedException | ExecutionException e) { statusLabel.setText(上传异常 e.getMessage()); } } }publish(percent)发出的Double不会马上到达process而是会积攒一批后一次性传给UI线程。这是SwingWorker的批量通知机制所以我们从chunks里拿最后一个值舍弃中间过期进度。这段代码最值得学习的是耗时操作放在doInBackgroundUI更新放在process结束收尾放在done三个方法各司其职。有人会问既然CopyStreamListener已经回调了能不能直接在回调里调progressBar.setValue千万别这么干。回调发生在FTPClient的IO线程里直接操作Swing组件会触发线程安全异常。一定要用publish/process的桥接这是这个Demo的底线约束。3.4 上传方法把本地文件和远程路径对接起来FtpUploader里的upload方法负责把上面几块拼起来public boolean upload(String localFile, String remoteDir, ConsumerDouble onProgress) throws IOException { File file new File(localFile); try (InputStream in new BufferedInputStream( new FileInputStream(file))) { String remotePath remoteDir / file.getName(); ProgressListener listener new ProgressListener(file.length(), onProgress); ftp.setCopyStreamListener(listener); // 关键注册监听器 return ftp.storeFile(remotePath, in); // 同步阻塞上传 } }setCopyStreamListener是FTPClient在传输前调用的注册方法整个上传过程就靠它把底层字节流转成进度信号。这里用了try-with-resources上传结束自动关闭本地输入流。注意file.getName()拿到的文件名如果有中文需要和前面connect方法里的encoding配合这正好呼应避坑章的乱码问题。参数说明localFile本地绝对路径例如C:/uploads/test.exeremoteDir服务器上的目标目录注意是目录不是完整文件名onProgress进度回调接收0到100的百分比返回值storeFile的布尔值true代表服务器已接收完毕4. 避坑指南FTP上传最容易翻车的5个细节4.1 中文文件名上传后变成乱码现象本地叫“测试报告.zip”上传到服务器后变成“æµè¯æ¥å.zip”或者“????.zip”。原因FTP控制连接默认按ISO-8859-1解析而操作系统和服务器普遍使用UTF-8或GBK。中文经过错误解码再编码就变成宽字符乱码。解决在connect方法里把控制连接编码设成和服务器一致并保持两端相同ftp.setControlEncoding(UTF-8); // 服务器locale是UTF-8时用这个如果服务器是Windows的IIS FTP或老Linux系统可能要改成GBK。我建议你在测试时分别试两个值看服务器端列目录时哪个正常就定格哪个。注意这个设置在login之前调用才稳妥。4.2 进度条不涨或者一次跳满现象上传小文件时进度条瞬间从0跳到100或一直停在0然后突然跳到100上传大文件时进度条呈阶梯式跳动。原因底层IO缓冲区默认4KB每次回调的字节数太小回调频率太高而界面刷新不需要那么密。SwingWorker的publish做了合并导致本应平滑的进度变成一坨一坨的。解决在setCopyStreamListener之前设置一个合适的缓冲区大小ftp.setBufferSize(1024 * 64); // 64KB缓冲回调频率降下来同时不要在process里每次都重绘标签可以在百分比变化超过1时才更新。常见做法是保留上次值如果abs(当前值-上次值) 1就跳过这帧进度显示会更稳。4.3 上传大文件时UI还是卡死现象明明用了SwingWorker上传2GB文件时窗口还是转圈圈按钮点不动。原因虽然上传在后台线程但process里的UI更新频率过高或者doInBackground里在创建InputStream时做了太耗时的目录扫描。还有一个隐蔽点如果我们用了progressBar.setString每次更新都触发repaint导致UI线程被连续重绘塞满。解决第一把上传前的本地文件校验放到点击按钮时做不放进Worker第二在process里增加限流比如每帧最多刷新20次long lastUpdate System.currentTimeMillis(); // 只有距离上次刷新超过50ms才更新进度条 if (System.currentTimeMillis() - lastUpdate 50) { progressBar.setValue(...); lastUpdate System.currentTimeMillis(); }这个限流对超大文件尤其管用我上传过5GB的备份文件加上这个限制后UI基本保持流畅。4.4 能连上FTP服务器但一传就超时现象连接正常login也正常一调用storeFile就卡住直到数据超时报IOException。原因九成是主动模式/被动模式不匹配。服务器在云平台内网没有授权主动模式的数据端口客户端在主动模式下发PORT命令服务器连不回来。解决强制使用被动模式并且把本地防火墙放行TCP出口。如果还不行抓包看服务器返回的PASV地址是否是一个无法路由的内网地址常见于FTP服务器配置了内网PASV地址而客户端在外网。解决方法是检查服务器端pasv_address配置或改用FTPClient.enterLocalPassiveMode()并设置本地活动端口范围ftp.enterLocalPassiveMode();实在不济可以让服务器开PASV对外地址但那是服务器配置的活客户端代码里能做的也就是固定用被动模式。4.5 进度回调里直接改UI导致闪退现象上传过程中窗口突然报并发异常或者进度条乱跳。原因FTPClient上的IO线程不能直接触碰Swing组件。CopyStreamListener是在IO线程执行的直接progressBar.setValue等于跨线程更新UI。解决所有UI更新都必须经过SwingWorker的publish/process或者用SwingUtilities.invokeLater包裹。这个Demo的做法是让UploadTask持有ConsumerConsumer内部只做publish不碰任何组件。记住这条纪律IO回调里只做数据转换UI更新永远发生在EDT线程。5. 二次开发从单文件上传升级到断点续传和批量任务5.1 批量上传给UploadTask加一个任务队列单文件跑通后第一个想加的基本都是批量上传。我建议你别图省事在循环里连续new UploadTask那样会把SwingWorker压垮。常见做法是维护一个ListString待上传文件每完成一个就从队列头取下一个状态栏显示“3/12”。改造点很小把UploadTask的doInBackground改成循环或者用一个统一的ExecutorService提交多个任务。我更推荐后者因为多个UploadTask并发执行时CopyStreamListener互不干扰进度条可以按文件维度切换。核心思路是为每个文件创建一个临时状态对象在SwingWorker之外维护“当前正在传哪个文件”的索引。5.2 断点续传的两种实现思路断点续传是这个Demo最值得扩展的功能。第一种是服务器支持APPE命令追加文件先查远程文件大小如果比本地小就从本地偏移量处追加long remoteSize getRemoteSize(remotePath); if (remoteSize 0 remoteSize localFile.length()) { ftp.setRestartOffset(remoteSize); // 服务器端从偏移量开始写 in.skip(remoteSize); // 本地跳过已传部分 }第二种思路是使用FTPClient.restart(offset)加appendFile组合本质上和上面一致。要注意的是断点续传必须保证服务器目录里有同名文件且二进制模式一致否则续传后的文件损坏率极高。这个功能加完上传5GB大文件时突然断网就不会心态崩了。5.3 验证方案与我的一个习惯改完这些功能后我建议你至少在三种环境验证一遍本机FTP服务软件、局域网另一台Linux服务器的vsftpd、以及云厂商的FTP托管服务。每种环境重点看PASV模式和字符集是否配置正确。我有一个血泪经验每次改动完上传逻辑都会强制用“上传一个4GB随机文件→对比SHA-256”的方式验收一次字节错一个都能查出音频文件损坏问题。从那以后我再也不相信“看起来传完了”这个结论。如果你需要这份带进度条的FTP上传源码拿回去把FTP参数改成你的服务器地址就能用。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站