凌峰创科服务平台

java http 文件服务器

我们将使用 Java 标准库中的 java.netjava.nio 包,无需任何第三方依赖。

java http 文件服务器-图1
(图片来源网络,侵删)

核心思路

一个简单的 HTTP 文件服务器主要做以下几件事:

  1. 监听端口:在指定的网络端口上等待客户端(通常是浏览器)的连接请求。
  2. 接收请求:当一个客户端连接时,读取它发送的 HTTP 请求,请求包含了客户端想要访问的资源路径(/index.html)。
  3. 处理请求
    • 根据请求路径,在服务器上查找对应的文件。
    • 如果文件存在,读取文件内容。
    • 根据文件类型(如 .html, .css, .jpg),构建一个 HTTP 响应头,并返回文件内容。
    • 如果文件不存在,返回一个 "404 Not Found" 错误。
  4. 关闭连接:将响应发送给客户端后,关闭与该客户端的连接。

基础单线程实现

这个版本最简单,易于理解,但一次只能处理一个客户端请求,不适用于生产环境。

SimpleFileServer.java

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
public class SimpleFileServer {
    // 定义服务器监听的端口
    private static final int PORT = 8080;
    // 定义要提供服务的文件根目录
    private static final String WEB_ROOT = "./public";
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("Server started on port " + PORT);
            System.out.println("Serving files from: " + new File(WEB_ROOT).getAbsolutePath());
            // 循环等待客户端连接
            while (true) {
                try {
                    Socket clientSocket = serverSocket.accept();
                    System.out.println("Accepted connection from " + clientSocket.getInetAddress());
                    // 处理请求 (这里是单线程,会阻塞)
                    handleRequest(clientSocket);
                } catch (IOException e) {
                    System.err.println("Error handling client connection: " + e.getMessage());
                }
            }
        } catch (IOException e) {
            System.err.println("Could not start server on port " + PORT + ": " + e.getMessage());
        }
    }
    private static void handleRequest(Socket clientSocket) throws IOException {
        // try-with-resources 确保流被正确关闭
        try (
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            OutputStream out = clientSocket.getOutputStream();
        ) {
            // 1. 读取请求行 ( GET /index.html HTTP/1.1)
            String requestLine = in.readLine();
            if (requestLine == null || requestLine.isEmpty()) {
                return; // 无效请求
            }
            System.out.println("Request: " + requestLine);
            String[] requestParts = requestLine.split(" ");
            if (requestParts.length < 2) {
                sendErrorResponse(out, 400, "Bad Request");
                return;
            }
            String method = requestParts[0];
            String path = requestParts[1];
            // 只处理 GET 方法
            if (!"GET".equalsIgnoreCase(method)) {
                sendErrorResponse(out, 501, "Not Implemented");
                return;
            }
            // 2. 处理路径,防止目录遍历攻击
            Path requestedFile = Paths.get(WEB_ROOT, path).normalize().toAbsolutePath();
            if (!requestedFile.startsWith(Paths.get(WEB_ROOT).toAbsolutePath())) {
                sendErrorResponse(out, 403, "Forbidden");
                return;
            }
            // 3. 检查文件是否存在
            if (!Files.exists(requestedFile) || !Files.isRegularFile(requestedFile)) {
                sendErrorResponse(out, 404, "Not Found");
                return;
            }
            // 4. 发送成功响应
            sendFileResponse(out, requestedFile);
        } finally {
            clientSocket.close();
        }
    }
    private static void sendFileResponse(OutputStream out, Path filePath) throws IOException {
        byte[] fileContent = Files.readAllBytes(filePath);
        String contentType = guessContentType(filePath);
        // HTTP 响应头
        String header = "HTTP/1.1 200 OK\r\n" +
                        "Content-Type: " + contentType + "\r\n" +
                        "Content-Length: " + fileContent.length + "\r\n" +
                        "Connection: close\r\n" + // 告诉客户端连接关闭
                        "\r\n"; // 空行,标志着头的结束
        out.write(header.getBytes());
        out.write(fileContent);
        out.flush();
    }
    private static void sendErrorResponse(OutputStream out, int statusCode, String statusText) throws IOException {
        String response = "HTTP/1.1 " + statusCode + " " + statusText + "\r\n" +
                          "Content-Type: text/plain\r\n" +
                          "Connection: close\r\n" +
                          "\r\n" +
                          statusCode + " " + statusText;
        out.write(response.getBytes());
        out.flush();
    }
    private static String guessContentType(Path filePath) {
        String fileName = filePath.getFileName().toString().toLowerCase();
        if (fileName.endsWith(".html") || fileName.endsWith(".htm")) {
            return "text/html";
        } else if (fileName.endsWith(".css")) {
            return "text/css";
        } else if (fileName.endsWith(".js")) {
            return "application/javascript";
        } else if (fileName.endsWith(".jpg") || fileName.endsWith(".jpeg")) {
            return "image/jpeg";
        } else if (fileName.endsWith(".png")) {
            return "image/png";
        } else if (fileName.endsWith(".gif")) {
            return "image/gif";
        } else {
            return "application/octet-stream"; // 未知类型,作为二进制流处理
        }
    }
}

