引進nio的目的

 

之前的 I/O 編程

在 JDK 1.4 之前,自由地使用線程是處理阻塞問題最典型的辦法。但這個解決辦法會產生它自己的問題 ― 即線程開銷,線程開銷同時影響性能和可伸縮性。

服務器的典型工作過程。黑體線表示處於阻塞的操作。

用 Java 語言寫的服務器,由於其線程與客戶機之比幾乎是一比一,因而易於受到大量線程開銷的影響,其結果是既導致了性能問題又缺乏可伸縮性。
爲了解決這個問題,Java 平臺引入了一組新的類。java.nio 包充滿了解決線程開銷問題的技巧,包中最重要的是新的 SelectableChannel 類和 Selector 類。 通道(channel)是客戶機和服務器之間的一種通信方式。 選擇器(selector)與 Windows 消息循環類似,它從不同客戶機捕獲各種事件並將它們分派到相應的事件處理程序。

通道和選擇器

NIO 的非阻塞 I/O 機制是圍繞 選擇器和 通道構建的。 Channel 類表示服務器和客戶機之間的一種通信機制。與反應器模式一致, Selector 類是 Channel 的多路複用器。 Selector 類將傳入客戶機請求多路分用並將它們分派到各自的請求處理程序。

我們將仔細考察 Channel 類和 Selector 類的各個功能,以及這兩個類如何協同工作,創建非阻塞 I/O 實現。

通道做什麼

通道表示連到一個實體(例如:硬件設備、文件、網絡套接字或者能執行一個或多個不同 I/O 操作(例如:讀或寫)的程序組件)的開放連接。可以異步地關閉和中斷 NIO 通道。所以,如果一個線程在某條通道的 I/O 操作上阻塞時,那麼另一個線程可以將這條通道關閉。類似地,如果一個線程在某條通道的 I/O 操作上阻塞時,那麼另一個線程可以中斷這個阻塞線程。
java.nio.channels 的類層次結構


在 java.nio.channels 包中有不少通道接口。我們主要關心 java.nio.channels.SocketChannel 接口和 java.nio.channels.ServerSocketChannel 接口。 這兩個接口可用來分別代替 java.net.Socket 和 java.net.ServerSocket 。儘管我們當然將把注意力放在以非阻塞方式使用通道上,但通道可以以阻塞方式或非阻塞方式使用。

創建一條非阻塞通道

 

爲了實現基礎的非阻塞套接字讀和寫操作,我們要處理兩個新類。它們是來自 java.net 包的 InetSocketAddress 類,它指定連接到哪裏,以及來自 java.nio.channels 包的 SocketChannel 類,它執行實際的讀和寫操作。

這部分中的代碼片段顯示了一種經過修改的、非阻塞的辦法來創建基礎的服務器-套接字程序。請注意這些代碼樣本與第一個示例中所用的代碼之間的變化,從添加兩個新類開始:

String host = ......;
   InetSocketAddress socketAddress = new InetSocketAddress(host, 80);
	
SocketChannel channel = SocketChannel.open();
   channel.connect(socketAddress);


爲了使通道成爲非阻塞的,我們在通道上調用 configureBlockingMethod(false) ,如下所示:

channel.configureBlockingMethod(false);

在阻塞模式中,線程將在讀或寫時阻塞,一直到讀或寫操作徹底完成。如果在讀的時候,數據尚未完全到達套接字,則線程將在讀操作上阻塞,一直到數據可用。

在非阻塞模式中,線程將讀取已經可用的數據(不論多少),然後返回執行其它任務。如果將真(true)傳遞給 configureBlockingMethod() ,則通道的行爲將與在 Socket 上進行阻塞讀或寫時的行爲完全相同。唯一的主要差別,如上所述,是這些阻塞讀和寫可以被其它線程中斷。

單靠 Channel 創建非阻塞 I/O 實現是不夠的。要實現非阻塞 I/O, Channel 類必須與 Selector 類配合進行工作。

 

選擇器做什麼

