天天看點

NIO入門系列之第8章:連網和異步 I/O

連網是學習異步 I/O 的很好基礎,而異步 I/O 對于在 Java 語言中執行任何輸入/輸出過程的人來說,無疑都是必須具備的知識。NIO 中的連網與 NIO 中的其他任何操作沒有什麼不同——它依賴通道和緩沖區,而您通常使用InputStream 和 OutputStream來獲得通道。

本節首先介紹異步 I/O 的基礎—它是什麼以及它不是什麼,然後轉向更實用的、程式性的例子。

異步 I/O 是一種沒有阻塞地讀寫資料的方法。通常,在代碼進行 read() 調用時,代碼會阻塞直至有可供讀取的資料。同樣, write() 調用将會阻塞直至資料能夠寫入。

另一方面,異步I/O調用不會阻塞。相反,您将注冊對特定 I/O 事件的興趣——可讀的資料的到達、新的套接字連接配接,等等,而在發生這樣的事件時,系統将會告訴您。

異步I/O的一個優勢在于,它允許您同時根據大量的輸入和輸出執行 I/O。同步程式常常要求助于輪詢,或者建立許許多多的線程以處理大量的連接配接。使用異步 I/O,您可以監聽任何數量的通道上的事件,不用輪詢,也不用額外的線程。

我們将通過研究一個名為MultiPortEcho.java 的例子程式來檢視異步 I/O 的實際應用。這個程式就像傳統的 echo server,它接受網絡連接配接并向它們回響它們可能發送的資料。不過它有一個附加的特性,就是它能同時監聽多個端口,并處理來自所有這些端口的連接配接。并且它隻在單個線程中完成所有這些工作。

本節的闡述對應于MultiPortEcho 的源代碼中的 go() 方法的實作,是以應該看一下源代碼,以便對所發生的事情有個更全面的了解。

異步 I/O 中的核心對象名為 Selector。Selector 就是您注冊對各種 I/O 事件的興趣的地方,而且當那些事件發生時,就是這個對象告訴您所發生的事件。

是以,我們需要做的第一件事就是建立一個 Selector:

然後,我們将對不同的通道對象調用 register() 方法,以便注冊我們對這些對象中發生的 I/O 事件的興趣。register()的第一個參數總是這個Selector。

為了接收連接配接,我們需要一個ServerSocketChannel。事實上,我們要監聽的每一個端口都需要有一個 ServerSocketChannel 。對于每一個端口,我們打開一個 ServerSocketChannel,如下所示:

第一行建立一個新的ServerSocketChannel ,最後三行将它綁定到給定的端口。第二行将 ServerSocketChannel 設定為非阻塞的。我們必須對每一個要使用的套接字通道調用這個方法,否則異步 I/O 就不能工作。

下一步是将新打開的ServerSocketChannels 注冊到 Selector上。為此我們使用 ServerSocketChannel.register() 方法,如下所示:

register() 的第一個參數總是這個 Selector。第二個參數是 OP_ACCEPT,這裡它指定我們想要監聽 accept 事件,也就是在新的連接配接建立時所發生的事件。這是适用于 ServerSocketChannel 的唯一事件類型。

請注意對register() 的調用的傳回值。SelectionKey 代表這個通道在此Selector 上的這個注冊。當某個Selector 通知您某個傳入事件時,它是通過提供對應于該事件的 SelectionKey 來進行的。SelectionKey 還可以用于取消通道的注冊。

現在已經注冊了我們對一些 I/O 事件的興趣,下面将進入主循環。使用Selectors 的幾乎每個程式都像下面這樣使用内部循環:

首先,我們調用Selector 的 select() 方法。這個方法會阻塞,直到至少有一個已注冊的事件發生。當一個或者更多的事件發生時, select() 方法将傳回所發生的事件的數量。

接下來,我們調用Selector 的selectedKeys() 方法,它傳回發生了事件的SelectionKey 對象的一個集合。

我們通過疊代SelectionKeys 并依次處理每個SelectionKey 來處理事件。對于每一個SelectionKey,您必須确定發生的是什麼I/O 事件,以及這個事件影響哪些I/O 對象。

程式執行到這裡,我們僅注冊了ServerSocketChannel,并且僅注冊它們“接收”事件。為确認這一點,我們對 SelectionKey 調用 readyOps() 方法,并檢查發生了什麼類型的事件:

可以肯定地說,readOps() 方法告訴我們該事件是新的連接配接。

因為我們知道這個伺服器套接字上有一個傳入連接配接在等待,是以可以安全地接受它;也就是說,不用擔心 accept() 操作會阻塞:

下一步是将新連接配接的SocketChannel 配置為非阻塞的。而且由于接受這個連接配接的目的是為了讀取來自套接字的資料,是以我們還必須将 SocketChannel 注冊到 Selector上,如下所示:

注意我們使用register() 的 OP_READ 參數,将 SocketChannel 注冊用于讀取而不是接受新連接配接。

在處理SelectionKey 之後,我們幾乎可以傳回主循環了。但是我們必須首先将處理過的 SelectionKey 從標明的鍵集合中删除。如果我們沒有删除處理過的鍵,那麼它仍然會在主集合中以一個激活的鍵出現,這會導緻我們嘗試再次處理它。我們調用疊代器的 remove() 方法來删除處理過的 SelectionKey:

現在我們可以傳回主循環并接受從一個套接字中傳入的資料(或者一個傳入的 I/O 事件)了。

當來自一個套接字的資料到達時,它會觸發一個 I/O 事件。這會導緻在主循環中調用Selector.select(),并傳回一個或者多個I/O 事件。這一次, SelectionKey 将被标記為 OP_READ 事件,如下所示:

與以前一樣,我們取得發生I/O 事件的通道并處理它。在本例中,由于這是一個 echo server,我們隻希望從套接字中讀取資料并馬上将它發送回去。關于這個過程的細節,請參見參考資料中的源代碼 (MultiPortEcho.java)。

每次傳回主循環,我們都要調用select 的 Selector()方法,并取得一組 SelectionKey。每個鍵代表一個 I/O 事件。我們處理事件,從標明的鍵集中删除 SelectionKey,然後傳回主循環的頂部。

這個程式有點過于簡單,因為它的目的隻是展示異步 I/O 所涉及的技術。在現實的應用程式中,您需要通過将通道從 Selector 中删除來處理關閉的通道。而且您可能要使用多個線程。這個程式可以僅使用一個線程,因為它隻是一個示範,但是在現實場景中,建立一個線程池來負責 I/O 事件進行中的耗時部分會更有意義。

本文出自 “” 部落格,請務必保留此出處