如何运行

  1. 创建项目目录和文件:

    my-file-server/
    ├── public/
    │   ├── index.html
    │   └── style.css
    └── SimpleFileServer.java
  2. 编写 public/index.html:

    java http 文件服务器-图2
    (图片来源网络,侵删)
    <!DOCTYPE html>
    <html>
    <head>
        <title>My Simple Server</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <h1>Hello from a Java HTTP Server!</h1>
        <p>This page is served by a simple Java program.</p>
    </body>
    </html>
  3. 编写 public/style.css:

    body {
        font-family: sans-serif;
        background-color: #f0f0f0;
        color: #333;
        text-align: center;
        padding-top: 50px;
    }
    h1 {
        color: #0056b3;
    }
  4. 编译并运行:

    # 确保你在 my-file-server 目录下
    javac SimpleFileServer.java
    java SimpleFileServer
  5. 访问: 打开浏览器,访问 http://localhost:8080/index.htmlhttp://localhost:8080/style.css,你应该能看到页面和样式。


多线程实现(推荐)

基础版本的问题是性能很差,一个客户端正在下载大文件时,其他客户端必须等待,为了解决这个问题,我们需要使用多线程。

java http 文件服务器-图3
(图片来源网络,侵删)

核心思想是:当接受到一个新连接时,不是立即处理,而是创建一个新的 ThreadRunnable 任务来处理这个连接,然后主线程立即回去等待下一个连接。

MultiThreadFileServer.java

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class MultiThreadFileServer {
    private static final int PORT = 8080;
    private static final String WEB_ROOT = "./public";
    public static void main(String[] args) {
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("Server started on port " + PORT);
            System.out.println("Serving files from: " + new File(WEB_ROOT).getAbsolutePath());
            while (true) {
                Socket clientSocket = serverSocket.accept();
                // 为每个客户端连接创建一个新线程
                Thread thread = new Thread(new ClientHandler(clientSocket));
                thread.start();
            }
        } catch (IOException e) {
            System.err.println("Could not start server: " + e.getMessage());
        }
    }
}
// 一个专门用于处理客户端请求的类
class ClientHandler implements Runnable {
    private final Socket clientSocket;
    public ClientHandler(Socket socket) {
        this.clientSocket = socket;
    }
    @Override
    public void run() {
        // 可以在这里打印客户端信息,方便调试
        System.out.println("Handling request from " + clientSocket.getInetAddress() + " in thread: " + Thread.currentThread().getName());
        // 复用方案一中的 handleRequest 逻辑
        try (
            BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
            OutputStream out = clientSocket.getOutputStream();
        ) {
            String requestLine = in.readLine();
            if (requestLine == null || requestLine.isEmpty()) return;
            System.out.println("Request: " + requestLine);
            String[] requestParts = requestLine.split(" ");
            if (requestParts.length < 2) {
                SimpleFileServer.sendErrorResponse(out, 400, "Bad Request");
                return;
            }
            String method = requestParts[0];
            String path = requestParts[1];
            if (!"GET".equalsIgnoreCase(method)) {
                SimpleFileServer.sendErrorResponse(out, 501, "Not Implemented");
                return;
            }
            Path requestedFile = Paths.get(SimpleFileServer.WEB_ROOT, path).normalize().toAbsolutePath();
            if (!requestedFile.startsWith(Paths.get(SimpleFileServer.WEB_ROOT).toAbsolutePath())) {
                SimpleFileServer.sendErrorResponse(out, 403, "Forbidden");
                return;
            }
            if (!Files.exists(requestedFile) || !Files.isRegularFile(requestedFile)) {
                SimpleFileServer.sendErrorResponse(out, 404, "Not Found");
                return;
            }
            SimpleFileServer.sendFileResponse(out, requestedFile);
        } catch (IOException e) {
            System.err.println("Error handling client: " + e.getMessage());
        } finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                System.err.println("Error closing client socket: " + e.getMessage());
            }
        }
    }
}

