在之前講解的網絡相關的channel,都有講到非阻塞模式,只簡單說明了那些方法在非阻塞模式下的返回情況,並沒有實際的應用;本節要講到的selector就是NIO中非阻塞模式使用的一大優點;
一、概述
selector,選擇器,同過一個選擇器,程序可以通過一個線程處理多個channel,而不需要像之前ServerSocketChannel那樣每接收一個請求都單開一個線程處理通信;selector基於事件驅動的方式處理多個通道I/O;
二、selector使用
1、創建
一個selector的創建,都是通過簡單open靜態方法獲取:
Selector selector = Selector.open();
2、通道註冊
通道要通過selector管理,必須先將通道註冊到一個selector上:
serverChannel.configureBlocking(false);
SelectionKey key = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
1.register方法是在SelectableChannel抽象類中定義的,所以只有基礎了SelectableChannel類的通道類型纔可註冊到selector,如ServerSocketChannel、SocketChannel,而且通道必須是設置爲非阻塞模式,所以想FileChannel通道,是不能與selector配合使用的;
2.register方法中的第二個參數,表示這個通道註冊所感興趣的事件,總共有4個取值:
connect-連接事件
accept-連接接收事件
read-讀事件
write-寫事件
如果有多個感興趣事件,入參可以使用 | 操作
3.register方法的返回值是一個SelectionKey對象,後面再詳細講解這個對象;
3、通過selector選擇通道
- 向一個selector註冊完一個或多個通道後,就可以通過三個select方法獲取感興趣事件已就緒的通道:
int select() 阻塞直至有一個註冊通道的事件發送
int select(long timeout) 阻塞超時時間爲timeout
int selectNow() 不阻塞,立即返回,如沒有通道事件發生,返回值爲0
select方法返回的int值表示有多少個通道在上一次select後發生了註冊感興趣事件
select方法在阻塞期間,如果有其它線程調用了selector的wakeUp方法,正在阻塞的select方法會立即返回,如果wakeUp方法調用時,selector沒有select方法在阻塞,那麼下次有調用select方法會立即返回;
調用select方法得知有一個或多個通道就緒後,通過selectedKeys方法獲取已選擇鍵值(select key set)
Set<SelectionKey> selectedKeys = selector.selectedKeys();
註冊通道時register方法也是會返回一個SelectionKey對象,可以認爲該對象包裝了對應的通道,可以通過SelectionKey對象獲取以下內容:
//獲取註冊的感興趣事件集,可以通過
//interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;或者
//key.isConnectable(); 判斷事件類型
int interestOps = key.interestOps();
//獲取已經就緒的事件集,類型判斷同上
int readyOps = key.readyOps();
//獲取附加對象,該對象可以通過key.attach(obj);方法添加
//也可以在通道註冊的時候通過register方法第三個參數帶入
//改附加對象可以是通道實用緩存區、用於判斷通道的標識等
Object attachment = key.attachment();
//獲取這個key所對應的通道對象
SelectableChannel channel2 = key.channel();
//獲取這個key所對應的selector
Selector selector = key.selector();
- 獲取到已選擇鍵值(其實是已就緒的通道),就可以遍歷處理這個就緒的集合了,一般方式如下:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()) {
//連接請求時間
} else if (selectionKey.isReadable()) {
//可讀事件
}
else if (selectionKey.isConnectable()) {
//連接事件
}
else if (selectionKey.isWritable()){
//可寫事件
}
//處理完成後,需要移除
iterator.remove();
}
通過4個isXXXXable判斷事件類型,並可類型轉換爲對應的通道類型處理IO事件;
每次循環後需要移除處理完的事件,否則下次selectedKeys()還會再次獲取到這事件;
三、實例說明
在ServerSocketChannel與SocketChannel一節的例子中,演示一個簡單的網絡通信:服務端使用主線程接收請求,每成功接收到一個請求後,創建一個獨立的線程處理與客戶端的通信;本節使用selector改造這個演示,只使用一個線程處理請求和通信:
客戶端:
public class ChannelSelector {
public static void main(String args[]) throws IOException {
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(1234));
serverChannel.configureBlocking(false);
Selector selector = Selector.open();
SelectionKey key = serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int select = selector.select();
if (select > 0) {
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 接收連接請求
if (selectionKey.isAcceptable()) {
ServerSocketChannel channel = (ServerSocketChannel) selectionKey
.channel();
SocketChannel socketChannel = channel.accept();
System.out.println("接收到連接請求:"
+ socketChannel.getRemoteAddress().toString());
socketChannel.configureBlocking(false);
//每接收請求,註冊到同一個selector中處理
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
// read
receiveMessage(selectionKey);
}
iterator.remove();
}
}
}
}
public static void receiveMessage(SelectionKey selectionKey)
throws IOException {
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
String remoteName = socketChannel.getRemoteAddress().toString();
ByteBuffer buffer = ByteBuffer.allocate(1024);
ByteBuffer sizeBuffer = ByteBuffer.allocate(4);
StringBuilder sb = new StringBuilder();
byte b[];
try {
sizeBuffer.clear();
int read = socketChannel.read(sizeBuffer);
if (read != -1) {
sb.setLength(0);
sizeBuffer.flip();
int size = sizeBuffer.getInt();
int readCount = 0;
b = new byte[1024];
// 讀取已知長度消息內容
while (readCount < size) {
buffer.clear();
read = socketChannel.read(buffer);
if (read != -1) {
readCount += read;
buffer.flip();
int index = 0;
while (buffer.hasRemaining()) {
b[index++] = buffer.get();
if (index >= b.length) {
index = 0;
sb.append(new String(b, "UTF-8"));
}
}
if (index > 0) {
sb.append(new String(b, "UTF-8"));
}
}
}
System.out.println(remoteName + ":" + sb.toString());
}
} catch (Exception e) {
System.out.println(remoteName + " 斷線了,連接關閉");
try {
//取消這個通道的註冊,關閉資源
selectionKey.cancel();
socketChannel.close();
} catch (IOException ex) {
}
}
}
}
服務端還是與之前一樣:
public class SocketChanneClient {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(1234));
while (true) {
Scanner sc = new Scanner(System.in);
String next = sc.nextLine();
sendMessage(socketChannel, next);
}
}
public static void sendMessage(SocketChannel socketChannel, String mes) throws IOException {
if (mes == null || mes.isEmpty()) {
return;
}
byte[] bytes = mes.getBytes("UTF-8");
int size = bytes.length;
ByteBuffer buffer = ByteBuffer.allocate(size);
ByteBuffer sizeBuffer = ByteBuffer.allocate(4);
sizeBuffer.putInt(size);
buffer.put(bytes);
buffer.flip();
sizeBuffer.flip();
ByteBuffer dest[] = {sizeBuffer,buffer};
System.out.println("send message size=" + size + ",content=" + mes);
while (sizeBuffer.hasRemaining() || buffer.hasRemaining()) {
socketChannel.write(dest);
}
}
}
四、selector非阻塞IO的優點
1、阻塞IO的缺點:
- 當客戶端連接多時,需要使用大量線程處理,佔用更多的系統資源;
- 多個線程間的切換許多情況下是無意義的,因爲未知阻塞時間;
2、非阻塞IO的優點:
- 由一個線程來專門處理所有IO事件,並可分發;
- 基於事件驅動機制,當事件就緒時觸發,而不是同步監視事件;