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

核心思路
一个简单的 HTTP 文件服务器主要做以下几件事:
- 监听端口:在指定的网络端口上等待客户端(通常是浏览器)的连接请求。
- 接收请求:当一个客户端连接时,读取它发送的 HTTP 请求,请求包含了客户端想要访问的资源路径(
/index.html)。 - 处理请求:
- 根据请求路径,在服务器上查找对应的文件。
- 如果文件存在,读取文件内容。
- 根据文件类型(如
.html,.css,.jpg),构建一个 HTTP 响应头,并返回文件内容。 - 如果文件不存在,返回一个 "404 Not Found" 错误。
- 关闭连接:将响应发送给客户端后,关闭与该客户端的连接。
基础单线程实现
这个版本最简单,易于理解,但一次只能处理一个客户端请求,不适用于生产环境。
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"; // 未知类型,作为二进制流处理
}
}
}
如何运行
-
创建项目目录和文件:
my-file-server/ ├── public/ │ ├── index.html │ └── style.css └── SimpleFileServer.java -
编写
public/index.html:
(图片来源网络,侵删)<!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> -
编写
public/style.css:body { font-family: sans-serif; background-color: #f0f0f0; color: #333; text-align: center; padding-top: 50px; } h1 { color: #0056b3; } -
编译并运行:
# 确保你在 my-file-server 目录下 javac SimpleFileServer.java java SimpleFileServer
-
访问: 打开浏览器,访问
http://localhost:8080/index.html或http://localhost:8080/style.css,你应该能看到页面和样式。
多线程实现(推荐)
基础版本的问题是性能很差,一个客户端正在下载大文件时,其他客户端必须等待,为了解决这个问题,我们需要使用多线程。

核心思想是:当接受到一个新连接时,不是立即处理,而是创建一个新的 Thread 或 Runnable 任务来处理这个连接,然后主线程立即回去等待下一个连接。
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 {
// ... (代码同上)
}
这个版本是性能和资源消耗之间一个很好的平衡点,是构建生产级服务器的良好起点。
总结与扩展
| 特性 | 方案一 (单线程) | 方案二 (多线程) | 方案三 (线程池) |
|---|---|---|---|
| 并发能力 | 差,一次只处理一个 | 好,可同时处理多个 | 最好,可控资源消耗 |
| 资源消耗 | 低 | 高(连接数多时) | 可控,复用线程 |
| 代码复杂度 | 最低 | 中等 | 中等 |
| 适用场景 | 学习、调试 | 简单应用 | 生产环境推荐 |
可以进一步扩展的功能:
- 支持更多 HTTP 方法:如
POST(用于上传文件)、PUT、DELETE。 - 支持断点续传:通过解析请求头
Range实现。 - 支持虚拟主机:根据
Host请求头将请求路由到不同的文件根目录。 - 支持 HTTPS:使用
SSLServerSocket替代ServerSocket,为通信加密。 - 支持目录列表:当请求的是一个目录而不是文件时,列出目录下的文件和子目录。
- 更高效的文件传输:对于大文件,使用
Files.copy()和Channels进行零拷贝传输,比readAllBytes()更节省内存。 - 使用成熟的框架:对于复杂的项目,直接使用成熟的 Web 框架如 Spring Boot 或 Vert.x 会更高效、更安全,它们已经处理了底层的网络通信、线程池、路由、模板引擎等所有复杂问题。
希望这个从简到繁的指南能帮助你理解如何在 Java 中构建一个 HTTP 文件服务器!
