Java?NIO?中?Selector?解析
一、Selector 簡(jiǎn)介
1、Selector 和 Channel 關(guān)系
Selector 一般稱為選擇器,可以翻譯為 多路復(fù)用。它是 Java NIO 核心組件中的一個(gè),用于檢查一個(gè)或者多個(gè) NIO Channel (通道) 的狀態(tài)是否處于可讀、可寫(xiě)。如此可以實(shí)現(xiàn)單線程管理多個(gè) Channels , 也就是可以管理多個(gè)網(wǎng)絡(luò)鏈接。

使用 Selector 的好處在于:使用更少的線程就可以來(lái)處理通道了,相比使用多個(gè)線程,避免了線程上下文切換帶來(lái)的開(kāi)銷。
2、可選擇通道(SelectableChannel)
(1)不是所有的 Channel 都是可以被 Selector 復(fù)用的。比方說(shuō), FileChannel 就不能被選擇器復(fù)用。判斷一個(gè) Channel 能被 Selector 復(fù)用,有一個(gè)前提:判斷他是否繼承了一個(gè)抽象類 SelectableChannel。如果繼承了 SelectableChannel , 則可以被復(fù)用,否則不能。
(2)SelectableChannel 提供了實(shí)現(xiàn)通道選擇性所需要的公共方法。它是所有支持就緒檢查通道類的父類,所有 socket 通道,都繼承 SelectableChannel 類都是可選擇的,包括從管道(Pipe) 對(duì)象的中獲取得到的通道。而 FileChannel 類,沒(méi)有繼承 SelectableChannel , 因此是不是可選通道。
(3)一個(gè)通道可以被注冊(cè)到多個(gè)選擇器上,但對(duì)每個(gè)選擇器而言只能被注冊(cè)一次。通道和選擇器之間的關(guān)系,使用注冊(cè)的方式完成。SelectableChannel 可以被注冊(cè)到 Selector 對(duì)象上,在注冊(cè)時(shí)候,需要指定通道的那些操作,是 Selector 感興趣的。

