選擇器是Java NIO組件踊东,它可以檢查一個或多個NIO通道产徊,并確定哪些通道準備好 閱讀或?qū)懽鳌?這樣一個單一的線程可以管理多個通道,從而可以管理多個網(wǎng)絡連接井赌。
為什么要用Selector
使用單個線程來處理多個通道的優(yōu)點是您需要較少的線程來處理通道谤逼。 實際上,你可以只用一個線程來處理你所有的頻道仇穗。 線程之間的切換對于操作系統(tǒng)而言是昂貴的流部,并且每個線程也占用操作系統(tǒng)中的一些資源(存儲器)。 因此纹坐,你使用的線程越少越好枝冀。
但請記住,現(xiàn)代操作系統(tǒng)和CPU在多任務處理方面越來越好耘子,所以多線程的開銷隨著時間的推移而變小果漾。 事實上,如果一個CPU有多個核心谷誓,那么你可能會因為沒有多任務而浪費CPU資源绒障。 無論如何,這個設計討論屬于不同的文本捍歪。 在這里說一下就足夠了户辱,你可以使用一個Selector來處理單個線程的多個通道。
下面是一個使用Selector處理3個Channel的線程的例子:
創(chuàng)建一個Selector
您可以通過調(diào)用Selector.open()方法創(chuàng)建一個Selector糙臼,如下所示:
Selector selector = Selector.open();
用Selector注冊channel
為了將頻道與選擇器一起使用庐镐,您必須使用選擇器注冊頻道。 這是使用SelectableChannel.register()方法完成的弓摘,如下所示:
channel.configureBlocking(false);//非阻塞
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
通道必須處于非阻塞模式才能與選擇器一起使用焚鹊。 這意味著你不能使用FileChannel和Selector,因為FileChannel不能切換到非阻塞模式韧献。 套接字通道將正常工作末患。
注意第二個參數(shù),這是一個按照類型設置的锤窑,意味著你設置的監(jiān)聽Channel的事件類型璧针,經(jīng)過選擇器,有四個不通的事件:
- Connect
- Accept
- Read
- Write
“發(fā)生事件”的頻道也被認為是“準備就緒”的事件渊啰。 因此探橱,成功連接到另一臺服務器的通道是“連接Connect就緒”申屹。 接受傳入連接的服務器套接字通道是“接受Accept”就緒。 準備好讀取數(shù)據(jù)Read的通道已經(jīng)準備就緒隧膏。 一個準備好寫入數(shù)據(jù)的通道Write已經(jīng)準備好了哗讥。
這四個事件由四個SelectionKey常量表示: - SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
如果你需要設置多個事件,或者將常量放在一起胞枕,就像這樣:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey
正如你在上一節(jié)看到的杆煞,當你用一個選擇器注冊一個Channel時,register()方法返回一個SelectionKey對象腐泻。 這個SelectionKey對象包含一些有趣的屬性:
- The interest set
- The ready set
- The Channel
- The Selector
- An attached object (optional)
下面我來介紹下這幾個屬性
Interest Set
The interest set是您在“選擇”中感興趣的事件集合决乎,如“用選擇器注冊通道”部分所述。 您可以通過SelectionKey讀取和寫入如下所示的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;
正如你所看到的,您可以用給定的SelectionKey常數(shù)和興趣組發(fā)現(xiàn)如果一個特定的事件是為了集合派桩。
Ready Set
Ready Set是通道準備好的一組操作构诚。 您將主要在選擇后訪問Ready Set。 選擇在后面的章節(jié)中解釋铆惑。 你可以這樣訪問readyset:
int readySet = selectionKey.readyOps();
您可以按照與興趣集相同的方式測試頻道準備好的事件/操作范嘱。 但是,您也可以使用這四種方法鸭津,而這些方法都是重新構造一個布爾值:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Channel + Selector
從SelectionKey訪問通道+選擇器是很簡潔的彤侍。 這是如何完成的:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
Attaching Objects
您可以將對象附加到SelectionKey,這是識別給定通道channel或?qū)⒏嘈畔⒏郊拥酵ǖ纁hannel的便捷方式逆趋。 例如,您可以將您正在使用的緩沖區(qū)與通道或包含更多聚合數(shù)據(jù)的對象連接起來晒奕。 這里是你如何附加對象:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
在register()方法中闻书,您也可以在向Selector注冊Channel時附加一個對象。 這是如何看起來如此:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
通過選擇器來選擇通道
一旦你用一個Selector注冊了一個或多個通道脑慧,你可以調(diào)用其中一個select()方法魄眉。 這些方法返回您感興趣的事件(連接,接受闷袒,讀取或?qū)懭耄┑摹皽蕚浜谩钡耐ǖ馈?換句話說坑律,如果您對準備閱讀的頻道感興趣,您將收到準備從select()方法讀取的通道囊骤。
這里是select()方法:
- int select()
- int select(long timeout)
- int selectNow()
int select()晃择,直到至少有一個通道準備好注冊的事件。
int select(long timeout):和select()一樣的也物,除了它的最大超時毫秒(參數(shù))宫屠。
int selectNow():它返回立即用什么渠道都準備好了。
select()方法返回的int告訴準備好多少個通道滑蚯。 也就是說浪蹂,自上次調(diào)用select()以來已經(jīng)準備了多少個通道抵栈。 如果你調(diào)用select(),并返回1坤次,因為一個通道已經(jīng)準備好了古劲,而且你再次調(diào)用select(),并且另外一個通道已經(jīng)準備就緒缰猴,它將再次返回1产艾。 如果您已經(jīng)準備好的第一個頻道沒有做任何事情,那么您現(xiàn)在有2個就緒頻道洛波,但在每個select()調(diào)用之間只有一個頻道已準備就緒胰舆。
selectedKeys()
一旦你調(diào)用了一個select()方法,并且它的返回值已經(jīng)表明一個或者多個通道已經(jīng)準備就緒蹬挤,你可以通過調(diào)用選擇器selectedKeys()方法來通過“selected key set”訪問ready channels缚窿。 這是如何看起來如此:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
當您使用Selector注冊頻道時,Channel.register()方法將返回SelectionKey對象焰扳。 這個鍵表示通道注冊到該選擇器倦零。 這些鍵可以通過selectedKeySet()方法訪問。 從SelectionKey吨悍。
您可以迭代此選定的selected key集以訪問就緒通道扫茅。 這是如何看起來如此:
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();
}
此循環(huán)迭代所選鍵集中的鍵。 對于每個SelectionKey 育瓜,它測試SelectionKey 以確定SelectionKey 引用的通道已準備好葫隙。
注意每次迭代結束時的keyIterator.remove()調(diào)用。 選擇器不會從選定的SelectionKey 集本身中刪除SelectionKey實例躏仇。 當您完成頻道處理后恋脚,您必須執(zhí)行此操作。 通道下一次變?yōu)椤熬途w”時焰手,選擇器將再次將其添加到所選的selected key 糟描。
SelectionKey.channel()方法返回的頻道應該被轉換為你需要使用的頻道,例如ServerSocketChannel或SocketChannel等书妻。
wakeUp()
調(diào)用被阻塞的select()方法的線程可以離開select()方法船响,即使沒有通道尚未準備好。 這是通過讓不同的線程調(diào)用Selector上的Selector.wakeup()方法來完成的躲履,第一個線程調(diào)用了select()见间。 在select()中等待的線程將立即返回。
如果一個不同的線程調(diào)用wakeup()并且select()內(nèi)部當前沒有線程被阻塞崇呵,那么調(diào)用select()的下一個線程將立即“喚醒”缤剧。
close()
當你完成Selector時,你調(diào)用close()方法域慷。 這將關閉選擇器荒辕,并使在此選擇器中注冊的所有SelectionKey實例失效汗销。 渠道本身并沒有關閉。
完整的Selector的例子
這是一個完整的例子抵窒,它打開一個選擇器弛针,注冊一個通道(通道實例被省略),并持續(xù)監(jiān)控選擇器是否準備好四個事件(接受李皇,連接削茁,讀取,寫入)掉房。
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();
}
}