这个版本将处理逻辑封装到了 ClientHandler 类中,主线程只负责接受连接并分发任务,大大提高了服务器的并发处理能力。


更健壮的实现(使用线程池)

虽然多线程比单线程好,但如果并发连接数非常大,创建和销毁线程本身也会消耗大量资源,更优的方案是使用线程池

线程池会预先创建一组线程,当有新任务时,从池中取一个线程来执行,而不是每次都新建,任务执行完毕后,线程会回到池中等待下一个任务,避免了频繁创建和销毁的开销。

ThreadPoolFileServer.java

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolFileServer {
    private static final int PORT = 8080;
    private static final String WEB_ROOT = "./public";
    // 根据CPU核心数来设置线程池大小,是一个常见的做法
    private static final int THREAD_POOL_SIZE = Runtime.getRuntime().availableProcessors();
    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("Server started on port " + PORT);
            System.out.println("Serving files from: " + new File(WEB_ROOT).getAbsolutePath());
            System.out.println("Using a thread pool with size: " + THREAD_POOL_SIZE);
            while (true) {
                Socket clientSocket = serverSocket.accept();
                // 将任务提交给线程池执行,而不是创建新线程
                threadPool.execute(new ClientHandler(clientSocket));
            }
        } catch (IOException e) {
            System.err.println("Could not start server: " + e.getMessage());
        } finally {
            // 优雅关闭服务器时,需要关闭线程池
            threadPool.shutdown();
        }
    }
}
// ClientHandler 类与方案二中的完全相同
class ClientHandler implements Runnable {
    // ... (代码同上)
}

这个版本是性能和资源消耗之间一个很好的平衡点,是构建生产级服务器的良好起点。

总结与扩展

特性 方案一 (单线程) 方案二 (多线程) 方案三 (线程池)
并发能力 差,一次只处理一个 好,可同时处理多个 最好,可控资源消耗
资源消耗 高(连接数多时) 可控,复用线程
代码复杂度 最低 中等 中等
适用场景 学习、调试 简单应用 生产环境推荐

可以进一步扩展的功能:

  1. 支持更多 HTTP 方法:如 POST(用于上传文件)、PUTDELETE
  2. 支持断点续传:通过解析请求头 Range 实现。
  3. 支持虚拟主机:根据 Host 请求头将请求路由到不同的文件根目录。
  4. 支持 HTTPS:使用 SSLServerSocket 替代 ServerSocket,为通信加密。
  5. 支持目录列表:当请求的是一个目录而不是文件时,列出目录下的文件和子目录。
  6. 更高效的文件传输:对于大文件,使用 Files.copy()Channels 进行零拷贝传输,比 readAllBytes() 更节省内存。
  7. 使用成熟的框架:对于复杂的项目,直接使用成熟的 Web 框架如 Spring BootVert.x 会更高效、更安全,它们已经处理了底层的网络通信、线程池、路由、模板引擎等所有复杂问题。

希望这个从简到繁的指南能帮助你理解如何在 Java 中构建一个 HTTP 文件服务器!

分享:
扫描分享到社交APP
上一篇
下一篇