一、项目介绍
在移动互联网时代,Android 端不仅需要作为客户端发起请求,还能胜任轻量级的服务端角色,提供简单的数据或文件分发服务。本项目目标是在 Android 设备(或模拟器)上同时运行一个 HTTP WebServer(服务端)和一个 HTTP Client(客户端),实现设备本地局域网内的文件浏览、上传下载及简单 API 接口调用。
-
应用场景
-
局域网内播放本地媒体文件
-
简易分布式调试:多台设备互相请求数据
-
边缘设备数据采集与展示
-
-
核心功能
-
启动 WebServer,监听指定端口(如 8080)。
-
服务端响应
/files
列表当前设备指定目录下文件。 -
客户端发起 GET/POST 请求,支持文件下载与上传。
-
WebServer 支持简单的 REST 风格接口。
-
二、相关知识
在动手编码之前,需要读者对以下知识有一定掌握:
-
HTTP 协议基础
-
请求行、请求头、请求体
-
响应行、响应头、响应体
-
常见状态码(200、404、500 等)
-
-
Android 网络编程
-
HttpURLConnection
与OkHttp
库 -
异步请求:
AsyncTask
(兼容老版本)、Thread
+Handler
、ExecutorService
、RxJava
-
-
多线程与并发
-
Java
Thread
、Runnable
-
线程池管理
-
同步与线程安全
-
-
文件读写权限
-
Android 6.0+ 动态权限申请 (
READ_EXTERNAL_STORAGE
、WRITE_EXTERNAL_STORAGE
) -
Storage Access Framework
(可选)
-
-
常用第三方库
-
NanoHTTPD:轻量级 Java HTTP 服务器
-
OkHttp:高效 HTTP 客户端
-
三、实现思路
-
整体架构
-
MainActivity:启动与停止服务器,展示客户端请求结果界面。
-
WebServer:继承自
NanoHTTPD
,重写serve()
方法处理请求。 -
HttpClientHelper:封装客户端 HTTP 请求逻辑,支持同步/异步接口调用及文件上传下载。
-
-
流程图
[MainActivity]
│
用户点击“启动服务器”
│
▼
[WebServer.start()] ──┬── 监听 8080 端口
│
└─ 进入请求处理循环
serve(session) ── 分发到对应 URI → 返回 Response
-
模块职责划分
模块 职责 MainActivity UI 控制,启动/停止服务,发起客户端请求 WebServer 接收并响应 HTTP 请求 HttpClientHelper HTTP 请求工具(GET/POST/下载/上传) PermissionManager 动态权限申请管理
四、实现代码
下面将所有源文件与布局文件整合到同一个代码块中,用注释标注文件边界,并附带详细注释。
// ---------------------- 文件: MainActivity.java ----------------------
package com.example.webserverapp;
import android.Manifest;
import android.os.Bundle;
import android.os.Environment;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import java.io.IOException;
// MainActivity:负责启动/停止服务器,发起客户端请求,并展示结果
public class MainActivity extends AppCompatActivity {
private Button btnStartServer, btnStopServer, btnGetFiles;
private TextView tvResult;
private WebServer webServer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main); // 加载布局文件 activity_main.xml
// 初始化 UI 元素
btnStartServer = findViewById(R.id.btn_start_server);
btnStopServer = findViewById(R.id.btn_stop_server);
btnGetFiles = findViewById(R.id.btn_get_files);
tvResult = findViewById(R.id.tv_result);
// 点击“启动服务器”
btnStartServer.setOnClickListener(v -> {
// 确保已申请存储权限
if (PermissionManager.hasStoragePermission(this)) {
startWebServer();
} else {
PermissionManager.requestStoragePermission(this);
}
});
// 点击“停止服务器”
btnStopServer.setOnClickListener(v -> stopWebServer());
// 点击“获取文件列表”
btnGetFiles.setOnClickListener(v -> {
// 异步请求 /files 接口
HttpClientHelper.get("http://127.0.0.1:8080/files", new HttpClientHelper.Callback() {
@Override
public void onSuccess(String response) {
runOnUiThread(() -> tvResult.setText(response));
}
@Override
public void onFailure(Exception e) {
runOnUiThread(() -> tvResult.setText("请求失败: " + e.getMessage()));
}
});
});
}
/** 启动 WebServer */
private void startWebServer() {
try {
// 将根目录设为设备外部存储 Download 文件夹
String rootPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).getPath();
webServer = new WebServer(8080, rootPath);
webServer.start();
tvResult.setText("服务器已启动,监听端口 8080");
} catch (IOException e) {
tvResult.setText("服务器启动失败: " + e.getMessage());
}
}
/** 停止 WebServer */
private void stopWebServer() {
if (webServer != null) {
webServer.stop();
tvResult.setText("服务器已停止");
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
// 处理权限申请回调
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
PermissionManager.handlePermissionResult(this, requestCode, permissions, grantResults);
}
}
// ---------------------- 文件: WebServer.java ----------------------
package com.example.webserverapp;
import fi.iki.elonen.NanoHTTPD;
import java.io.File;
import java.io.FileInputStream;
import java.util.Map;
// WebServer:继承自 NanoHTTPD,重写 serve() 实现自定义路由与文件服务
public class WebServer extends NanoHTTPD {
private String rootDir;
public WebServer(int port, String rootDir) {
super(port);
this.rootDir = rootDir;
}
@Override
public Response serve(IHTTPSession session) {
String uri = session.getUri(); // 获取请求路径
Method method = session.getMethod(); // 请求方法:GET/POST
Map<String, String> params = session.getParms(); // 请求参数
// 路由分发
if (uri.equals("/files") && Method.GET.equals(method)) {
return listFiles();
} else if (uri.equals("/upload") && Method.POST.equals(method)) {
return handleUpload(session);
} else {
return newFixedLengthResponse(Response.Status.NOT_FOUND, "text/plain", "404 Not Found");
}
}
/** 列举 rootDir 下所有文件名,并以 JSON 格式返回 */
private Response listFiles() {
File dir = new File(rootDir);
StringBuilder sb = new StringBuilder("[");
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
sb.append("\"").append(f.getName()).append("\",");
}
if (sb.charAt(sb.length() - 1) == ',') sb.deleteCharAt(sb.length() - 1);
}
sb.append("]");
return newFixedLengthResponse(Response.Status.OK, "application/json", sb.toString());
}
/** 处理文件上传(POST /upload) */
private Response handleUpload(IHTTPSession session) {
// TODO: 解析 multipart/form-data,保存文件到 rootDir
return newFixedLengthResponse(Response.Status.OK, "text/plain", "上传成功");
}
}
// ---------------------- 文件: HttpClientHelper.java ----------------------
package com.example.webserverapp;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
// HttpClientHelper:封装 GET/POST 及文件下载上传
public class HttpClientHelper {
public interface Callback {
void onSuccess(String response);
void onFailure(Exception e);
}
/** 发起 GET 请求 */
public static void get(String urlStr, Callback cb) {
new Thread(() -> {
try {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
InputStream in = new BufferedInputStream(conn.getInputStream());
StringBuilder sb = new StringBuilder();
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) != -1) sb.append(new String(buf, 0, len));
in.close();
cb.onSuccess(sb.toString());
} catch (Exception e) {
cb.onFailure(e);
}
}).start();
}
/** 简单文件下载示例 */
public static void downloadFile(String urlStr, String destPath, Callback cb) {
new Thread(() -> {
try {
URL url = new URL(urlStr);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
try (InputStream in = conn.getInputStream();
FileOutputStream fos = new FileOutputStream(destPath)) {
byte[] buf = new byte[8192];
int len;
while ((len = in.read(buf)) != -1) fos.write(buf, 0, len);
}
cb.onSuccess("下载完成: " + destPath);
} catch (Exception e) {
cb.onFailure(e);
}
}).start();
}
// TODO: 实现文件上传 uploadFile()
}
// ---------------------- 文件: PermissionManager.java ----------------------
package com.example.webserverapp;
import android.app.Activity;
import android.content.pm.PackageManager;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
// PermissionManager:动态权限申请工具类
public class PermissionManager {
private static final int REQ_STORAGE = 1001;
public static boolean hasStoragePermission(Activity act) {
return ContextCompat.checkSelfPermission(act, Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
public static void requestStoragePermission(Activity act) {
ActivityCompat.requestPermissions(act,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
REQ_STORAGE);
}
public static void handlePermissionResult(Activity act, int requestCode,
String[] permissions, int[] grantResults) {
if (requestCode == REQ_STORAGE) {
// 简化:未授权则提示并退出
if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) {
// 提示用户开启权限
}
}
}
}
// ---------------------- 文件: res/layout/activity_main.xml ----------------------
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:padding="16dp"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/btn_start_server"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="启动服务器" />
<Button
android:id="@+id/btn_stop_server"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="停止服务器" />
<Button
android:id="@+id/btn_get_files"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="获取文件列表" />
<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="16dp" />
</LinearLayout>
五、代码解读
-
MainActivity.startWebServer():创建并启动
WebServer
实例,根目录设为Download
文件夹。 -
MainActivity.stopWebServer():调用
webServer.stop()
关闭服务器监听。 -
WebServer.serve(IHTTPSession):核心路由分发,根据 URI 与 Method 调度到对应的处理方法。
-
WebServer.listFiles():扫描指定目录文件,返回 JSON 数组。
-
WebServer.handleUpload(...):(待完善)处理 multipart/form-data 上传。
-
HttpClientHelper.get(...):异步 GET 请求并回调结果。
-
HttpClientHelper.downloadFile(...):使用
HttpURLConnection
下载文件到指定路径。 -
PermissionManager 系列方法:检查和申请存储权限,确保读写外部存储。
六、项目总结
-
成果回顾
-
成功在 Android 端同时运行轻量级 HTTP 服务端和客户端。
-
支持基本 REST 接口与文件服务。
-
-
技术收获
-
深刻理解 HTTP 协议请求/响应流程。
-
掌握了 NanoHTTPD 在 Android 中的集成与使用。
-
强化了 Android 异步网络请求与多线程模型。
-
-
后续优化
-
完善文件上传逻辑,支持大文件断点续传。
-
增加界面友好度,如文件列表分页、下载进度显示。
-
引入
OkHttp
库替换原生HttpURLConnection
,提升性能与稳定性。 -
将业务逻辑拆分为 MVVM 模式,提高可维护性。
-