使用NIO實現非阻塞Socket通信

從JDK 1.4開始,Java提供的NIO API來開發高性能網絡服務器,前面介紹的網絡通信程序是基於阻塞式API的——即當程序執行輸入、輸出操作後,在這些操作返回之前會一直阻塞該線程,所以服務器必須爲每個客戶端都提供一條獨立線程進行處理,當服務器需要同時處理大量客戶端時,這種做法會導致性能下降。使用NIO API則可以讓服務器使用一個或有限幾個線程來同時處理連接到服務器上的所有客戶端。

如果讀者忘記了NIO裏Channel、Buffer、Charset等API的概念和用法,讀者可以再次閱讀本書第15章關於新IO的內容。

Java的NIO爲非阻塞式的Socket通信提供瞭如下幾個特殊類:

Selector:它是SelectableChannel對象的多路複用器,所有希望採用非阻塞方式進行通信的Channel都應該註冊到Selector對象。可通過調用此類的靜態open()方法來創建Selector實例,該方法將使用系統默認的Selector來返回新的Selector。

Selector可以同時監控多個SelectableChannel的IO狀況,是非阻塞IO的核心。一個Selector實例有3個SelectionKey的集合:

所有SelectionKey集合:代表了註冊在該Selector上的Channel,這個集合可以通過keys()方法返回。

被選擇的SelectionKey集合:代表了所有可通過select()方法監測到、需要進行IO處理的Channel,這個集合可以通過selectedKeys()返回。

被取消的SelectionKey集合:代表了所有被取消註冊關係的Channel,在下一次執行select()方法時,這些Channel對應的SelectionKey會被徹底刪除,程序通常無須直接訪問該集合。

除此之外,Selector還提供了系列和select()相關的方法,如下所示:

int select():監控所有註冊的Channel,當它們中間有需要處理的IO操作時,該方法返回,並將對應的SelectionKey加入被選擇的SelectionKey集合中,該方法返回這些Channel的數量。

int select(long timeout):可以設置超時時長的select()操作。

int selectNow():執行一個立即返回的select()操作,相對於無參數的select()方法而言,該方法不會阻塞線程。

Selector wakeup():使一個還未返回的select()方法立刻返回。

SelectableChannel:它代表可以支持非阻塞IO操作的Channel對象,可以將其註冊到Selector上,這種註冊的關係由SelectionKey實例表示。
Selector對象提供了一個select()方法,該方法允許應用程序同時監控多個IO Channel。

應用程序可調用SelectableChannel 的register()方法將其註冊到指定Selector上,當該Selector上某些SelectableChannel上有需要處理的IO操作時,程序可以調用Selector實例的select()方法獲取它們的數量,並可以通過selectedKeys()方法返回它們對應的SelectKey集合——通過該集合就可以獲取所有需要處理IO操作的SelectableChannel集。

SelectableChannel對象支持阻塞和非阻塞兩種模式(所有channel默認都是阻塞模式),必須使用非阻塞式模式纔可以利用非阻塞IO操作。
SelectableChannel提供瞭如下兩個方法來設置和返回該Channel的模式狀態:

SelectableChannel configureBlocking(boolean block):設置是否採用阻塞模式。

boolean isBlocking():返回該Channel是否是阻塞模式。

不同的SelectableChannel所支持的操作不一樣,例如ServerSocketChannel代表一個ServerSocket,它就只支持OP_ACCEPT操作。
SelectableChannel提供如下方法來返回它支持的所有操作:

int validOps() :返回一個bit mask,表示這個channel上支持的IO操作。

在SelectionKey中,用靜態常量定義了4種IO操作:OP_READ(1)、OP_WRITE(4)、OP_CONNECT(8)、OP_ACCEP(16),這四值任意2個、3個、4個進行按位或的結果和相加的結果相等,而且它們任意2個、3個、4個相加的結果總是互不相同,所以系統可以根據validOps()方法的返回值確定該SelectableChannel支持的操作。例如返回5,我們知道它支持讀(1)和寫(4)。

除此之外,SelectableChannel還提供瞭如下幾個方法來獲取它的註冊狀態:

boolean isRegistered():返回該Channel是否已註冊在一個或多個Selector上。

SelectionKey keyFor(Selector sel):返回該Channel和sel Selector之間的註冊關係,如果不存在註冊關係,則返回null。

SelectionKey:該對象代表SelectableChannel和Selector之間的註冊關係。

ServerSocketChannel:支持非阻塞操作,對應於java.net.ServerSocket這個類,提供了TCP協議IO接口,只支持OP_ACCEPT操作。該類也提供了accept()方法,功能相當於ServerSocket提供的accept()方法。

SocketChannel:支持非阻塞操作,對應於java.net.Socket這個類,提供了TCP協議IO接口,支持OP_CONNECT,OP_READ和OP_WRITE操作。這個類還實現了ByteChannel接口、ScatteringByteChannel接口和GatheringByteChannel接口,所以可以直接通過SocketChannel來讀寫ByteBuffer對象。

顯示了使用NIO實現非阻塞式服務器的示意圖:

圖17.6

從圖17.6中可以看出,服務器上所有Channel(包括ServerSocketChannel和SocketChannel)都需要向Selector註冊,而該Selector則負責監視這些Socket的IO狀態,當其中任意一個或多個Channel具有可用的IO操作時,該Selector的select()方法將會返回大於0的整數,該整數值就表示該Selector上有多少個Channel具有可用的IO操作,並提供了selectedKeys()方法來返回這些Channel對應的SelectionKey集合。正是通過Selector,使得服務器端只需要不斷地調用Selector實例的select()方法即可知道當前所有Channel是否有需要處理的IO操作。

當Selector上註冊的所有Channel都沒有需要處理的IO操作時,select()方法將被阻塞,調用該方法的線程被阻塞。

本示例程序使用NIO實現了多人聊天室的功能,服務器使用循環不斷獲取Selector的select()方法返回值,當該返回值大於0時就處理該Selector上被選擇SelectionKey所對應的Channel。

服務器端需要使用ServerSocketChannel來監聽客戶端的連接請求,Java中該類的設計比較糟糕:它不是ServerSocket的完整抽象,所以不能直接讓該Channel監聽某個端口;而且不允許使用ServerSoceket的getChannel()方法來獲取ServerSocketChannel實例。程序必須先調用它的socket()方法獲得關聯ServerSocket對象,再用該ServerSocket對象綁定到來指定監聽IP和端口。創建一個可用的ServerSocketChannel需採用如下代碼片段:

//通過open方法來打開一個未綁定的ServerSocketChannel實例
ServerSocketChannel server = ServerSocketChannel.open();
InetSocketAddress isa = new InetSocketAddress("127.0.0.1", 30000); 
//將該ServerSocketChannel綁定到指定IP地址
server.socket().bind(isa);

 如果需要使用非阻塞方式來處理該ServerSocketChannel,還應該設置它的非阻塞模式,並將其註冊到指定的Selector。如下代碼片段:

//設置ServerSocket以非阻塞方式工作
server.configureBlocking(false);
//將server註冊到指定Selector對象
server.register(selector, SelectionKey.OP_ACCEPT);

 

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