3、Channel 注冊(cè)到 Selector
(1)使用Channel.register(Selector sel, int pos) 方法,將一個(gè)通道注冊(cè)到一個(gè)選擇器時(shí)。第一個(gè)參數(shù),指定通道要注冊(cè)的選擇器。第二個(gè)參數(shù)指定選擇器需要查詢的通道操作。
(2)可供選擇器查詢的通道操作,從類型類分,包括一下四種:
- 可讀:SelectionKey.OP_READ
- 可寫(xiě):SelectionKey.OP_WRITE
- 連接:SelectionKey.OP_CONNECT
- 接收:SelectionKey.OP_ACCEPT
如果 Selector 對(duì)通道的多操作類型感興趣,可以用“位或”操作符來(lái)實(shí)現(xiàn):
比如int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
(3)選擇器查詢的不是通道的操作,而是通道的某個(gè)操作的一種就緒狀態(tài)。什么操作的就緒狀態(tài)?一旦通道具備完成某個(gè)操作的條件,表示該通道的某個(gè)操作已經(jīng)就緒,就可以被 Selector 查詢到,程序可以對(duì)通道進(jìn)行對(duì)應(yīng)的操作。比方說(shuō),某個(gè) SocketChannel 通道可以連接到一個(gè)服務(wù)器,則處于“連接就緒”狀態(tài)(OP_CONNECT)。 再比方說(shuō),一個(gè)ServerSocketChannel 服務(wù)器通道準(zhǔn)備好接收新進(jìn)入的連接,則處于“接收就緒”(OP_ACCEPT)狀態(tài)。還比方說(shuō),一個(gè)數(shù)據(jù)可讀的通道,可以說(shuō)是“讀就緒”(OP_READ)。一個(gè)等待寫(xiě)數(shù)據(jù)的通道可以說(shuō)是“寫(xiě)就緒”(OP_WRITE)。
4、選擇鍵(SelectionKey)
(1)Channel 注冊(cè)之后,并且一旦通道處于某種就緒狀態(tài),就可以被選擇器查詢到。這個(gè)工作使用選擇器 Selector 的 select() 方法完成。select 方法的作用,對(duì)感興趣的通道操作,進(jìn)行就緒狀態(tài)的查詢。
(2)Selector 可以不斷的查詢 Channel 中發(fā)生的操作的就緒狀態(tài)。并且選擇甘心去的操作就緒狀態(tài)。一旦通道有操作的就緒狀態(tài)達(dá)成,并且是 Selecor 感興趣的操作,就會(huì)被 Selector 選中,放入選擇鍵集合中。
(3)一個(gè)選擇鍵,首先包含了注冊(cè)在 Selector 的通道操作的類型,比方說(shuō): SelectionKey.OP_READ . 也包含了特定的通道與特定的選擇器之間的注冊(cè)關(guān)系。
開(kāi)發(fā)應(yīng)用程序是,選擇鍵是編程的關(guān)鍵,NIO 編程,就是更具對(duì)應(yīng)的選擇鍵,進(jìn)行不同的業(yè)務(wù)邏輯處理。
(4)選擇鍵的概念,和事件的概念比較相似。一個(gè)選擇鍵類似監(jiān)聽(tīng)器模式里面的一個(gè)事件。由于 Selector 不是事件觸發(fā)的模式,而是主動(dòng)去查詢的模式,所以不叫事件 Event, 而是叫 SelectionKey 選擇鍵。
二、Selector 的使用方法
1、Selector 的創(chuàng)建
通過(guò) Selector.open() 方法創(chuàng)建一個(gè) Selector 對(duì)象。如下;
// 獲取 Selector 選擇器 Selector selector = Selector.open();
2、注冊(cè) Channel 到 Selector
要實(shí)現(xiàn) Selector 管理 Channel , 需要將 channel 注冊(cè)到相應(yīng)的 Selector 上
// 1. 獲取 Selector 選擇器 Selector selector = Selector.open(); // 2. 獲取通道 ServerSocketChannel socketChannel = ServerSocketChannel.open(); // 3. 設(shè)置為非阻塞 socketChannel.configureBlocking(false); // 4. 綁定連接 socketChannel.bind(new InetSocketAddress(9999)); // 5. 將通道注冊(cè)到選擇器 socketChannel.register(selector, SelectionKey.OP_ACCEPT);
上面通過(guò)調(diào)用通道的 register() 方法會(huì)將它注冊(cè)到一個(gè)選擇器上。
需要注意的是:
(1)與 Selector 一起使用, channel 必須處于非阻塞模式下,否則將拋出異常 IllegalBlockingModeException 。 這意味著,F(xiàn)ileChannel 不能與 Selector 一起使用,因?yàn)?FileChannel 不能切換到非阻塞模式,而套接字相關(guān)的所有通道都可以。
(2)一個(gè)通道,并沒(méi)有一定要持有所有的四種操作。比如服務(wù)器通道 ServerSocketChannel 支持 Accept 接收操作,而 SocketChannel 客戶端通道則不支持??梢酝ㄟ^(guò)通道上的 vildOps() 方法,來(lái)獲取特定通道下所支持的操作集合。
3、輪訓(xùn)查詢就緒操作
(1) 通過(guò) Selector 的 select() 方法, 可以查詢出已經(jīng)就緒的通道操作,有些就緒的狀態(tài)集合,包含在一個(gè)元素是 Selectionkey 對(duì)象的 Set 集合中
(2) 下面是 Selector 幾個(gè)重載的查詢 select() 方法:
select()阻塞到至少有一個(gè)通道在你注冊(cè)的事件上就緒。select(long timeout)和select()一樣,但最長(zhǎng)阻塞事件為 timeout 毫秒。selectNow()非阻塞,只要有通道就立即返回。select()方法返回的 int 之,表示有多少通道已經(jīng)就緒,準(zhǔn)確的說(shuō)目前一次 select
方法來(lái)到這一次 select 方法之間的時(shí)間段上,有多少個(gè)通道編程了就緒狀態(tài)。
例如:首次調(diào)用 select() 方法,如果有一個(gè)通道編程了就緒狀態(tài),返回了 1 , 若子啊次調(diào)用 select() 方法,如果另外一個(gè)通道就緒了,它會(huì)再次返回 1。 如果第一個(gè)就緒的 chnanel 么有做任何操作,現(xiàn)在就有兩個(gè)就緒通道,但是每次 select() 方法調(diào)用之間,只有一個(gè)通道就緒了。
一旦調(diào)用 select() 方法,并且返回值部位 0 時(shí),在 Selector 中有一個(gè) seletedKeys() 方法,用來(lái)范圍已選擇鍵集合,迭代集合的每個(gè)以元素,根據(jù)就緒操作的類型,完成對(duì)應(yīng)的操作
// 查詢已經(jīng)就緒的通道操作
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
? ? SelectionKey key = iterator.next();
? ? // 判斷 key 就緒狀態(tài)操作
? ? 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
? ? }
}
iterator.remove();4、停止選擇的方法
選擇器執(zhí)行選擇的過(guò)程匯總,系統(tǒng)底層會(huì)依次詢問(wèn)每個(gè)通道是否已經(jīng)就緒,這個(gè)過(guò)程可能會(huì)造成調(diào)用線程進(jìn)入阻塞狀態(tài),那么我們有一下三種方式可以喚醒在 select()方法中阻塞的線程。
wakeup() 方法:通過(guò)調(diào)用 Selector 對(duì)象的 wakeup() 方法讓處于阻塞狀態(tài)的 select() 方法立刻返回
該方法使得選擇器上的第一個(gè)哈沒(méi)有返回的選擇操作立即返回。如果當(dāng)前沒(méi)有進(jìn)行中的選擇操作,那么下一次會(huì)對(duì) select() 方法的一次調(diào)用立即返回。
close() 方法: 通過(guò) close() 方法關(guān)閉 selector
該方法使得任何一個(gè)在選擇操作中阻塞的線程都被喚醒(類似 wakeup()) , 同時(shí)使的注冊(cè)到該 Selector 的所有 Channel 被注銷,所有的鍵都被取消,但是 Channel 本身不會(huì)關(guān)閉。
三、示例代碼
1、服務(wù)端代碼
@Test
public void server() throws IOException {
? ? //1. 獲取服務(wù)端通道
? ? ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
? ? //2. 切換非阻塞模式
? ? serverSocketChannel.configureBlocking(false);
? ? //3. 創(chuàng)建 buffer
? ? ByteBuffer readBuffer = ByteBuffer.allocate(1024);
? ? ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
? ? writeBuffer.put("收到了。。。。".getBytes(StandardCharsets.UTF_8));
? ? //4. 綁定端口號(hào)
? ? serverSocketChannel.bind(new InetSocketAddress(20000));
? ? //5. 獲取 selector 選擇器
? ? Selector selector = Selector.open();
? ? //6. 通道注冊(cè)到選擇器,進(jìn)行監(jiān)聽(tīng)
? ? serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
? ? //7. 選擇器進(jìn)行輪訓(xùn),進(jìn)行后續(xù)操作
? ? while (selector.select() > 0) {
? ? ? ? Set<SelectionKey> selectionKeys = selector.selectedKeys();
? ? ? ? Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
? ? ? ? // 循環(huán)
? ? ? ? while (selectionKeyIterator.hasNext()) {
? ? ? ? ? ? // 獲取就緒狀態(tài)
? ? ? ? ? ? SelectionKey k = selectionKeyIterator.next();
? ? ? ? ? ? // 操作判斷
? ? ? ? ? ? if (k.isAcceptable()) {
? ? ? ? ? ? ? ? // 獲取連接
? ? ? ? ? ? ? ? SocketChannel accept = serverSocketChannel.accept();
? ? ? ? ? ? ? ? // 切換非阻塞模式
? ? ? ? ? ? ? ? accept.configureBlocking(false);
? ? ? ? ? ? ? ? // 注冊(cè)
? ? ? ? ? ? ? ? accept.register(selector, SelectionKey.OP_READ);
? ? ? ? ? ? } else if (k.isReadable()) {
? ? ? ? ? ? ? ? SocketChannel socketChannel = (SocketChannel) k.channel();
? ? ? ? ? ? ? ? readBuffer.clear();
? ? ? ? ? ? ? ? socketChannel.read(readBuffer);
? ? ? ? ? ? ? ? readBuffer.flip();
? ? ? ? ? ? ? ? System.out.println("received:" + new String(readBuffer.array(), StandardCharsets.UTF_8));
? ? ? ? ? ? ? ? k.interestOps(SelectionKey.OP_WRITE);
? ? ? ? ? ? } else if (k.isWritable()) {
? ? ? ? ? ? ? ? writeBuffer.rewind();
? ? ? ? ? ? ? ? SocketChannel socketChannel = (SocketChannel) k.channel();
? ? ? ? ? ? ? ? socketChannel.write(writeBuffer);
? ? ? ? ? ? ? ? k.interestOps(SelectionKey.OP_READ);
? ? ? ? ? ? }
? ? ? ? }
? ? }
}2、客戶端代碼
@Test
public void client() throws IOException {
? ? //1. 獲取通道,綁定主機(jī)和端口號(hào)
? ? SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(20000));
? ? //2. 切換到非阻塞模式
? ? socketChannel.configureBlocking(false);
? ? //3. 創(chuàng)建 buffer
? ? ByteBuffer buffer = ByteBuffer.allocate(1024);
? ? //4. 寫(xiě)入 buffer 數(shù)據(jù)
? ? buffer.put(new Date().toString().getBytes(StandardCharsets.UTF_8));
? ? //5. 模式切換
? ? buffer.flip();
? ? //6. 寫(xiě)入通道
? ? socketChannel.write(buffer);
? ? //7. 關(guān)閉
? ? buffer.clear();
? ? socketChannel.close();
}3、NIO 編程步驟總結(jié)
- 1、創(chuàng)建一個(gè) ServerSocketChannel 通道
- 2、設(shè)置為非阻塞模式
- 3、創(chuàng)建一個(gè) Selector 選擇器
- 4、Channel 注冊(cè)到選擇器中,監(jiān)聽(tīng)連接事件
- 5、調(diào)用 Selector 中的 select 方法(循環(huán)調(diào)用),監(jiān)聽(tīng)通道是否是就緒狀態(tài)
- 6、調(diào)用 SelectKeys() 方法就能獲取 就緒 channel 集合
- 7、遍歷就緒的 channel 集合,判斷就緒事件類型,實(shí)現(xiàn)具體的業(yè)務(wù)操作。
- 8、根據(jù)業(yè)務(wù)流程,判斷是否需要再次注冊(cè)事件監(jiān)聽(tīng)事件,重復(fù)執(zhí)行。
到此這篇關(guān)于Java NIO 中 Selector 解析的文章就介紹到這了,更多相關(guān)Java NIO 中的Selector內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
關(guān)于Spring MVC同名參數(shù)綁定問(wèn)題的解決方法
Spring MVC中的參數(shù)綁定還是蠻重要的,最近在使用中遇到了同名參數(shù)綁定的問(wèn)題,想著總結(jié)分享出來(lái),下面這篇文章主要給大家介紹了關(guān)于Spring MVC同名參數(shù)綁定問(wèn)題的解決方法,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-08-08
Java使用pulsar-flink-connector讀取pulsar catalog元數(shù)據(jù)代碼剖析
這篇文章主要介紹了Java使用pulsar-flink-connector讀取pulsar catalog元數(shù)據(jù)代碼剖析,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08
springmvc Controller方法沒(méi)有加@ResponseBody導(dǎo)致api訪問(wèn)404問(wèn)題
這篇文章主要介紹了springmvc Controller方法沒(méi)有加@ResponseBody導(dǎo)致api訪問(wèn)404問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01
解析Java中的Timer和TimerTask在Android中的用法和實(shí)例
本篇文章主要介紹了解析Java中的Timer和TimerTask在Android中的用法,主要介紹了Timer和TimerTask的用法,有需要的可以了解一下。2016-11-11
PrintStream和PrintWriter的區(qū)別簡(jiǎn)介
這篇文章主要介紹了PrintStream和PrintWriter的區(qū)別簡(jiǎn)介,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01
詳解mall整合SpringBoot+MyBatis搭建基本骨架
這篇文章主要介紹了詳解mall整合SpringBoot+MyBatis搭建基本骨架,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-08-08
Spring boot 跳轉(zhuǎn)到j(luò)sp頁(yè)面的實(shí)現(xiàn)方法
本篇文章主要介紹了Spring boot 跳轉(zhuǎn)到j(luò)sp頁(yè)面的實(shí)現(xiàn)方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-04-04
Java 代碼檢查工具之PMD入門(mén)使用詳細(xì)教程
這篇文章主要介紹了Java 代碼檢查工具之PMD入門(mén)使用詳細(xì)教程,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03

