Java 網絡 IO 模型

本文首發於 jaychen.cc
做者 JayChenjava

在進入主題以前先看個 Java 網絡編程的一個簡單例子:代碼很簡單,客戶端和服務端進行通訊,對於客戶端的每次輸入,服務端回覆 get。注意,服務端能夠同時容許多個客戶端鏈接。git

服務端端代碼:github

// 建立服務端 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"); 
        }
    }
}複製代碼

客戶端代碼 :web

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 並不會被阻塞,可是它仍然不斷的調用函數檢查數據是否已經可讀,這種現象在代碼中是以這種形式展示:socket

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 模型的概念對於編寫代碼有很大的幫助。

相關文章
相關標籤/搜索