Java NIO系列(四) - Selector
前言
Selector
是 Java NIO
中的一個組件,用於檢查一個或多個通道 Channel
的狀態是否處於可讀、可寫狀態。如此可以實現單線程管理多個通道,也就是可以管理多個網絡連接。
為什麽使用Selector?
用單線程處理多個 Channel
的好處是我需要更少的線程來處理 Channel
。實際上,你甚至可以用一個線程來處理所有的Channel
。從操作系統的角度來看,切換線程的開銷是比較昂貴的,並且每個線程都需要占用系統資源,因此暫用線程越少越好。
簡而言之,通過 Selector
我們可以實現單線程操作多個 Channel
。下面是單線程使用一個 Selector
處理 3
個 Channel
正文
Selector的組件
Java NIO Selector
中有三個重要的組成:Selector
、SelectableChannel
和 SelectionKey
。
(一) 選擇器(Selector)
Selector
選擇器類管理著一個被註冊的通道集合的信息和它們的就緒狀態。選擇器所在線程不停地更新通道的就緒狀態,對通道註冊的連接、數據讀寫事件等事件進行響應。
(二) 可選擇通道(SelectableChannel)
SelectableChannel
是一個抽象類,提供了通道的可選擇性所需要的公共方法的實現,它是所有支持就緒檢查的通道類的父類。
因為 FileChannel
SelectableChannel
,因此不是可選通道。而所有 Socket
通道都是可選擇的,包括從管道 (Pipe
) 對象的中獲得的通道。SelectableChannel
可以被註冊到 Selector
對象上,並且註冊時可以指定感興趣的事件操作,比如:數據讀取、數據寫入操作。一個通道可以被註冊到多個選擇器上,但對每個選擇器而言只能被註冊一次。
(三) 選擇鍵(SelectionKey)
選擇鍵封裝了特定的通道與特定的選擇器的註冊關系。選擇鍵對象由被 SelectableChannel.register()
返回並提供一個表示這種註冊關系的標記。選擇鍵包含了兩個比特集(以整數的形式進行編碼),指示了該註冊關系所關心的通道操作
Selector的使用
(一) 創建Selector對象
Selector
對象是通過調用靜態工廠方法 open()
來實例化的,如下:
1
|
Selector Selector = Selector.open();
|
(二) 將SelectableChannel註冊到Selector
為了將 Channel
和 Selector
配合使用,必須將 Channel
註冊到 Selector
上。通過 SelectableChannel.register()
方法來實現,如下:
1
|
channel.configureBlocking(false);
|
與 Selector
一起使用時,Channel
必須處於非阻塞模式下。這意味著不能將 FileChannel
與 Selector
一起使用,因為 FileChannel
不能切換到非阻塞模式,而套接字通道都可以。
註意 register()
方法的第二個參數。這是一個興趣 (interest
) 集合,意思是在通過 Selector
監聽 Channel
時對什麽事件感興趣。可以監聽四種不同類型的事件:
- 連接操作(Connect):監聽
SocketChannel
到來的連接事件。 - 接受操作(Accept):對應常量
SelectionKey.OP_ACCEPT
,專註於監聽ServerSocketChannel
接受SocketChannel
的事件。 - 讀操作(Read):對應常量
SelectionKey.OP_READ
,監聽數據完全到達,通道可讀的事件。 - 寫操作(Write):對應常量
SelectionKey.OP_READ
,監聽數據準備完成,通道可寫的事件。
註意:並非所有的操作在所有的可選擇通道上都能被支持。比如
ServerSocketChannel
支持Accept
操作,而SocketChannel
中不支持。我們可以通過通道上的validOps()
方法來獲取特定通道下所有支持的操作集合。
以上四種事件用 SelectionKey
的四個常量來表示:
1
|
public static final int OP_READ = 1 << 0; // 1
|
如果一個通道同時對多種操作感興趣,可以用 “位或” 操作符將常量連接起來,如下:
1
|
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
|
(三) 為SelectionKey綁定附加對象
可以將一個對象或者更多信息附著到 SelectionKey
上,這樣就能方便的識別某個給定的通道。例如,可以附加與通道一起使用的 Buffer
,或是包含聚集數據的某個對象。使用方法如下:
1
|
selectionKey.attach(theObject);
|
還可以在用 register()
方法向 Selector
註冊 Channel
的時候附加對象,例如:
1
|
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
|
如果要取消該對象,則可以通過該種方式:
1
|
selectionKey.attach(null);
|
(四) 通過Selector選擇通道
一旦向 Selector
註冊了一或多個通道,就可以調用幾個重載的 select()
方法。這些方法返回你所感興趣的事件 (如連接、接受、讀或寫) 已經準備就緒的那些通道。換句話說,如果你對“讀就緒”的通道感興趣,select()
方法會返回讀事件已經就緒的那些通道的 SelectionKey
。
下面是 select()
方法的幾個重載:
- int select():阻塞到至少有一個通道在此選擇器註冊的事件上就緒了。
- int select(long timeout):
select(long timeout)
和select()
一樣,除了最長會阻塞timeout
毫秒(參數)。 - int selectNow():不會阻塞,不管什麽通道就緒都立刻返回。如果沒有通道變成可選擇的,則此方法直接返回
0
。
也可以通過遍歷 SelectionKey
上的已選擇鍵集合來訪問就緒的通道,如下:
1
|
Set<SelectionKey> selectedKeys = selector.selectedKeys();
|
註意:每次叠代完成時
Selector
自己不會將已經處理完成的SelectionKey
實例移除,在叠代的末尾需要調用keyIterator.remove()
方法手動移除。
SelectionKey.channel()
方法返回的通道需要強轉為你要處理的類型,如:ServerSocketChannel
或 SocketChannel
等。
Selector完整實例
服務端代碼
1
|
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
|
服務端操作過程
- 創建
ServerSocketChannel
實例,設置為非阻塞模式,並綁定指定的服務端口; - 創建
Selector
實例; - 將
serverSocketChannel
註冊到selector
上面,並指定事件OP_ACCEPT
,最底層的socket
通過channel
和selector
建立關聯; - 如果沒有準備好 (
Accept
) 的socket
,select
方法會被阻塞一段時間並返回0
; - 如果底層有
socket
已經準備好,selector
的select()
方法會返回socket
的個數,而且selectedKeys
方法會返回socket
對應的事件(connect
、accept
、read
和write
); - 根據事件類型,進行不同的處理邏輯。
總結
這裏簡單的介紹了 Java NIO
中選擇器的用法,有關 Selector
底層的實現原理需要進一步查看源碼。
歡迎關註技術公眾號: 零壹技術棧
本帳號將持續分享後端技術幹貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分布式和微服務,架構學習和進階等學習資料和文章。
Java NIO系列(四) - Selector