ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FTP上传进度条实战:Java后端与Android端完整实现

2026/9/7 1:43:00 拓冰建站 浏览量
FTP上传进度条实战:Java后端与Android端完整实现 简介FTP上传实例带进度条是一套基于C# WinForm的FTP文件上传示例项目面向需要实现文件上传功能并希望提供可视化进度反馈的开发者。实例演示了从建立FTP连接、身份验证、切换工作目录到分块上传的完整流程核心思想是通过回调函数跟踪已上传字节数与总字节数的比例实时驱动进度条更新适合课程设计、毕业设计或日常开发参考。压缩包共35个文件、约78KB以12个.cs源代码文件为核心搭配.resx资源文件、.config配置文件、.exe可执行程序及.pdb调试符号并包含.sln与.csproj工程文件可在Visual Studio中直接打开运行和二次开发。该实例已有866人学习下载代码中封装了FTP帮助类方便复用连接、登录、上传等方法同时设计了针对网络延迟、权限限制等异常的处理思路帮助开发者在实际应用中规避常见上传失败问题。整体结构简洁清晰适合C#初学者学习FTP协议、WinForm交互以及文件传输类功能的开发也可为实际项目的上传体验提供参考。 公司内部系统要从旧流程迁移文件传输这块一直用FTP扛着业务方提了个需求上传文件的时候界面上给个能真实反映进度的条别让我干等。FTP这个协议最坑的地方就在于它默认只跟你聊命令文件传了多少、传到哪一步它根本不想告诉你。我去翻了现成组件和网上现成代码真正能跑通还带进度条的demo其实很少要么进度条是伪造的要么直接卡死在io上。这篇就把我从零写出来的FTP上传实例带进度条完整拆开讲包括Java后端的实现原理、Android端的接入方式还有实测中踩过的一堆坑。无论你是写后端接口还是做客户端这套思路都能直接用上。1. 进度条FTP上传先想清楚方案再动手1.1 为什么现成工具和网上代码救不了你FTP上传的现成工具一大把FileZilla、WinSCP都用得挺顺但业务系统要内嵌上传能力时你不可能在甲方电脑上弹出一个FileZilla窗口让操作员手动传。更尴尬的是网上搜FTP上传的确能搜到几十篇代码可仔细一看绝大多数是上传成功/失败这种二值反馈压根没有进度回调少数有进度的实现是在上传前后开两个线程去轮询服务器上的文件大小这种方案看着能用实际上对SIZE命令的支持、服务端缓存刷新时机都很敏感进度条经常卡在99%不走或者传到一半跳回80%。我从一开始就决定不走这条路。选型上Java生态里Apache Commons Net依然是做FTP最老牌、最稳的选择。它不依赖Spring不挑环境Android上也能直接用。Commons Net的FTPClient类原生提供了一套CopyStreamListener回调机制可以在文件流真正往FTP服务器写数据时拿到当前已经传了多少字节这个信息。这就是进度条的核心数据来源也是我最终敲定方案的关键原因。1.2 两条进度方案的对比轮询 vs 字节流监听在动手之前我把两条主流实现路线摆在桌面上比了一轮方案实时性准确性对服务端依赖实现成本轮询FTP服务器目标文件大小受轮询间隔限制通常2~5秒受服务端SIZE命令实现和缓存影响依赖FTP服务器支持SIZE命令低监听本地传输流字节数高每写一个缓冲块就上报一次精确到字节和本地文件大小直接对比无中轮询方案在局域网里偶尔能跑通一旦换成跨网络、跨机房甚至经过代理的FTP链路SIZE命令返回的可能是缓存值进度条就开始表演薛定谔的99%。字节流监听方案是从源头拿数据——文件从本地InputStream读进socket走了多少字节是一清二楚的。所以最终采用的是Commons Net的setCopyStreamListener方案配合本地File.length()拿到的文件总大小计算真实百分比。2. 核心原理让FTP传输的字节流开口说话2.1 CopyStreamListener到底在监听什么很多人第一次看Commons Net的文档会懵因为FTPClient这个类的方法太多了。其实进度条相关的核心就一个接口org.apache.commons.net.io.CopyStreamListener。FTPClient.storeFile(String, InputStream)内部并不是直接一把梭把流写完而是通过一个copyStream方法按缓冲块从本地输入流读数据、写入FTP数据连接的输出流。每写完一个缓冲块它就会调用一次CopyStreamListener的bytesTransferred回调把累计传输的字节数告诉外部。说白了这就是一个装在数据管道上的计数器每过一块数据就叮一声。接口里有两个重载方法实际开发中我强烈建议只关注带long totalBytesTransferred的这个public void bytesTransferred(long totalBytesTransferred, int bytesTransferred, long streamSize)第一个参数是截至目前累计传输的总字节数第二个参数是本次这一个数据块传递的字节数第三个参数是流的预估大小。日常写代码时用CopyStreamAdapter这个适配器类就行它把两个方法都做了空实现我们只需要重写上面这个方法。2.2 最容易搞错的一个参数streamSize这里有个细节很多人踩坑但不自知通过FTPClient.storeFile(String, InputStream)上传时streamSize参数通常并不是你的文件总大小。Commons Net内部对输入流的长度感知是有限制的有些场景下它拿到的可能是UNKNOWN_STREAM_SIZE也就是-1。所以我强烈建议文件总大小不要依赖回调里的streamSize而是在调用上传之前用File.length()提前算好存成局部变量回调里拿这个值和totalBytesTransferred做百分比计算。long totalSize localFile.length(); ftpClient.setCopyStreamListener(new CopyStreamAdapter() { Override public void bytesTransferred(long totalBytesTransferred, int bytesTransferred, long streamSize) { int percent (int) (totalBytesTransferred * 100L / totalSize); listener.onProgress(totalBytesTransferred, totalSize); } });这个用本地文件大小作为分母的做法保证了进度条的最终值一定是从0到100闭环的不会出现进度条还没满文件却已经传完、或者进度到120%的灵异事件。2.3 回调线程与UI更新的关系CopyStreamListener的回调发生在FTP传输线程中这点特别重要。如果你在Android端直接在这个回调里更新ProgressBar轻则卡顿重则直接抛CalledFromWrongThreadException。Java后端场景虽然不涉及UI但如果你想把进度写到数据库、Redis或者通过SSE推给前端同样要注意这个回调的执行频率比想象中高得多实际写入时要做节流不能每个缓冲块都写一次存储。3. 完整Java工具类连接、上传、进度回调一站封装3.1 可以直接抄走的FtpUploader下面这个工具类是我在实际项目里用了很久的版本。它封装了连接、登录、设置被动模式、设置二进制传输、设置控制编码、上传、退出所有核心步骤都在一个方法里完成import org.apache.commons.net.ftp.FTP; import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.net.io.CopyStreamAdapter; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; public class FtpUploader { public interface ProgressListener { void onProgress(long transferred, long total); } public boolean upload(String host, int port, String username, String password, String remoteDirectory, File localFile, ProgressListener listener) throws IOException { FTPClient ftpClient new FTPClient(); long totalSize localFile.length(); try { // 1. 建立连接与登录 ftpClient.connect(host, port); int replyCode ftpClient.getReplyCode(); if (replyCode 200 || replyCode 300) { throw new IOException(FTP连接被拒绝响应码: replyCode); } if (!ftpClient.login(username, password)) { throw new IOException(FTP登录失败); } // 2. 三个关键配置被动模式、二进制、UTF-8 ftpClient.enterLocalPassiveMode(); ftpClient.setFileType(FTP.BINARY_FILE_TYPE); ftpClient.setControlEncoding(UTF-8); // 3. 切换远程目录 if (!ftpClient.changeWorkingDirectory(remoteDirectory)) { ftpClient.makeDirectory(remoteDirectory); ftpClient.changeWorkingDirectory(remoteDirectory); } // 4. 注册进度回调核心就在这一步 ftpClient.setCopyStreamListener(new CopyStreamAdapter() { Override public void bytesTransferred(long totalBytesTransferred, int bytesTransferred, long streamSize) { if (listener ! null) { listener.onProgress(totalBytesTransferred, totalSize); } } }); // 5. 执行上传流式处理不会把整文件读入内存 String remoteFile remoteDirectory / localFile.getName(); boolean ok false; try (InputStream inputStream new FileInputStream(localFile)) { ok ftpClient.storeFile(remoteFile, inputStream); } // 6. 校验FTP命令完成状态 if (!ok) { throw new IOException(FTP storeFile命令失败: ftpClient.getReplyString()); } return true; } finally { if (ftpClient.isConnected()) { try { ftpClient.logout(); } catch (IOException ignored) { } try { ftpClient.disconnect(); } catch (IOException ignored) { } } } } }这个类有几个设计点是刻意为之的我一个个说。remoteDirectory如果不存在代码会尝试makeDirectory。实际使用中FTP服务器的权限五花八门有的账号只有上传权限没有建目录权限这时makeDirectory会返回false不影响后续changeWorkingDirectory但如果你没有先试一次就上传可能直接抛目录不存在的异常。这个先试再建再切的顺序能覆盖大多数FTP服务器的权限差异。try-with-resources关闭FileInputStream是必须的。有些版本的Commons Net在storeFile内部不会帮你关闭输入流如果不手动关本地文件句柄泄漏Windows系统上文件会被一直锁住后续没法删除或覆盖。finally块里断开连接前没有先logout会怎样大多数情况下直接disconnect就完了但有些严格的服务端会记录异常断开连接甚至临时封禁IP。养成先logout再disconnect的习惯能让服务器日志干净很多。3.2 这个工具类为什么不带断点续传你可能会问文件传到一半断了怎么办工具类没做断点续传是因为FTP断点续传牵扯到REST命令、文件追加模式、本地偏移量记录还和服务端实现强相关硬塞进来会让代码复杂度翻倍。我的建议是先保证上传功能本身稳定可靠断点续传作为独立模块按需加。Commons Net的FTPClient提供了setRestartOffset和appendFile方法思路是上传前先查远端文件大小如果远端已有部分内容本地流先skip对应字节数再以追加模式继续上传。实际做的时候这个远端文件大小查询本身又会被SIZE命令的兼容性问题坑到所以我把它归类为进阶需求而不是基础功能。4. Android端接入进度条界面与异步线程实践4.1 权限和基础布局Android项目里用这个工具类第一步是加网络权限uses-permission android:nameandroid.permission.INTERNET /然后是一个最简单的水平进度条布局ProgressBar android:idid/progressBar style?android:attr/progressBarStyleHorizontal android:layout_widthmatch_parent android:layout_heightwrap_content android:max100 android:progress0 / TextView android:idid/tvProgress android:layout_widthwrap_content android:layout_heightwrap_content android:text0% /ProgressBar要设置为水平样式否则默认的圆形等待条没法显示具体进度。4.2 线程池 runOnUiThread更新进度Android网络请求不允许在主线程执行这块没有悬念。我习惯用Executors.newSingleThreadExecutor()而不是直接new Thread()原因很简单多次点击上传按钮时线程池能合理排队而每次new Thread很容易在你手滑连点的时候创建出多个线程同时打FTP服务器连接数一多服务器直接拒绝服务。进度回调发生在工作线程UI更新必须切到主线程ExecutorService executor Executors.newSingleThreadExecutor(); executor.execute(() - { FtpUploader uploader new FtpUploader(); try { boolean ok uploader.upload(host, port, user, pass, remoteDir, file, (transferred, total) - { runOnUiThread(() - { int percent (int) (transferred * 100L / total); progressBar.setProgress(Math.min(percent, 100)); tvProgress.setText(percent %); }); }); runOnUiThread(() - tvProgress.setText(ok ? 上传成功 : 上传失败)); } catch (Exception e) { runOnUiThread(() - tvProgress.setText(上传异常: e.getMessage())); } finally { executor.shutdown(); } });这里有个细节值得注意Math.min(percent, 100)。虽然理论上百分比不会超过100但我在第2章讲过FTP传输完毕后个别服务端还会发送额外的控制信息这个过程中CopyStreamListener回调可能再多触发一两次导致transferred恰好比total大一点。加一层Math.min能避免进度条拉到100之后又回落或者溢出。4.3 页面销毁时的连接释放Android客户端有一个很现实的问题用户上传传了一半按返回键退出页面这时候FTP连接还挂在后台线程里。如果不处理线程池和连接都会泄漏后续再进页面重新上传时服务器可能还在处理旧连接。我的处理方式是在onDestroy里给FtpUploader增加一个abort()方法核心动作就是断开FTPClient连接。因为storeFile是阻塞的粗暴地中断Thread并不可靠Commons Net更推荐的方式是调用disconnect让正在进行的IO抛出异常从而使工作线程退出。这在UI层面可以提供一个取消上传按钮实际效果比教科书里说的interrupt可靠得多。5. 实测定会遇到的坑从连不上到进度条跳变5.1 坑一上传二进制文件后文件损坏第一个坑出现在我最开始跑通能传文件的时候。测试用的文本文件上传后看着没问题但换了一张图片测试服务端打开直接报文件已损坏。根因是FTPClient默认的文件传输类型是ASCII这种模式下客户端和服务端会对换行符做转换图片、压缩包这些二进制文件经过转换后字节流直接被改写。解决方式就是第3章代码里的那一行ftpClient.setFileType(FTP.BINARY_FILE_TYPE);如果说这个实例只记住一个设置那一定是这一行。凡是上传非纯文本文件必须显式设置二进制模式而且要在上传命令开始前设置传完文件千万不要顺手把它改回去因为同一个FTPClient连接可能还要复用。5.2 坑二连接正常但上传瞬间Connection reset现象是connect成功login也成功一到storeFile就抛SocketException: Connection reset。查下去的根因是FTP的主动/被动模式。FTP协议有两种数据连接建立方式主动模式下客户端打开端口等服务器来连被动模式下客户端主动连接服务器指定的端口。在大多数内网、NAT、云服务器环境下主动模式必炸因为服务器根本连不回客户端的监听端口。解决方式就是ftpClient.enterLocalPassiveMode();这是我半年内踩过最深的一个坑也是为什么从第3章的工具类开始我就把被动模式写死在所有示例里。被动模式也不是银弹极个别FTP服务端被动模式端口范围受限这时候需要在服务端防火墙放行对应端口段。一旦遇到上传总是超时、但下载正常这种诡异情况优先怀疑被动模式端口被防火墙拦了。5.3 坑三中文文件名乱码FTP控制连接上跑的是命令和文件名编码方式在协议里没有强制规定。Commons Net默认用系统编码发命令Java跑在Linux上系统编码通常是UTF-8Windows上可能是GBK而FTP服务端可能是另一个编码两边对不上中文文件名传到服务器上就变成乱码。处理方式是setControlEncoding但关键是编码值要和FTP服务端保持一致。企业内网常见的老Windows FTP服务端用GBKLinux下用vsftpd的常用UTF-8。如果设置UTF-8后还乱码把值改成GBK试一次基本能解决。这个试错法听起来不优雅却是跨平台FTP中文问题最有效的排查路径。5.4 坑四进度条卡在0%不动进度条完全不动的排查看起来吓人其实多半不是上传没在进行而是回调频率或数据缓冲设置的问题。Commons Net内部复制缓冲默认大小是1024字节对小文件来说一个缓冲块很快就传完了回调次数少进度条当然容易从0直接跳满对大文件来说回调每秒上千次如果你在回调里做了日志输出性能会被严重拖垮。我的做法是调整缓冲大小并做节流ftpClient.setBufferSize(1024 * 64); // 回调内做节流 long lastUpdateTime 0; long currentTime System.currentTimeMillis(); if (currentTime - lastUpdateTime 200) { lastUpdateTime currentTime; listener.onProgress(transferred, total); }setBufferSize影响的是FTP数据连接的收发缓冲这在IO性能和进度回调频率之间做了一个平衡。节流逻辑保证最短200毫秒才回调一次Android端刷新UI的次数大幅下降拖动进度条更顺滑。5.5 坑五进度条到100%但上传失败进度显示100%上层却报上传失败这个现象看似矛盾其实原因是FTP的数据连接传输完成和控制连接确认命令成功是两回事。如果使用的是storeFileStream(String)这类方法写完输出流后必须显式调用ftpClient.completePendingCommand();这个方法负责读取控制连接上FTP服务端返回的226 Transfer complete响应。漏调用时storeFileStream写完数据就返回上层以为传完了实际上服务端可能还在做最后的落盘处理于是出现进度已完成但状态未知甚至失败的诡异局面。我封装的FtpUploader用的是storeFile(String, InputStream)这个便利方法它在内部已经处理了pending command的逻辑所以这种问题在封装类里不会出现。但如果你在网上抄到的是storeFileStream那套代码遇到文件传完但方法返回false或抛出EOF异常第一反应检查一下completePendingCommand()是不是漏了。5.6 坑六在上传方法的finally里忘记恢复连接状态如果你把FTP连接做成复用的连接池还需要注意一点一次上传结束后控制连接的会话状态会被改变比如还在刚才的目录、文件类型可能被改过、可能残留未读取的响应。下一次复用这条连接时最稳妥的做法是先调用ftpClient.completePendingCommand()确认没有残留再重新login或者用ftpClient.setFileType恢复已知状态。我自己吃过亏连接池里的连接第二次使用时因为没恢复二进制模式直接传了个文本文件上去业务方第二天反馈文件打开是乱码排查半天问题出在连接复用上而不是传输环节。这里建议不做连接复用每次上传新建连接、用后即焚对大多数中小项目都足够了。6. 扩展思路如果前端是浏览器进度条怎么设计6.1 浏览器到后端后端到FTP这是两段进度很多项目现在是前后端分离浏览器端用Vue或React后端Java最终的下载/上传目标还是FTP服务器。这时候有个概念必须先理清文件从浏览器到后端是一段HTTP传输从后端到FTP服务器又是另一段FTP传输。两段时间并不重合进度条必须分两段设计不能混在一起算。浏览器到后端的进度前端用XMLHttpRequest或fetch自带的upload.onprogress事件就能拿到这是浏览器层做的能力。后端到FTP服务器的进度才需要用到前面讲的CopyStreamListener方案而且这个进度在后端前端无法直接感知。6.2 用SSE把FTP进度推给前端最直接的做法是在后端维护一个上传任务状态FTP传输过程中通过SSEServer-Sent Events把百分比推给浏览器。SSE和WebSocket的区别在于SSE是单向的服务端推送断线自动重连实现简单非常适合进度通知这种只从服务端往客户端发数据的场景。在Spring Boot里大致长这样GetMapping(value /upload/progress, produces MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter uploadProgress(RequestParam String taskId) { SseEmitter emitter new SseEmitter(60000L); // 用taskId找到正在进行的FTP上传任务把emitter注册进去 ftpTaskRegistry.registerEmitter(taskId, emitter); return emitter; }然后在CopyStreamListener回调里把计算好的百分比emitter.send()出去。前端用EventSource接收并更新进度条。这里要注意的是SSE连接超时和浏览器事件源重连机制传输大文件时不能把SseEmitter的超时设得太短否则文件还没传完连接先断了。6.3 简单场景直接轮询进度接口如果SSE对团队来说引入成本偏高另一个简单方案是后端把每个上传任务的进度放内存Map或Redis里前端每500毫秒轮询一次接口拿最新进度。这个方案实现成本低、排查问题直观代价是实时性最多到500毫秒对进度条显示来说完全够用。我个人的经验是内部管理系统这种并发量不高的场景轮询方案比SSE更省心对外提供上传能力、用户量大时再换SSE或WebSocket不迟。最后再分享一个小技巧。FTP上传这套东西把被动模式、二进制模式、控制编码三个设置做成可配置项后续换服务器、换网络环境时能省去大量排查时间。很多今天能传明天不能传的问题最后查出来都是这三个设置的组合在不同服务端上表现不同导致的。稳定的上传工具不是写一次就一劳永逸而是把不确定因素都暴露成配置让运维人员在改配置而不是改代码。本文还有配套的精品资源点击获取