【正文】netty死磕1.4: html
Java NIO Selector 一文全解 java
Java NIO的核心組件包括:編程
(1)Channel(通道)服務器
(2)Buffer(緩衝區)網絡
(3)Selector(選擇器)異步
其中Channel和Buffer比較好理解 ,聯繫也比較密切,他們的關係簡單來講就是:數據老是從通道中讀到buffer緩衝區內,或者從buffer寫入到通道中。socket
選擇器和他們的關係又是什麼?ide
選擇器(Selector) 是 Channel(通道)的多路複用器,Selector 能夠同時監控多個 通道的 IO(輸入輸出) 情況。性能
Selector的做用是什麼?學習
選擇器提供選擇執行已經就緒的任務的能力。從底層來看,Selector提供了詢問通道是否已經準備好執行每一個I/O操做的能力。Selector 容許單線程處理多個Channel。僅用單個線程來處理多個Channels的好處是,只須要更少的線程來處理通道。事實上,能夠只用一個線程處理全部的通道,這樣會大量的減小線程之間上下文切換的開銷。
並非全部的Channel,都是能夠被Selector 複用的。比方說,FileChannel就不能被選擇器複用。爲何呢?
判斷一個Channel 能被Selector 複用,有一個前提:判斷他是否繼承了一個抽象類SelectableChannel。若是繼承了SelectableChannel,則能夠被複用,不然不能。
SelectableChannel類是何方神聖?
SelectableChannel類提供了實現通道的可選擇性所須要的公共方法。它是全部支持就緒檢查的通道類的父類。全部socket通道,都繼承了SelectableChannel類都是可選擇的,包括從管道(Pipe)對象的中得到的通道。而FileChannel類,沒有繼承SelectableChannel,所以是否是可選通道。
通道和選擇器註冊以後,他們是綁定的關係嗎?
答案是否是。不是一對一的關係。一個通道能夠被註冊到多個選擇器上,但對每一個選擇器而言只能被註冊一次。
通道和選擇器之間的關係,使用註冊的方式完成。SelectableChannel能夠被註冊到Selector對象上,在註冊的時候,須要指定通道的哪些操做,是Selector感興趣的。
使用Channel.register(Selector sel,int ops)方法,將一個通道註冊到一個選擇器時。第一個參數,指定通道要註冊的選擇器是誰。第二個參數指定選擇器須要查詢的通道操做。
能夠供選擇器查詢的通道操做,從類型來分,包括如下四種:
(1)可讀 : SelectionKey.OP_READ
(2)可寫 : SelectionKey.OP_WRITE
(3)鏈接 : SelectionKey.OP_CONNECT
(4)接收 : SelectionKey.OP_ACCEPT
若是Selector對通道的多操做類型感興趣,能夠用「位或」操做符來實現:int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;
注意,操做一詞,是一個是使用很是氾濫,也是一個容易混淆的詞。特別提醒的是,選擇器查詢的不是通道的操做,而是通道的某個操做的一種就緒狀態。
什麼是操做的就緒狀態?
一旦通道具有完成某個操做的條件,表示該通道的某個操做已經就緒,就能夠被Selector查詢到,程序能夠對通道進行對應的操做。比方說,某個SocketChannel通道能夠鏈接到一個服務器,則處於「鏈接就緒」(OP_CONNECT)。再比方說,一個ServerSocketChannel服務器通道準備好接收新進入的鏈接,則處於「接收就緒」(OP_ACCEPT)狀態。還比方說,一個有數據可讀的通道,能夠說是「讀就緒」(OP_READ)。一個等待寫數據的通道能夠說是「寫就緒」(OP_WRITE)。
Channel和Selector的關係肯定好後,而且一旦通道處於某種就緒的狀態,就能夠被選擇器查詢到。這個工做,使用選擇器Selector的select()方法完成。select方法的做用,對感興趣的通道操做,進行就緒狀態的查詢。
Selector能夠不斷的查詢Channel中發生的操做的就緒狀態。而且挑選感興趣的操做就緒狀態。一旦通道有操做的就緒狀態達成,而且是Selector感興趣的操做,就會被Selector選中,放入選擇鍵集合中。
一個選擇鍵,首先是包含了註冊在Selector的通道操做的類型,比方說SelectionKey.OP_READ。也包含了特定的通道與特定的選擇器之間的註冊關係。
開發應用程序是,選擇鍵是編程的關鍵。NIO的編程,就是根據對應的選擇鍵,進行不一樣的業務邏輯處理。
選擇鍵的概念,有點兒像事件的概念。
選擇鍵和事件的關係是什麼?
一個選擇鍵有點兒像監聽器模式裏邊的一個事件,可是又不是。因爲Selector不是事件觸發的模式,而是主動去查詢的模式,因此不叫事件Event,而是叫SelectionKey選擇鍵。
Selector對象是經過調用靜態工廠方法open()來實例化的,以下:
// 一、獲取Selector選擇器 Selector selector = Selector.open();
Selector的類方法open()內部是向SPI發出請求,經過默認的SelectorProvider對象獲取一個新的實例。
要實現Selector管理Channel,須要將channel註冊到相應的Selector上,以下:
// 二、獲取通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 3.設置爲非阻塞 serverSocketChannel.configureBlocking(false); // 四、綁定鏈接 serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT)); // 五、將通道註冊到選擇器上,並制定監聽事件爲:「接收」事件 serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
上面經過調用通道的register()方法會將它註冊到一個選擇器上。
首先須要注意的是:
與Selector一塊兒使用時,Channel必須處於非阻塞模式下,不然將拋出異常IllegalBlockingModeException。這意味着,FileChannel不能與Selector一塊兒使用,由於FileChannel不能切換到非阻塞模式,而套接字相關的全部的通道均可以。
另外,還須要注意的是:
一個通道,並無必定要支持全部的四種操做。好比服務器通道ServerSocketChannel支持Accept 接受操做,而SocketChannel客戶端通道則不支持。能夠經過通道上的validOps()方法,來獲取特定通道下全部支持的操做集合。
萬事俱備,能夠開幹。下一步是查詢就緒的操做。
經過Selector的select()方法,能夠查詢出已經就緒的通道操做,這些就緒的狀態集合,包存在一個元素是SelectionKey對象的Set集合中。
下面是Selector幾個重載的查詢select()方法:
(1)select():阻塞到至少有一個通道在你註冊的事件上就緒了。
(2)select(long timeout):和select()同樣,但最長阻塞事件爲timeout毫秒。
(3)selectNow():非阻塞,只要有通道就緒就馬上返回。
select()方法返回的int值,表示有多少通道已經就緒,更準確的說,是自前一次select方法以來到這一次select方法之間的時間段上,有多少通道變成就緒狀態。
一旦調用select()方法,而且返回值不爲0時,下一步工幹啥?
經過調用Selector的selectedKeys()方法來訪問已選擇鍵集合,而後迭代集合的每個選擇鍵元素,根據就緒操做的類型,完成對應的操做:
Set selectedKeys = selector.selectedKeys(); Iterator 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(); }
處理完成後,直接將選擇鍵,從這個集合中移除,防止下一次循環的時候,被重複的處理。鍵能夠但不能添加。試圖向已選擇的鍵的集合中添加元素將拋出java.lang.UnsupportedOperationException。
package com.crazymakercircle.iodemo.base; import com.crazymakercircle.config.SystemConfig; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; public class SelectorDemo { static class Client { /** * 客戶端 */ public static void testClient() throws IOException { InetSocketAddress address= new InetSocketAddress(SystemConfig.SOCKET_SERVER_IP, SystemConfig.SOCKET_SERVER_PORT); // 一、獲取通道(channel) SocketChannel socketChannel = SocketChannel.open(address); // 二、切換成非阻塞模式 socketChannel.configureBlocking(false); // 三、分配指定大小的緩衝區 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); byteBuffer.put("hello world ".getBytes()); byteBuffer.flip(); socketChannel.write(byteBuffer); socketChannel.close(); } public static void main(String[] args) throws IOException { testClient(); } } static class Server { public static void testServer() throws IOException { // 一、獲取Selector選擇器 Selector selector = Selector.open(); // 二、獲取通道 ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); // 3.設置爲非阻塞 serverSocketChannel.configureBlocking(false); // 四、綁定鏈接 serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT)); // 五、將通道註冊到選擇器上,並註冊的操做爲:「接收」操做 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 六、採用輪詢的方式,查詢獲取「準備就緒」的註冊過的操做 while (selector.select() > 0) { // 七、獲取當前選擇器中全部註冊的選擇鍵(「已經準備就緒的操做」) Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator(); while (selectedKeys.hasNext()) { // 八、獲取「準備就緒」的時間 SelectionKey selectedKey = selectedKeys.next(); // 九、判斷key是具體的什麼事件 if (selectedKey.isAcceptable()) { // 十、若接受的事件是「接收就緒」 操做,就獲取客戶端鏈接 SocketChannel socketChannel = serverSocketChannel.accept(); // 十一、切換爲非阻塞模式 socketChannel.configureBlocking(false); // 十二、將該通道註冊到selector選擇器上 socketChannel.register(selector, SelectionKey.OP_READ); } else if (selectedKey.isReadable()) { // 1三、獲取該選擇器上的「讀就緒」狀態的通道 SocketChannel socketChannel = (SocketChannel) selectedKey.channel(); // 1四、讀取數據 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); int length = 0; while ((length = socketChannel.read(byteBuffer)) != -1) { byteBuffer.flip(); System.out.println(new String(byteBuffer.array(), 0, length)); byteBuffer.clear(); } socketChannel.close(); } // 1五、移除選擇鍵 selectedKeys.remove(); } } // 七、關閉鏈接 serverSocketChannel.close(); } public static void main(String[] args) throws IOException { testServer(); } } }
NIO編程的難度比同步阻塞BIO大不少。
請注意以上的代碼中並無考慮「半包讀」和「半包寫」,若是加上這些,代碼將會更加複雜。
(1)客戶端發起的鏈接操做是異步的,能夠經過在多路複用器註冊OP_CONNECT等待後續結果,不須要像以前的客戶端那樣被同步阻塞。
(2)SocketChannel的讀寫操做都是異步的,若是沒有可讀寫的數據它不會同步等待,直接返回,這樣I/O通訊線程就能夠處理其餘的鏈路,不須要同步等待這個鏈路可用。
(3)線程模型的優化:因爲JDK的Selector在Linux等主流操做系統上經過epoll實現,它沒有鏈接句柄數的限制(只受限於操做系統的最大句柄數或者對單個進程的句柄限制),這意味着一個Selector線程能夠同時處理成千上萬個客戶端鏈接,並且性能不會隨着客戶端的增長而線性降低。所以,它很是適合作高性能、高負載的網絡服務器。
代碼工程: JavaNioDemo.zip
下載地址:在瘋狂創客圈QQ羣文件共享。
瘋狂創客圈 Netty 死磕系列 10多篇深度文章: 【博客園 總入口】 QQ羣:104131248