理解 HTTP 協議與 Socket 編程是後端開發的內功心法。本文從 HTTP 協議四步工作原理出發,逐步構建一個支持多線程併發、GET 請求處理、靜態文件服務、錯誤響應的輕量級 Web 服務器,代碼量不足 200 行卻包含服務器核心要素,是學習網絡編程、Servlet 容器原理的最佳切入點。
public class WebServer {
public static void main(String[] args) throws IOException {
int port = 8080;
ServerSocket server = new ServerSocket(port);
System.out.println("Web Server listening on port " + server.getLocalPort());
int counter = 1;
while (true) {
Socket client = server.accept(); // 阻塞等待連接
new ConnectionThread(client, counter++).start(); // 每連接一線程
}
}
}
class ConnectionThread extends Thread {
private final Socket client;
private final int id;
public ConnectionThread(Socket client, int id) {
this.client = client;
this.id = id;
}
@Override
public void run() {
try {
String clientIP = client.getInetAddress().toString();
int clientPort = client.getPort();
System.out.println("Connection " + id + ": " + clientIP + ":" + clientPort);
PrintStream out = new PrintStream(client.getOutputStream());
DataInputStream in = new DataInputStream(client.getInputStream());
String requestLine = in.readLine(); // 讀取請求行
System.out.println("Received: " + requestLine);
if (isGetRequest(requestLine)) {
String filename = getFilename(requestLine);
File file = new File(filename);
if (file.exists() && file.isFile()) {
serveFile(out, file);
} else {
serve404(out);
}
}
client.close();
} catch (IOException e) {
System.err.println("Connection " + id + " error: " + e.getMessage());
}
}
private boolean isGetRequest(String line) {
return line != null && line.toUpperCase().startsWith("GET ");
}
private String getFilename(String requestLine) {
// GET /path/file.html HTTP/1.0
String path = requestLine.substring(4, requestLine.indexOf(' ', 4));
if (path.startsWith("/")) path = path.substring(1);
if (path.isEmpty()) path = "index.html";
return path;
}
private void serveFile(PrintStream out, File file) throws IOException {
out.println("HTTP/1.0 200 OK");
out.println("MIME_version: 1.0");
out.println("Content_Type: text/html");
out.println("Content_Length: " + file.length());
out.println(); // 空行分隔頭體
try (DataInputStream in = new DataInputStream(new FileInputStream(file))) {
byte[] buffer = new byte[(int) file.length()];
in.readFully(buffer);
out.write(buffer);
out.flush();
}
}
private void serve404(PrintStream out) {
String body = "Not Found " +
"Error 404 - File Not Found
";
out.println("HTTP/1.0 404 Not Found");
out.println("Content_Type: text/html");
out.println("Content_Length: " + body.length());
out.println();
out.println(body);
out.flush();
}
}
// 標準請求行格式
GET /index.html HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0
Accept: text/html
Connection: keep-alive
// 響應格式
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 1234
Date: Mon, 23 Jan 2023 10:00:00 GMT
Server: MiniJavaWeb/1.0
<html>...</html>
關鍵點:雙換行符(`\r\n\r\n`)分隔頭與體,`Content-Length` 必須準確,否則客戶端會掛起等待。
| 功能模塊 | 實現方向 | 參考組件 |
|---|---|---|
| 靜態資源優化 | 緩存、壓縮、ETag、Range 請求 | Nginx 靜態模塊 |
| 動態請求處理 | Servlet 規範實現、Filter 鏈、Session 管理 | Tomcat Catalina |
| HTTPS 支持 | SSLContext + ServerSocketFactory | JSSE |
| 虛擬主機 | Host Header 路由、多域名證書 | Tomcat Host/Context |
| 監控指標 | QPS、延遲分位數、錯誤率、線程池狀態 | Micrometer + Prometheus |
$ javac WebServer.java
$ java WebServer
Web Server listening on port 8080
Connection 1: /127.0.0.1:54321
Received: GET /index.html HTTP/1.1
瀏覽器訪問 `https://localhost:8080/index.html` 即可看到服務效果。