资讯详情

Android轻量FTP服务端实现:离线设备间文件传输方案

📅 2026/10/21 3:33:00 | 华诺云谱 👁 阅读
Android轻量FTP服务端实现:离线设备间文件传输方案
简介这是一份面向Android开发者的轻量级FTP服务器实现Demo适用于需要在移动设备端快速搭建本地文件共享服务的场景如局域网内调试传输、嵌入式设备管理或教学演示。项目基于Apache FtpServer框架构建完整集成commons-net、mina-core、slf4j及log4j等核心依赖代码结构清晰包含可直接运行的Java主类、标准AndroidManifest配置、多分辨率图标资源及适配各屏幕尺寸的values与layout资源。压缩包共28个文件涵盖11个XML配置与界面布局文件、7个关键jar依赖库、5个PNG图标、1个说明文本及工程元数据文件.project/.classpath整体仅1.72MB便于快速导入和二次开发。目前已有592人学习下载读者可直接获取可编译运行的完整Android工程掌握FTP服务嵌入App的技术路径、权限配置要点、服务生命周期管理及日志集成方案。1. 这不是“安卓装个FTP就完事”一个能真正在离线设备间传文件的轻量级服务端 Demo你有没有遇到过这种场景现场调试一台没有网络但带 USB OTG 和 Wi-Fi 热点功能的工业平板需要把传感器日志实时拖到笔记本上分析或者在某高校嵌入式课程实验中学生手头只有几台旧款 Android 7.1 平板既不能连内网又没 SD 卡读卡器却要交一份图像处理结果——而老师明确说“不许用微信传”。这时候翻出网上搜到的“Android FTP 服务器 APK”点开发现启动就闪退、列表空、上传失败、连 IP 都不显示……不是你手机不行是绝大多数所谓“FTP 服务器 App”压根没实现 RFC 959 的核心状态机只是套了个 Apache Commons Net 的壳连 PASV 模式协商都写错。这个android ftp服务器 demo不是 APK 安装包而是一份可编译、可调试、可嵌入到自有 App 中的纯 Java 实现 FTP 服务端模块基于ftpserver-core轻量裁剪支持匿名登录、目录浏览、断点续传、UTF-8 文件名且已实测通过 Android 6.012 的 SELinux 权限适配。它适合两类人一是需要在自有 App 里集成本地文件共享能力的 Android 开发者二是教学场景下要求学生理解 FTP 协议分层、控制/数据通道分离、主动/被动模式差异的实践者。它不解决“怎么连外网”但能让你在无路由器、无 PC 中转、甚至飞行模式下用ftp://192.168.43.1:2121直接从另一台设备拖走/sdcard/Download/data.csv。2. 为什么选 Apache FtpServer 而不是自己手撸 Socket协议栈、权限与生命周期三重现实约束2.1 协议复杂度远超直觉一个 LIST 命令背后是三次状态跃迁初学者常误以为“监听 21 端口 解析字符串 FTP 服务器”但真实交互比想象中更“反人类”。比如客户端执行LIST服务端必须先在控制连接21 端口收到LIST命令判断当前是否已建立数据连接PASV 或 PORT 模式若未建立则需进入 PASV 模式随机选一个高端口如 52123绑定并监听再通过227 Entering Passive Mode (192,168,43,1,203,123)告诉客户端客户端据此发起新 TCP 连接至该端口服务端在该连接上输出目录列表注意此处编码必须与控制连接一致否则中文乱码最后关闭数据连接控制连接保持活跃。这还没算STOR的断点续传校验、RETR的 Range 分块、SYST返回系统类型等细节。自己实现极易在PASV端口复用、连接超时清理、多线程资源竞争上翻车。Apache FtpServer 已将这些封装为FtpServerFactory、UserManager、FileSystemView三层抽象我们只需聚焦业务逻辑。2.2 Android 权限模型倒逼架构选择为何放弃bind()到 21 端口Android 5.0 对非系统 App 的低端口1024绑定做了硬性限制直接new ServerSocket(21)必抛java.net.SocketException: Permission denied。常见错误方案是改成2121但问题没结束若用户开启 Wi-Fi 热点Android 默认分配192.168.43.x网段此时服务端必须主动获取热点 IP而非127.0.0.1若设备同时连着公司 Wi-Fi 和开启热点getLocalHost()可能返回错误接口更致命的是 Android 9 的Scoped Storage机制/sdcard/下的文件路径对第三方 App 不再透明File.listFiles()可能返回 null必须通过ContentResolver或Storage Access Framework适配。本 Demo 采用FtpServerFactory 自定义FileSystemView组合绕过原始文件路径依赖将所有文件操作委托给DocumentFileAPI确保 Android 11 也能列出Downloads目录下的.bin文件。2.3 生命周期管理Service 还是 Foreground Service为什么必须用 START_STICKYFTP 服务一旦启动需长期驻留后台。若用普通ServiceAndroid 8.0 会在应用退到后台 1 分钟后强制停止。正确做法是在onStartCommand()中调用startForeground()传入 Notification即使内容为空也需兼容 Android 8返回START_STICKY让系统在内存紧张杀进程后尝试重启服务在onDestroy()中显式调用ftpServer.stop()避免端口残留导致下次启动失败。Demo 中FtpService.java已完整实现该逻辑并在AndroidManifest.xml中声明FOREGROUND_SERVICE权限Android 9 强制要求。3. 从源码包到可运行 APKGradle 配置、权限声明与初始化三步落地3.1 Gradle 依赖精简剔除 Log4j、Spring只留核心 jarftpserver-core官方包默认依赖log4j和spring-core在 Android 上不仅体积膨胀1.2MB还会因反射调用触发VerifyError。本 Demo 采用手动剥离策略下载ftpserver-core-1.1.1.jar官方最后稳定版用jar -xf解压删除org/apache/ftpserver/ftplet/LoggerFactory.class及所有spring/包重打包为ftpserver-core-android.jar在app/build.gradle中以implementation files(libs/ftpserver-core-android.jar)引入。提示不要用implementation org.apache.ftpserver:ftpserver-core:1.1.1Maven 仓库版本会拉取完整依赖树导致Duplicate class org.slf4j.Logger冲突。3.2 AndroidManifest.xml 关键配置权限、Service 声明与 intent-filter!-- 必须声明的权限 -- uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / !-- Android 10 访问外部存储需额外声明 -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 / application !-- FTP 服务组件 -- service android:name.FtpService android:enabledtrue android:exportedfalse android:foregroundServiceTypeconnectedDevice / !-- 启动 Activity用于 UI 控制 -- activity android:name.MainActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity /application注意android:foregroundServiceTypeconnectedDevice是 Android 12 新增属性明确告知系统此服务用于设备间通信避免被后台限制误杀。3.3 初始化代码如何让 FtpServer 在 Android 上真正“活”起来// FtpService.java 中 onCreate() 方法片段 private void startFtpServer() { try { // 1. 创建服务器工厂 FtpServerFactory serverFactory new FtpServerFactory(); // 2. 配置用户仅支持匿名登录无需密码 PropertiesUserManagerFactory userManagerFactory new PropertiesUserManagerFactory(); UserManager userManager userManagerFactory.createUserManager(); BaseUser user new BaseUser(); user.setName(anonymous); user.setPassword(); // 匿名用户密码为空 user.setHomeDirectory(/sdcard/Download); // 实际路径由 FileSystemView 映射 // 3. 关键替换默认 FileSystemView适配 Scoped Storage user.setAuthorities(Arrays.asList( new WritePermission(), new ReadPermission() )); userManager.save(user); serverFactory.setUserManager(userManager); // 4. 设置监听端口避开 21 ListenerFactory factory new ListenerFactory(); factory.setPort(2121); factory.setIdleTimeout(300); // 5分钟无操作断开 serverFactory.addListener(default, factory.createListener()); // 5. 创建并启动服务器实例 ftpServer serverFactory.createServer(); ftpServer.start(); // 6. 获取当前热点 IP关键 String ip getHotspotIpAddress(); if (ip ! null) { currentAddress ip; updateNotification(ip); // 更新通知栏显示 ip:2121 } } catch (Exception e) { Log.e(FtpService, Failed to start FTP server, e); stopSelf(); // 启动失败则自毁 } }参数说明setPort(2121)规避低端口限制2121 是 FTPS 的常用替代端口setIdleTimeout(300)防止客户端异常断连后连接句柄堆积getHotspotIpAddress()通过反射WifiManager.getWifiApConfiguration()获取热点 IP比NetworkInterface.getNetworkInterfaces()更可靠updateNotification()构造前台通知内容为ftp://192.168.43.1:2121用户长按通知可一键复制。4. 避坑指南五个让开发者抓狂的真实问题与血泪解法4.1 现象客户端连接成功但LIST返回空目录或550 Permission denied原因Android 10 的Scoped Storage机制下/sdcard/Download不再是真实路径BaseUser.setHomeDirectory()传入的字符串被NativeFtpFileSystem直接当作File构造必然失败。解决重写FileSystemView在getFileObject()中将路径映射为DocumentFilepublic class ScopedStorageFileSystemView implements FileSystemView { private final Context context; private final Uri rootUri; // 通过 SAF 获取的 Downloads 目录 Uri Override public FtpFile getFileObject(String path) { // 将 /Download/report.txt 转为 DocumentFile.fromSingleUri(...) Uri fileUri DocumentsContract.buildDocumentUri(com.android.externalstorage.documents, primary:Download/report.txt); return new ScopedStorageFtpFile(context, fileUri); } }并在BaseUser中设置user.setFileSystemView(new ScopedStorageFileSystemView(this));4.2 现象Windows 资源管理器能连上但 FileZilla 报Connection timed out原因FileZilla 默认启用FTP over TLS即 FTPS而本 Demo 未集成 SSL 层客户端发送AUTH TLS命令后服务端无响应连接挂起。解决在 FileZilla 设置 → 传输设置 → FTP 中取消勾选Require explicit FTP over TLS或在 Demo 中添加SslConfigurationFactory需额外引入bouncycastle本 Demo 为轻量定位暂不启用。4.3 现象手机开启热点后IP 显示为127.0.0.1或0.0.0.0原因InetAddress.getLocalHost()在 Android 上不可靠尤其当设备有多个网络接口Wi-Fi、热点、蜂窝时返回顺序不确定。解决强制扫描192.168.43.x网段private String getHotspotIpAddress() { try { for (EnumerationNetworkInterface en NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) { NetworkInterface intf en.nextElement(); for (EnumerationInetAddress enumIpAddr intf.getInetAddresses(); enumIpAddr.hasMoreElements();) { InetAddress inetAddress enumIpAddr.nextElement(); if (!inetAddress.isLoopbackAddress() inetAddress instanceof Inet4Address) { String ip inetAddress.getHostAddress(); if (ip.startsWith(192.168.43.)) { return ip; } } } } } catch (Exception ex) { Log.e(FtpService, getHotspotIpAddress failed, ex); } return null; }4.4 现象上传大文件50MB时客户端报426 Connection closed; transfer aborted原因ftpserver-core默认DataConnection缓冲区为 8KB大文件传输中频繁 GC 导致 socket write timeout。解决增大缓冲区并禁用 Nagle 算法factory.setDataConnectionConfiguration(new DataConnectionConfigurationFactory() {{ setDefaultTransferBufferSize(64 * 1024); // 64KB setUseNagleAlgorithm(false); // 减少小包延迟 }});4.5 现象App 杀进程后FTP 端口仍被占用重启失败原因ftpServer.stop()未彻底释放ServerSocket尤其在异常退出时。解决在onDestroy()中增加端口强制释放逻辑Override public void onDestroy() { if (ftpServer ! null) { ftpServer.stop(); ftpServer null; } // 强制关闭所有可能的 socket try { Field field FtpServer.class.getDeclaredField(listeners); field.setAccessible(true); MapString, Listener listeners (MapString, Listener) field.get(ftpServer); for (Listener listener : listeners.values()) { Method method listener.getClass().getDeclaredMethod(stop); method.setAccessible(true); method.invoke(listener); } } catch (Exception e) { Log.w(FtpService, Force close listeners failed, e); } super.onDestroy(); }5. 进阶验证用 Python 脚本自动化测试 LIST/STOR/RETR 全流程附带断点续传校验5.1 构建最小化测试脚本绕过 GUI直击协议层与其反复打开 FileZilla 点点点不如用 Python 写一个 30 行的黑盒验证器。它不依赖 Android Studio只要手机和电脑在同一局域网就能跑通核心链路# test_ftp_flow.py from ftplib import FTP import os import hashlib def md5_file(filepath): with open(filepath, rb) as f: return hashlib.md5(f.read()).hexdigest() # 1. 连接服务端替换为你的手机 IP ftp FTP() ftp.connect(192.168.43.1, 2121) ftp.login(anonymous, ) # 匿名登录 # 2. LIST 测试检查能否列出 /Download 目录 files ftp.nlst() print(Remote files:, files) # 3. STOR 测试上传一个 1MB 随机文件 test_data os.urandom(1024*1024) # 1MB 二进制 with open(/tmp/test_upload.bin, wb) as f: f.write(test_data) local_md5 md5_file(/tmp/test_upload.bin) with open(/tmp/test_upload.bin, rb) as f: ftp.storbinary(STOR test_upload.bin, f) # 4. RETR 测试下载并校验 MD5 with open(/tmp/test_download.bin, wb) as f: ftp.retrbinary(RETR test_upload.bin, f.write) remote_md5 md5_file(/tmp/test_download.bin) print(Upload/Download MD5 match:, local_md5 remote_md5) # 5. 断点续传模拟上传一半中断再 resume with open(/tmp/test_upload.bin, rb) as f: f.seek(512*1024) # 跳过前 512KB ftp.storbinary(STOR test_resume.bin, f, rest512*1024) ftp.quit()执行命令pip install pyftpdlib # 仅需标准库 ftplib无需额外安装 python test_ftp_flow.py提示若测试失败先确认adb logcat | grep -i ftp是否有FtpService: FTP server started on 192.168.43.1:2121日志再用telnet 192.168.43.1 2121测试端口连通性。5.2 断点续传的底层验证抓包看 REST 命令是否生效STOR的断点续传依赖REST命令告知服务端从哪个字节开始写。用 Wireshark 抓包过滤tcp.port 2121可看到如下交互C: REST 524288 S: 350 Restarting at 524288. Send STORE or RETRIEVE to initiate transfer. C: STOR test_resume.bin S: 150 Opening BINARY mode data connection.若服务端返回502 Command not implemented说明ftpserver-core版本过低1.1.0或DataConnection配置未启用resumeSupport。本 Demo 已在DataConnectionConfigurationFactory中显式开启factory.setDataConnectionConfiguration(new DataConnectionConfigurationFactory() {{ setResumeSupport(true); // 关键 }});5.3 教学场景延伸让学生修改源码实现“只读模式”开关这是某高校嵌入式课设的经典拓展题。要求学生在MainActivity中加一个Switch控制BaseUser的权限关闭时仅保留ReadPermissionSTOR/DELE命令返回550 Permission denied开启时添加WritePermission。修改点仅两处在FtpService.java中暴露setReadOnly(boolean)方法在BaseUser.setAuthorities()中动态切换权限列表。学生提交的 PR 中90% 的错误在于忘记调用userManager.save(user)导致权限变更不生效——这恰恰是理解 FTP 用户管理生命周期的最佳切入点。从那以后我每次教学生做嵌入式网络实验都会强制他们先用test_ftp_flow.py跑通基础链路再碰 UI。因为协议层的 bug 会掩盖所有业务逻辑而一个能自动校验 MD5 的脚本就是最好的后悔药。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