Android局域网文件传输实战:基于NanoHTTPD实现任意文件共享
简介面向Android开发学习者的局域网文件传输课程设计资源以Wifi热点配对替代传统WiFi Direct方案实现手机间任意文件的高效传输相比蓝牙或传统直连方式配对更简单、覆盖更广。资源包含完整设计报告Word文档与可运行项目源码适合计算机相关专业学生作为课程设计、毕业设计或自学者快速上手的参考。压缩包共49个文件核心为14个Java源文件与11个XML布局配置文件Java文件承担传输逻辑与界面控制XML完成布局与配置声明另附Gradle构建脚本、Properties参数、PNG资源图片及设计报告等整体仅421KB结构紧凑便于阅读。目前已有1540人浏览学习方案在设备配对便捷性与传输逻辑上均有细致实现读者可从中掌握Socket通信、热点连接管理、文件流处理等关键知识点并可直接运行项目观察实际效果。1. 局域网文件传输的痛点与这道题的真正门槛手机之间传文件大多数人第一反应是微信或者网盘。可一旦脱离公网环境——出差在酒店、团建在户外、或者在保密要求比较高的内网微信传不了大文件网盘连不上服务器数据线和U盘又未必有合适的转接头。这时候两台Android手机如果能直接通过Wi-Fi组成一个局域网用浏览器或App就能互相拉取文件传输速度往往能跑到几十MB/s比任何云端中转都快。这个标题真正的难点不在“网络发现”也不在“socket握手”而在于Android从11开始强制了分区存储机制/sdcard/Android/data等目录不允许直接读写如果你打算遍历整台设备“任意文件”第一道坎就是绕过这个限制。这篇文章会基于当前稳定的实操路径讲透协议怎么选、权限怎么开、代码怎么写以及真机联调时最容易翻车的几个坑。2. 局域网传输的协议选型与Android存储权限边界2.1 为什么HTTP服务器模式是Android局域网传输的最优解接到“实现Android手机之间在局域网下传输任意文件”这个需求从业者通常会先想一遍现有方案FTP服务器、SMB共享、Wi-Fi Direct点对点、还有自绘socket协议。我在实际项目里测试过几轮结论是以轻量HTTP服务器为核心最稳原因有三条。第一HTTP的客户端几乎不用开发。另一台手机只要浏览器里敲个地址就能直接浏览文件列表并下载不需要预先安装任何额外App。这对临时传输场景极其重要你不可能要求对方为了收个文件再去装一个客户端。第二HTTP的Range请求天然支持断点续传传输大文件时网络波动不必从头来过。第三Android系统对开放端口并没有像iOS那样严格限制随便绑定一个四位数空闲端口就能提供服务。相比Wi-Fi Direct需要处理复杂的组网和socket连接管理HTTP服务器的实现路径短、排错容易而且只要把URL发出去任何操作系统都能收。2.2 Android 11以上“任意文件”到底卡在哪“任意文件”这四个字是所有开发者的噩梦。从Android 11API 30开始分区存储成为强制行为应用不能直接读写其他应用专属目录下的文件也不允许通过File路径访问/storage/emulated/0/Android/data/下的内容。如果服务端确实只需要读取媒体文件图片、视频、音频用MediaStore就能解决但“任意文件”意味着要去拿压缩包、PDF、文档、APK安装包这类普通文件这需要申请MANAGE_EXTERNAL_STORAGE这个特殊权限。这个权限不能像普通运行时权限那样弹窗授予必须引导用户去系统设置里的“所有文件访问”开关页手动打开。代码里判断是否已授权的方法是调用Environment.isExternalStorageManager()如果返回false就需要通过Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION这个Intent跳转到授权页。而且需要提醒你国内部分定制ROM把这个入口藏得比较深用户即使跳过去也可能找不到开关这一点必须在UI上给予明确指引。2.3 真机调试时如何避免ADB授权死循环如果你打算用adb命令快速授权不要踩这个坑appops set --uid 包名 MANAGE_EXTERNAL_STORAGE allow这条命令在某些系统版本上执行后Environment.isExternalStorageManager()仍然返回false。我一般会直接引导用户手动设置或者用UI Automator脚本帮忙自动点击。开发阶段其实不需要打正式包直接把android:requestLegacyExternalStoragetrue写进AndroidManifest.xml的application标签配合targetSdkVersion降到29就可以暂时绕过分区存储限制为你集中精力调协议争取时间。注意这只是开发阶段的缓兵之计Play Store上架时审核会被拒但内网自用没问题。3. 基于NanoHTTPD实现最小可用的局域网文件收发服务3.1 引入依赖与清点必须声明的三项权限我们选用的HTTP服务器内核是NanoHTTPD这是一个单文件即可运行的开源HTTP服务器库不依赖Android系统组件体积只有几十KB在没有网络的情况下通过gradle离线缓存也能编译。在build.gradle里加入依赖implementation org.nanohttpd:nanohttpd:2.3.1清单文件里需要声明以下三项权限uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.ACCESS_WIFI_STATE / uses-permission android:nameandroid.permission.MANAGE_EXTERNAL_STORAGE /INTERNET权限是开启ServerSocket的必需项具体来说是因为ServerSocket绑定端口时需要通过socket系统调用而Android对应用联网有一套独立的permission校验。ACCESS_WIFI_STATE用来后续读取Wi-Fi信息时拿到本机IP虽然严格来说通过NetworkInterface遍历也能拿但加了它更稳妥。MANAGE_EXTERNAL_STORAGE是访问任意文件的前提。永远不要在onCreate里同步启动服务器否则如果绑定端口失败会直接抛出BindException导致应用闪退。3.2 服务端核心代码遍历根目录与下载响应下面这段代码是服务器的主体我做了注释可以直接嵌入你的Activity或独立Service。它监听一个动态分配的端口每当有HTTP请求进来就解析URL路径映射到本地文件系统。public class FileServer extends NanoHTTPD { private final File rootDir; public FileServer(int port, File rootDir) throws IOException { super(port); this.rootDir rootDir; start(NanoHTTPD.SOCKET_READ_TIMEOUT, false); } Override public Response serve(IHTTPSession session) { String uri session.getUri(); // 解码URL中的中文文件名 try { uri java.net.URLDecoder.decode(uri, UTF-8); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } File targetFile new File(rootDir, uri); // 防止路径穿越 String canonicalPath; String rootCanonicalPath; try { canonicalPath targetFile.getCanonicalPath(); rootCanonicalPath rootDir.getCanonicalPath(); } catch (IOException e) { return newFixedLengthResponse(Response.Status.FORBIDDEN, text/plain, Invalid path); } if (!canonicalPath.startsWith(rootCanonicalPath)) { return newFixedLengthResponse(Response.Status.FORBIDDEN, text/plain, Forbidden); } if (targetFile.isDirectory()) { // 生成HTML文件列表 StringBuilder sb new StringBuilder(); sb.append(!DOCTYPE htmlhtmlheadmeta charsetUTF-8title文件列表/title/headbody); sb.append(h2局域网共享 - 当前目录: ).append(uri).append(/h2); File[] children targetFile.listFiles(); if (children ! null) { for (File child : children) { String name child.getName(); String childUri uri.endsWith(/) ? uri name : uri / name; if (child.isDirectory()) { sb.append(pa href).append(childUri).append([DIR] ).append(name).append(/a/p); } else { long size child.length(); sb.append(pa href).append(childUri).append().append(name) .append(/a ().append(formatSize(size)).append()/p); } } } sb.append(/body/html); return newFixedLengthResponse(sb.toString()); } else if (targetFile.isFile()) { // 设置支持断点续传的响应头 Response response newChunkedResponse(Response.Status.OK, application/octet-stream, new FileInputStream(targetFile)); response.addHeader(Content-Disposition, attachment; filename\ targetFile.getName() \); response.addHeader(Accept-Ranges, bytes); return response; } else { return newFixedLengthResponse(Response.Status.NOT_FOUND, text/plain, File not found); } } private String formatSize(long size) { if (size 1024) return size B; if (size 1024 * 1024) return String.format(%.1f KB, size / 1024.0); return String.format(%.1f MB, size / 1048576.0); } }这段代码的逻辑分三层。第一层解析并解码URI因为Android文件系统上中文文件名非常常见浏览器会自动做URL编码不解码的话永远找不到文件。第二层做路径穿越检查必须用getCanonicalPath()而不是getAbsolutePath()否则用户通过/../../system这种路径就能跳出共享根目录直接读到系统文件。第三层根据请求目标是目录还是文件分别响应目录返回HTML列表页文件返回application/octet-stream二进制流并附带Content-Disposition头让浏览器自动触发下载。newChunkedResponse是NanoHTTPD处理大文件的关键它不会把整个文件加载进内存而是以流的形式边读边写内存占用恒定。3.3 客户端如何从浏览器下载与断点续传原理服务端跑起来之后同一局域网内的另一台手机打开浏览器输入http://192.168.x.x:8080/就能看到根目录下的文件列表。点击任意文件浏览器就开始下载。之所以说断点续传开箱即用是因为HTTP协议标准里定义了Range头当客户端下载中断再次请求时会带上Range: bytes10240-这样的字段NanoHTTPD在返回响应时会自动处理这个头部从指定字节位置继续读文件。不过需要说明的是NanoHTTPD内置的断点续传支持比较基础实测大文件的续传偶有不稳定现象如果你想更严谨可以捕获IHTTPSession里的headers对象自行解析range字段后用FileInputStream.skip()跳过去再返回206 Partial Content状态码。4. 多文件批量打包、并发传输与稳定性调优4.1 用ZipOutputStream实时打包多选文件“任意文件”在真实使用场景中很少是单发多数时候用户会一次性勾选几十个文件打包传输。服务端可以提供一个特殊的APIURL里带上?zip1patha.jpg,b.jpg,c.jpg服务端读取这些路径后实时打包。直接在内存里构造ZipOutputStream会有内存瓶颈正确做法是用org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream并将响应内容类型设为application/zip这样浏览器会把它当压缩包下载。需要注意ZipArchiveOutputStream写入的条目名不能包含绝对路径否则解压时会把文件释放到任意目录存在Zip Slip安全隐患。我一般会把文件名最前面的/去掉并且过滤掉所有..片段。4.2 同样内网传输为什么别人能跑满链路而你的卡在10MB/s这个问题往往不是CPU瓶颈而是TCP拥塞控制和读写缓冲区的锅。Android手机默认的TCP接收窗口比较保守加上Wi-Fi的丢包率比有线网高吞吐量会被压制在较低水平。优化手段是在服务端代码里显式设置socket参数NanoHTTPD虽然没直接暴露但可以反射拿到内部的ServerSocket或者在自定义的ServerSocketFactory里做手脚。更直接的方案是改用OkHttp的MockWebServer来做底层HTTP协议解析或者将文件读入缓冲区改成64KB的大块读取。我实测过一组数据同一台手机通过5GHz Wi-Fi热点传输2GB视频文件默认缓冲区的吞吐约12MB/s把读取缓冲提升到256KB后能到30MB/s左右。缓冲区太大的副作用是内存不连续分配容易触发GC抖动所以建议设置为32KB到64KB之间比较平衡。另外如果热点功率不足导致信号弱这个数值怎么调都上不去先检查iw dev wlan0 link的比特率衰减指标。4.3 多设备同时拉取文件是阻塞还是并发NanoHTTPD默认的serve方法是单线程处理这意味着当一部手机正在下载大文件时第二部手机会一直等到超时。解决方式是重写serve方法每次启动一个新线程处理请求但要注意并发控制。最稳的做法是引入java.util.concurrent.ThreadPoolExecutor核心线程数设为Runtime.getRuntime().availableProcessors()的1.5倍左右最大线程数不超过16。线程太多反而会因为锁竞争导致文件读取变慢。另外如果多个线程同时读取同一个大文件可以为每个文件建立一个RandomAccessFile实例各自维护独立的文件指针切忌共享同一个FileInputStream。4.4 传输过程中因AP省电策略导致Wi-Fi休眠默认情况下手机解锁屏幕亮着Wi-Fi一直工作没问题。但一旦锁屏一分钟系统会启动省电模式禁用网络以延长续航。服务端必须持有WifiManager.WifiLock防止Wi-Fi进入休眠。获取锁的代码可以放在Application层WifiManager wifiManager (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE); WifiManager.WifiLock wifiLock wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, transfer_lock); wifiLock.setReferenceCounted(false); wifiLock.acquire();WIFI_MODE_FULL_HIGH_PERF比旧的WIFI_MODE_FULL能拿到更高的传输吞吐量但对应的功耗也更高。在传输完成回调里记得释放这个锁否则后台持续跑热点还会导致设备发热严重。释放的时机选在最后一个客户端断开后延迟30秒避免频繁开关。5. 安全过滤、真实IP地址获取与打包发布技巧5.1 限制访问来源而非开放监听所有端口服务端默认监听0.0.0.0意味着对整个局域网开放这在一台公共Wi-Fi路由器下非常危险任何人只要扫描端口就能连上你共享的文件。我建议在启动服务前先获取当前已连接Wi-Fi的网段只允许来自该网段的IP访问。具体实现是拿到已连接的SSID从WifiInfo里读取getIpAddress()得到本机IP再通过子网掩码算出网段。在serve方法开头解析session.getRemoteIpAddress()如果不在白名单内直接返回403。还有一种更轻量的方式在URL里带一个随机token作为访问密钥但鉴于手机之间传输方便性白名单过滤比token更顺滑。5.2 手机热点模式下如何拿到服务端IP这里有个高频坑WifiManager.getConnectionInfo().getIpAddress()在部分机型上拿到的是上次连接Wi-Fi的IP而不是当前手机开启热点后的IP。在热点模式下本机IP一般是192.168.43.1小米、三星等或192.168.137.1部分国产。我一般不用系统API直接遍历所有网络接口adb shell ifconfig或者代码里用NetworkInterface.getNetworkInterfaces()遍历找到满足inet4Address且isLoopback为false的那个地址把广播地址过滤掉后展示给用户。把IP和端口组合成一个二维码让接收方用相机扫码直接打开浏览器比手输IP方便得多。二维码内容就是http://192.168.43.1:8080/Android系统相机扫到HTTP链接会提示用浏览器打开。5.3 把所有文件列表导出成HTML预览增强可读性纯文字列表在手机上体验很差特别是文件名很长时容易折行。我一般会在HTML列表页里嵌入一点JavaScript让每行文件后面多一个“复制链接”按钮并且根据文件尾缀显示一个小图标图片显示缩略图视频显示播放按钮。缩略图可以直接用img srcfile:///...吗不行浏览器环境下file协议被禁用你需要单独开一个/thumb?path接口服务端读取图片文件后缩小重采样再输出。这个接口注意做缓存否则切一次目录就要重新压缩一遍大图非常消耗CPU。5.4 发布前必须处理的顽固细节清单打包成正式的APK之前有几个细节我每次都会核对。第一AndroidManifest.xml里android:usesCleartextTraffic要设为true否则从Android 9开始默认禁止明文HTTP流量对端浏览器访问时会出现连接失败。第二服务器服务建议用前台服务并附带通知栏常驻否则进程一旦被系统回收文件传输立即中断。第三做混淆时保留NanoHTTPD的类名映射-keep class fi.iki.elonen.** { *; }这个规则的目的是防止混淆后反射获取内部类失败。最后真机调试时如果传输速度异常慢用adb shell dumpsys wifi | grep -E mWifiInfo|Supplicant查看当前连接的链路速率如果只有54Mbps说明手机连的是2.4GHz热点切到5GHz后再观察。本文还有配套的精品资源点击获取