在 Java 1.4 之前,標準 I/O (java.io) 採用阻塞模型:每個連線佔用一條執行緒。當連線數增長至數百上千,執行緒上下文切換、記憶體佔用 (每條約 512KB~1MB 棧空間) 成為系統瓶頸。Web 伺服器若採用「一連線一執行緒」,極易因執行緒耗盡而拒絕服務。JDK 1.4 引入 java.nio (New I/O),以 Channel (通道)、Buffer (緩衝區)、Selector (選擇器) 三大核心組件,實現非阻塞、多路複用,讓少量執行緒即可處理大量並發連線。
// 主迴圈:每個執行緒獨立運行
while (true) {
Socket socket = serverSocket.accept(); // 阻塞等待連線
handleRequest(socket); // 完整處理請求/回應
socket.close();
}
採用 Reactor 模式,拆分為四大角色協作:
| 角色 | 類別 | 職責 | 執行緒 |
|---|---|---|---|
| 主啟動器 | NIOHttpd | 開啟 ServerSocketChannel、啟動 Acceptor/Selector | Main |
| 連線接收器 | Acceptor | 迴圈 serverSocketChannel.accept(),將新 SocketChannel 封裝為 Connection 送入佇列 | 專屬執行緒 |
| 多路分派器 | ConnectionSelector | 持有 Selector,selector.select() 阻塞等待就緒事件,處理佇列註冊、分派讀/寫任務 | 專屬執行緒 |
| 連線狀態機 | Connection | 持有 SocketChannel、Buffer、SelectionKey,實作讀取請求、解析 HTTP、準備回應、寫入回應、檔案傳輸、關閉清理 | 由 Selector 執行緒回呼 |
while (running) {
SocketChannel sc = serverSocketChannel.accept(); // 非阻塞模式下可能返回 null
if (sc != null) {
sc.configureBlocking(false);
connectionSelector.queue(new Connection(sc));
}
}
while (running) {
int ready = selector.select(); // 阻塞直到有事件或 wakeup()
registerQueuedConnections(); // 從佇列註冊新連線 (OP_READ)
Set keys = selector.selectedKeys();
for (SelectionKey key : keys) {
Connection conn = (Connection) key.attachment();
if (key.isReadable()) conn.readRequest();
else if (key.isWritable()) conn.writeResponse();
}
keys.clear();
}
OP_READ,分配請求緩衝區 (512 bytes)。socketChannel.read(buffer),若未讀完整行 → 返回等待下次事件;若讀完 → 解析 HTTP → 若合法開啟 FileChannel 準備檔案 → 切換 key.interestOps(OP_WRITE) + selector.wakeup()。responseLineBuffer),再用 fileChannel.transferTo() 零拷貝傳送檔案內容 (上限 64KB/次 確保公平)。key.cancel()、關閉 Channel。| 維度 | 傳統 BIO (Httpd) | NIO (NIOHttpd) |
|---|---|---|
| 執行緒模型 | 一連線一執行緒 | 固定少量執行緒 (1 Acceptor + 1 Selector + 可擴充 Worker) |
| 擴展性 | 受限於 OS 執行緒上限 (~數千) | 理論支援任意連線數 (C10K/C100K) |
| 記憶體佔用 | 高 (執行緒棧 + 緩衝區) | 低 (共享 Selector、Heap/Direct Buffer) |
| 程式複雜度 | 低 (線性邏輯) | 高 (狀態機、事件驅動、佇列同步) |
| 適用場景 | 低併發、長連線計算密集 | 高併發、大量閒置/慢速連線、靜態資源服務 |
synchronized(queue) 為 ConcurrentLinkedQueue 或 LMAX Disruptor。ByteBuffer.allocateDirect() 分配/GC 壓力。transferTo() 依賴 OS `sendfile`,大檔案傳輸極高效。下一篇將以數學模型量化分析兩者在不同並發數、檔案大小、客戶端頻寬下的處理時間差異,並給出具體倍數比較表格。