在上一篇的JAVA中NIO再深刻咱們學會了如何使用Buffer
,而在Java中IO和NIO中咱們略微瞭解到Channel
的概念,咱們知道了Channel
就像礦洞裏的鐵軌同樣,Buffer
就像鐵軌上的礦車,對於數據真正的操做都是對於Buffer
的操做。而在NIO中還有一個很是重要的概念就是Selector
,它就像礦洞裏的調度系統同樣。segmentfault
要理解爲何要有Selector?這個問題,咱們首先得知道在UNIX系統中有五種I/O模型:同步阻塞I/O、同步非阻塞I/O、I/O多路複用、信號驅動I/O和異步I/O。這個幾個I/O模型都是什麼意思呢,大概比喻一下。bash
阻塞與非阻塞是指應用程序在發起I/O操做時,是當即返回仍是等待。而同步和異步是指應用程序在於內核通訊時,數據從內核空間到應用空間的拷貝,是由內核發起仍是由應用程序來觸發。服務器
而所謂的I/O就是計算機內存與外部設備之間數據拷貝的過程,咱們知道CPU訪問內存的速度遠遠高於外部設備,所以CPU一般就是先將外部設備的數據讀取到內存中,而後再進行處理。而後此時有個場景,當那你的用戶程序經過CPU向外部設備發送了一個讀的指令,數據從外部設備到內存中是須要一段時間的,那麼此時CPU是休息呢?仍是讓給別人?仍是不斷的詢問,到了嗎?到了嗎?到了嗎……?這個就是I/O模型所要解決的問題。網絡
而咱們的NIO模擬的I/O模型就是I/O複用模型。經過只阻塞Selector
這一個線程,經過Selector
不斷的查詢Channel
中的狀態,從而達到了一個線程控制Selector
,而一個Selector
控制多個Channel
的目的。用圖表示就是這樣。異步
從圖上面咱們就能夠猜出來大概的Selector
該如何來使用socket
經過調用Selector.open()
方法來建立一個Selector。post
Selector selector = Selector.open();
複製代碼
咱們知道NIO中的Channel分爲四種類型ui
FileChannel
:文件通道DatagramChannel
:經過UDP讀取網絡中的數據SocketChannel
:經過TCP讀取網絡中的數據ServerSocketChannel
:能夠監聽進來的鏈接,對於每一個進來的鏈接都會建立一個SocketChannel
在這四個通道中有一個不能和Selector
配合使用,由於從圖中能夠看出,咱們的Selector
是不斷的輪詢註冊在Selector
中的每一個通道的狀態,不能阻塞在其中一個通道,即每一個通道必須是非阻塞狀態的,可是FileChannel
的通道是阻塞狀態且不能更改,因此FileChannel
不能和Selector
配合使用。spa
ServerSocketChannel socketChannel = ServerSocketChannel.open();
socketChannel.socket().bind(new InetSocketAddress(8080));
//設置爲非阻塞模式
socketChannel.configureBlocking(false);
複製代碼
爲了便於Selector
管理Channel
,咱們將Channel
註冊到Selector
上。線程
//將Channel註冊到Selector上
SelectionKey selectionKey = socketChannel.register(selector,SelectionKey.OP_READ);
複製代碼
咱們能夠看到第一個參數就是咱們本身的Selector
,而第二個參數就是選擇要監聽的事件類型,一共有四種
SelectionKey.OP_CONNECT
:鏈接繼續事件,表示服務器監聽到了客戶鏈接,服務器能夠接收這個鏈接了SelectionKey.OP_ACCEPT
:鏈接就緒事件,服務端收到客戶端的一個鏈接請求會觸發SelectionKey.OP_READ
:讀就緒事件,表示通道中已經有可讀的數據了,能夠執行讀操做SelectionKey.OP_WRITE
:寫就緒事件,表示已經能夠向通道寫數據了
ServerSocketChannel
的有效事件是OP_ACCEPT
,SocketChannel
的有效事件是OP_CONNECT
、OP_READ
、OP_WRITE
在上一步咱們已經將所須要的Channel
註冊到了Selector
中,那麼咱們如今能夠調用Selector.select()
方法進行遍歷獲得已經準備好的Channel
。
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
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
}
keyIterator.remove();
}
複製代碼
注意此時咱們在遍歷完成後,完成相應操做後都要調用
keyIterator.remove();
方法將其移除掉,由於select()
方法只是獲得了全部已經準備好的Channel
的key值集合,若是不刪除的話,那麼下次遍歷依然仍是會調用相應的事件。
作一個簡單的服務器監聽的程序。監聽本機的8080端口,打印出發送過來的數據。
public class TestNIO {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel socketChannel = ServerSocketChannel.open();
socketChannel.socket().bind(new InetSocketAddress(8080));
//設置爲非阻塞模式
socketChannel.configureBlocking(false);
//將Channel註冊到Selector上
socketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true){
int readyChannel = selector.select();
if (readyChannel == 0){
continue;
}
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
keyIterator.remove();
if (key.isAcceptable()){
System.out.println("isAcceptable");
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(key.selector(),SelectionKey.OP_READ);
}
else if (key.isConnectable()){
System.out.println("isConnectable");
}
else if (key.isReadable()){
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
clientChannel.read(byteBuffer);
System.out.println(new String(byteBuffer.array()));
}else if (key.isWritable()){
System.out.println("isWritable");
}
}
}
}
}
複製代碼
此時能夠經過在控制檯用命令telnet localhost 8080
便可與服務器鏈接。