目錄
NIO(一、概述)
NIO(二、Buffer)
NIO(三、Channel)
NIO(四、Selector)
Selector
前面兩個章節都描述了Buffer和Channel,那這個章節就描述NIO三個最核心部分的最后一塊內容 - 選擇器(Selector)
如何使用
在前面的章節中描述過多路復用,一個線程通過選擇器處理和管理多個通道。由此可見,選擇器是用來處理多個通道並監聽其通道事件的組件。
- Create
只需要調用 open() 即可創建一個Selector對象:
Selector selector = Selector.open();
- Register
通過 register() 方法注冊通道:
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT);
在注冊通道之前,把通道設置成非阻塞模式,觀察源碼會發現 register() 會校驗當前通道是否為非阻塞模式,當是阻塞模式時,會拋出IllegalBlockingModeException 異常。在前面一個章節也提過,為什么FileChannel沒有繼承SelectableChannel,因為它不需要多路復用,所以在使用通道的時候,只有FileChannel不能向選擇器注冊通道,凡是繼承SelectableChannel都能夠向選擇器注冊通道。
注冊通道方法的第二個參數是SelectionKey中定義的操作類型,你可以填入任何你感興趣的操作類型,只要這個通道支持,同樣,在執行 register() 方法時也會校驗該通道是否能夠支持該操作。
注冊方法同樣也會返回一個SelectionKey對象。
- Attach Object
注冊通道的 register() 方法有一個重載方法,可以向選擇器注冊通道的時候,選擇想要帶上的附加對象:
public abstract SelectionKey register(Selector sel, int ops, Object att)
throws ClosedChannelException;
例如,使用時附加上一個字符串:
String ch_name = "123";
SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT,ch_name);
獲取這個字符串可通過 attachment() 來獲取:
// 接收數據
String ch_name_accept = (String) selectionKey.attachment();
當然,注冊時返回的SelectionKey對象也可以在使用時候附加你想要的附加對象:
selectionKey.attach(ch_name);
- Block
因為是一個線程通過選擇器來操作通道,那么選擇器在操作通道時,必定在處理一個通道的時候,另一個事件已就緒的通道處於等待狀態。在確定一個通道事件就緒之后,才能去操作這個通道。上文中講到使用注冊方法register使用的代碼示例,將ServerSocketChannel對象向選擇器注冊,同時關注了這個通道的OP_ACCEPT操作類型事件,那么我們什么時候能確定該通道的accept事件就緒,可以操作這個通道了。選擇器為我們提供了三個重載的 select() 方法,這三個方法的主要功能就是選擇是否阻塞直到該選擇器中的通道所關注的事件就緒,來讓程序繼續往下運行。
首先看 select() 方法,該方法會一直阻塞下去,直到選擇器中的通道關注的事件就緒:
selector.select();
參數5000是5秒,參數以毫秒為單位。這個方法會一直阻塞5秒,5秒之內如果沒有通道事件就緒的話程序會往下運行:
selector.select(5000);
selectNow()其實就是非阻塞,無論有無通道事件就緒,程序都會向下執行:
selector.selectNow();
這三個方法的本質區別無非是選擇器阻塞或者等待一個或多個通道的事件就緒有多長時間。
- Keys & SelectionKeys
我們每為一個通道執行 register() 注冊方法,就會返回一個SelectionKey,那么這個選擇器所有已就緒的SelectionKey就是通過selectedKeys()來獲取:
Set<SelectionKey> selectionKeys = selector.selectedKeys();
一般這個方法是在select() 之后執行,因為到這一步就意味着要通過這個輪詢每個就緒的通道。
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
// 執行通道的操作
}
//執行完成移除
iterator.remove();
}
到這里說的是已就緒的通道,那么所有的 SelectionKey 集可以通過 keys() 方法獲取:
Set<SelectionKey> keys = selector.keys();
- wake up
在使用Selector對象的 select() 或者 select(long) 方法時候,當前線程很可能一直阻塞下去,那么用另一個線程去執行 Selector.wakeUp() 方法會喚醒當前被阻塞的線程,使其 select() 立即返回。
當然,如果當前線程沒有阻塞,那么執行了wakeUp() 方法之后,下一個線程的 select() 方法會被立即返回,不再被阻塞下去。 - close
顯然,close() 方法能夠關閉當前的選擇器。
當一個線程當前呈阻塞狀態,那么中止這種狀態需要執行選擇器的 wakeUp() 方法,close()方法的實現正是這么做的,先喚醒被阻塞的線程,然后繼續接下來的操作。接下來就會會置空所有的通道、所有就緒的SelectionKey,讓這個選擇器上的輪詢組件也閑置下來。
SelectionKey
SelectionKey的功能類似於通道的一個注冊令牌。
這個類定義了4個操作類型,每種操作類型都對應了相應的事件,通過監聽這幾種不同的事件,在觸發該事件時表示所對應的操作已准備就緒:
操作類型 | 值 | 描述 |
---|---|---|
OP_READ | 1 << 0 | 讀操作 |
OP_WRITE | 1 << 2 | 寫操作 |
OP_CONNECT | 1 << 3 | 連接socket操作 |
OP_ACCEPT | 1 << 4 | 接受socket操作 |
這里得提一句,所有繼承SelectableChannel的通道都會定義自己能夠支持的操作類型,可以通過具體通道的 validOps() 方法查看,例如SocketChannel支持read、write、connect這幾種操作:
// SocketChannel類
public final int validOps() {
return (SelectionKey.OP_READ | SelectionKey.OP_WRITE | SelectionKey.OP_CONNECT);
}
- interestOps & readyOps
這是SelectionKey的實現類中定義的變量:
private volatile int interestOps;
private int readyOps;
interestOps用來存儲感興趣的操作集,readyOps用來存儲已經就緒的操作集。
其中 interestOps() 方法和 nioInterestOps() 都會返回interestOps,不同的是interestOps()會校驗是否已執行 cancel() ,如果已經取消則會拋出 CancelledKeyException 異常。readyOps同樣也有 readyOps() 和 nioReadyOps() 方法,邏輯與interestOps幾乎一致。
觀察這段SelectionKey抽象類已經實現的代碼:
// SelectionKey 類
public final boolean isAcceptable() {
return (readyOps() & OP_ACCEPT) != 0;
}
當判斷是否訪問就緒的時候,只要 readyOps() 與相應的操作類型相與,非零就返回true,代表接受請求操作已就緒。這個是SelectionKey已提供的方法,但是SelectionKey並未提供同樣返回boolean判斷某個操作在interestOps集是否存在,我們可以自己實現這些方法:
private boolean isInterestRead(SelectionKey selectionKey){
return (selectionKey.interestOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_READ;
}
private boolean isInterestWrite(SelectionKey selectionKey){
return (selectionKey.interestOps() & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE;
}
private boolean isInterestConnect(SelectionKey selectionKey){
return (selectionKey.interestOps() & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT;
}
private boolean isInterestAccept(SelectionKey selectionKey){
return (selectionKey.interestOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
}
- Channel、Selector
在SelectionKey中獲取通道或者選擇器只需要調用其中的兩個方法即可:
SelectableChannel selectableChannel = selectionKey.channel();
Selector sel = selectionKey.selector();