Java NIO Selector選擇器


Selector是Java NIO中的一個組件,用於檢查一個或多個NIO Channel的狀態是否處於可讀、可寫。如此可以實現單線程管理多個channels,也就是可以管理多個網絡鏈接。

為什么使用Selector(Why Use a Selector?)

用單線程處理多個channels的好處是我需要更少的線程來處理channel。實際上,你甚至可以用一個線程來處理所有的channels。從操作系統的角度來看,切換線程開銷是比較昂貴的,並且每個線程都需要占用系統資源,因此暫用線程越少越好。

需要留意的是,現代操作系統和CPU在多任務處理上已經變得越來越好,所以多線程帶來的影響也越來越小。如果一個CPU是多核的,如果不執行多任務反而是浪費了機器的性能。不過這些設計討論是另外的話題了。簡而言之,通過Selector我們可以實現單線程操作多個channel。

這有一幅示意圖,描述了單線程處理三個channel的情況:

overview-selectors.png

Java NIO: A Thread uses a Selector to handle 3 Channel's

創建Selector(Creating a Selector)

創建一個Selector可以通過Selector.open()方法:

Selector selector = Selector.open();

注冊Channel到Selector上(Registering Channels with the Selector)

為了同Selector掛了Channel,我們必須先把Channel注冊到Selector上,這個操作使用SelectableChannel。register():

channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

Channel必須是非阻塞的。所以FileChannel不適用Selector,因為FileChannel不能切換為非阻塞模式。Socket channel可以正常使用。

注意register的第二個參數,這個參數是一個“關注集合”,代表我們關注的channel狀態,有四種基礎類型可供監聽:

  1. Connect
  2. Accept
  3. Read
  4. Write

一個channel觸發了一個事件也可視作該事件處於就緒狀態。因此當channel與server連接成功后,那么就是“連接就緒”狀態。server channel接收請求連接時處於“可連接就緒”狀態。channel有數據可讀時處於“讀就緒”狀態。channel可以進行數據寫入時處於“寫就緒”狀態。

上述的四種就緒狀態用SelectionKey中的常量表示如下:

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

如果對多個事件感興趣可利用位的或運算結合多個常量,比如:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; 

SelectionKey's

在上一小節中,我們利用register方法把Channel注冊到了Selectors上,這個方法的返回值是SelectionKeys,這個返回的對象包含了一些比較有價值的屬性:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object (optional)

這5個屬性都代表什么含義呢?下面會一一介紹。

Interest Set

這個“關注集合”實際上就是我們希望處理的事件的集合,它的值就是注冊時傳入的參數,我們可以用按為與運算把每個事件取出來:

int interestSet = selectionKey.interestOps(); boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT; boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT; boolean isInterestedInRead = interestSet & SelectionKey.OP_READ; boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE; 

Ready Set

"就緒集合"中的值是當前channel處於就緒的值,一般來說在調用了select方法后都會需要用到就緒狀態,select的介紹在胡須文章中繼續展開。

int readySet = selectionKey.readyOps();

從“就緒集合”中取值的操作類似月“關注集合”的操作,當然還有更簡單的方法,SelectionKey提供了一系列返回值為boolean的的方法:

selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable();

Channel + Selector

從SelectionKey操作Channel和Selector非常簡單:

Channel channel = selectionKey.channel(); Selector selector = selectionKey.selector(); 

Attaching Objects

我們可以給一個SelectionKey附加一個Object,這樣做一方面可以方便我們識別某個特定的channel,同時也增加了channel相關的附加信息。例如,可以把用於channel的buffer附加到SelectionKey上:

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

附加對象的操作也可以在register的時候就執行:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

從Selector中選擇channel(Selecting Channels via a Selector)

一旦我們向Selector注冊了一個或多個channel后,就可以調用select來獲取channel。select方法會返回所有處於就緒狀態的channel。 select方法具體如下:

  • int select()
  • int select(long timeout)
  • int selectNow()

select()方法在返回channel之前處於阻塞狀態。 select(long timeout)和select做的事一樣,不過他的阻塞有一個超時限制。

selectNow()不會阻塞,根據當前狀態立刻返回合適的channel。

select()方法的返回值是一個int整形,代表有多少channel處於就緒了。也就是自上一次select后有多少channel進入就緒。舉例來說,假設第一次調用select時正好有一個channel就緒,那么返回值是1,並且對這個channel做任何處理,接着再次調用select,此時恰好又有一個新的channel就緒,那么返回值還是1,現在我們一共有兩個channel處於就緒,但是在每次調用select時只有一個channel是就緒的。

selectedKeys()

在調用select並返回了有channel就緒之后,可以通過選中的key集合來獲取channel,這個操作通過調用selectedKeys()方法:

Set<SelectionKey> selectedKeys = selector.selectedKeys(); 

還記得在register時的操作吧,我們register后的返回值就是SelectionKey實例,也就是我們現在通過selectedKeys()方法所返回的SelectionKey。

遍歷這些SelectionKey可以通過如下方法:

Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading } else if (key.isWritable()) { // a channel is ready for writing } keyIterator.remove(); }

上述循環會迭代key集合,針對每個key我們單獨判斷他是處於何種就緒狀態。

注意keyIterater.remove()方法的調用,Selector本身並不會移除SelectionKey對象,這個操作需要我們收到執行。當下次channel處於就緒是,Selector任然會吧這些key再次加入進來。

SelectionKey.channel返回的channel實例需要強轉為我們實際使用的具體的channel類型,例如ServerSocketChannel或SocketChannel.

wakeUp()

y由於調用select而被阻塞的線程,可以通過調用Selector.wakeup()來喚醒即便此時已然沒有channel處於就緒狀態。具體操作是,在另外一個線程調用wakeup,被阻塞與select方法的線程就會立刻返回。

close()

當操作Selector完畢后,需要調用close方法。close的調用會關閉Selector並使相關的SelectionKey都無效。channel本身不管被關閉。

完整的Selector案例(Full Selector Example)

這有一個完整的案例,首先打開一個Selector,然后注冊channel,最后錦亭Selector的狀態:

Selector selector = Selector.open();

channel.configureBlocking(false); SelectionKey key = channel.register(selector, SelectionKey.OP_READ); while(true) { int readyChannels = selector.select(); if(readyChannels == 0) continue; Set<SelectionKey> selectedKeys = selector.selectedKeys(); Iterator<SelectionKey> keyIterator = selectedKeys.iterator(); while(keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if(key.isAcceptable()) { // a connection was accepted by a ServerSocketChannel. } else if (key.isConnectable()) { // a connection was established with a remote server. } else if (key.isReadable()) { // a channel is ready for reading } else if (key.isWritable()) { // a channel is ready for writing } keyIterator.remove(); } }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM