java NIO selector全面深入理解
阿新 • • 發佈:2019-01-01
最近在學習java NIO,發現java nio selector 相對 channel ,buffer 這兩個概念是比較難理解的 ,把學習理解的東西以文字的東西記錄下來,就像從記憶體落地到硬碟,把記憶體中內容換成該知識點的索引。
在介紹Selector之前先明確以下3個問題:
1、selector的作用是什麼?
2、selector如何做到網路通訊的?
下面就針對上面的2個問題來展開介紹:
1、Selector(選擇器)是Java NIO中能夠檢測一到多個NIO渠道,並能夠知曉渠道是否為諸如讀寫事件做好準備的元件。這樣,一個單獨的執行緒可以管理多個channel,從而管理多個網路連線。
2、selector 操作的過程
2.1 首先建立Selector
Selector selector = Selector.open();
2.2 向selector註冊channel,和感興趣的事件,
channel.configureBlocking(false);
SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
注意register()方法的第二個引數, 這是一個interest集合,意思是在通過Selector監聽Channel時對什麼事件感興趣。可以監聽以下4中不同型別的事件:
服務端接收客戶端連線事件 SelectionKey.OP_ACCEPT
客戶端連線服務端事件 SelectionKey.OP_CONNECT
讀事件 SelectionKey.OP_READ
寫事件 SelectionKey.OP_WRITE
如果你對不止一種事件感興趣,那麼可以用“位或”操作符將常量連線起來如下:
SelectionKey key = channel.register(selector,Selectionkey.OP_READ|SelectionKey.OP_WRITE);
當向Selector註冊Channel時,registor()方法會返回一個SelectorKey物件。這個物件包含了一些你感興趣的屬性。
interest集合
ready集合
Channel
Selector
附加的物件(可選)
2.2.1 interest集合
就像向Selector註冊通道一節中所描述的,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集合中。
2.2.2 ready集合
ready 集合是通道已經準備就緒的操作的集合。在一次選擇(Selection)之後,你會首先訪問這個ready set。Selection將在下一小節進行解釋。可以這樣訪問ready集合:
int readySet = selectionKey.readyOps();
可以用像檢測interest集合那樣的方法,來檢測channel中什麼事件或操作已經就緒。但是,也可以使用以下四個方法,它們都會返回一個布林型別:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
2.2.3 Channel + Selector
從SelectionKey訪問Channel和Selector很簡單。如下:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
2.2.4 附加物件
可以將一個物件或者更多資訊附著到SelectionKey上,這樣就能方便的識別某個給定的通道。例如,可以附加 與通道一起使用的Buffer,或是包含聚集資料的某個物件。使用方法如下:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
還可以在用register()方法向Selector註冊Channel的時候附加物件。如:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
2.3 通過Selector選擇通道
一旦向Selector註冊了一或多個通道,就可以呼叫幾個過載的select()方法。這些方法返回你所感興趣的事件(如連線、接受、讀或寫)已經準備就緒的那些通道。
換句話說,如果你對“讀就緒”的通道感興趣,select()方法會返回讀事件已經就緒的那些通道。
下面是select()方法:(該方法是阻塞方法)
int select()
int select(long timeout)
int selectNow()
select()阻塞到至少有一個通道在你註冊的事件上就緒了。
select(long timeout)和select()一樣,除了最長會阻塞timeout毫秒(引數)。
selectNow()不會阻塞,不管什麼通道就緒都立刻返回(譯者注:此方法執行非阻塞的選擇操作。如果自從前一次選擇操作後,沒有通道變成可選擇的,則此方法直接返回零。)。
select()方法返回的int值表示有多少通道已經就緒。亦即,自上次呼叫select()方法後有多少通道變成就緒狀態。如果呼叫select()方法,因為有一個通道變成就緒狀態,返回了1,
若再次呼叫select()方法,如果另一個通道就緒了,它會再次返回1。如果對第一個就緒的channel沒有做任何操作,現在就有兩個就緒的通道,但在每次select()方法呼叫之間,只有一個通道就緒了。
2.3.1 selectedKeys()
一旦呼叫了select()方法,並且返回值表明有一個或更多個通道就緒了,然後可以通過呼叫selector的selectedKeys()方法,訪問“已選擇鍵集(selected key set)”中的就緒通道。如下所示:
Set selectedKeys = selector.selectedKeys();
當像Selector註冊Channel時,Channel.register()方法會返回一個SelectionKey 物件。這個物件代表了註冊到該Selector的通道。可以通過SelectionKey的selectedKeySet()方法訪問這些物件。
可以遍歷這個已選擇的鍵集合來訪問就緒的通道。如下:
Set selectedKeys = selector.selectedKeys();
Iterator 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();
}
這個迴圈遍歷已選擇鍵集中的每個鍵,並檢測各個鍵所對應的通道的就緒事件。
注意每次迭代末尾的keyIterator.remove()呼叫。Selector不會自己從已選擇鍵集中移除SelectionKey例項。必須在處理完通道時自己移除。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。
SelectionKey.channel()方法返回的通道需要轉型成你要處理的型別,如ServerSocketChannel或SocketChannel等。
wakeUp()
某個執行緒呼叫select()方法後阻塞了,即使沒有通道已經就緒,也有辦法讓其從select()方法返回。只要讓其它執行緒在第一個執行緒呼叫select()方法的那個物件上呼叫Selector.wakeup()方法即可。阻塞在select()方法上的執行緒會立馬返回。如果有其它執行緒呼叫了wakeup()方法,但當前沒有執行緒阻塞在select()方法上,下個呼叫select()方法的執行緒會立即“醒來(wake up)”。
close()
在介紹Selector之前先明確以下3個問題:
1、selector的作用是什麼?
2、selector如何做到網路通訊的?
下面就針對上面的2個問題來展開介紹:
1、Selector(選擇器)是Java NIO中能夠檢測一到多個NIO渠道,並能夠知曉渠道是否為諸如讀寫事件做好準備的元件。這樣,一個單獨的執行緒可以管理多個channel,從而管理多個網路連線。
2、selector 操作的過程
2.1 首先建立Selector
Selector selector = Selector.open();
2.2 向selector註冊channel,和感興趣的事件,
channel.configureBlocking(false);
SelectionKey key = channel.register(selector,Selectionkey.OP_READ);
注意register()方法的第二個引數, 這是一個interest集合,意思是在通過Selector監聽Channel時對什麼事件感興趣。可以監聽以下4中不同型別的事件:
服務端接收客戶端連線事件 SelectionKey.OP_ACCEPT
客戶端連線服務端事件 SelectionKey.OP_CONNECT
讀事件 SelectionKey.OP_READ
寫事件 SelectionKey.OP_WRITE
如果你對不止一種事件感興趣,那麼可以用“位或”操作符將常量連線起來如下:
SelectionKey key = channel.register(selector,Selectionkey.OP_READ|SelectionKey.OP_WRITE);
當向Selector註冊Channel時,registor()方法會返回一個SelectorKey物件。這個物件包含了一些你感興趣的屬性。
interest集合
ready集合
Channel
Selector
附加的物件(可選)
2.2.1 interest集合
就像向Selector註冊通道一節中所描述的,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集合中。
2.2.2 ready集合
ready 集合是通道已經準備就緒的操作的集合。在一次選擇(Selection)之後,你會首先訪問這個ready set。Selection將在下一小節進行解釋。可以這樣訪問ready集合:
int readySet = selectionKey.readyOps();
可以用像檢測interest集合那樣的方法,來檢測channel中什麼事件或操作已經就緒。但是,也可以使用以下四個方法,它們都會返回一個布林型別:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
2.2.3 Channel + Selector
從SelectionKey訪問Channel和Selector很簡單。如下:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
2.2.4 附加物件
可以將一個物件或者更多資訊附著到SelectionKey上,這樣就能方便的識別某個給定的通道。例如,可以附加 與通道一起使用的Buffer,或是包含聚集資料的某個物件。使用方法如下:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
還可以在用register()方法向Selector註冊Channel的時候附加物件。如:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
2.3 通過Selector選擇通道
一旦向Selector註冊了一或多個通道,就可以呼叫幾個過載的select()方法。這些方法返回你所感興趣的事件(如連線、接受、讀或寫)已經準備就緒的那些通道。
換句話說,如果你對“讀就緒”的通道感興趣,select()方法會返回讀事件已經就緒的那些通道。
下面是select()方法:(該方法是阻塞方法)
int select()
int select(long timeout)
int selectNow()
select()阻塞到至少有一個通道在你註冊的事件上就緒了。
select(long timeout)和select()一樣,除了最長會阻塞timeout毫秒(引數)。
selectNow()不會阻塞,不管什麼通道就緒都立刻返回(譯者注:此方法執行非阻塞的選擇操作。如果自從前一次選擇操作後,沒有通道變成可選擇的,則此方法直接返回零。)。
select()方法返回的int值表示有多少通道已經就緒。亦即,自上次呼叫select()方法後有多少通道變成就緒狀態。如果呼叫select()方法,因為有一個通道變成就緒狀態,返回了1,
若再次呼叫select()方法,如果另一個通道就緒了,它會再次返回1。如果對第一個就緒的channel沒有做任何操作,現在就有兩個就緒的通道,但在每次select()方法呼叫之間,只有一個通道就緒了。
2.3.1 selectedKeys()
一旦呼叫了select()方法,並且返回值表明有一個或更多個通道就緒了,然後可以通過呼叫selector的selectedKeys()方法,訪問“已選擇鍵集(selected key set)”中的就緒通道。如下所示:
Set selectedKeys = selector.selectedKeys();
當像Selector註冊Channel時,Channel.register()方法會返回一個SelectionKey 物件。這個物件代表了註冊到該Selector的通道。可以通過SelectionKey的selectedKeySet()方法訪問這些物件。
可以遍歷這個已選擇的鍵集合來訪問就緒的通道。如下:
Set selectedKeys = selector.selectedKeys();
Iterator 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();
}
這個迴圈遍歷已選擇鍵集中的每個鍵,並檢測各個鍵所對應的通道的就緒事件。
注意每次迭代末尾的keyIterator.remove()呼叫。Selector不會自己從已選擇鍵集中移除SelectionKey例項。必須在處理完通道時自己移除。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。
SelectionKey.channel()方法返回的通道需要轉型成你要處理的型別,如ServerSocketChannel或SocketChannel等。
wakeUp()
某個執行緒呼叫select()方法後阻塞了,即使沒有通道已經就緒,也有辦法讓其從select()方法返回。只要讓其它執行緒在第一個執行緒呼叫select()方法的那個物件上呼叫Selector.wakeup()方法即可。阻塞在select()方法上的執行緒會立馬返回。如果有其它執行緒呼叫了wakeup()方法,但當前沒有執行緒阻塞在select()方法上,下個呼叫select()方法的執行緒會立即“醒來(wake up)”。
close()
用完Selector後呼叫其close()方法會關閉該Selector,且使註冊到該Selector上的所有SelectionKey例項無效。通道本身並不會關閉。
總結:1、建立Selector,
2、建立Channel,()
3、向Selector中註冊Channel,及感興趣的事件,
4.1、等待註冊的事件到達,不然就一直等待 selector.select()
4.2、獲取selector中選中項的迭代器,選中的項為註冊的事件 Iterator ite = this.selector.selectedKeys().iterator();
4.3、針對選中的事件對感興趣的事件進行處理。
5、要想快速理解selector過程,結合程式碼來學習。
2.3.2 server端例項程式碼
- package com.anders.selector;
- import java.io.IOException;
- import java.net.InetSocketAddress;
- import java.nio.ByteBuffer;
- import java.nio.channels.SelectableChannel;
- import java.nio.channels.SelectionKey;
-
import
- import java.nio.channels.ServerSocketChannel;
- import java.nio.channels.SocketChannel;
- import java.util.Iterator;
- import java.util.Set;
- publicclass NIOServer {
- // 通道管理器
- private Selector selector;
- publicvoid initServer(int port) throws Exception {
- // 獲得一個ServerSocket通道
- ServerSocketChannel serverChannel = ServerSocketChannel.open();
- // 設定通道為 非阻塞
- serverChannel.configureBlocking(false);
- // 將該通道對於的serverSocket繫結到port埠
- serverChannel.socket().bind(new InetSocketAddress(port));
- // 獲得一耳光通道管理器
- this.selector = Selector.open();
- // 將通道管理器和該通道繫結,併為該通道註冊selectionKey.OP_ACCEPT事件
- // 註冊該事件後,當事件到達的時候,selector.select()會返回,
- // 如果事件沒有到達selector.select()會一直阻塞
- serverChannel.register(selector, SelectionKey.OP_ACCEPT);
- }
- // 採用輪訓的方式監聽selector上是否有需要處理的事件,如果有,進行處理
- publicvoid listen() throws Exception {
- System.out.println("start server");
- // 輪詢訪問selector
- while (true) {
- // 當註冊事件到達時,方法返回,否則該方法會一直阻塞
- selector.select();
- // 獲得selector中選中的相的迭代器,選中的相為註冊的事件
- Iterator ite = this.selector.selectedKeys().iterator();
- while (ite.hasNext()) {
- SelectionKey key = (SelectionKey) ite.next();
- // 刪除已選的key 以防重負處理
- ite.remove();
- // 客戶端請求連線事件
- if (key.isAcceptable()) {
- ServerSocketChannel server = (ServerSocketChannel) key.channel();
- // 獲得和客戶端連線的通道
- SocketChannel channel = server.accept();
- // 設定成非阻塞
- channel.configureBlocking(false);
- // 在這裡可以傳送訊息給客戶端
- channel.write(ByteBuffer.wrap(new String("hello client").getBytes()));
- // 在客戶端 連線成功之後,為了可以接收到客戶端的資訊,需要給通道設定讀的許可權
- channel.register(this.selector, SelectionKey.OP_READ);
- // 獲得了可讀的事件
- } elseif (key.isReadable()) {
- read(key);
- }
- }
- }
- }
- // 處理 讀取客戶端發來的資訊事件
- privatevoid read(SelectionKey key) throws Exception {
- // 伺服器可讀訊息,得到事件發生的socket通道
- SocketChannel channel = (SocketChannel) key.channel();
- // 穿件讀取的緩衝區
- ByteBuffer buffer = ByteBuffer.allocate(10);
- channel.read(buffer);
- byte[] data = buffer.array();
- String msg = new String(data).trim();
- System.out.println("server receive from client: " + msg);
- ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
- channel.write(outBuffer);
- }
- publicstaticvoid main(String[] args) throws Throwable {
- NIOServer server = new NIOServer();
- server.initServer(8989);
- server.listen();
- }
- }
2.3.3 client 例項程式碼
- package com.anders.selector;
- import java.io.IOException;
- import java.net.InetSocketAddress;
- import java.nio.ByteBuffer;
- import java.nio.channels.SelectionKey;
- import java.nio.channels.Selector;
- import java.nio.channels.SocketChannel;
- import java.util.Iterator;
- publicclass NIOClient {
- // 通道管理器
- private Selector selector;
- /**
- * * // 獲得一個Socket通道,並對該通道做一些初始化的工作 * @param ip 連線的伺服器的ip // * @param port
- * 連線的伺服器的埠號 * @throws IOException
- */
- publicvoid initClient(String ip, int port) throws IOException { // 獲得一個Socket通道
- SocketChannel channel = SocketChannel.open(); // 設定通道為非阻塞
- channel.configureBlocking(false); // 獲得一個通道管理器
- this.selector = Selector.open(); // 客戶端連線伺服器,其實方法執行並沒有實現連線,需要在listen()方法中調
- // 用channel.finishConnect();才能完成連線
- channel.connect(new InetSocketAddress(ip, port));
- // 將通道管理器和該通道繫結,併為該通道註冊SelectionKey.OP_CONNECT事件。
- channel.register(selector, SelectionKey.OP_CONNECT);
- }
- /**
- * * // 採用輪詢的方式監聽selector上是否有需要處理的事件,如果有,則進行處理 * @throws // IOException
- * @throws Exception
- */
- @SuppressWarnings("unchecked")
- publicvoid listen() throws Exception { // 輪詢訪問selector
- while (true) {
- // 選擇一組可以進行I/O操作的事件,放在selector中,客戶端的該方法不會阻塞,
- // 這裡和服務端的方法不一樣,檢視api註釋可以知道,當至少一個通道被選中時,
- // selector的wakeup方法被呼叫,方法返回,而對於客戶端來說,通道一直是被選中的
- selector.select(); // 獲得selector中選中的項的迭代器
- Iterator ite = this.selector.selectedKeys().iterator();
- while (ite.hasNext()) {
- SelectionKey key = (SelectionKey) ite.next(); // 刪除已選的key,以防重複處理
- ite.remove(); // 連線事件發生
- if (key.isConnectable()) {