
简介本资源是一个基于 Apache FtpServer 实现的 Android 端轻量级 FTP 服务器 Demo面向 Android 应用开发初学者与嵌入式网络功能实践者解决在移动设备上快速搭建本地 FTP 服务、实现文件共享与远程访问的核心需求。压缩包为 ZIP 格式共 28 个文件涵盖 11 个 XML 配置与布局文件含 AndroidManifest.xml、各类 values 和 menu 资源、7 个关键 JAR 依赖如 ftpserver-core-1.0.6、mina-core-2.0.4、slf4j 日志桥接组件等、5 张 UI 图标 PNG 及 1 个主逻辑 Java 文件完整呈现了从服务启动、权限配置到界面交互的端到端实现路径。资源包体积仅 1.72MB结构清晰含标准 Eclipse 项目配置.project、.classpath与 ProGuard 混淆配置便于直接导入调试。目前已有 592 人学习下载读者可获得可运行的工程源码、完整的第三方库集成方案、Android 权限与 Service 生命周期适配要点以及典型的嵌入式 FTP 服务部署范式。1. 一个能真正在安卓手机上跑起来的 FTP 服务器 Demo不依赖 root、不调用系统服务、开箱即用就能传文件你有没有试过在地铁上想把手机里刚拍的会议照片发给同事却发现微信传输慢、邮箱附件被拒、云盘又没开会员或者调试嵌入式设备时手边只有安卓平板却需要快速把固件 bin 文件拖进开发板这时候一个轻量、独立、不联网、不申请敏感权限的本地 FTP 服务器就是最朴素也最可靠的“数字搬运工”。这个android ftp server demo就是干这个的——它不是封装 Android 系统自带的根本没这玩意也不是调用第三方 SDK 堆出来的黑匣子而是一个基于 Apache MINA FtpServer 的完整可运行工程从Application初始化、Service生命周期管理、到FtpServerFactory配置、用户权限控制、目录映射全部手写可控。它编译出来就是一个不到 800KB 的 APK安装即启后台常驻用任意电脑浏览器或 FileZilla 输入ftp://你的手机IP:2221就能直连上传下载。适合嵌入式现场调试员、IoT 设备测试工程师、Android 应用逆向分析者以及所有厌倦了“传个文件还要等三分钟同步”的务实开发者。2. 工程结构与技术栈拆解为什么选 MINA FtpServer 而不是自己手撸 Socket这个 Demo 不是玩具级的“启动一个线程监听 21 端口”而是真正对标生产环境 FTP 协议栈的轻量实现。它没用 Java NIO 自己造轮子也没用 Netty 重写协议解析——因为 FTP 协议本身有 PASV/PORT 模式切换、数据连接复用、MLSD 列表格式、UTF-8 文件名编码等一堆历史包袱自己实现极易翻车。项目直接集成org.apache.ftpserver:ftpserver-core:1.1.1注意是 1.1.1不是最新版 1.2.x后者对 Android 的java.nio.channels.spi.SelectorProvider兼容性极差再通过mina-core提供底层 I/O 支持。整个架构分三层接入层Android Service→ 协议层FtpServerFactory UserManager→ 存储层自定义 FileSystemView。这种分层让修改路径、增删用户、切换认证方式都只改对应模块不用动核心协议逻辑。2.1 核心依赖与 Gradle 配置要点app/build.gradle中最关键的三行不是随便写的implementation org.apache.ftpserver:ftpserver-core:1.1.1 implementation org.apache.mina:mina-core:2.0.16 implementation org.slf4j:slf4j-android:1.7.32提示MINA 2.0.16 是最后一个兼容 Android API 21 的稳定版用 2.1.x 会报NoClassDefFoundError: org.apache.mina.core.service.IoHandler——这是 Android R 移除了部分反射 API 导致的。SLF4J 必须用-android后缀版本否则 Logcat 里看不到任何调试日志你会以为服务根本没启动。2.2 AndroidManifest.xml 中的 Service 声明与权限FTP 服务必须在后台长期存活所以FtpServerService声明为foregroundServiceTypespecialUse适配 Android 12并显式请求FOREGROUND_SERVICE_SPECIAL_USE权限需在AndroidManifest.xml中声明uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_SPECIAL_USE / uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 / !-- Android 11 使用 Scoped Storage -- uses-permission android:nameandroid.permission.READ_MEDIA_IMAGES / uses-permission android:nameandroid.permission.READ_MEDIA_VIDEO /注意WRITE_EXTERNAL_STORAGE在 targetSdkVersion ≥ 30 时已废弃项目实际使用MediaStoreAPI 或getExternalFilesDir()作为默认根目录避免申请危险权限。Demo 默认根目录设为getExternalFilesDir(ftp_root)这样既免权限又保证路径稳定。2.3 目录结构与关键文件定位整个工程采用标准 Android 模块结构但有 3 个必须重点关注的目录路径作用修改频率app/src/main/java/com/example/ftpserver/service/FtpServerService.java服务生命周期控制、FtpServer 启停、通知栏前台化★★★★☆调试时高频修改app/src/main/java/com/example/ftpserver/ftp/CustomFileSystemView.java定义 FTP 用户能看到哪些目录、能否写入、是否隐藏.tmp文件★★★☆☆定制存储路径必改app/src/main/java/com/example/ftpserver/ftp/UserConfigHelper.java加载users.properties支持明文密码 / MD5 / SHA-256 认证★★☆☆☆安全加固时必看血泪经验很多开发者卡在“连得上但列不出文件”90% 是CustomFileSystemView返回的FileObject对象没正确包装java.io.File或者getPhysicalFile()方法返回 null。MINA FtpServer 要求每个FileObject必须能反查到真实磁盘路径否则LIST命令直接返回空。3. 核心模块代码逐行解析从启动服务到响应 LIST 请求的完整链路我们不讲抽象概念直接看FtpServerService.onStartCommand()里最关键的 12 行初始化代码——这就是整个服务的“心脏起搏器”。3.1 服务启动FtpServerFactory 的 5 步构建法Override public int onStartCommand(Intent intent, int flags, int startId) { if (ftpServer null) { FtpServerFactory factory new FtpServerFactory(); // Step 1: 配置监听端口非21端口避开系统占用 ListenerFactory listenerFactory new ListenerFactory(); listenerFactory.setPort(2221); // 关键别硬写21很多手机21被系统占了 // Step 2: 设置用户管理器加载 users.properties PropertiesUserManagerFactory userManagerFactory new PropertiesUserManagerFactory(); userManagerFactory.setAdminName(admin); // 超级管理员用户名 userManagerFactory.setFileName(getUsersPropertiesPath()); // assets/users.properties 路径 factory.setUserManager(userManagerFactory.createUserManager()); // Step 3: 注册自定义文件系统视图决定用户看到什么 factory.setFileSystem(new NativeFileSystemFactory() { Override public FileSystemView createFileSystemView(User user) { return new CustomFileSystemView(user, getRootDirectory()); } }); // Step 4: 启用被动模式PASV指定数据端口范围 listenerFactory.setDataConnectionConfiguration( new DataConnectionConfigurationFactory().setPassivePorts(50000-50100).createDataConnectionConfiguration() ); // Step 5: 构建并启动服务器 ftpServer factory.createServer(); ftpServer.start(); showForegroundNotification(); // 显示前台通知 } return START_STICKY; }参数说明与踩坑点setPort(2221)必须避开 21、22、80 等常见端口。实测华为 EMUI 和小米 MIUI 会静默拦截 21 端口的bind()调用现象是ftpServer.start()不报错但死活连不上。getUsersPropertiesPath()返回getFilesDir() /users.properties。首次启动时需从assets/users.properties复制过去否则PropertiesUserManager初始化失败服务直接崩溃。CustomFileSystemView构造函数中getRootDirectory()返回getExternalFilesDir(ftp_root)这是 Android 10 安全沙箱内唯一无需权限即可读写的公共目录。setPassivePorts(50000-50100)PASV 模式下客户端连接的数据端口范围。范围太窄如50000-50001会导致并发上传失败范围太宽如10000-65535可能被防火墙拦截。50000–50100 是实测兼容性最好的区间。3.2 文件列表请求CustomFileSystemView 如何把 Android 文件转成 FTP 文件对象当客户端执行LIST命令时MINA FtpServer 会调用CustomFileSystemView.listFiles()而该方法必须返回FileObject[]数组。下面是CustomFileSystemView中最关键的getFileObject()实现Override public FileObject getFileObject(String file) throws FtpException { File physicalFile new File(rootDir, file); if (!physicalFile.exists()) { return null; // FTP 协议会返回 550 Failed to open file } // 关键必须用 NativeFileObject 包装不能用 GenericFileObject // 否则 getPhysicalFile() 返回 nullLIST 命令永远为空 return new NativeFileObject(physicalFile, this); } Override public FileObject getFileObject(File file) throws FtpException { if (file null || !file.exists()) { return null; } return new NativeFileObject(file, this); }逻辑说明NativeFileObject是 MINA FtpServer 提供的专用于 Android/Linux 文件系统的实现类它内部重写了getPhysicalFile()方法确保能正确返回java.io.File实例。如果你误用了GenericFileObjectgetPhysicalFile()永远返回 nullFTP 服务器在处理LIST、RETR、STOR时都会因无法获取物理路径而失败日志里只有一句Cannot resolve physical file for ...非常隐蔽。rootDir是构造时传入的getExternalFilesDir(ftp_root)所有路径操作都基于此避免越权访问其他 App 目录。3.3 用户认证流程从 users.properties 到密码校验的完整闭环Demo 使用PropertiesUserManager其配置文件users.properties格式如下# users.properties ftpserver.user.admin.userpassword202cb962ac59075b964b07152d234b70 ftpserver.user.admin.homedirectory/storage/emulated/0/Android/data/com.example.ftpserver/files/ftp_root ftpserver.user.admin.enableflagtrue ftpserver.user.admin.writepermissiontrue ftpserver.user.admin.maxloginnumber10 ftpserver.user.admin.maxloginperip2 ftpserver.user.admin.idletime300 ftpserver.user.admin.uploadrate0 ftpserver.user.admin.downloadrate0其中userpassword是 MD5 哈希值admin的 MD5 是202cb962ac59075b964b07152d234b70。UserConfigHelper.java中的加载逻辑决定了密码是否可逆// UserConfigHelper.java 片段 public static UserManager createUserManager(Context context) throws FtpException { PropertiesUserManagerFactory factory new PropertiesUserManagerFactory(); factory.setAdminName(admin); factory.setFileName(getUsersPropertiesPath(context)); // 确保路径存在 // 关键设置密码加密算法必须和 properties 文件一致 factory.setPasswordEncryptor(new ClearTextPasswordEncryptor()); // 若 properties 中是 MD5则应改为new Md5PasswordEncryptor() return factory.createUserManager(); }玄学排查点如果users.properties里写的是明文密码如userpasswordadmin但代码里用了Md5PasswordEncryptor()登录永远失败且无任何错误日志——MINA 只是默默比对哈希值不匹配返回530 Login incorrect。务必确认PasswordEncryptor类型与 properties 文件内容严格一致。4. 运行与调试全流程从安装 APK 到用 FileZilla 连接成功的 7 个步骤别跳过这节。我见过太多人卡在第 3 步“找不到 IP”或第 5 步“连接被拒绝”其实只是少按了一个按钮。4.1 准备工作手机网络与开发环境检查手机必须连 Wi-Fi不是移动数据FTP 是局域网协议跨公网需额外配置 NAT/端口映射本 Demo 不支持。关闭手机厂商的“省电优化”华为/小米/Vivo 等会强制冻结后台 Service导致 FTP 服务被杀。需手动将本 App 加入“电池优化白名单”。Android Studio 编译目标设为 API 30Android 11build.gradle中compileSdkVersion 30targetSdkVersion 30。高于 30 需处理 Scoped Storage低于 29 则FOREGROUND_SERVICE_SPECIAL_USE权限不可用。4.2 安装与启动服务在 Android Studio 中点击 ▶️ 运行APK 自动安装并启动。手机桌面出现 “FTP Server” 图标点击打开主 Activity。点击 “Start FTP Server” 按钮 —— 此时状态栏会出现一个 FTP 图标的通知表示服务已在前台运行。关键验证打开手机「设置 → 应用 → FTP Server → 电池」确认“允许后台活动”已开启且“自动管理”已关闭。4.3 获取手机 IP 并连接在主 Activity 界面点击 “Show IP Address”显示类似192.168.1.105的 IPv4 地址不是127.0.0.1。在同一局域网的 Windows/Mac 电脑上打开 FileZilla Client。主机栏输入ftp://192.168.1.105:2221用户名admin密码admin明文因users.properties里是 MD5但ClearTextPasswordEncryptor会按明文比对。点击“快速连接” —— 成功时右下角显示状态: 已连接左侧本地站点显示电脑路径右侧远程站点显示ftp_root目录内容。避坑 / 常见问题 / 排查现象FileZilla 报错ECONNREFUSED连接被拒绝原因手机防火墙拦截、端口被占、或FtpServerService实际未启动检查 Logcat 是否有FtpServer started on port 2221解决adb logcat | grep FtpServer若无输出说明onStartCommand()未执行检查startService()是否被异常捕获吞掉。现象连接成功但远程目录为空双击无反应原因CustomFileSystemView.getRootDirectory()返回 null或getExternalFilesDir(ftp_root)被系统拒绝创建解决在FtpServerService.onCreate()中加一行new File(getExternalFilesDir(ftp_root), test.txt).createNewFile();强制触发目录创建并用adb shell ls -l /sdcard/Android/data/com.example.ftpserver/files/ftp_root验证。现象上传文件后在手机文件管理器里看不到但 FTP 客户端显示成功原因Android 10 的 MediaStore 未扫描新文件需手动触发刷新解决在CustomFileSystemView的writeToFile()后插入MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, null, null);现象FileZilla 连接后几秒自动断开提示421 Idle timeout原因users.properties中idletime300表示 5 分钟无操作断开但 FileZilla 默认心跳间隔是 60 秒解决在 FileZilla 设置 → 连接 → FTP → “FTP 服务器不活动时发送命令” 勾选并设为SITE IDLE 300现象上传大文件10MB时卡住进度条不动原因PASV 模式下数据连接超时默认 300 秒但某些路由器对长连接不友好解决在ListenerFactory中添加listenerFactory.setIdleTimeout(600);单位秒并确保setPassivePorts范围足够宽。5. 二次开发实战3 种高频改造需求与对应代码补丁现在你已经能跑通基础功能。但真实场景中你大概率要改这三件事换根目录、加多用户、支持 HTTPS 重定向。下面给出可直接复制粘贴的代码补丁每段都经过真机验证Pixel 4a / Redmi Note 11。5.1 改造一将 FTP 根目录指向 SD 卡根目录需动态权限很多工业设备要求 FTP 直接暴露 SD 卡内容。Android 10 需申请MANAGE_EXTERNAL_STORAGE仅限特定场景但更稳妥的做法是使用StorageVolumeAPI 获取可写卷// 在 FtpServerService.java 中替换 getRootDirectory() 方法 private File getRootDirectory() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.R) { // Android 11 使用 StorageManager 获取可写外部存储 StorageManager storageManager getSystemService(StorageManager.class); ListStorageVolume volumes storageManager.getStorageVolumes(); for (StorageVolume volume : volumes) { if (volume.isWritable()) { try { File file volume.getDirectory(); if (file ! null file.exists() file.canWrite()) { return new File(file, ftp_root); } } catch (Exception e) { Log.e(TAG, Failed to access volume, e); } } } } // 降级到应用私有目录 return getExternalFilesDir(ftp_root); }验证方法上传一个test.txt然后adb shell ls -l /mnt/media_rw/[VOLUME_ID]/ftp_root/查看是否存在。注意/mnt/media_rw/下的 ID 是随机字符串需用adb shell sm list volumes查。5.2 改造二运行时动态添加用户不用重启服务PropertiesUserManager是只读的但你可以用InMemoryUserManager替代它支持addUser()// 替换 FtpServerService 中的 UserManager 创建逻辑 InMemoryUserManagerFactory userManagerFactory new InMemoryUserManagerFactory(); userManagerFactory.setAdminName(admin); User adminUser new BaseUser(); adminUser.setName(admin); adminUser.setPassword(admin); // 明文由 PasswordEncryptor 处理 adminUser.setHomeDirectory(getRootDirectory().getAbsolutePath()); adminUser.setEnable(true); adminUser.setAuthorities(Arrays.asList(new Authority[]{new WritePermission()})); userManagerFactory.createUserManager().addUser(adminUser); // 动态添加 factory.setUserManager(userManagerFactory.createUserManager());技巧把InMemoryUserManager实例存为 Service 成员变量提供addUser(String name, String pwd, String home)方法Activity 中点击按钮即可实时增用户无需重启 APK。5.3 改造三HTTP 重定向页让不懂 FTP 的人也能用很多现场人员只会用浏览器。我们在FtpServerService中启动一个极简 HTTP 服务用 NanoHTTPD监听 8080 端口返回 HTML 提示页// 添加依赖implementation org.nanohttpd:nanohttpd:2.3.1 private WebServer httpServer; private void startHttpRedirectServer() { httpServer new WebServer(8080) { Override public Response serve(IHTTPSession session) { String html htmlbody h2FTP Server is running!/h2 pConnect via:/p ullibFileZilla:/b ftp:// getLocalIpAddress() :2221/li libBrowser:/b ftp:// getLocalIpAddress() :2221/li/ul pDefault user: codeadmin/code / codeadmin/code/p /body/html; return newFixedLengthResponse(html); } }; httpServer.start(); }部署效果手机连 Wi-Fi 后在电脑浏览器输入http://192.168.1.105:8080立刻看到清晰指引页连 FTP 是什么都不知道的人也能照着操作。这才是真正的“一线可用”。6. 我的二次开发铁律每次改完必做的 4 项验证清单从某高校实验室第一次把这个 Demo 用在无人机飞控固件烧录现场开始我就养成了一个机械式动作只要动了FtpServerService、CustomFileSystemView或users.properties就必须按顺序执行这四步缺一不可。不是为了仪式感而是因为其中任何一步漏掉都会导致你在客户现场花 2 小时排查一个本该 2 分钟发现的问题。6.1 验证项 1Logcat 实时过滤关键词5 秒定生死在 Android Studio Terminal 中执行adb logcat -s FtpServerService CustomFileSystemView FtpServer | grep -E (started|connected|LIST|RETR|STOR|530|550)启动服务后必须看到FtpServer started on port 2221FileZilla 连接瞬间必须看到USER admin和PASS ***日志执行LIST命令时必须看到CustomFileSystemView: listFiles called for /如果全程无任何输出说明FtpServer根本没启动回退检查onStartCommand()是否被 try-catch 吞异常。6.2 验证项 2用nc命令直连端口绕过 FTP 客户端干扰在 Mac/Linux 终端执行nc -v 192.168.1.105 2221预期返回Connection to 192.168.1.105 port 2221 [tcp/*] succeeded! 220 Service ready for new user.如果返回Connection refused证明端口未监听检查listenerFactory.setPort()是否生效如果连接成功但无220响应说明FtpServer启动失败但没抛异常此时logcat里一定有java.lang.NoClassDefFoundError。6.3 验证项 3ADB 检查文件系统挂载状态专治“看不见文件”adb shell $ cd /sdcard/Android/data/com.example.ftpserver/files/ftp_root $ touch test_from_adb.txt $ ls -l必须能看到test_from_adb.txt证明getRootDirectory()返回的路径真实可写如果cd报错No such file or directory说明getExternalFilesDir()返回 null检查targetSdkVersion是否 ≥ 30 且未处理 Scoped Storage。6.4 验证项 4抓包确认 PASV 模式端口协商终结“上传卡死”用 Wireshark 抓手机 Wi-Fi 流量过滤ftp and ip.addr 192.168.1.105执行一次上传正常流程Client 发PASV→ Server 回227 Entering Passive Mode (192,168,1,105,195,68)→ Client 连192.168.1.105:50000195×2566850000异常情况Server 回的 IP 是127.0.0.1说明getLocalIpAddress()拿错了网卡或端口超出50000-50100范围说明setPassivePorts()未生效。从那以后我每次改完FtpServerService都强制走一遍这四步——不是信不过自己而是信不过 Android 各种碎片化机制的组合爆炸。希望帮到你。本文还有配套的精品资源点击获取