前面兩篇文章介紹了NIO中的Buffer和Channel,有了之前的基礎,這篇文章來介紹一下另一個比較重要的概念----Selector。我們知道系統執行緒的切換是消耗系統資源的,如果我們每一個連線都用一個執行緒來管理,資源的開銷會非常大,這個時候就可以用Selector。通過Selector可以實現一個執行緒管理多個Channel,如下圖:
Selector使用
開啟
使用之前獲得一個Selector物件
Selector selector = Selector.open();
複製程式碼
註冊
要把Channel註冊到Selector上,Channel必需是非阻塞的。因此FileChannel是無法註冊到Selector的。如果註冊的時候不呼叫configureBlocking
方法就會丟擲IllegalBlockingModeException
異常。
SelectionKey
SelectionKey共有四種
- OP_ACCEPT
- OP_CONNECT
- OP_WRITE
- OP_READ
ServerSocketChannel註冊
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
複製程式碼
ServerSocketChannel的Operation Set只能是OP_ACCEPT,如果在註冊的時候新增了OP_CONNECT、OP_WRITE或OP_READ會報異常。例如按照以下寫法
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT | SelectionKey.OP_CONNECT);
複製程式碼
就會丟擲下面的異常
Exception in thread "main" java.lang.IllegalArgumentException
at java.nio.channels.spi.AbstractSelectableChannel.register(AbstractSelectableChannel.java:199)
at java.nio.channels.SelectableChannel.register(SelectableChannel.java:280)
at com.nio.sample.selector.SelectorServerSocketChannelSample.main(SelectorServerSocketChannelSample.java:27)
複製程式碼
ServerSocketChannel的validOps
可以看到只有OP_ACCEPT是合法的
public final int validOps() {
return SelectionKey.OP_ACCEPT;
}
複製程式碼
SocketChannel註冊
socketChannel.register(selector, SelectionKey.OP_CONNECT);
複製程式碼
SocketChannel的Operation Set只能是OP_CONNECT、OP_WRITE和OP_READ,如果在註冊的時候新增了OP_ACCEPT同樣會報異常。
SocketChannel的validOps
可以看到只有OP_READ、OP_WRITE、OP_CONNECT是合法的
public final int validOps() {
return (SelectionKey.OP_READ
| SelectionKey.OP_WRITE
| SelectionKey.OP_CONNECT);
}
複製程式碼
註冊成功之後,我們通過一個demo實現,客戶端和服務端互動:
服務端:
public static void main(String[] args) throws Exception {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9000));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
// configureBlocking 如果不設定非阻塞,register的時候會報異常
// java.nio.channels.IllegalBlockingModeException
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
int selected = selector.select();
if (selected > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isAcceptable()) {
System.err.println("Acceptable");
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
System.err.println("Readable");
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(128);
socketChannel.read(buffer);
System.out.println("接收來自客戶端的資料:" + new String(buffer.array()));
selectionKey.interestOps(SelectionKey.OP_WRITE);
} else if (selectionKey.isWritable()) {
System.err.println("Writable");
SocketChannel channel = (SocketChannel) selectionKey.channel();
String content = "向客戶端傳送資料 : " + System.currentTimeMillis();
ByteBuffer buffer = ByteBuffer.wrap(content.getBytes());
channel.write(buffer);
selectionKey.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
複製程式碼
我們來看一下服務端的邏輯
1、服務端註冊到selector,然後interest set(ops)設定為SelectionKey.OP_ACCEPT等待客戶端連線。
2、客戶端連線到達,呼叫到selectionKey.isAcceptable()方法,接收客戶端連線,然後獲得一個channel,並把
interest set設定為SelectionKey.OP_READ等待從通道中讀資料。
3、當客戶端傳送的資料到達,selectionKey.isReadable() 被觸發,接收客戶端的資料並列印,然後把selectionKey.interestOps 設定為SelectionKey.OP_WRITE,向客戶端傳送資料。
4、當可寫之後selectionKey.isWritable()被觸發,向客戶端傳送資料,同時selectionKey.interestOps再次設定為
SelectionKey.OP_READ等待客戶端資料到達。
客戶端:
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
Selector selector = Selector.open();
socketChannel.register(selector, SelectionKey.OP_CONNECT);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000));
while (true) {
int select = selector.select();
if (select > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isConnectable()) {
System.err.println("Connectable");
SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
clientChannel.finishConnect();
selectionKey.interestOps(SelectionKey.OP_WRITE);
} else if (selectionKey.isReadable()) {
System.out.println("Readable");
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer buffer = ByteBuffer.allocate(128);
channel.read(buffer);
selectionKey.interestOps(SelectionKey.OP_WRITE);
System.out.println("收到服務端資料" + new String(buffer.array()));
} else if (selectionKey.isWritable()) {
SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
String str = "qiwoo mobile";
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
clientChannel.write(buffer);
selectionKey.interestOps(SelectionKey.OP_READ);
System.out.println("向服務端傳送資料" + new String(buffer.array()));
}
iterator.remove();
}
}
}
}
複製程式碼
再來看一下服務端的邏輯
1、向服務端發起連線請求。
2、selectionKey.isConnectable()被觸發,連線成功之後,selectionKey.interestOps設定為SelectionKey.OP_WRITE,準備向服務端傳送資料。
3、channel可寫之後selectionKey.isWritable()被觸發,向服務端傳送資料,之後selectionKey.interestOps設定為SelectionKey.OP_READ,等待服務端過來的資料。
4、服務端資料發過來之後,selectionKey.isReadable()被觸發,讀取服務端資料之後selectionKey.interestOps設定為SelectionKey.OP_WRITE向服務端寫資料。