從零手寫 HTTP 服務器是理解網路程式設計、Servlet 容器原理的最佳途徑。本文提供基於傳統阻塞 IO(BIO)與非阻塞 IO(NIO)兩種架構的完整實現代碼,對比兩種模型差異。
三大核心類職責分離:
public class HttpServer {
public static final String WEB_ROOT = System.getProperty("user.dir") + "\\src\\test\\resources\\webroot";
public static final int SERVER_PORT = 8888;
public static final String SERVER_IP = "127.0.0.1";
public static void main(String[] args) {
new HttpServer().await();
}
public void await() {
try (ServerSocket serverSocket = new ServerSocket(SERVER_PORT, 1, InetAddress.getByName(SERVER_IP))) {
while (true) {
try (Socket socket = serverSocket.accept();
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream()) {
Request request = new Request(in);
request.read(); // 解析請求
Response response = new Response(out);
response.setRequest(request);
response.response(); // 處理響應
} catch (IOException e) {
e.printStackTrace();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class Request {
private InputStream in;
private String uri;
public Request(InputStream in) { this.in = in; }
public void read() {
StringBuffer sb = new StringBuffer();
byte[] buffer = new byte[2048];
try {
int count = in.read(buffer);
for (int i = 0; i < count; i++) {
sb.append((char) buffer[i]);
}
takeUri(sb);
} catch (IOException e) { e.printStackTrace(); }
}
private void takeUri(StringBuffer sb) {
int idx1 = sb.indexOf(" ");
if (idx1 != -1) {
int idx2 = sb.indexOf(" ", idx1 + 1);
if (idx2 > idx1) {
uri = sb.substring(idx1 + 1, idx2);
// 簡單路由:僅允許 index.html 和 test.jpg
if (!(uri.endsWith("/index.html") || uri.endsWith("/test.jpg"))) {
uri = "/404.html";
}
}
}
}
// getter...
}
public class Response {
private static final int BUFFER_SIZE = 1024;
private OutputStream out;
private Request request;
public Response(OutputStream out) { this.out = out; }
public void setRequest(Request request) { this.request = request; }
public void response() {
try {
File file = new File(HttpServer.WEB_ROOT, request.getUri());
if (file.exists()) {
try (FileInputStream fis = new FileInputStream(file)) {
byte[] buffer = new byte[BUFFER_SIZE];
int count;
while ((count = fis.read(buffer)) > 0) {
out.write(buffer, 0, count);
}
}
} else {
// 404 響應
String resp = "HTTP/1.1 404 File Not Found\r\n" +
"Content-Type: text/html\r\n" +
"Content-Length: 24\r\n\r\n" +
"File Not Found!
";
out.write(resp.getBytes());
}
} catch (IOException e) { e.printStackTrace(); }
}
}
public class HttpNIOServer {
private ServerSocketChannel serverSocketChannel;
private Selector selector;
private Request request;
private static final int SERVER_PORT = 8888;
private boolean shutdown = false;
public static void main(String[] args) {
new HttpNIOServer().start();
}
public HttpNIOServer() {
try {
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false); // 非阻塞模式
serverSocketChannel.socket().bind(new InetSocketAddress(SERVER_PORT));
selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO Server init on port " + SERVER_PORT);
} catch (IOException e) { e.printStackTrace(); }
}
public void start() {
try {
while (!shutdown) {
selector.select(); // 阻塞等待就緒事件
Set keys = selector.selectedKeys();
Iterator it = keys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove();
handleRequest(key);
}
}
} catch (IOException e) { e.printStackTrace(); }
}
private void handleRequest(SelectionKey key) {
try {
if (key.isAcceptable()) {
// 新連接接受
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 讀取請求
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(2048);
StringBuffer sb = new StringBuffer();
while (sc.read(buffer) > 0) {
buffer.flip();
int limit = buffer.limit();
for (int i = 0; i < limit; i++) {
sb.append((char) buffer.get(i));
}
buffer.clear();
}
if (sb.length() > 0) {
request = new Request();
request.takeUri(sb);
sc.register(selector, SelectionKey.OP_WRITE);
}
} else if (key.isWritable()) {
// 寫入響應
SocketChannel sc = (SocketChannel) key.channel();
Response response = new Response(sc);
response.setRequest(request);
response.responseNIO();
sc.register(selector, SelectionKey.OP_READ); // 繼續監聽讀取
}
} catch (IOException e) { e.printStackTrace(); }
}
}
public class Response {
// ... 構造方法接受 SocketChannel
public Response(SocketChannel osc) { this.osc = osc; }
public void responseNIO() {
try {
File file = new File(HttpServer.WEB_ROOT, request.getUri());
ByteBuffer buffer = ByteBuffer.allocate(1024);
if (file != null && file.exists()) {
try (FileInputStream fis = new FileInputStream(file)) {
while (fis.read(buffer.array()) > 0) {
osc.write(buffer);
buffer.clear();
}
}
} else {
String resp = "HTTP/1.1 404 File Not Found\r\n" +
"Content-Type: text/html\r\n" +
"Content-Length: 24\r\n\r\n" +
"File Not Found!
";
osc.write(ByteBuffer.wrap(resp.getBytes()));
}
} catch (IOException e) { e.printStackTrace(); }
}
}
| 維度 | BIO (HttpServer) | NIO (HttpNIOServer) |
|---|---|---|
| 程式模型 | 同步阻塞,一連接一執行緒 | 同步非阻塞,單執行緒多路複用 |
| 擴展性 | 受限於執行緒數,C10K 問題 | 單執行緒支援萬級連接 |
| 代碼複雜度 | 簡單直觀 | 狀態機管理、事件驅動、較複雜 |
| 記憶體佔用 | 每連接一棧幀 | 共用 Selector、緩衝區可控 |
| 適用場景 | 連接數少、邏輯簡單 | 高併發、長連接、即時通訊 |
java HttpServer 或 java HttpNIOServerhttps://localhost:8888/index.htmlhttp-server/
├── src/
│ └── main/java/org/example/httpserver/
│ ├── HttpServer.java
│ ├── HttpNIOServer.java
│ ├── Request.java
│ └── Response.java
└── src/test/resources/webroot/
├── index.html
├── test.jpg
└── 404.html
https://github.com/windwant/http-test
手寫 HTTP 服務器能深度理解:
建議學習路徑:先跑通 BIO 版本理解流程 → 改造 NIO 版本體會非阻塞難點 → 研究 Netty 框架學習生產級最佳實踐。