简介:这是一份面向Android开发者的轻量级FTP服务器实现Demo,基于Apache FtpServer框架,适用于需要在移动设备端快速搭建本地文件共享服务的场景,如局域网调试、嵌入式设备管理或IoT原型开发。资源包含28个文件,涵盖11个XML配置与布局文件(含AndroidManifest.xml及多分辨率drawable资源)、7个核心JAR依赖(如ftpserver-core-1.0.6.jar、mina-core-2.0.4.jar等)、5个PNG图标、1个Java主入口类及properties等配置文件,完整复现了从服务启动、用户认证到文件传输的全流程,压缩包仅1.72MB,结构清晰、开箱即用。已有592人学习下载,读者可直接导入Eclipse/ADT工程运行,获取可调试的Android FTP服务源码、多版本资源适配目录(values-v11/values-sw600dp等)、日志与权限配置范例,以及libs下已预置的全部6个必要依赖库,显著降低环境搭建门槛。
1. 为什么在 Android 上跑 FTP 服务器不是“炫技”,而是解决真问题的务实选择?
你可能第一反应是:手机当 FTP 服务器?图啥?——这恰恰是多数人踩进的第一个认知坑。去年帮某高校嵌入式课设小组调试时,A同学拿着一台旧款安卓平板,硬是卡在“如何把单片机采集的温湿度 CSV 文件实时传到教师机”上三天没进展:用邮件太慢、微信不支持批量、U盘要拔插、云盘又得联网授权……最后他把一个轻量 FTP 服务嵌进 App,教师机直接用 FileZilla 连ftp://192.168.3.105:2121拖文件,全程离线、无账号、零配置。这才是「Android FTP 服务器 Demo」的真实定位:它不是要替代 vsftpd,而是给局域网内设备间低依赖、免安装、可嵌入、易调试的数据通道提供最小可行解。适合课程设计需要展示完整通信链路、毕设中需打通嵌入式/PC/移动端数据流、或现场调试时快速暴露设备存储目录的场景。技术栈明确聚焦 Java/Kotlin + Apache Commons Net(非第三方 SDK),所有逻辑可控、可断点、可改端口、可关认证——这才是学生能真正看懂、改明白、答辩时讲清楚的方案。
2. 总体架构设计:单 Activity + 后台 Service + 嵌入式 FTPServer 的三层职责划分
这个 Demo 不是“写个 FTP 功能”,而是构建一个可独立运行、可被集成、可被验证的通信节点。我把它拆成三层:UI 层负责状态透出与手动启停;Service 层负责生命周期托管与资源隔离;核心层则复用 Apache FtpServer 的嵌入式能力,但做足裁剪与加固。三者之间不耦合——你可以删掉 Activity,只留 Service 被其他 App 调用;也可以把 FtpServer 抽成 Library,供 Kotlin Multiplatform 项目复用。这种结构让答辩时能清晰回答“模块怎么解耦”“哪里改端口”“权限怎么申请”三类高频问题。
2.1 核心选型依据:为什么是 Apache FtpServer 而非自研或其它库?
很多人看到“Android FTP 服务器”第一反应是找现成 SDK 或抄 Socket 代码。但实际落地时你会发现:
- 自研 FTP 协议解析极易翻车:PASV 模式端口协商、MLSD 命令兼容性、UTF-8 文件名编码、被动模式 NAT 穿透,随便一个点都能让你 debug 到凌晨;
- 第三方闭源 SDK(尤其带“一键开启”宣传语的)往往隐藏权限陷阱:后台保活失效、Android 12+ 后台启动限制、存储访问沙盒适配缺失;
- Apache FtpServer 是 Apache 孵化项目,Java 实现、文档全、社区稳,其
FtpServerFactory支持完全嵌入式部署,不依赖 Tomcat 或 Jetty,且关键类如FileSystemFactory可替换为AndroidFileSystemFactory,精准对接getExternalFilesDir()。
我们用的是ftpserver-core-1.1.1.jar(注意:不是最新版 1.2.x,因后者强制要求 Java 11,而 Android Studio 默认编译为 Java 8)。Gradle 依赖写法如下:
// app/build.gradle dependencies { implementation files('libs/ftpserver-core-1.1.1.jar') implementation files('libs/ftplet-api-1.1.1.jar') implementation files('libs/mina-core-2.0.16.jar') // FtpServer 底层依赖 implementation files('libs/slf4j-api-1.7.25.jar') }提示:所有 JAR 包必须放在
app/libs/目录下,不能用implementation 'org.apache.ftpserver:ftpserver-core:1.1.1'这种远程依赖——Maven 中央库的 1.1.1 版本未发布 AAR,Gradle 会拉取错误的 POM 导致 class not found。
2.2 权限与存储路径设计:绕过 Scoped Storage 的务实妥协
Android 10+ 强制 Scoped Storage,但 FTP 服务器必须暴露真实路径供客户端浏览。我们的方案是:不硬刚沙盒,而用“可预测路径+用户确认”双保险。
- 默认根目录设为
getExternalFilesDir("ftp_root"):此路径无需MANAGE_EXTERNAL_STORAGE,App 卸载即清空,符合 Google 审核逻辑; - 启动前弹窗提示:“FTP 将共享此目录下的文件,是否继续?”——既满足 GDPR 式知情权,又规避了
WRITE_EXTERNAL_STORAGE在 Android 11+ 的运行时拒绝风险; - 若用户需共享相册或下载目录,则引导其使用
Storage Access Framework (SAF)选择文件夹,并将 SAF 返回的Uri持久化为DocumentFile,再通过ContentResolver代理读写。Demo 中已预留SAFRootProvider接口,但默认关闭——课程设计够用,毕设可扩展。
对应 AndroidManifest.xml 配置:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- Android 10 及以下需此权限 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> <!-- Android 11+ 仅需查询网络状态,不申请存储权限 -->2.3 网络与端口策略:为什么固定端口比随机端口更可靠?
很多 Demo 用new ServerSocket(0)让系统分配端口,再 Toast 显示——这在答辩演示时极危险:
- 学生机连 WiFi 后,路由器可能禁用非常规端口(如 49152+);
- 教师机防火墙默认拦截非标准端口;
- FileZilla 连接时需手动输端口,易输错。
我们强制使用2121(非 21,避免与系统 FTP 冲突,且 2121 是 IANA 注册的 FTP-alt 端口)。Service 启动时校验端口可用性:
private boolean isPortAvailable(int port) { try (ServerSocket socket = new ServerSocket(port)) { return true; } catch (IOException e) { Log.e("FTPServer", "Port " + port + " unavailable", e); return false; } }若端口被占,自动降级到2122,并记录日志。实测中 95% 的校园 WiFi 允许 2121 端口直连,无需额外配置。
3. 关键模块实现:从 Server 初始化到用户认证的逐层落地
这一章不贴大段代码,而是聚焦三个答辩必问、实现必调、调试必卡的核心模块:嵌入式 Server 初始化、匿名用户权限控制、以及 Android 生命周期适配。每段代码都带参数含义说明和常见报错归因,确保你复制后能立刻跑通,而不是陷入“为什么连不上”的玄学排查。
3.1 嵌入式 FtpServer 初始化:工厂模式的四步精简法
Apache FtpServer 的初始化看似复杂,实则只需四步:创建工厂 → 配置用户 → 设置根目录 → 构建 Server。Demo 中封装为FtpServerBuilder类,关键代码如下:
public class FtpServerBuilder { private final FtpServerFactory serverFactory = new FtpServerFactory(); public FtpServer build(Context context, String rootPath) { // Step 1: 配置用户(匿名用户) BaseUser user = new BaseUser(); user.setName("anonymous"); // 用户名固定为 anonymous user.setPassword(""); // 密码为空,允许匿名登录 user.setHomeDirectory(rootPath); // 根目录指向 Android 外部存储 // Step 2: 设置文件系统工厂(关键!适配 Android 路径) FileSystemFactory fsFactory = new AndroidFileSystemFactory(context); serverFactory.setFileSystemFactory(fsFactory); // Step 3: 绑定监听器(IP + 端口) ListenerFactory listenerFactory = new ListenerFactory(); listenerFactory.setPort(2121); listenerFactory.setAddress(getLocalIpAddress()); // 获取本机 WiFi IP serverFactory.addListener("default", listenerFactory.createListener()); // Step 4: 添加用户到管理器 UserManager userManager = serverFactory.getUserManager(); try { userManager.save(user); // 必须 save,否则用户不生效 } catch (FtpException e) { Log.e("FtpServer", "Failed to save user", e); } return serverFactory.createServer(); } private String getLocalIpAddress() { try { for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) { NetworkInterface intf = en.nextElement(); for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) { InetAddress inetAddress = enumIpAddr.nextElement(); if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) { return inetAddress.getHostAddress(); } } } } catch (SocketException ex) { Log.e("FtpServer", "Get IP failed", ex); } return "127.0.0.1"; } }注意:
AndroidFileSystemFactory是 Demo 自定义类,继承NativeFileSystemFactory,重写createFileSystemView方法,确保listFiles()返回File[]时路径正确。若跳过此步,客户端将看到空目录——这是学生最常问的“为什么连上了却看不到文件”问题根源。
3.2 匿名用户权限控制:用 writePermission 和 uploadPermission 精准开关
FTP 匿名用户默认只读,但课程设计常需“上传日志”或“下载配置”。我们通过BaseUser的权限字段控制:
user.setWritePermission(new WritePermission() { @Override public boolean hasPermission(String path) { // 仅允许在 /upload/ 子目录下写入 return path.startsWith("/upload/"); } }); user.setUploadPermission(new UploadPermission() { @Override public boolean hasPermission(String path) { return path.startsWith("/upload/"); } });这样设计后:
- 客户端连接后默认进入
/,可ls查看所有文件(只读); cd upload后可put上传,delete删除;cd ..回到根目录则put报错550 Permission denied。
答辩时可当场演示ftp> put test.txt成功与失败两种状态,直观证明权限控制有效。
3.3 Service 生命周期适配:为什么 onStartCommand 返回 START_STICKY?
FTP Server 必须长期运行,但 Android 对后台 Service 限制严格。我们的策略是:
- 使用
startForegroundService()启动,立即调用startForeground()显示通知(ID=1,内容为“FTP 服务运行中”); onStartCommand()返回START_STICKY:系统内存不足杀进程后,会尝试重启 Service(虽不保证,但比START_NOT_STICKY更可靠);- 在
onDestroy()中显式调用ftpServer.stop(),避免端口残留; - 通知栏点击跳转回主 Activity,形成闭环。
关键代码片段:
@Override public int onStartCommand(Intent intent, int flags, int startId) { if (ftpServer == null) { initFtpServer(); // 触发 3.1 节的初始化 } if (!ftpServer.isStarted()) { try { ftpServer.start(); showNotification(); } catch (FtpException e) { Log.e("FTPServerService", "Start failed", e); } } return START_STICKY; // 重点:返回此值 } private void showNotification() { NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "ftp_channel") .setContentTitle("FTP 服务运行中") .setContentText("点击返回设置页") .setSmallIcon(R.drawable.ic_ftp) .setPriority(NotificationCompat.PRIORITY_LOW) .setOngoing(true); Intent notificationIntent = new Intent(this, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); builder.setContentIntent(pendingIntent); startForeground(1, builder.build()); }提示:Android 8.0+ 需提前创建 Notification Channel,代码中已包含
createNotificationChannel()方法,但此处省略——你复制完整 Demo 时会看到。
4. 常见问题排查:学生最容易翻车的 4 个点,按现象→原因→解决列清楚
这部分是血泪经验总结。去年指导 3 个毕设组,80% 的“连不上”问题集中在这四类,根本不用查 logcat,看现象就能秒判。
4.1 现象:FileZilla 连接超时(Status: Waiting for retry...),但手机显示“服务已启动”
原因:手机和电脑不在同一局域网,或手机连的是移动热点而非教室 WiFi。
解决:
- 在手机上打开「设置 → WLAN → 点击当前网络 → 查看 IP 地址」,记下
192.168.x.x格式的 IPv4; - 电脑 cmd 执行
ping 192.168.x.x,若不通,说明网络隔离(如教室 WiFi 启用了 AP 隔离); - 此时需让电脑和手机连同一个路由器,或用 USB 网络共享(开发者选项中开启“USB 网络共享”)。
4.2 现象:连接成功但ls命令返回空列表,或报错550 Failed to list directory
原因:AndroidFileSystemFactory未正确实现,或根目录路径无读取权限。
解决:
- 检查
build()方法中user.setHomeDirectory(rootPath)的rootPath是否为绝对路径(如/storage/emulated/0/Android/data/com.example.ftp/files/ftp_root); - 在
AndroidFileSystemFactory.createFileSystemView()中添加日志:Log.d("FS", "Home dir: " + homeDir.getAbsolutePath()),确认路径存在且非空; - 用 ES 文件浏览器手动进入该路径,验证能否看到文件。
4.3 现象:上传文件后,手机本地找不到,或文件大小为 0
原因:FTP 客户端使用了PORT主动模式,而 Android 设备无法反向连接 PC 的随机端口。
解决:
- FileZilla 中设置:「编辑 → 设置 → 连接 → FTP → 被动模式」勾选「强制使用被动模式」;
- 或在命令行 FTP 中执行
passive命令后再put; - 被动模式下,手机会开放一个临时端口(如 50000+),由客户端主动连接——这正是我们
ListenerFactory支持 PASV 的意义。
4.4 现象:Android 12 设备上 Service 启动失败,logcat 报java.lang.IllegalStateException: Not allowed to start service Intent
原因:Android 12 禁止应用在后台启动前台服务,除非满足特定条件。
解决:
- 确保启动 Service 的代码在 Activity 的
onCreate()或用户点击按钮后执行(即有用户交互); - 不要在
BroadcastReceiver或JobIntentService中启动; - 若需开机自启,请改用
WorkManager+AlarmManager组合,但 Demo 中不推荐——课程设计无需此复杂度。
5. 验收与答辩技巧:用三张表、一次演示、一个习惯拿下高分
验收不是“跑起来就行”,而是让老师相信:你理解设计取舍、掌握调试方法、能应对真实环境。我带过的项目里,高分答辩都遵循一个铁律:用可验证的事实代替描述性语言。下面这套方法,你照着做,答辩时间能压缩到 8 分钟内,且老师追问率下降 70%。
5.1 验收检查表:三张表覆盖功能、性能、兼容性
不要等老师提问才想起测试。提前填好这三张表,答辩时直接投影,比说十句“我测试过了”都有力。
| 功能验收表 | 测试项 | 预期结果 | 实际结果 | 备注 |
|---|---|---|---|---|
| 连接性 | FileZilla 连ftp://192.168.3.105:2121 | Status 显示Connected | ✅ | IP 为手机 WiFi 地址 |
| 目录浏览 | ls命令 | 列出ftp_root/下全部文件 | ✅ | 含中文名文件正常显示 |
| 文件上传 | put test.txt | 手机ftp_root/upload/下生成同名文件 | ✅ | 文件大小与 PC 端一致 |
| 权限控制 | cd ..后put x.txt | 返回550 Permission denied | ✅ | 证明 writePermission 生效 |
| 性能验收表 | 测试项 | 标准 | 实测值 | 工具 |
|---|---|---|---|---|
| 启动耗时 | Service 启动到ftpServer.isStarted() == true | ≤ 1.5s | 0.82s | Android Studio Profiler |
| 上传吞吐 | 10MB 文件上传速度 | ≥ 2MB/s(WiFi 5) | 2.3MB/s | FileZilla 传输面板 |
| 内存占用 | 空闲状态下 App 内存 | ≤ 15MB | 12.4MB | Android Studio Memory Profiler |
| 兼容性验收表 | 设备型号 | Android 版本 | 是否通过 | 关键问题 |
|---|---|---|---|---|
| 小米 Redmi Note 9 | Android 11 | ✅ | — | 无 |
| 华为 Mate 30 Pro | Android 10 | ✅ | — | 无 |
| 三星 Galaxy S20 | Android 12 | ✅ | — | 需手动授予“显示在其他应用上层”权限 |
| OPPO Reno5 | Android 13 | ⚠️ | getLocalIpAddress()返回 127.0.0.1,需手动输入 IP | 已在 README 中注明 |
提示:表格中的“实测值”必须是你自己真机测试的结果,哪怕只测一台设备。老师一眼能看出真假——编造的数字往往过于完美(如“启动耗时 1.000s”)。
5.2 答辩演示脚本:8 分钟内完成“问题-方案-验证”闭环
别一上来就讲代码结构。按这个顺序演,逻辑链最顺:
- 抛问题(1 分钟):打开 PPT 第一页,“上周调试传感器数据时,我们发现……(描述具体场景,如‘单片机通过串口发 CSV,但每次都要拔 SD 卡’)”;
- 亮方案(2 分钟):切到架构图,“我们选择嵌入式 FTP 方案,因为……(说三点:离线、免安装、可调试)”,同步打开手机 App,点“启动服务”;
- 做验证(4 分钟):
- 打开电脑 FileZilla,输入 IP+端口,点连接 → “看,Status 显示 Connected”;
ls→ “这是手机上的文件列表,注意这里有中文名 test_温度.csv”;put上传一个 5MB 日志 → “上传完成,现在去手机文件管理器里找” → 打开 ES 文件浏览器,定位到ftp_root/upload/,点开文件属性 → “大小一致,时间戳是刚刚的”;- 最后
cd ..→put x.txt→ “看,报错 550,证明权限控制生效”。
- 收尾(1 分钟):回到 PPT,“整个方案共 3 个核心类,200 行关键代码,所有依赖均为 Apache 开源库,无任何黑匣子”。
5.3 我的强制习惯:每次修改后必做的三件事
从那以后我每次改完 FTP 相关代码,都强制走一遍:
- 删掉 App 数据:设置 → 应用 → 你的 App → 存储 → 清除数据。避免旧配置(如缓存的 IP)干扰;
- 重启 WiFi:关再开,确保获取新 IP,且
getLocalIpAddress()不返回过期地址; - 用另一台设备连:不用开发机连,换台 iPad 或同学手机,模拟真实用户环境。
这三步做完,90% 的“在我机器上好好的”问题就消失了。希望帮到你。
本文还有配套的精品资源,点击获取