资讯动态

Android FTP 服务器搭建:权限、存储与前台服务保活

发布时间:2026/10/26 18:40:51 来源:尧图企业网站定制
简介这份资源是一个在 Android 平台上搭建 FTP 服务器的完整示例工程基于 Apache FtpServer 实现面向希望在移动端实现文件读写与局域网共享的 Android 开发者也适合用来理解服务端组件与 Android 应用生命周期如何结合。压缩包共 28 个文件约 1.72MB包含 java 源码、描述界面与权限的 xml 配置、properties 配置、工程描述文件以及 7 个核心 jar 依赖——ftpserver-core 提供 FTP 服务主体mina-core 负责底层网络通信commons-net-ftp 补充 FTP 协议支持log4j 与 slf4j 组合完成日志输出整体结构清晰可直接导入工程对照阅读。代码围绕 FtpServer 的启动、监听端口设置、用户与目录权限配置展开并配有 png 图标与布局文件方便观察服务运行状态。已有 593 人学习下载可作为移动端轻量 FTP 服务的入门参考帮助读者快速理解依赖组合方式、服务初始化流程与常见配置项减少自行摸索依赖版本匹配与日志框架集成的时间成本。1. 从「PC 端连不上」说起Android 上搭 FTP 服务器的版本与权限地图某开发者把一台旧手机翻出来当文件中转站按网上教程下了一个 FTP 服务器 App装上、给了存储权限、开了热点PC 端 FileZilla 填上ftp://手机IP:2221连接一直卡在“正在连接”。他以为是 App 的问题卸载重装了三遍最后发现是 Android 10 之后作用域存储把/sdcard的写路径切掉了FTP 服务端能启动但根目录不可写。这个场景太常见了Android 上跑 FTP麻烦从来不在“FTP 协议”本身而在系统对后台服务、存储路径、网络权限的层层收紧。这份 demo 适合要在一台 Android 设备上快速拉起本地文件服务的开发者——用来做同局域网文件互传、旧手机改简易 NAS、或是给嵌入式设备做文件投递口。装不好会怎样服务能起、日志不报错、PC 端就是连不上或者连上了目录是空的。所以先把版本、权限、存储根这三件事对齐再往下装。2. 工程骨架先立住Gradle 依赖、AndroidManifest 权限与存储适配这一章解决“能不能编译过、能不能拿到权限”的问题。很多 demo 下载下来跑不起来九成是 Gradle 版本和 compileSdk 不匹配剩下的一成是权限清单漏了前台服务声明。2.1 Gradle 依赖与仓库配置先看依赖。Apache FtpServer 1.2.0 是当前在 Android 上比较稳的一版配套 MINA 2.1.7 作为底层 IO。这两个版本组合在 Android 814 上都能跑不要图新去上 SNAPSHOT。android { namespace com.demo.ftpserver compileSdk 34 defaultConfig { applicationId com.demo.ftpserver minSdk 26 // Android 8.0 起前台服务模型统一低于此版本要额外兼容 targetSdk 34 versionCode 1 versionName 1.0 } compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } } dependencies { implementation org.apache.ftpserver:ftpserver-core:1.2.0 // FTP 协议核心 implementation org.apache.mina:mina-core:2.1.7 // 底层非阻塞 IO implementation org.slf4j:slf4j-android:1.7.36 // 日志桥接避免 NoClassDefFoundError implementation androidx.appcompat:appcompat:1.6.1 }这段配置里有三个点值得说明。minSdk我定在 26是因为 Android 8.0 开始前台服务的通知渠道成为强制项低于 26 要写一套兼容分支demo 没必要背这个包袱。compileSdk和targetSdk都上到 34是为了让系统按最新存储策略走避免开着旧模式自欺欺人。slf4j-android是最容易被漏掉的——FtpServer 内部用 slf4j 打日志不桥接这一层运行到FtpServerFactory.createServer()时直接崩。提示如果 Gradle 同步时报Duplicate class org.slf4j...说明项目里还引了别的日志库用exclude group: org.slf4j把重复的排掉不要两个都留。2.2 AndroidManifest 权限与存储适配权限这块Android 的 FTP 服务最少要四组uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_NETWORK_STATE / uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE / uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_DATA_SYNC / uses-permission android:nameandroid.permission.POST_NOTIFICATIONS / !-- Android 12 及以下需要13 起改用细粒度媒体权限 -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion32 / uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE android:maxSdkVersion28 /INTERNET和两个网络状态权限是 FTP 监听的底子FOREGROUND_SERVICE加FOREGROUND_SERVICE_DATA_SYNC是 Android 14 强制要求的组合缺了DATA_SYNC子类型启动前台服务会直接抛SecurityExceptionPOST_NOTIFICATIONS是 Android 13 起通知要用户授权不申请的话前台服务通知不显示服务虽然跑着但你会以为它死了。存储适配是这套 demo 最容易翻车的地方。WRITE_EXTERNAL_STORAGE到 Android 10 起就基本失效maxSdkVersion28就是给它划一条死线。真正能让 FTP 服务稳定读写的根目录我一般选应用私有外部目录// 优先用应用私有外部目录无需存储权限卸载即清理 File ftpRoot new File(getExternalFilesDir(null), ftp); if (!ftpRoot.exists()) { ftpRoot.mkdirs(); }getExternalFilesDir(null)返回的是/sdcard/Android/data/包名/files/这个路径在 Android 1014 上都不需要动态申请存储权限FTP 客户端读写都在沙箱里最省心。缺点是 PC 端文件管理器直接看不到得靠 FTP 客户端进出。如果 demo 的诉求是“把整个相册或下载目录暴露出去”那就得走 SAF 或 MANAGE_EXTERNAL_STORAGE复杂度是另一个量级建议先跑通私有目录版本再考虑。2.3 编译目标与 SDK 版本选择为什么强调compileSdk 34因为 Apache FtpServer 1.2.0 里的javax.net.ssl相关类和 Android 34 的 desugar 兼容最好降到 30 以下部分反射调用会被 R8 裁掉表现为运行时找不到SslConfiguration。如果项目必须留在低版本就在proguard-rules.pro里加一条保留规则-keep class org.apache.ftpserver.** { *; } -keep class org.apache.mina.** { *; } -dontwarn org.slf4j.**这三行的作用是不混淆 FtpServer 和 MINA 的类因为这两个库大量用反射加载配置和命令处理器dontwarn是让编译期放过 slf4j 的静态绑定警告。很多“debug 包能跑、release 包崩”的案例就是漏了这段。3. 核心配置落地Apache FtpServer 的监听、用户与根目录参数能编译、能拿权限之后才轮到 FTP 服务本身。这一章的三个小节分别对应监听端口、用户模型和根目录每一块都有 demo 级的最简写法也有踩过一次就不想再踩的坑。3.1 FtpServerFactory 与监听端口配置FtpServer 的启动入口是FtpServerFactory先配监听器再配用户最后createServer()。// 1. 创建监听器工厂指定端口和绑定地址 ListenerFactory listenerFactory new ListenerFactory(); listenerFactory.setPort(2221); // 避开 21 特权端口普通应用绑不上 listenerFactory.setServerAddress(0.0.0.0); // 绑定所有网卡热点和 Wi-Fi 都能进 // 2. 组装服务器 FtpServerFactory serverFactory new FtpServerFactory(); serverFactory.addListener(default, listenerFactory.createListener()); // 3. 用户配置见 3.2这里先占位 // serverFactory.setUserManager(...); FtpServer server serverFactory.createServer(); server.start(); // 非阻塞启动后立刻返回端口必须避开 21。Android 不给普通应用绑定 1024 以下的端口写 21 会在start()时抛java.net.BindException: Permission denied日志里不一定显眼新手容易以为是防火墙问题。2221、2121、8021 是常见替代。setServerAddress(0.0.0.0)这行也别省默认绑定可能只落在127.0.0.1本机测试能通、PC 连不上就是它。server.start()是非阻塞的执行完服务已经在跑了但主线程继续往下走。所以真正要收尾的是资源释放在onDestroy()或停止按钮里调server.stop()否则端口一直被占下次启动报“Address already in use”。3.2 用户管理与权限模型FTP 服务必须有用户哪怕 demo 只给一个匿名账号。用PropertiesUserManager最省事不用接数据库// 1. 用户管理器用内存属性文件存用户 PropertiesUserManagerFactory userManagerFactory new PropertiesUserManagerFactory(); userManagerFactory.setFile(new File(getFilesDir(), users.properties)); // 2. 定义一个 demo 用户 BaseUser user new BaseUser(); user.setName(demo); user.setPassword(demo123); user.setHomeDirectory(ftpRoot.getAbsolutePath()); // 3. 授权读、写、列表、删除、重命名、建目录 ListAuthority authorities new ArrayList(); authorities.add(new WritePermission()); user.setAuthorities(authorities); UserManager userManager userManagerFactory.createUserManager(); userManager.save(user); serverFactory.setUserManager(userManager);这段代码的关键在authorities。WritePermission这个类名有点误导——它实际包含读、写、列表、删除、重命名、建目录一整套权限只加它一个就够了只加ReadPermission的话客户端能登录、能看目录但上传直接 550 拒绝现象是“连上了但传不上去”排查起来很绕。密码在 demo 里写死没关系但要清楚这是明文存进users.properties的。正式一点的做法是实现PasswordEncryptor接口做 MD5 或 PBKDF2 加密userManagerFactory.setPasswordEncryptor(...)挂上去。demo 阶段先跑通别在密码加密上耗太久否则会卡在“加密对了但登录还是失败”的循环里——那多半是加密器在保存和校验时用了不同的盐。匿名访问也可以开serverFactory.setUserManager里塞一个AnonymousUser但匿名用户只能挂只读权限否则同局域网任何人都能删你文件。3.3 文件系统根目录的三种放法根目录的选择决定了这套 demo 能干什么、不能干什么我把三种常见放法列出来对比根目录位置获取方式需要权限适用场景应用私有外部目录getExternalFilesDir(null)无demo 默认卸载即清公共下载/文档目录Environment.getExternalStoragePublicDirectory()Android 10 需 SAF 或 MANAGE需要 PC 端直读文件应用内部目录getFilesDir()无只给本应用自己用PC 不可见demo 里我固定用第一种。它有一个隐性好处路径跟包名绑定不会和别的应用冲突也有一个隐性坏处PC 端在资源管理器里直接翻不到必须通过 FTP 客户端进出。如果 demo 想演示“手机当 U 盘”的效果就得走第二种配合MANAGE_EXTERNAL_STORAGE权限和用户手动授权页流程会长一倍建议作为第二阶段再说。还有一个小坑根目录路径传给setHomeDirectory()时必须是绝对路径传相对路径或null用户登录后PWD返回的是/但LIST直接空。现象是“登录成功、目录空”原因就是 home 目录没解析对。4. 让服务活过锁屏Android 814 的前台服务与保活配置服务起得来不代表活得久。Android 8 之后后台服务会被系统随时回收屏幕一锁、切到后台FTP 连接就断。这一章解决“服务怎么活过锁屏和切后台”。4.1 前台服务与通知渠道必须把 FTP 服务跑成前台服务并挂一条常驻通知。Android 8 起要先建通知渠道Android 13 起还要动态申请通知权限。// 1. 建通知渠道Android 8 必须 if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { NotificationChannel channel new NotificationChannel( ftp_service, // 渠道 ID和 startForeground 里一致 FTP 服务, NotificationManager.IMPORTANCE_LOW); // LOW 不会响铃只常驻 channel.setDescription(文件传输服务运行中); getSystemService(NotificationManager.class).createNotificationChannel(channel); } // 2. 组装前台通知 Notification notification new NotificationCompat.Builder(this, ftp_service) .setContentTitle(FTP 服务运行中) .setContentText(端口 2221等待连接) .setSmallIcon(R.drawable.ic_ftp) .setOngoing(true) // 用户滑不掉避免误杀 .build(); // 3. 启动前台服务Android 8 if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { startForegroundService(new Intent(this, FtpService.class)); } else { startService(new Intent(this, FtpService.class)); }IMPORTANCE_LOW这个等级要选对。选IMPORTANCE_DEFAULT每次服务启动会响一声用户体验很差选IMPORTANCE_MIN通知会被折叠Android 14 上容易触发“前台服务通知不可见”的合规警告。setOngoing(true)让通知不能被滑动清除减少用户误杀服务的概率。Android 13 起还要在 Activity 里动态申请POST_NOTIFICATIONS不申请通知不显示但服务本身还会跑——这种“跑着但看不见”是排查时最容易误判的情况先查通知权限再查服务逻辑。4.2 服务生命周期与开机自启FtpService继承Service在onStartCommand()里调用startForeground()和server.start()在onDestroy()里server.stop()。生命周期函数顺序必须是先startForeground再启动 FTP反过来在 Android 12 上会抛ForegroundServiceDidNotStartInTimeException。Override public int onStartCommand(Intent intent, int flags, int startId) { startForeground(1, buildNotification()); // 必须先于 FTP 启动 try { ftpServer buildFtpServer(); ftpServer.start(); } catch (FtpException e) { stopSelf(); // 启动失败就自杀避免僵尸服务 return START_NOT_STICKY; } return START_STICKY; // 被系统杀掉后尝试重启 } Override public void onDestroy() { if (ftpServer ! null) { ftpServer.stop(); ftpServer null; } super.onDestroy(); }START_STICKY让系统在资源紧张杀掉服务后尽量重启但注意它不保证重启时 Intent 还存在所以所有配置都要能从持久化里读回来别指望 Intent 传参。START_NOT_STICKY用在启动失败分支避免反复重启失败。开机自启要谨慎。RECEIVE_BOOT_COMPLETED权限加上BOOT_COMPLETED广播可以做到但 Android 10 起后台启动限制收紧开机自启不保证成功且容易被用户视为流氓行为。demo 里我建议不加需要时手动点一下图标简单直接。4.3 电源与网络状态适配手机锁屏后 Wi-Fi 会进入省电模式FTP 长连接会掉。要保证传输稳定得申请WifiLock和WakeLockWifiManager wifiManager (WifiManager) getApplicationContext() .getSystemService(Context.WIFI_SERVICE); WifiManager.WifiLock wifiLock wifiManager.createWifiLock( WifiManager.WIFI_MODE_FULL_HIGH_PERF, ftp:wifi); wifiLock.acquire(); PowerManager powerManager (PowerManager) getSystemService(Context.POWER_SERVICE); PowerManager.WakeLock wakeLock powerManager.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, ftp:cpu); wakeLock.acquire();两个锁的释放必须和获取严格配对在onDestroy()里release()。WIFI_MODE_FULL_HIGH_PERF在 Android 10 上只建议在传输期间持有长期持有会耗电且被系统警告PARTIAL_WAKE_LOCK只保 CPU 不亮屏影响相对小。如果 demo 只是偶尔传个文件可以不申请这两个锁传大文件前让用户保持屏幕常亮即可。注意Android 12 起WifiManager的部分 API 被标记 deprecated编译期只是警告运行期仍可用但如果目标设备是 Android 13WIFI_MODE_FULL_HIGH_PERF在部分厂商 ROM 上会失效别把它当成 100% 保险。5. 排查Android 上 FTP 服务器连不上的五类常见问题这一章是被问得最多的部分每条按“现象 → 原因 → 解决”走一遍方便对号入座。第一条PC 端一直卡在“正在连接”日志里看不到任何连接记录。现象很直接——PC 端 FileZilla 卡住手机端 App 日志安静。原因通常是监听地址没绑对外网卡或者防火墙/路由器把端口挡了。先确认setServerAddress(0.0.0.0)写了再在手机浏览器里访问http://127.0.0.1:2221看是否被拒FTP 端口不能被 HTTP 访问但至少能证明有进程在监听最后确认 PC 和手机在同一网段手机热点模式下 PC 连的是手机热点而非别的 Wi-Fi。第二条能连上、能登录但目录列表为空。现象是LIST返回空或超时。原因九成在 home 目录路径setHomeDirectory()传了相对路径、null、或者目录没创建。解决方法是启动前ftpRoot.mkdirs()并打印ftpRoot.getAbsolutePath()到日志里对一遍确保传进去的是/data/user/0/.../files/ftp这种完整绝对路径。第三条登录成功上传时报 550 Permission denied。现象是读没问题、写被拒。原因是BaseUser.setAuthorities()只加了ReadPermission没加WritePermission。解决方法是换成new WritePermission()这一个对象已覆盖读写全套改完记得清掉users.properties缓存PropertiesUserManager会把旧的权限持久化不清就会读到旧配置。第四条服务能起App 切到后台或锁屏后连接立刻断。现象是前台传输正常一按 Home 键 10 秒内连接被重置。原因是服务没跑成前台服务被系统按后台服务回收。检查onStartCommand()里是否先调了startForeground()再ftpServer.start()再检查 Android 14 下FOREGROUND_SERVICE_DATA_SYNC权限是否声明最后看通知是不是被用户清了——清了通知在某些 ROM 上会连带杀服务。第五条debug 包正常release 包一启动就崩ClassNotFoundException。现象是打包给测试就崩日志里org.apache.ftpserver...找不到。原因是 R8 把反射用到的类裁掉了。解决是proguard-rules.pro里加-keep class org.apache.ftpserver.** { *; }和-dontwarn org.slf4j.**重新打 release 包验证。这条在 3.3 节末尾也提过但实际排查时它是独立的一类问题单独列出来。6. 装完怎么验证从 PC 端跑一遍回环传输最后一章落到验证。服务能起、日志不报错不等于 FTP 能用。我习惯按“本机 → 同网段 → 传输”三段验证每一段都能单独定位问题。先做本机回环排除服务端本身的问题# 在 Android 设备上用 adb shell 进到设备内本地连一下 adb shell curl -v ftp://127.0.0.1:2221/ --user demo:demo123curl带-v能看到完整握手过程220欢迎语、331用户名正确、230登录成功、150开始传输。四行都出现说明服务端配置没问题问题在网络层。如果卡在331之后就是密码或用户管理器的问题卡在220之前就是监听没起来。再做同网段验证。手机和 PC 连同一个 Wi-FiPC 端用 FileZilla 或 Windows 资源管理器ftp://手机IP:2221连接。这一步重点看两件事PC 拿到的手机 IP 是不是热点/局域网网段192.168.x.x或172.20.10.x以及能不能LIST出根目录内容。手机 IP 可以在系统设置里看或者 App 启动时把NetworkInterface枚举结果打到通知副标题上省得每次翻设置。最后做传输验证。从 PC 上传一个 10MB 左右的文件到手机根目录再从手机下载回来比较 MD5# PC 端算上传前的 MD5 md5sum test-10m.bin # FTP 会话内下载回本地后再算一次 ftp get /ftp/test-10m.bin md5sum test-10m.bin两次一致说明读写链路、权限、磁盘空间都通过。如果不一致先看是否传了 0 字节权限问题再看是否传了一半Wi-Fi 省电导致连接中断回 4.3 节补锁。验证通过之后还有一个小习惯把端口、根目录、用户名三个参数写进 App 首页的配置卡片里每次启动时回显一遍。这套 demo 我前前后后给三台旧手机装过每次换设备最耗时的不是安装而是忘了上一台用的什么端口、什么根目录。从那以后我每次搭这套环境都强制走一遍“回显参数 → 本机 curl → PC LIST → MD5 比对”四步少一步后面都要补。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