系列文章:
Android 網絡系列更新計劃
Android NIO 系列教程(一) NIO概述
Android NIO 系列教程(二) – Channel
Android NIO 系列教程(三) – Buffer
Android NIO 系列教程(四) – Selector
前面幾篇文章,我們已經認識了 selector ,它是一個可以檢測 一個 或 多個 channel ,並且能夠知道該 channel 的讀寫狀態的組件,通過這種方式,一個線程可以管理多個channel,從而管理這個網絡連接。
一、爲什麼使用 selector ?
一個好處是你可以使用一個線程去管理多個channel。對於操作系統來說,線程的切換開銷很大,且佔用資源(內存),因此,使用的線程越少越好。
但是,實際上,現在的 CPU 和 系統 性能越來越好,多線程的開銷,隨着時間的推移,也變得越來越少;實際上,多喝 cpu 不使用多線程去開發,是很浪費資源的。但是我們不深入討論這個問題,只需要知道 selector 是使用單個線程去處理多個 channel 的即可。
如下 一個 selector 監聽 三個 channel 的案例:
二、創建 selector 的實例
如何創建 selector 的實例呢,我們需要使用 open 方法:
Selector selector = Selector.open();
三、向 selector 註冊 channel
接着我們需要註冊 channel,但這裏的 channel 必須是非阻塞的,所以,我們將不能註冊 FileChannel,因爲它爲 非阻塞 IO, Socket 的channel是可以使用的,事實上,NIO 也是偏向網絡的IO.如:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
注意register()方法的第二個參數。這是一個“interest集合”,意思是在通過Selector監聽Channel時對什麼事件感興趣。可以監聽四種不同類型的事件:
- Connect
- Accept
- Read
- Write
channel 的觸發事件也可以叫做準備事件,所以,當一個 channel 跟服務器連接成功叫做 Connected狀態,server socket 的channel 等待接收事件到來叫做 Accept狀態;一個可讀的 channel 則被稱爲 Read狀態;同理一個可寫的 chanenl 被稱爲Write狀態。
這四種事件,可以用 SelectionKey 裏的常量表示:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
如果對多個事件感興趣,可以使用以下方式:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
四、SelectionKey
在上面,當我們通過 register() 方法向 selector 註冊 channel 時,會返回一個 SelectionKey 的對象,該 SelectionKey 可以獲得以下數據:
- Interest 集合
- Ready 集合
- Channel 通道
- Selector
- 附加對象
4.1 Interest 集合
Interest 集合如上面所述,是你感興趣的集合,可以通過SelectionKey讀寫interest集合,像這樣:
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;
可以看到,用“位與”操作interest 集合和給定的SelectionKey常量,可以確定某個確定的事件是否在interest 集合中。
4.2 Ready 集合
ready 集合是 channel 準備就緒的集合,在選擇(Selection)之後,會優先選擇 ready 集合,selection 後面講,這裏我們就能拿到 ready 集合了:
int readySet = selectionKey.readyOps();
可以用像檢測interest集合那樣的方法,來檢測channel中什麼事件或操作已經就緒。當然,也可以使用以下四個方法,它們都會返回一個布爾類型:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
4.3 Channel + Selector
可以通過 SelctionKey 訪問到 channel 和 selector 的實例,如下:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
4.4 附加對象
你可以將對象附加到 SelectionKey 上,這是給channel 或附加更多信息的一個比較簡便的方法。比如,你可以把channel 正在使用的 buffer ,或其他信息附加到 selectionkey 上,如:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
你也可以通過 register 方法,在 channel 註冊給 selector 的時候,把對象附加進去:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
五、通過 selector 選擇 channel
當你想 selector 註冊一個或多個 channel 時,你可以使用 select() 方法,來選擇你感興趣的事件 (如 connect, accept, read or write),已經準備就緒的事件,換句話,如果你對 channel 中的 read 事件感興趣,selector 就會返回已經就緒的那些通道。
selector 有以下方法:
- int select()
- int select(long timeout)
- int selectNow()
select() 會一直阻塞,直到有註冊且準備就緒的channel到來
select(long timeout) 和select()一樣,除了最長會阻塞timeout毫秒
selectNow() 不會阻塞,無論什麼通道就緒立即返回。
select()方法返回的int值告訴我們有多少通道已經準備好了。也就是說,自上次調用select()以來,已經準備好了多少通道。如果你調用select(),它返回1,因爲一個通道已經就緒,再次調用select(),並且一個通道已經就緒,它將再次返回1。如果沒有對第一個就緒的通道執行任何操作,那麼現在就有了兩個就緒通道,但是在每個select()調用之間只有一個通道已經就緒。
六、selectedKeys()
一旦你調用了 select() 方法,且有返回一個或多個就緒的 channel,就可以使用 selector 的 selectionKey 集合了。像這樣:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
當像Selector註冊Channel時,Channel.register()方法會返回一個SelectionKey 對象。這個對象代表了註冊到該Selector的通道。可以通過SelectionKey的selectedKeySet()方法訪問這些對象。
可以遍歷這個已選擇的鍵集合來訪問就緒的通道。如下:
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
}
// 記得要 remove 移除實例,不然下次事件過來就接收不到了
keyIterator.remove();
}
這個循環遍歷已選擇鍵集中的每個鍵,並檢測各個鍵所對應的通道的就緒事件。
注意每次迭代末尾的keyIterator.remove()調用。Selector不會自己從已選擇鍵集中移除SelectionKey實例。必須在處理完通道時自己移除。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。
SelectionKey.channel()方法返回的通道需要轉型成你要處理的類型,如ServerSocketChannel或SocketChannel等。
七、wakeUp()
當我們調用 select 方法,線程會一直阻塞;但幾遍沒有就緒的事件,我們也可以喚醒,只要讓其它線程在第一個線程調用select()方法的那個對象上調用Selector.wakeup()方法即可。阻塞在select()方法上的線程會立馬返回。
如果有其它線程調用了wakeup()方法,但當前沒有線程阻塞在select()方法上,下個調用select()方法的線程會立即“醒來(wake up)
八、close()
當使用完 selector ,可以調用它的 close 方法,該方法會使 selector 關閉,並且讓 SelectionKey 註冊的事件失效。但 channel 本身不會關閉。
完整實例
下面是一個實例,通過 open 創建 selector,通過register 向 selector 註冊 channel,並監聽自己感興趣的事件(accept, connect, read, write):
//獲取 selector 實例
Selector selector = Selector.open();
//設置非阻塞狀態
channel.configureBlocking(false);
//向 selector 註冊 channel 事件
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
//監聽感興趣的事件,當有就緒事件時立即返回
int readyChannels = selector.selectNow();
//防止 cpu 空轉100%的問題
if(readyChannels == 0) continue;
//拿到 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
}
// 移除 selector 實例,方便下次接入
keyIterator.remove();
}
}
下一章,我們繼續學習 Channel 的一些實例