Selector是Java NIO中的一個組件,用於檢查一個或多個NIO Channel的狀態是否處於可讀、可寫。如此可以實現單線程管理多個channels,也就是可以管理多個網絡鏈接。
為什么使用Selector(Why Use a Selector?)
用單線程處理多個channels的好處是我需要更少的線程來處理channel。實際上,你甚至可以用一個線程來處理所有的channels。從操作系統的角度來看,切換線程開銷是比較昂貴的,並且每個線程都需要占用系統資源,因此暫用線程越少越好。
需要留意的是,現代操作系統和CPU在多任務處理上已經變得越來越好,所以多線程帶來的影響也越來越小。如果一個CPU是多核的,如果不執行多任務反而是浪費了機器的性能。不過這些設計討論是另外的話題了。簡而言之,通過Selector我們可以實現單線程操作多個channel。
這有一幅示意圖,描述了單線程處理三個channel的情況:
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狀態,有四種基礎類型可供監聽:
- Connect
- Accept
- Read
- Write
一個channel觸發了一個事件也可視作該事件處於就緒狀態。因此當channel與server連接成功后,那么就是“連接就緒”狀態。server channel接收請求連接時處於“可連接就緒”狀態。channel有數據可讀時處於“讀就緒”狀態。channel可以進行數據寫入時處於“寫就緒”狀態。
上述的四種就緒狀態用SelectionKey中的常量表示如下:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- 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(); } }