Android实现WebServer服务端和客户端(附带源码)

一、项目介绍

在移动互联网时代,Android 端不仅需要作为客户端发起请求,还能胜任轻量级的服务端角色,提供简单的数据或文件分发服务。本项目目标是在 Android 设备(或模拟器)上同时运行一个 HTTP WebServer(服务端)和一个 HTTP Client(客户端),实现设备本地局域网内的文件浏览、上传下载及简单 API 接口调用。

  • 应用场景

    • 局域网内播放本地媒体文件

    • 简易分布式调试:多台设备互相请求数据

    • 边缘设备数据采集与展示

  • 核心功能

    1. 启动 WebServer,监听指定端口(如 8080)。

    2. 服务端响应 /files 列表当前设备指定目录下文件。

    3. 客户端发起 GET/POST 请求,支持文件下载与上传。

    4. WebServer 支持简单的 REST 风格接口。


二、相关知识

在动手编码之前,需要读者对以下知识有一定掌握:

  1. HTTP 协议基础

    • 请求行、请求头、请求体

    • 响应行、响应头、响应体

    • 常见状态码(200、404、500 等)

  2. Android 网络编程

    • HttpURLConnectionOkHttp

    • 异步请求:AsyncTask(兼容老版本)、Thread + HandlerExecutorServiceRxJava

  3. 多线程与并发

    • Java ThreadRunnable

    • 线程池管理

    • 同步与线程安全

  4. 文件读写权限

    • Android 6.0+ 动态权限申请 (READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE)

    • Storage Access Framework(可选)

  5. 常用第三方库

    • NanoHTTPD:轻量级 Java HTTP 服务器

    • OkHttp:高效 HTTP 客户端


三、实现思路

  1. 整体架构

    • MainActivity:启动与停止服务器,展示客户端请求结果界面。

    • WebServer:继承自 NanoHTTPD,重写 serve() 方法处理请求。

    • HttpClientHelper:封装客户端 HTTP 请求逻辑,支持同步/异步接口调用及文件上传下载。

  2. 流程图

[MainActivity] 
     │
  用户点击“启动服务器”
     │
     ▼
[WebServer.start()] ──┬── 监听 8080 端口
                     │
                     └─ 进入请求处理循环
                          serve(session) ── 分发到对应 URI → 返回 Response
  1. 模块职责划分

    模块职责
    MainActivityUI 控制,启动/停止服务,发起客户端请求
    WebServer接收并响应 HTTP 请求
    HttpClientHelperHTTP 请求工具(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 系列方法:检查和申请存储权限,确保读写外部存储。


六、项目总结

  1. 成果回顾

    • 成功在 Android 端同时运行轻量级 HTTP 服务端和客户端。

    • 支持基本 REST 接口与文件服务。

  2. 技术收获

    • 深刻理解 HTTP 协议请求/响应流程。

    • 掌握了 NanoHTTPD 在 Android 中的集成与使用。

    • 强化了 Android 异步网络请求与多线程模型。

  3. 后续优化

    • 完善文件上传逻辑,支持大文件断点续传。

    • 增加界面友好度,如文件列表分页、下载进度显示。

    • 引入 OkHttp 库替换原生 HttpURLConnection,提升性能与稳定性。

    • 将业务逻辑拆分为 MVVM 模式,提高可维护性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值