ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android FTP客户端实战:从Commons Net到文件传输闭环

Android FTP客户端实战:从Commons Net到文件传输闭环 简介面向Android开发者的FTP功能实现资源包基于Apache commons-net-3.7.jar解决应用内文件上传、下载、远程文件管理等需求。包内含完整工程文件与可运行APK共477个文件以flat布局资源、json数据、xml配置、jar库为主另有dex、class编译产物、gradle构建脚本及png图标等压缩包总大小约10.44MB。已有421人学习浏览。资源可直接导入Android Studio展示了FTPClient连接、被动模式设置、文件流读写、completePendingCommand处理及删除重命名等操作并包含网络异常处理与基础安全考虑。适合具备一定Android基础、希望快速集成FTP功能的开发者参考。 上个月接手一个外包交付的工程包里面一个名为 AndroidFtp.rar 的压缩文件特别显眼。解压完我发现这其实是一个典型的安卓 FTP 传输模块手机扫码或者输 IP 地址就能和电脑、NAS、Linux 服务器之间的文件做互传。我猜做这个工程包的人大概率是被“局域网内多个安卓设备文件同步”这个需求逼出来的因为现在微信发文件要压缩画质网盘又绕一大圈公司内网环境下 FTP 反而成了最简单粗暴的方案。项目本身不大但“Android FTP”这个组合踩坑点非常多权限、明文流量限制、分区存储、FileProvider、中文文件名、被动模式……每一个都能让首次上手的人卡半天。这篇就把我解包、改造、调通、上线全过程中的实操思路和排查经验写清楚代码片段直接复制可用适合做 Android 开发、运维或者嵌入式调试的朋友参考。1. 这个压缩包到底在解决什么问题1.1 为什么移动端还要用 FTP很多人第一反应是都什么年代了还 FTP我一开始也这么想但真正做了几个现场项目之后发现 FTP 在移动端的使用频率远比想象中高。典型的场景有这么几类局域网内批量传输大文件。比如工控机上的日志、几 GB 的视频素材通过 HTTP 上传需要写服务端接口通过 USB 线又要物理连接FTP 只需两端在同一网段就能跑。和 NAS、Windows 共享文件夹互通。家里或办公室的 NAS 基本都带 FTP 服务手机装个客户端就能把相册备份进去。嵌入式设备调试。很多开发板、单片机模块内置了 FTP 协议栈用来做固件升级、配置下发、数据上报。Android 设备作为调试终端时用 FTP 跟它们通信比串口方便得多。服务端一键部署无需改代码。Linux 服务器上 vsftpd 装好就能用不像自制 HTTP 上传接口还要考虑鉴权、并发、超时这些事。对比一下HTTP 上传文件虽然更“现代”但服务端必须配合往往还需要 SSL 证书SMB 在安卓上兼容性参差不齐蓝牙传输慢、距离近USB 又不适合远程。FTP 胜在协议简单、实现成熟、服务端零改造成本所以在特定场景下依然是可靠选项。1.2 从压缩包结构看方案选型解开 AndroidFtp.rar 之后目录结构很清晰基本是一个标准的 Android 工程app/src/main/java/下有 MainActivity、FtpHelper、常量配置类。app/libs/里是commons-net-3.10.0.jar。res/xml/下有 FileProvider 的路径配置文件。附带一个 README写了服务器 IP、账号密码的替换位置。这种结构的选型思路值得说一说作者没有自己写 FTP 协议而是选了 Apache Commons Net 这个老牌 Java 库。原因很简单FTP 底层的命令交互、主动/被动模式、数据连接管理都是一堆容易出 bug 的细节自己实现一轮至少要一周而 Commons Net 在 Java 领域跑了很多年API 稳定社区案例多。它的核心类FTPClient用起来其实就是几行代码后面我会给完整示例。2. Android FTP 客户端落地的几个关键点2.1 权限、明文流量和 Android 9 的默认限制在 Android 上做 FTP 客户端第一关是权限和网络策略。FTP 默认是明文协议不走 TLS 加密而 Android 9API 28开始默认禁止应用使用明文网络流量。也就是说你不做任何配置直接连ftp://192.168.x.x大概率会抛Cleartext HTTP traffic not permitted的异常。处理方式有两种。第一种是在AndroidManifest.xml里的application标签直接加android:usesCleartextTraffictrue这个最简单但代价是应用内所有明文网络请求都被放行如果项目里还有其他敏感网络请求安全性会比较差。第二种是只对特定域名或 IP 放开明文权限在res/xml/network_security_config.xml里配置network-security-config domain-config cleartextTrafficPermittedtrue domain includeSubdomainstrue192.168.1.100/domain /domain-config /network-security-config然后在application中引用android:networkSecurityConfigxml/network_security_config这样做更稳妥尤其是将来要把 FTP 地址换成公网 IP 的时候白名单机制能避免把整个应用的明文流量都暴露出去。另外别忘记在AndroidManifest.xml里加网络权限uses-permission android:nameandroid.permission.INTERNET /以及如果你要把文件保存到公共存储目录还需要处理 Android 11 以上的存储权限适配这块下面单独讲。2.2 FTPClient 连接参数的正确配置权限搞定之后真正连接 FTP 服务器时会遇到一堆细节问题。拿我最常用的 Commons Net 来说一段最基础的连接代码如下import org.apache.commons.net.ftp.FTP; import org.apache.commons.net.ftp.FTPClient; FTPClient client new FTPClient(); client.setConnectTimeout(10_000); client.setDataTimeout(15_000); client.connect(host, port); int reply client.getReplyCode(); if (!FTPReply.isPositiveCompletion(reply)) { client.disconnect(); throw new RuntimeException(连接被拒绝); } client.login(username, password); client.setControlEncoding(UTF-8); client.enterLocalPassiveMode(); client.setFileType(FTP.BINARY_FILE_TYPE);这里有几个参数必须说清楚不然很容易被坑enterLocalPassiveMode()进入被动模式。主动模式下服务器会主动连回客户端的某个端口安卓设备在 NAT 后或者用移动网络时服务器根本连不回来被动模式则是由客户端主动发起数据连接成功率明显更高。局域网内的 FTP 服务基本都推荐用被动模式。setControlEncoding(UTF-8)控制连接的编码格式。很多 FTP 服务器默认用本地编码Windows 自带的 FTP 服务尤其容易输出 GBK 编码的目录名如果不统一编码列目录时中文文件名直接乱码。setFileType(FTP.BINARY_FILE_TYPE)强制二进制传输。如果不设成 BINARY默认的 ASCII 模式会在传输过程中做换行符转换传 APK、图片、压缩包这些二进制文件时文件会被写坏。2.3 中文文件名、路径分隔符与重连文件名的坑多到让我一度想放弃 FTP——但其实是没找对方法。第一个坑是远程路径分隔符。FTP 协议里路径统一用/分隔不要因为 Android 本地路径用File.separator就把远程路径也拼成\这在 Windows FTP 服务器上还能碰巧工作在 Linux vsftpd 上直接 550 错误。第二个坑是中文文件名的 URL 编码问题。如果服务器开启了 UTF-8 特性客户端设置setControlEncoding(UTF-8)之后中文名通常没问题但如果服务器没启用 UTF-8可能需要手动转码。我的处理方式是在connect成功之后主动探测一下服务器是否支持 UTF-8if (client.hasFeature(UTF8)) { client.setControlEncoding(UTF-8); }第三个坑是连接复用。移动端的网络不稳定一次长时间 FTP 会话很容易被服务端踢掉。我的习惯是每次传输任务独立建立连接传完就logout()和disconnect()短连接重连比长时间保活靠谱得多。如果要做“ftp 监控”这种轮询任务就用 ScheduledExecutorService 定时执行重连逻辑不要尝试维护一个永久连接。3. 从零搭一个“手机 ↔ 电脑”FTP 互传闭环3.1 服务端怎么快速起一个 FTP要测客户端先得有一个服务端。Linux 上最常用的是 vsftpdCentOS 7 装起来很快yum install -y vsftpd systemctl start vsftpd systemctl enable vsftpd默认配置下匿名用户可能开着为了数据安全建议修改/etc/vsftpd/vsftpd.confanonymous_enableNO local_enableYES write_enableYES local_umask022 pasv_min_port30000 pasv_max_port31000然后创建系统用户并设置密码useradd ftpuser passwd ftpuserPassive 端口范围这里特别重要如果服务器有防火墙光放行 21 端口是不够的数据连接会随机落在 30000-31000 区间不放行就会表现为“能登录但传不了文件”。开放命令firewall-cmd --permanent --add-port21/tcp firewall-cmd --permanent --add-port30000-31000/tcp firewall-cmd --reloadWindows 上不想装第三方软件的话可以用控制面板的“启用或关闭 Windows 功能”打开“Internet Information Services”里的“FTP 服务器”然后在 IIS 管理器里建站点。不过 Windows 自带 FTP 的配置相对麻烦还是建议直接用 FileZilla Server图形界面点几下就能跑起来。3.2 Android 11 之后文件读取怎么做才对客户端把文件上传到 FTP第一步是让用户选择本地文件。很多老代码还在用file:///storage/emulated/0/...这种硬编码路径这在现代 Android 上已经行不通了。Android 10 开始强制分区存储应用不能随意访问外部存储的任意目录Android 11 更进一步连Android/data目录都不让普通应用访问了。热搜里那一串content://com.xxx.fileprovider/...的报错本质上就是 FileProvider 和分区存储碰撞后的并发症。正确的做法是用系统文件选择器SAF让用户自己授权文件访问权val intent Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type */* } startActivityForResult(intent, REQUEST_CODE_PICK_FILE)拿到content://的 Uri 之后不要尝试转换成 File 路径应该直接用 ContentResolver 打开输入流val inputStream contentResolver.openInputStream(uri)这个 InputStream 就能直接交给 FTPClient 上传。同理下载下来的文件如果不想放在应用私有目录建议通过MediaStore写入公共 Download 目录或者让用户选择保存位置。3.3 上传下载的完整示例下面这段代码是我改造后最终跑通的简化版上传逻辑很清晰fun upload(uri: Uri, remotePath: String): Boolean { val client FTPClient() return try { client.connect(host, port) client.login(username, password) client.enterLocalPassiveMode() client.setFileType(FTP.BINARY_FILE_TYPE) client.setControlEncoding(UTF-8) contentResolver.openInputStream(uri)?.use { input - return client.storeFile(remotePath, input) } ?: false } catch (e: Exception) { e.printStackTrace() false } finally { try { client.logout() client.disconnect() } catch (_: Exception) { } } }下载方向的代码同样是对称的只是方向反过来fun download(remotePath: String, outputStream: OutputStream): Boolean { val client FTPClient() return try { client.connect(host, port) client.login(username, password) client.enterLocalPassiveMode() client.setFileType(FTP.BINARY_FILE_TYPE) client.retrieveFile(remotePath, outputStream) } catch (e: Exception) { e.printStackTrace() false } finally { try { client.logout() client.disconnect() } catch (_: Exception) { } } }如果你要做的是“ftp 监控”即定期扫描远程目录里有没有新文件并下载可以在上面的基础上加一个client.listFiles()的轮询逻辑。最好把轮询放到 WorkManager 里指定周期任务避免每次都启动一个常驻 Service 耗电。4. 实战中我踩过的坑和排查清单4.1 连不上先按这个顺序查在多个项目里拿到过“FTP 连不上”的问题大多数不是代码的问题而是网络环境和服务端配置的问题。我整理了一个排查顺序排查点检查内容常见原因服务端进程端口是否监听服务没启动或端口被占用网络连通性手机和服务器能否相互 ping 通Wi-Fi 隔离、跨网段路由问题防火墙21 端口和数据端口是否放行安全组只放行 21没放行被动端口主动/被动模式服务器是否支持被动模式某些旧服务器只允许主动模式账号权限登录后能否列出目录目录权限不够或 chroot 限制客户端超时连接超时、数据超时设置网络慢时默认超时太短其中最坑的是“能登录但 listFiles 卡死”的问题十次里有八次是防火墙没有放行被动端口范围。你把 30000-31000 放行之后问题立刻消失。4.2 “FTP over TLS is not enabled” 警告到底要不要管你可能会在服务器日志里看到一条 warningFTP over TLS is not enabled, users cannot securely log in.这句话的意思是当前 FTP 服务没有启用 TLS 加密用户登录时的账号密码会以明文形式在网络上传输。内网环境比如家庭局域网、公司内部调试网络风险相对可控很多老项目直接忽略这条警告。但如果你的 FTP 服务器暴露在公网或者传输的是敏感数据就必须启用 FTPS。Android 客户端这边可以用 Commons Net 里的FTPSClient替换FTPClientval client FTPSClient(TLS) client.connect(host, port) client.login(username, password) client.execPBSZ(0) client.execPROT(P)注意公网环境不要用自签名证书否则客户端需要额外做证书信任处理维护成本会明显上升。能上 SFTP 的话就更好了。4.3 文件路径和 content:// 转换失败这半年里最常被问的问题就是那一串content://com.baidu.searchbox.fileprovider/...或者content://com.ss.android.uri.key/external_root/...的报错。核心原因就是有的应用去拿 FileProvider 暴露的 Uri然后试图转成绝对路径去访问结果被分区存储拦截。正确的做法是永远不要对content://调用toString()之后拼到文件路径里而是用ContentResolver.openInputStream()拿流。如果你确实需要知道文件名可以用OpenableColumns.DISPLAY_NAME查询val cursor contentResolver.query(uri, null, null, null, null) cursor?.use { if (it.moveToFirst()) { val displayName it.getString( it.getColumnIndex(OpenableColumns.DISPLAY_NAME) ) } }这个文件名可以当作 FTP 的远程文件名来用避免中文名乱码的同时也绕开了路径权限问题。最后再分享一个我实际项目里的小技巧把 FTP 的上传/下载任务封装成一个FtpTask的普通 Java/Kotlin 类用线程池并发执行每个任务内部独立连接和断开。这样既不会因为多个任务共用一个 FTPClient 导致状态错乱也方便给每个任务加独立的超时和重试机制。我试过很多次短连接虽看着效率低但稳定性远好于长连接尤其是在手机网络切 Wi-Fi 的情况下长连接几乎必断短连接反而是最省心的方案。本文还有配套的精品资源点击获取
返回列表