1. 程式人生 > >Java 網絡 IO 模型

Java 網絡 IO 模型

完整 端端 信號 新增 spa 簡單例子 展現 如果 ref

技術分享

在進入主題之前先看個 Java 網絡編程的一個簡單例子:代碼很簡單,客戶端和服務端進行通信,對於客戶端的每次輸入,服務端回復 get。註意,服務端可以同時允許多個客戶端連接。

服務端端代碼:

// 創建服務端 socket
ServerSocket serverSocket = new ServerSocket(20000);
client = serverSocket.accept();

// 客戶端連接成功,輸出提示
System.out.println("客戶端連接成功");
// 啟動一個新的線程處理客戶端請求
new Thread(new ServerThread(client)).start();

// 子線程中處理客戶端的輸入
class ServerThread implements Runnable {
    .....
    @Override
    public void run() {
        boolean flag = true;
        while (flag) {

            // 讀取客戶端發送來的數據
            String str = buf.readLine();

            // 回復給客戶端 get 表示收到數據
            out.println("get"); 
        }
    }
}

客戶端代碼 :

Socket client = new Socket("127.0.0.1", 20000);
boolean flag = true;
while (flag) {

    // 讀取用戶從鍵盤的輸入
    String str = input.readLine();
    // 把用戶的輸入發送給服務端
    out.println(str);

    // 接受到服務端回傳的 get 字符串
    String echo = buf.readLine();
    System.out.println(echo);
    }
}

考慮到完整的 Java 示例代碼太過龐大影響閱讀,所以這裏不完整貼出,如果需要在 github 直接下載,這裏是下載地址。

可以看到,server 為了能夠同時處理多個 client 的請求,需要為每個 client 開啟一個 thread,這種 one-thread-per-client 的模式對於 server 而言壓力是很大的。假設有 1k 個 client,對應的 server 應該啟動 1k 個 thread,那麽 server 所耗費的內存,以及 thread 切換時候占用的時間等等都是致命傷。即使使用線程池的技術來限制線程個數,這種 blocking-IO 的模型還是沒辦法支撐大量連接。

技術分享

每個 client 都需要一個 thread 來請求處理。

NIO

上面這種 one-thread-per-client 的模式無法支撐大量連接的主要原因在於 readLine

會 阻塞 IO,即在 readLine 沒能夠讀取到數據的時候,會一直阻塞線程,使得線程無法繼續執行,那麽 server 為了可以同時處理多個 client,只能同時開啟多個線程。

所以,Java 1.4 之後引入了一套 NIO 接口。NIO 中最主要的一個功能就是可以進行非阻塞 IO 操作:如果沒能夠讀取到數據,非阻塞 IO 不會阻塞線程,而是直接返回 0。這種情況下,線程通過返回值判斷數據還沒有準備好,就可以處理其他事情,而不會被阻塞。

技術分享

上圖是阻塞 IO 和非阻塞 IO 的區別,可以看出雖然 非阻塞 IO 並不會被阻塞,但是它仍然不斷的調用函數檢查數據是否已經可讀,這種現象在代碼中是以這種形式展現:

while((str = read()) == 0) {

}
 // 繼續讀取到數據之後的邏輯。

可以明白,雖然非阻塞 IO 不會阻塞線程,但是由於沒有數據可讀,線程也沒有辦法繼續執行下面的邏輯,只能不斷的調用判斷,等待數據到來。這種情況下稱為同步 IO。所以綜上,NIO 本質上是一個非阻塞同步 IO。

IO 復用

由於 NIO 不會因為數據還沒有到達而被阻塞,那麽就沒有必要每一個 client 都分配一個 thread 不斷去輪詢判斷是否有數據可讀。可以使用一個 thread 監聽所有的 client 連接,由這個 thread 循環判斷是否有某個 client 的數據可讀,如果有就告知其他 thread 某個 client 連接由數據可讀。這種行為就被稱之為 IO 復用。 在 NIO 中提供了 Selector 類來監聽所有 client 連接是否有數據可讀。

技術分享

使用 Selector 來實現 IO 復用,只有一個 thread 需要關心數據是否到來,其他線程等待通知就好。如此一來,只有監聽線程會一直循環判斷,並不會占據太多 CPU 資源。提到 NIO 中的 Selector,不得不說一下 Linux 編程中的 IO 復用,因為 NIO 中的 Selector 底層就是使用系統級的 IO 復用方案。

Linux 系統的 IO 復用實現方案有 2 種:

  • select
  • epoll

在 Linux 2.6+ 的版本上 NIO 底層使用的是 epoll,在 2.4.x 的版本使用的是 select 函數。epoll 函數在性能方面比 select好很多,這裏可以不關心 Linux 編程具體細節。值得一提的是,Java 的 netty 網絡框架底層就是使用 NIO 技術。

AIO

回顧一下 NIO 中:使用監聽線程調用 select 函數來監聽所有請求是否有數據到達,如果有數據則通知其他線程來讀取數據。這裏在線程讀取數據的過程中,線程在數據沒有讀取完畢之前是處於阻塞狀態,只有數據讀取完畢之後線程才可以繼續執行邏輯。之前說過,這種稱之為同步 IO。JDK 7 中新增了一套新接口 AIO(Asynchronous IO)。

AIO 有一個神奇的特性:當發起 IO 操作之後,線程不用等待 IO 讀取完畢,而是可以直接返回,繼續執行其他操作。等到數據讀取完畢之後,系統會通知線程數據已經讀取完畢。這種發起 IO 操作,但是不必等待數據讀取完畢的 IO 操作稱之為異步 IO。如果使用 AIO,一個線程可以同時發起多個 IO 操作,這就意味著,一個線程可以同時處理多個請求。著名的 web 服務器 Nginx 就是用了異步 IO。關於更多的細節,可以參考下我的另一篇文章 <Apache--MPMs && Nginx事件驅動>。

End

到目前為止,文章解釋了阻塞/非阻塞 IO,同步/異步 IO 的區別,談起 IO 模型,不可避免會涉及 Linux 的 5 種 IO 模型

  • 阻塞 IO
  • 非阻塞 IO
  • IO 復用
  • 信號驅動 IO
  • 異步 IO

除去信號驅動 IO沒有提及,其他 4 種主要的 IO 模型都有所解釋,理解了這些 IO 模型的概念對於編寫代碼有很大的幫助。

Java學習交流QQ群:589809992 禁止閑聊,非喜勿進!

Java 網絡 IO 模型