在反應器模式情形中, Selector 類充當 Reactor 角色。 Selector 對多個 SelectableChannels 的事件進行多路複用。每個 Channel 向 Selector 註冊事件。當事件從客戶機處到來時, Selector 將它們多路分用並將這些事件分派到相應的 Channel 。

創建 Selector 最簡單的辦法是使用 open() 方法,如下所示:

Selector selector = Selector.open();


 

通道遇上選擇器

每個要爲客戶機請求提供服務的 Channel 都必須首先創建一個連接。下面的代碼創建稱爲 Server 的 ServerSocketChannel 並將它綁定到本地端口:

ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
InetAddress ia = InetAddress.getLocalHost();
InetSocketAddress isa = new InetSocketAddress(ia, port );
serverChannel.socket().bind(isa);


每個要爲客戶機請求提供服務的 Channel 都必須接着將自己向 Selector 註冊。 Channel 應根據它將處理的事件進行註冊。例如,接受傳入連接的 Channel 應這樣註冊,如下:

SelectionKey acceptKey = 
    channel.register( selector,SelectionKey.OP_ACCEPT);


 

Channel 向 Selector 的註冊用 SelectionKey 對象表示。滿足以下三個條件之一, Key 就失效:
Channel 被關閉。
Selector 被關閉。
通過調用 Key 的 cancel() 方法將 Key 本身取消。

Selector 在 select() 調用時阻塞。接着,它開始等待,直到建立了一個新的連接,或者另一個線程將它喚醒,或者另一個線程將原來的阻塞線程中斷。

註冊服務器

Server 是那個將自己向 Selector 註冊以接受所有傳入連接的 ServerSocketChannel ,如下所示:

SelectionKey acceptKey = serverChannel.register(sel, SelectionKey.OP_ACCEPT);
   while (acceptKey.selector().select() > 0 ){
     ......


Server 被註冊後,我們根據每個關鍵字(key)的類型以迭代方式對一組關鍵字進行處理。一個關鍵字被處理完成後,就都被從就緒關鍵字(ready keys)列表中除去,如下所示:

Set readyKeys = sel.selectedKeys();
    Iterator it = readyKeys.iterator();
while (it.hasNext()) 
{
SelectionKey key = (SelectionKey)it.next();
  it.remove();
  ....
  ....
  ....
 }


如果關鍵字是可接受(acceptable)的,則接受連接,註冊通道,以接受更多的事件(例如:讀或寫操作)。 如果關鍵字是可讀的(readable)或可寫的(writable),則服務器會指示它已經就緒於讀寫本端數據:

SocketChannel socket;
if (key.isAcceptable()) {
    System.out.println("Acceptable Key");
    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
    socket = (SocketChannel) ssc.accept();
    socket.configureBlocking(false);
    SelectionKey another = 
      socket.register(sel,SelectionKey.OP_READ|SelectionKey.OP_WRITE);
}
if (key.isReadable()) {
    System.out.println("Readable Key");
    String ret = readMessage(key);
    if (ret.length() > 0) {
      writeMessage(socket,ret);
    }
		    
}
if (key.isWritable()) {
    System.out.println("Writable Key");
    String ret = readMessage(key);
    socket = (SocketChannel)key.channel();   
    if (result.length() > 0 ) {
      writeMessage(socket,ret);
    }
    }


 

運行這個示例

安裝 JDK 1.4。
將兩個
源代碼文件 複製到您的目錄。
編譯和運行服務器, java NonBlockingServer 。
編譯和運行客戶機, java Client 。
輸入類文件所在目錄的一個文本文件或 java 文件的名稱。
服務器將讀取該文件並將其內容發送到客戶機。
客戶機將把從服務器接收到的數據打印出來。(由於所用的 ByteBuffer 的限制,所以將只讀取 1024 字節。)
輸入 quit 或 shutdown 命令關閉客戶機。

 

結束語

Merlin 的新 I/O 包覆蓋範圍很廣。Merlin 的新的非阻塞 I/O 實現的主要優點有兩方面:線程不再在讀或寫時阻塞,以及 Selector 能夠處理多個連接,從而大幅降低了服務器應用程序開銷。


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章