1. 程式人生 > >Java NIO系列(四) - Selector

Java NIO系列(四) - Selector

locking 一個表 非阻塞 進一步 gis 原理 關系 itl figure

前言

SelectorJava NIO 中的一個組件,用於檢查一個多個通道 Channel 的狀態是否處於可讀可寫狀態。如此可以實現單線程管理多個通道,也就是可以管理多個網絡連接

為什麽使用Selector?

單線程處理多個 Channel 的好處是我需要更少的線程來處理 Channel 。實際上,你甚至可以用一個線程來處理所有的Channel。從操作系統的角度來看,切換線程的開銷是比較昂貴的,並且每個線程都需要占用系統資源,因此暫用線程越少越好。

簡而言之,通過 Selector 我們可以實現單線程操作多個 Channel。下面是單線程使用一個 Selector 處理 3Channel

的示例圖:

技術分享圖片


正文

Selector的組件

Java NIO Selector中有三個重要的組成:SelectorSelectableChannelSelectionKey

(一) 選擇器(Selector)

Selector選擇器類管理著一個被註冊通道集合的信息和它們的就緒狀態選擇器所在線程不停地更新通道的就緒狀態,對通道註冊的連接數據讀寫事件等事件進行響應。

(二) 可選擇通道(SelectableChannel)

SelectableChannel 是一個抽象類,提供了通道可選擇性所需要的公共方法的實現,它是所有支持就緒檢查通道類父類

因為 FileChannel

類沒有繼承 SelectableChannel,因此不是可選通道。而所有 Socket 通道都是可選擇的,包括從管道 (Pipe) 對象的中獲得的通道。
SelectableChannel 可以被註冊到 Selector 對象上,並且註冊時可以指定感興趣的事件操作,比如:數據讀取數據寫入操作。一個通道可以被註冊到多個選擇器上,但對每個選擇器而言只能被註冊一次

(三) 選擇鍵(SelectionKey)

選擇鍵封裝了特定的通道特定的選擇器的註冊關系。選擇鍵對象由被 SelectableChannel.register() 返回並提供一個表示這種註冊關系的標記。選擇鍵包含了兩個比特集(以整數的形式進行編碼),指示了該註冊關系所關心的通道操作

,以及通道已經準備好的操作。

Selector的使用

(一) 創建Selector對象

Selector 對象是通過調用靜態工廠方法 open() 來實例化的,如下:

1
Selector Selector = Selector.open();

(二) 將SelectableChannel註冊到Selector

為了將 ChannelSelector 配合使用,必須將 Channel 註冊到 Selector 上。通過 SelectableChannel.register() 方法來實現,如下:

1
2
3
channel.configureBlocking(false);
// 對讀操作感興趣,向Selector註冊讀事件
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);

Selector 一起使用時,Channel 必須處於非阻塞模式下。這意味著不能將 FileChannelSelector 一起使用,因為 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
2
3
4
public static final int OP_READ = 1 << 0;  // 1
public static final int OP_WRITE = 1 << 2; // 4
public static final int OP_CONNECT = 1 << 3; // 8
public static final int OP_ACCEPT = 1 << 4; // 16

如果一個通道同時對多種操作感興趣,可以用 “位或” 操作符將常量連接起來,如下:

1
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

(三) 為SelectionKey綁定附加對象

可以將一個對象或者更多信息附著到 SelectionKey 上,這樣就能方便的識別某個給定的通道。例如,可以附加與通道一起使用的 Buffer,或是包含聚集數據的某個對象。使用方法如下:

1
2
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

還可以在用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// 一個連接被ServerSocketChannel接受
} else if (key.isConnectable()) {
// 與遠程服務器建立了連接
} else if (key.isReadable()) {
// 一個channel做好了讀準備
} else if (key.isWritable()) {
// 一個channel做好了寫準備
}
keyIterator.remove();
}

註意:每次叠代完成時 Selector 自己不會將已經處理完成的 SelectionKey實例移除,在叠代的末尾需要調用 keyIterator.remove() 方法手動移除。

SelectionKey.channel() 方法返回的通道需要強轉為你要處理的類型,如:ServerSocketChannelSocketChannel 等。

Selector完整實例

服務端代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.socket().bind(new InetSocketAddress(port));

Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {
int number = selector.select();
if (number == 0)
continue;

Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
// 獲取客戶端通道
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
socketChannel.configureBlocking(false);
// 將客戶端通道註冊到選擇器上
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(bufferSize));
}
if (selectionKey.isReadable()) {
handleRead(selectionKey);
}
if (selectionKey.isWritable()) {
handleWrite(selectionKey);
}
if (selectionKey.isConnectable()) {
System.out.println("Isonnectable := true");
}
iterator.remove();
}
}

服務端操作過程

  1. 創建 ServerSocketChannel 實例,設置為非阻塞模式,並綁定指定的服務端口
  2. 創建 Selector 實例;
  3. serverSocketChannel 註冊到 selector 上面,並指定事件 OP_ACCEPT,最底層的 socket 通過 channelselector 建立關聯;
  4. 如果沒有準備好 (Accept) 的socketselect方法會被阻塞一段時間並返回 0
  5. 如果底層有 socket 已經準備好,selectorselect() 方法會返回 socket 的個數,而且 selectedKeys 方法會返回 socket 對應的事件(connectacceptreadwrite);
  6. 根據事件類型,進行不同的處理邏輯。

總結

這裏簡單的介紹了 Java NIO 中選擇器的用法,有關 Selector 底層的實現原理需要進一步查看源碼。


歡迎關註技術公眾號: 零壹技術棧

技術分享圖片零壹技術棧

本帳號將持續分享後端技術幹貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分布式和微服務,架構學習和進階等學習資料和文章。

Java NIO系列(四) - Selector