在上一篇的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模型都是什麼意思呢,大概比喻一下。服務器
阻塞與非阻塞是指應用程序在發起I/O操做時,是當即返回仍是等待。而同步和異步是指應用程序在於內核通訊時,數據從內核空間到應用空間的拷貝,是由內核發起仍是由應用程序來觸發。網絡
而所謂的I/O就是計算機內存與外部設備之間數據拷貝的過程,咱們知道CPU訪問內存的速度遠遠高於外部設備,所以CPU一般就是先將外部設備的數據讀取到內存中,而後再進行處理。而後此時有個場景,當那你的用戶程序經過CPU向外部設備發送了一個讀的指令,數據從外部設備到內存中是須要一段時間的,那麼此時CPU是休息呢?仍是讓給別人?仍是不斷的詢問,到了嗎?到了嗎?到了嗎……?這個就是I/O模型所要解決的問題。異步
而咱們的NIO模擬的I/O模型就是I/O複用模型。經過只阻塞Selector
這一個線程,經過Selector
不斷的查詢Channel
中的狀態,從而達到了一個線程控制Selector
,而一個Selector
控制多個Channel
的目的。用圖表示就是這樣。socket
從圖上面咱們就能夠猜出來大概的Selector
該如何來使用post
經過調用Selector.open()
方法來建立一個Selector。線程
Selector selector = Selector.open();
咱們知道NIO中的Channel分爲四種類型code
FileChannel
:文件通道DatagramChannel
:經過UDP讀取網絡中的數據SocketChannel
:經過TCP讀取網絡中的數據ServerSocketChannel
:能夠監聽進來的鏈接,對於每一個進來的鏈接都會建立一個SocketChannel
在這四個通道中有一個不能和Selector
配合使用,由於從圖中能夠看出,咱們的Selector
是不斷的輪詢註冊在Selector
中的每一個通道的狀態,不能阻塞在其中一個通道,即每一個通道必須是非阻塞狀態的,可是FileChannel
的通道是阻塞狀態且不能更改,因此FileChannel
不能和Selector
配合使用。server
ServerSocketChannel socketChannel = ServerSocketChannel.open(); socketChannel.socket().bind(new InetSocketAddress(8080)); //設置爲非阻塞模式 socketChannel.configureBlocking(false);
爲了便於Selector
管理Channel
,咱們將Channel
註冊到Selector
上。blog
//將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
便可與服務器鏈接。