Tomcat、Jetty 等成熟容器底層均基於 ServerSocket + Socket 的 BIO 模型(早期版本)。本文通過 100 行核心代碼,實現一個支持靜態資源訪問、404 處理、優雅關閉的微型 HTTP Server,幫助理解 Servlet 容器核心原理。
package ex01.pyrmont;
import java.io.*;
import java.net.*;
import java.nio.file.*;
public class HttpServer {
public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";
private static final String SHUTDOWN_COMMAND = "/SHUTDOWN";
public static void main(String[] args) {
new HttpServer().await();
}
public void await() {
try (ServerSocket serverSocket = new ServerSocket(8080, 1, InetAddress.getByName("127.0.0.1"))) {
System.out.println("Server started on 127.0.0.1:8080");
while (true) {
try (Socket socket = serverSocket.accept();
InputStream input = socket.getInputStream();
OutputStream output = socket.getOutputStream()) {
Request request = new Request(input);
request.parse();
if (SHUTDOWN_COMMAND.equals(request.getUri())) {
System.out.println("Shutdown command received.");
return; // 退出主循環
}
Response response = new Response(output);
response.setRequest(request);
response.sendStaticResource();
} catch (IOException e) {
System.err.println("Request processing error: " + e.getMessage());
}
}
} catch (IOException e) {
System.err.println("Server socket error: " + e.getMessage());
System.exit(1);
}
}
}
package ex01.pyrmont;
import java.io.*;
public class Request {
private InputStream input;
private String uri;
public Request(InputStream input) { this.input = input; }
public void parse() throws IOException {
byte[] buffer = new byte[2048];
int bytesRead = input.read(buffer);
if (bytesRead <= 0) return;
String requestText = new String(buffer, 0, bytesRead);
System.out.println("=== Request ===\n" + requestText);
// 解析請求行: GET /index.html HTTP/1.1
int firstSpace = requestText.indexOf(' ');
int secondSpace = requestText.indexOf(' ', firstSpace + 1);
if (firstSpace != -1 && secondSpace > firstSpace) {
this.uri = requestText.substring(firstSpace + 1, secondSpace);
}
}
public String getUri() { return uri; }
}
package ex01.pyrmont;
import java.io.*;
import java.nio.file.*;
public class Response {
private static final int BUFFER_SIZE = 1024;
private OutputStream output;
private Request request;
public Response(OutputStream output) { this.output = output; }
public void setRequest(Request request) { this.request = request; }
public void sendStaticResource() throws IOException {
String uri = request.getUri();
Path filePath = Paths.get(HttpServer.WEB_ROOT, uri).normalize();
// 安全檢查:防止目錄遍歷攻擊
if (!filePath.startsWith(Paths.get(HttpServer.WEB_ROOT).normalize())) {
sendError(403, "Forbidden");
return;
}
if (Files.exists(filePath) && Files.isRegularFile(filePath)) {
// 發送 200 OK + 文件內容
String header = "HTTP/1.1 200 OK\r\n" +
"Content-Type: " + Files.probeContentType(filePath) + "\r\n" +
"Content-Length: " + Files.size(filePath) + "\r\n" +
"\r\n";
output.write(header.getBytes());
Files.copy(filePath, output);
} else {
sendError(404, "File Not Found");
}
}
private void sendError(int code, String msg) throws IOException {
String body = "" + code + " " + msg + "
";
String header = "HTTP/1.1 " + code + " " + msg + "\r\n" +
"Content-Type: text/html\r\n" +
"Content-Length: " + body.length() + "\r\n" +
"\r\n";
output.write((header + body).getBytes());
}
}
webroot 文件夾,放入 index.html、style.css、圖片等。HttpServer.main()。https://127.0.0.1:8080/index.html。https://127.0.0.1:8080/SHUTDOWN 關閉服務。| 模塊 | 關鍵技術點 |
|---|---|
| ServerSocket | bind/listen/accept 三步驟;backlog 參數控制等待隊列長度 |
| Socket | 雙向流;getInputStream()/getOutputStream() |
| HTTP 協議 | 請求行/頭/體;響應行/頭/體;\r\n\r\n 分隔頭體 |
| 靜態資源 | Files.probeContentType() 自動 MIME;Files.copy() 零拷貝輸出 |
| 安全 | 路徑規範化 normalize() + 前綴檢查防目錄遍歷 |
Content-Length + body 讀取)。ExecutorService)。ServletRequest/ServletResponse 接口、生命周期管理。ServerSocketChannel + Selector) 支持高併發。參考資料:《深入剖析 Tomcat》—— 從源碼級理解容器本質。