NIO總結

一  NIO介紹

  NIO主要有三大核心部分:Channel(通道),Buffer(緩衝區), Selector。傳統IO基於字節流和字符流進行操做,而NIO基於Channel和Buffer(緩衝區)進行操做,數據老是從通道讀取到緩衝區中,或者從緩衝區寫入到通道中。Selector(選擇區)用於監聽多個通道的事件(好比:鏈接打開,數據到達)。所以,單個線程能夠監聽多個數據通道。html

  NIO和傳統IO(一下簡稱IO)之間第一個最大的區別是,IO是面向流的,NIO是面向緩衝區的Java IO面向流意味着每次從流中讀一個或多個字節,直至讀取全部字節,它們沒有被緩存在任何地方。此外,它不能先後移動流中的數據。若是須要先後移動從流中讀取的數據,須要先將它緩存到一個緩衝區。linux

  NIO的緩衝導向方法略有不一樣。數據讀取到一個它稍後處理的緩衝區,須要時可在緩衝區中先後移動。這就增長了處理過程當中的靈活性。可是,還須要檢查是否該緩衝區中包含全部您須要處理的數據。並且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區裏還沒有處理的數據。編程

  IO的各類流是阻塞的。這意味着,當一個線程調用read() 或 write()時,該線程被阻塞,直到有一些數據能夠被讀取,或數據徹底寫入。該線程在此期間不能再幹任何事情了。數組

  NIO的非阻塞模式,使一個線程從某通道發送請求讀取數據,可是它僅能獲得目前可用的數據,若是目前沒有數據可用時,就什麼都不會獲取。而不是保持線程阻塞,因此直至數據變得能夠讀取以前,該線程能夠繼續作其餘的事情。 非阻塞寫也是如此。一個線程請求寫入一些數據到某通道,但不須要等待它徹底寫入,這個線程同時能夠去作別的事情。線程一般將非阻塞IO的空閒時間用於在其它通道上執行IO操做,因此一個單獨的線程如今能夠管理多個輸入和輸出通道(channel)。緩存

二  IO模型分類 

  按照《Unix網絡編程》的劃分,I/O模型能夠分爲:阻塞I/O模型、非阻塞I/O模型、I/O複用模型、信號驅動式I/O模型和異步I/O模型,按照POSIX標準來劃分只分爲兩類:同步I/O和異步I/O。服務器

  如何區分呢?首先一個I/O操做其實分紅了兩個步驟:發起IO請求(即內核準備數據報)和實際的IO操做(即將數據報從內核複製到用戶空間)。同步I/O和異步I/O的區別就在於第二個步驟是否阻塞,若是實際的I/O讀寫阻塞請求進程,那麼就是同步I/O,所以阻塞I/O、非阻塞I/O、I/O複用、信號驅動I/O都是同步I/O,若是不阻塞,而是操做系統幫你作完I/O操做再將結果返回給你,那麼就是異步I/O。網絡

  阻塞I/O和非阻塞I/O的區別在於第一步,發起I/O請求是否會被阻塞,若是阻塞直到完成那麼就是傳統的阻塞I/O,若是不阻塞,那麼就是非阻塞I/O。數據結構

(1)阻塞I/O模型 :在linux中,默認狀況下全部的socket都是阻塞的,即從發起請求到最終獲取到數據報都處於阻塞狀態。一個典型的讀操做流程大概是這樣:併發

    

(2)非阻塞I/O模型:linux下,能夠經過設置socket使其變爲non-blocking,當發起recvfrom系統調用時,若是內核中數據報尚未準備好,則直接返回一個EWOULDBLOCK標誌,而不阻塞用戶線程,應用經過輪詢調用recvfrom判斷數據報是否準備好,若是數據報已經準備好,這時再調用recvfrom系統調用,數據報會從內核複製到用戶空間,複製的這段時間會阻塞用戶進程。當對一個non-blocking socket執行讀操做時,流程是這個樣子:app

    

(3)I/O複用模型:咱們能夠調用selectpoll,阻塞在這兩個系統調用中的某一個之上,而不是真正的IO系統調用上,select/poll的好處就在於單個線程就能夠同時處理多個網絡鏈接的IO。它的基本原理就是select/epoll這個方法會不斷的輪詢所負責的全部socket,當某個socket有數據到達了,就通知用戶進程。當用戶進程調用了select,那麼整個進程會被block,而同時,內核會「監視」全部select負責的socket,當任何一個socket中的數據準備好了,select就會返回。這個時候用戶進程再調用read操做,將數據從內核拷貝到用戶進程。:

    

   名詞解釋:

  一、文件描述符fd

  Linux的內核將全部外部設備均可以看作一個文件。那麼對外部設備的操做均可以看作對文件進行操做。咱們對一個文件的讀寫,都經過調用內核提供的系統調用;內核給咱們返回一個filede scriptor(fd,文件描述符)。而對一個socket的讀寫也會有相應的描述符,稱爲socketfd(socket描述符)。描述符就是一個數字,指向內核中一個結構體(文件路徑,數據區等一些屬性)。那麼咱們的應用程序對文件的讀寫就經過對描述符的讀寫完成。

  二、select

  select 函數監視的文件描述符分3類,分別是writefds、readfds和exceptfds。調用後select函數會阻塞,直到有描述符就緒(有數據可讀、可寫或者有except)或者超時(timeout指定等待時間,若是當即返回設爲null便可)函數返回。當select函數返回後,能夠經過遍歷fdset,來找到就緒的描述符。

  缺點:

  一、select最大的缺陷就是單個進程所打開的FD是有必定限制的,它由FDSETSIZE設置,32位機默認是1024個,64位機默認是2048。 通常來講這個數目和系統內存關係很大,具體數目能夠cat /proc/sys/fs/file-max察看。

  二、對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低。 當套接字比較多的時候,每次select()都要經過遍歷FDSETSIZE個Socket來完成調度,無論哪一個Socket是活躍的,都遍歷一遍。這會浪費不少CPU時間。

  三、須要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大。

  三、poll

  poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,而後查詢每一個fd對應的設備狀態,若是設備就緒則在設備等待隊列中加入一項並繼續遍歷,若是遍歷完全部fd後沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒後它又要再次遍歷fd。這個過程經歷了屢次無謂的遍歷。

  它沒有最大鏈接數的限制,緣由是它是基於鏈表來存儲的,可是一樣有一個缺點:

    一、大量的fd的數組被總體複製於用戶態和內核地址空間之間,而無論這樣的複製是否是有意義。

    2 、poll還有一個特色是「水平觸發」,若是報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。

  注意:從上面看,select和poll都須要在返回後,經過遍歷文件描述符來獲取已經就緒的socket。事實上,同時鏈接的大量客戶端在一時刻可能只有不多的處於就緒狀態,所以隨着監視的描述符數量的增加,其效率也會線性降低。

  四、epoll

  epoll是在2.6內核中提出的,是以前的select和poll的加強版本。相對於select和poll來講,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。

  基本原理:epoll支持水平觸發和邊緣觸發,最大的特色在於邊緣觸發,它只告訴進程哪些fd剛剛變爲就緒態,而且只會通知一次。還有一個特色是,epoll使用「事件」的就緒通知方式,經過epoll_ctl註冊fd,一旦該fd就緒,內核就會採用相似callback的回調機制來激活該fd,epoll_wait即可以收到通知。

  epoll的優勢:

    一、沒有最大併發鏈接的限制,能打開的FD的上限遠大於1024(1G的內存上能監聽約10萬個端口)。

    二、效率提高,不是輪詢的方式,不會隨着FD數目的增長效率降低。 只有活躍可用的FD纔會調用callback函數;即Epoll最大的優勢就在於它只管你「活躍」的鏈接,而跟鏈接總數無關,所以在實際的網絡環境中,Epoll的效率就會遠遠高於select和poll。

    三、內存拷貝,利用mmap()文件映射內存加速與內核空間的消息傳遞;即epoll使用mmap減小複製開銷。

(4)信號驅動式I/O模型:咱們能夠用信號,讓內核在描述符就緒時發送SIGIO信號通知咱們:

    

(5)異步I/O模型:用戶進程發起read操做以後,馬上就能夠開始去作其它的事。而另外一方面,從內核的角度,當它受到一個asynchronousread以後,首先它會馬上返回,因此不會對用戶進程產生任何block。而後,內核會等待數據準備完成,而後將數據拷貝到用戶內存,當這一切都完成以後,內核會給用戶進程發送一個signal,告訴它read操做完成了:

    

三  傳統IO和NIO具體實際用法區別

傳統IO:

 public static void method2(){
        InputStream in = null;
        try{
            in = new BufferedInputStream(new FileInputStream("src/nomal_io.txt"));
            byte [] buf = new byte[1024];
            int bytesRead = in.read(buf);
            while(bytesRead != -1)
            {
                for(int i=0;i<bytesRead;i++)
                    System.out.print((char)buf[i]);
                bytesRead = in.read(buf);
            }
        }catch (IOException e)
        {
            e.printStackTrace();
        }finally{
            try{
                if(in != null){
                    in.close();
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

NIO:

public static void method1(){
        RandomAccessFile aFile = null;
        try{
            aFile = new RandomAccessFile("src/nio.txt","rw");
            FileChannel fileChannel = aFile.getChannel();//獲取通道
            ByteBuffer buf = ByteBuffer.allocate(1024);//建立Buffer並分配空間
            int bytesRead = fileChannel.read(buf);//從通道中讀取數據到Buffer中
            System.out.println(bytesRead);
            while(bytesRead != -1)
            {
                buf.flip();//切換爲讀模式
                while(buf.hasRemaining())
                {
                    System.out.print((char)buf.get());//從Buffer中讀取數據
                }
                buf.compact();//清空已讀的數據,未讀的數據會整理到Buffer頭部
                bytesRead = fileChannel.read(buf);
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(aFile != null){
                    aFile.close();
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

四  Buffer

  Buffer是一個對象,它包含一些要寫入或者要讀出的數據。在NIO庫裏,全部數據都是用緩衝區處理的,在讀取數據時,它是直接讀到緩衝區中的,在寫入數據時,寫入到緩衝區中。任什麼時候候訪問NIO中的數據,都是經過Buffer緩衝區進行操做。

  Buffer其實是一個數組。Buffer關注的是存放什麼類型的數據,只支持基本數據類型,並且不支持基本類型的boolean類型。因此基礎的Buffer,就是7種,對應Java的7個基礎類型:

  

  一、從上面的示例中能夠總結出使用Buffer通常遵循下面幾個步驟:

    (1)分配空間(ByteBuffer buf = ByteBuffer.allocate(1024); 還有一種allocateDirector後面再陳述)

    (2)寫入數據到Buffer(int bytesRead = fileChannel.read(buf);)

    (3)調用filp()方法( buf.flip();)

    (4)從Buffer中讀取數據(System.out.print((char)buf.get());)

    (5)調用clear()方法或者compact()方法

  二、Buffer顧名思義:緩衝區,其實是一個容器,一個連續數組。Channel提供從文件、網絡讀取數據的渠道,可是讀寫的數據都必須通過Buffer。以下圖:

    

  向Buffer中寫數據:

    (1)從Channel寫到Buffer (fileChannel.read(buf))

    (2)經過Buffer的put()方法 (buf.put(…))

  從Buffer中讀取數據:

    (1)從Buffer讀取到Channel (channel.write(buf))

    (2)使用get()方法從Buffer中讀取數據 (buf.get())

  能夠把Buffer簡單地理解爲一組基本數據類型的元素列表,它經過幾個變量來保存這個數據的當前位置狀態:capacity,position,limit,mark。其中capacity在讀寫模式下都是固定的,就是分配的緩衝大小,position相似於讀寫指針,表示當前讀(寫)到什麼位置,limit在寫模式下表示最多能寫入多少數據,此時和capacity相同,在讀模式下表示最多能讀多少數據,此時和緩存中的實際數據大小相同:

索引

說明

capacity

緩衝區數組的總長度

position

下一個要操做的數據元素的位置

limit

緩衝區數組中不可操做的下一個元素的位置:limit<=capacity

mark

用於記錄當前position的前一個位置或者默認是-1

  咱們經過ByteBuffer.allocate(11)方法建立了一個11個byte的數組的緩衝區,初始狀態圖,position的位置爲0,capacity和limit默認都是數組長度。  

     

  當寫入5個字節時,變化以下圖:

     

  當須要將緩衝區中的5個字節數據寫入Channel的通訊信道,因此咱們調用ByteBuffer.flip()方法,即調用flip方法後的變化以下圖所示(position設回0,並將limit設成以前的position的值):

     

  這時底層操做系統就能夠從緩衝區中正確讀取這個5個字節數據併發送出去了。在下一次寫數據以前再調用clear()方法,緩衝區的索引位置又回到了初始位置。

  在寫模式下調用flip()方法,buffer從寫模式切換到讀模式,limit會設置爲position當前的值(即當前寫了多少數據),postion會被置爲0,以表示讀操做從緩存的頭開始讀。也就是說調用flip以後,讀寫指針指到緩存頭部,而且設置了最多隻能讀出以前寫入的數據長度(而不是整個緩存的容量大小)。

  注意:buffer.flip();必定得有,若是沒有,就是從文件最後開始讀取的,固然讀出來的都是byte=0時候的字符。經過buffer.flip();這個語句,就能把buffer的當前位置更改成buffer緩衝區的第一個位置。

   調用clear()方法:position將被設回0,limit設置成capacity,換句話說,Buffer被清空了,其實Buffer中的數據並未被清除,只是這些標記告訴咱們能夠從哪裏開始往Buffer裏寫數據。若是Buffer中有一些未讀的數據,調用clear()方法,數據將「被遺忘」,意味着再也不有任何標記會告訴你哪些數據被讀過,哪些尚未。若是Buffer中仍有未讀的數據,且後續還須要這些數據,可是此時想要先寫些數據,那麼使用compact()方法。compact()方法將全部未讀的數據拷貝到Buffer起始處。而後將position設到最後一個未讀元素正後面。limit屬性依然像clear()方法同樣,設置成capacity。如今Buffer準備好寫數據了,可是不會覆蓋未讀的數據。

   經過調用Buffer.mark()方法,能夠標記Buffer中的一個特定的position,以後能夠經過調用Buffer.reset()方法恢復到這個position。Buffer.rewind()方法將position設回0,因此你能夠重讀Buffer中的全部數據。limit保持不變,仍然表示能從Buffer中讀取多少個元素。

五  Channel

  Channel是一個通道,網絡數據經過Channel讀取和寫入。Channel和IO中的Stream(流)是差很少一個等級的。只不過Stream是單向的,譬如:InputStream, OutputStream。而Channel是雙向的,既能夠用來進行讀操做,又能夠用來進行寫操做。

  NIO中的Channel的主要實現有:

  • FileChannel

  • DatagramChannel

  • SocketChannel

  • ServerSocketChannel

  分別能夠對應文件IO、UDP和TCP(Server和Client)。下面演示的案例基本上就是圍繞這4個類型的Channel進行陳述的。

  Channel 必需要配合 Buffer 一塊兒使用,咱們永遠不可能將數據直接寫入到 Channel 中,一樣也不可能直接從 Channel 中讀取數據。都是經過從 Channel 讀取數據到 Buffer 中或者從 Buffer 寫入數據到 Channel 中,以下:

  

   上面的示例代碼是FileChannel的使用方式。

  這裏使用SocketChannel來繼續探討NIO。NIO的強大功能部分來自於Channel的非阻塞特性,套接字的某些操做可能會無限期地阻塞。在傳統的Socket IO中,對accept()方法的調用可能會由於等待一個客戶端鏈接而阻塞;對read()方法的調用可能會由於沒有數據可讀而阻塞,直到鏈接的另外一端傳來新的數據。總的來講,建立/接收鏈接或讀寫數據等I/O調用,均可能無限期地阻塞等待,直到底層的網絡實現發生了什麼。慢速的,有損耗的網絡,或僅僅是簡單的網絡故障均可能致使任意時間的延遲。然而不幸的是,在調用一個方法以前沒法知道其是否阻塞。

  NIO的channel抽象的一個重要特徵就是能夠經過配置它的阻塞行爲,以實現非阻塞式的信道。

  channel.configureBlocking(false)

  在非阻塞式信道上調用一個方法老是會當即返回。這種調用的返回值指示了所請求的操做完成的程度。例如,在一個非阻塞式ServerSocketChannel上調用accept()方法,若是有鏈接請求來了,則返回客戶端SocketChannel,不然返回null。

  這裏先舉一個TCP應用案例,客戶端採用NIO實現,而服務端依舊使用BIO實現。

  客戶端代碼(案例3):

  public static void client(){
        ByteBuffer buffer = ByteBuffer.allocate(1024);//建立Buffer並分配空間
        SocketChannel socketChannel = null;
        try{
            socketChannel = SocketChannel.open();//獲取客戶端SocketChannel
            socketChannel.configureBlocking(false);//設置爲非阻塞
            socketChannel.connect(new InetSocketAddress("10.10.195.115",8080));//鏈接服務端地址
            if(socketChannel.finishConnect())//若是完成了鏈接
            {
                int i=0;
                while(true)
                {
                    TimeUnit.SECONDS.sleep(1);
                    String info = "I'm "+i+++"-th information from client";
                    buffer.clear();
                    buffer.put(info.getBytes());//寫入數據到Buffer
                    buffer.flip();//切換爲讀模式
                    while(buffer.hasRemaining()){//若是buffer沒有讀完
                        System.out.println(buffer);
                        socketChannel.write(buffer);//寫入數據到通道
                    }
                }
            }
        } catch (IOException | InterruptedException e){
            e.printStackTrace();
        } finally{
            try{
                if(socketChannel!=null){
                    socketChannel.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }

  服務端代碼(案例4):

  public static void server(){
        ServerSocket serverSocket = null;
        InputStream in = null;
        try {
            serverSocket = new ServerSocket(8080);//建立傳統的阻塞模式服務端Socket
            int recvMsgSize = 0;
            byte[] recvBuf = new byte[1024];
            while(true){
                Socket clntSocket = serverSocket.accept();//打開接受客戶端鏈接,若是沒有客戶端鏈接,則阻塞在這個方法上
                SocketAddress clientAddress = clntSocket.getRemoteSocketAddress();//獲取客戶端地址
                System.out.println("Handling client at "+clientAddress);
                in = clntSocket.getInputStream();//獲取客戶端輸入,阻塞IO模式
                while((recvMsgSize=in.read(recvBuf))!=-1){//若是尚未讀完
                    byte[] temp = new byte[recvMsgSize];
                    System.arraycopy(recvBuf, 0, temp, 0, recvMsgSize);
                    System.out.println(new String(temp));
                }
            }
        } catch (IOException e){
            e.printStackTrace();
        } finally{
            try{
                if(serverSocket!=null){
                    serverSocket.close();
                }
                if(in!=null){
                    in.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }

  根據上面的案例,總結一下SocketChannel的用法。

  (1)打開SocketChannel:

  socketChannel = SocketChannel.open();
  socketChannel.connect(new InetSocketAddress("10.10.195.115",8080));

  發送數據到Server:

  String info = "I'm "+i+++"-th information from client";
  buffer.clear();
  buffer.put(info.getBytes());//寫入數據到Buffer中
  buffer.flip();
  while(buffer.hasRemaining()){
    System.out.println(buffer);
    socketChannel.write(buffer);//從Buffer寫數據到channel中,併發送到Server端
  }

  注意SocketChannel.write()方法的調用是在一個while循環中的。write()方法沒法保證能寫多少字節到SocketChannel。因此,咱們重複調用write()直到Buffer沒有要寫的字節爲止。

  非阻塞模式下,read()方法在還沒有讀取到任何數據時可能就返回了。因此須要關注它的int返回值,它會告訴你讀取了多少字節。  

  關閉:

  socketChannel.close();

5、Selector

  Selector運行單線程處理多個Channel,若是你的應用打開了多個通道,但每一個鏈接的流量都很低,使用Selector就會很方便。例如在一個聊天服務器中。要使用Selector, 得向Selector註冊Channel,

而後調用它的select()方法。這個方法會一直阻塞到某個註冊的通道有事件就緒。一旦這個方法返回,線程就能夠處理這些事件,事件的例子有如新的鏈接進來、數據接收等。

  若是用傳統的方式來處理這麼多客戶端,使用的方法是循環地一個一個地去檢查全部的客戶端是否有I/O操做,若是當前客戶端有I/O操做,則可能把當前客戶端扔給一個線程池去處理,若是沒有

I/O操做則進行下一個輪詢,當全部的客戶端都輪詢過了又接着從頭開始輪詢;這種方法是很是笨並且也很是浪費資源,由於大部分客戶端是沒有I/O操做,咱們也要去檢查;

  一個Selector實例能夠同時檢查一組信道的I/O狀態。選擇器就是一個多路開關選擇器,由於一個選擇器可以管理多個信道上的I/O操做。它在內部能夠同時管理多個I/O,當一個信道有I/O操做的時候,他會通知Selector,Selector就是記住這個信道有I/O操做,而且知道是何種I/O操做,是讀呢?是寫呢?仍是接受新的鏈接;因此若是使用Selector,它返回的結果只有兩種結果,一種是0,即在你調用的時刻沒有任何客戶端須要I/O操做,另外一種結果是一組須要I/O操做的客戶端,這時你就根本不須要再檢查了,由於它返回給你的確定是你想要的。這樣一種通知的方式比那種主動輪詢的方式要高效得多!

  要使用選擇器(Selector),須要建立一個Selector實例(使用靜態工廠方法open())並將其註冊(register)到想要監控的信道上(注意,這要經過channel的方法實現,而不是使用selector的方法)。最後,調用選擇器的select()方法。該方法會阻塞等待,直到有一個或更多的信道準備好了I/O操做或等待超時。select()方法將返回可進行I/O操做的信道數量。如今,在一個單獨的線程中,經過調用select()方法就能檢查多個信道是否準備好進行I/O操做。若是通過一段時間後仍然沒有信道準備好,select()方法就會返回0,並容許程序繼續執行其餘任務。

  下面將上面的TCP服務端代碼改寫成NIO的方式(案例5):

public class ServerConnect{
    private static final int BUF_SIZE=1024;
    private static final int PORT = 8080;
    private static final int TIMEOUT = 3000;
    public static void main(String[] args){
        selector();
    }
    public static void handleAccept(SelectionKey key) throws IOException{
        ServerSocketChannel ssChannel = (ServerSocketChannel)key.channel();//獲取服務端通道
        SocketChannel sc = ssChannel.accept();//開啓接收客戶端鏈接
        sc.configureBlocking(false);//設置爲非阻塞
        sc.register(key.selector(), SelectionKey.OP_READ,ByteBuffer.allocateDirect(BUF_SIZE));//把通道註冊到Selector上,而且接收的事件的讀事件
    }
    public static void handleRead(SelectionKey key) throws IOException{
        SocketChannel sc = (SocketChannel)key.channel();
        ByteBuffer buf = (ByteBuffer)key.attachment();
        long bytesRead = sc.read(buf);
        while(bytesRead>0){
            buf.flip();
            while(buf.hasRemaining()){
                System.out.print((char)buf.get());
            }
            System.out.println();
            buf.clear();
            bytesRead = sc.read(buf);
        }
        if(bytesRead == -1){
            sc.close();
        }
    }
    public static void handleWrite(SelectionKey key) throws IOException{
        ByteBuffer buf = (ByteBuffer)key.attachment();
        buf.flip();
        SocketChannel sc = (SocketChannel) key.channel();
        while(buf.hasRemaining()){
            sc.write(buf);
        }
        buf.compact();
    }
    public static void selector() {
        Selector selector = null;
        ServerSocketChannel ssc = null;
        try{
            selector = Selector.open();//獲取Selector
            ssc= ServerSocketChannel.open();//開啓服務端通道
            ssc.socket().bind(new InetSocketAddress(PORT));//綁定監聽端口
            ssc.configureBlocking(false);//設置爲非阻塞
            ssc.register(selector, SelectionKey.OP_ACCEPT);//註冊到Selector上
            while(true){
                if(selector.select(TIMEOUT) == 0){//若是沒有客戶端鏈接
                    System.out.println("==");
                    continue;
                }
                Iterator<SelectionKey> iter = selector.selectedKeys().iterator();//
                while(iter.hasNext()){
                    SelectionKey key = iter.next();
                    if(key.isAcceptable()){
                        handleAccept(key);
                    }
                    if(key.isReadable()){
                        handleRead(key);
                    }
                    if(key.isWritable() && key.isValid()){
                        handleWrite(key);
                    }
                    if(key.isConnectable()){
                        System.out.println("isConnectable = true");
                    }
                    iter.remove();
                }
            }
        }catch(IOException e){
            e.printStackTrace();
        }finally{
            try{
                if(selector!=null){
                    selector.close();
                }
                if(ssc!=null){
                    ssc.close();
                }
            }catch(IOException e){
                e.printStackTrace();
            }
        }
    }
}
View Code

   與Selector一塊兒使用時,Channel必須處於非阻塞模式下。這意味着不能將FileChannel與Selector一塊兒使用,由於FileChannel不能切換到非阻塞模式。而套接字通道均可以。一個通道能夠被註冊到多個選擇器上,而對於同一個選擇器則只能被註冊一次。若是內核版本>=2.6則底層使用Linux操做系統的Epoll實現,不然使用poll實現。

  register()方法的第二個參數,是一個"interest集合",意思是在經過Selector監聽Channel時對什麼事件感興趣。能夠監聽四種不一樣類型的事件:

    1. Connect  --- 鏈接事件

    2. Accept   --- 接收事件

    3. Read    --- 讀事件

    4. Write   --- 寫事件

  通道觸發了一個事件意思是該事件已經就緒。因此,某個channel成功鏈接到另外一個服務器稱爲「鏈接就緒」。一個server socket channel準備好接收新進入的鏈接稱爲「接收就緒」。一個有數據可讀的通道能夠說是「讀就緒」。等待寫數據的通道能夠說是「寫就緒」。

  這四種事件用SelectionKey的四個常量來表示:

    1. SelectionKey.OP_CONNECT

    2. SelectionKey.OP_ACCEPT

    3. SelectionKey.OP_READ

    4. SelectionKey.OP_WRITE

   SelectionKey

  當向Selector註冊Channel時,register()方法會返回一個SelectionKey對象。這個對象包含了一些你感興趣的屬性:

    (1)interest集合  

    (2)ready集合

    (3)Channel

    (4)Selector

    (5)附加的對象(可選)

  interest集合:就像向Selector註冊通道一節中所描述的,interest集合是你所選擇的感興趣的事件集合。能夠經過SelectionKey讀寫interest集合。

  ready 集合是通道已經準備就緒的操做的集合。在一次選擇(Selection)以後,你會首先訪問這個ready set。能夠這樣訪問ready集合:

  int readySet = selectionKey.readyOps();

  能夠用像檢測interest集合那樣的方法,來檢測channel中什麼事件或操做已經就緒。可是,也可使用如下四個方法,它們都會返回一個布爾類型:

  selectionKey.isAcceptable();
  selectionKey.isConnectable();
  selectionKey.isReadable();
  selectionKey.isWritable();

  從SelectionKey訪問Channel和Selector很簡單。以下:

  Channel  channel  = selectionKey.channel();
  Selector selector = selectionKey.selector();

  能夠將一個對象或者更多信息附着到SelectionKey上,這樣就能方便的識別某個給定的通道。例如,能夠附加與通道一塊兒使用的Buffer,或是包含彙集數據的某個對象。使用方法以下:

  selectionKey.attach(theObject);
  Object attachedObj = selectionKey.attachment();

  還能夠在用register()方法向Selector註冊Channel的時候附加對象。如:

  SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

   經過Selector選擇通道

  一旦向Selector註冊了一或多個通道,就能夠調用幾個重載的select()方法。這些方法返回你所感興趣的事件(如鏈接、接受、讀或寫)已經準備就緒的那些通道。換句話說,若是你對「讀就緒」的通道感興趣,select()方法會返回讀事件已經就緒的那些通道。

  下面是select()方法:

    int select()

    int select(long timeout)

    int selectNow()

  select()阻塞到至少有一個通道在你註冊的事件上就緒了。

  select(long timeout)和select()同樣,最長會阻塞timeout毫秒(參數)。

  selectNow()不會阻塞,無論什麼通道就緒都馬上返回(譯者注:此方法執行非阻塞的選擇操做。若是自從前一次選擇操做後,沒有通道變成可選擇的,則此方法直接返回零。)。

  select()方法返回的int值表示有多少通道已經就緒,即自上次調用select()方法後有多少通道變成就緒狀態。若是調用select()方法,由於有一個通道變成就緒狀態,返回了1,若再次調用select()方法,若是另外一個通道就緒了,它會再次返回1。若是對第一個就緒的channel沒有作任何操做,如今就有兩個就緒的通道,但在每次select()方法調用之間,只有一個通道就緒了。

  一旦調用了select()方法,而且返回值代表有一個或更多個通道就緒了,而後能夠經過調用selector的selectedKeys()方法,訪問「已選擇鍵集(selected key set)」中的就緒通道。以下所示: 

  Set selectedKeys = selector.selectedKeys();

  當向Selector註冊Channel時,Channel.register()方法會返回一個SelectionKey 對象。這個對象表明了註冊到該Selector的通道。

  注意每次迭代末尾的keyIterator.remove()調用。Selector不會本身從已選擇鍵集中移除SelectionKey實例。必須在處理完通道時本身移除。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中。

  SelectionKey.channel()方法返回的通道須要轉型成你要處理的類型,如ServerSocketChannel或SocketChannel等。

  一個完整的使用Selector和ServerSocketChannel的案例能夠參考案例5的selector()方法。

六  淺談零拷貝

1  引言

  傳統的 Linux 操做系統的標準 I/O 接口是基於數據拷貝操做的,即 I/O 操做會致使數據在操做系統內核地址空間的緩衝區應用程序地址空間定義的緩衝區之間進行傳輸。這樣作最大的好處是能夠減小磁盤 I/O 的操做,由於若是所請求的數據已經存放在操做系統的高速緩衝存儲器中,那麼就不須要再進行實際的物理磁盤 I/O 操做。可是數據傳輸過程當中的數據拷貝操做卻致使了極大的 CPU 開銷,限制了操做系統有效進行數據傳輸操做的能力。

  零拷貝( zero-copy )技術能夠有效地改善數據傳輸的性能,在內核驅動程序(好比網絡堆棧或者磁盤存儲驅動程序)處理 I/O 數據的時候,零拷貝技術能夠在某種程度上減小甚至徹底避免沒必要要 CPU 數據拷貝操做。

  零拷貝就是一種避免 CPU 將數據從一塊存儲拷貝到另一塊存儲的技術。針對操做系統中的設備驅動程序、文件系統以及網絡協議堆棧而出現的各類零拷貝技術極大地提高了特定應用程序的性能,而且使得這些應用程序能夠更加有效地利用系統資源。這種性能的提高就是經過在數據拷貝進行的同時,容許 CPU 執行其餘的任務來實現的。

  零拷貝技術能夠減小數據拷貝和共享總線操做的次數,消除傳輸數據在存儲器之間沒必要要的中間拷貝次數,從而有效地提升數據傳輸效率。並且,零拷貝技術減小了用戶應用程序地址空間和操做系統內核地址空間之間由於上下文切換而帶來的開銷。進行大量的數據拷貝操做實際上是一件簡單的任務,從操做系統的角度來講,若是 CPU 一直被佔用着去執行這項簡單的任務,那麼這將會是很浪費資源的;若是有其餘比較簡單的系統部件能夠代勞這件事情,從而使得 CPU 解脫出來能夠作別的事情,那麼系統資源的利用則會更加有效。

  綜上所述,零拷貝技術的目標能夠歸納以下:

  1. 避免數據拷貝

  (1)避免操做系統內核緩衝區之間進行數據拷貝操做。

  (2)避免操做系統內核和用戶應用程序地址空間這二者之間進行數據拷貝操做。

  (3)用戶應用程序能夠避開操做系統直接訪問硬件存儲。

  (4)數據傳輸儘可能讓 DMA 來作。

  2. 將多種操做結合在一塊兒

  (1)避免沒必要要的系統調用和上下文切換。

  (2)須要拷貝的數據能夠先被緩存起來。

  (3)對數據進行處理儘可能讓硬件來作。

2  零拷貝原理

1  IO讀寫方式

(1)中斷  

  中斷方式的流程圖以下

  

  1. 用戶進程發起數據讀取請求

  2. 系統調度爲該進程分配cpu

  3. cpu向io控制器(ide,scsi)發送io請求

  4. 用戶進程等待io完成,讓出cpu

  5. 系統調度cpu執行其餘任務

  6. 數據寫入至io控制器的緩衝寄存器

  7. 緩衝寄存器滿了向cpu發出中斷信號

  8. cpu讀取數據至內存

  缺點:中斷次數取決於緩衝寄存器的大小

(2) DMA : 直接內存存取

  DMA方式的流程圖以下:

   

  1. 用戶進程發起數據讀取請求

  2. 系統調度爲該進程分配cpu

  3. cpu向DMA發送io請求

  4. 用戶進程等待io完成,讓出cpu

  5. 系統調度cpu執行其餘任務

  6. 數據寫入至io控制器的緩衝寄存器

  7. DMA不斷獲取緩衝寄存器中的數據(須要cpu時鐘)

  8. 傳輸至內存(須要cpu時鐘)

  9. 所需的所有數據獲取完畢後向cpu發出中斷信號

  優勢:減小cpu中斷次數,不用cpu拷貝數據

2  數據拷貝

傳統IO

  下面展現了傳統方式讀取數據後並經過網絡發送所發生的數據拷貝:

  

  1. 一個read系統調用後,DMA執行了一次數據拷貝,從磁盤到內核空間

  2. read結束後,發生第二次數據拷貝,由cpu將數據從內核空間拷貝至用戶空間

  3. send系統調用,cpu發生第三次數據拷貝,由cpu將數據從用戶空間拷貝至內核空間(socket緩衝區)

  4. send系統調用結束後,DMA執行第四次數據拷貝,將數據從內核拷貝至協議引擎

  5. 另外,這四個過程當中,每一個過程都發生一次上下文切換

以上過程總結以下:

  1. 數據須要從磁盤拷貝到內核空間,再從內核空間拷到用戶空間(JVM)。

  2. 程序可能進行數據修改等操做

  3. 再將數據拷貝到內核空間,內核空間再拷貝到網卡內存,經過網絡發送出去(或拷貝到磁盤)

  磁盤到內核空間屬於DMA拷貝(DMA即直接內存存取,原理是外部設備不經過CPU而直接與系統內存交換數據)。而內核空間到用戶空間則須要CPU的參與進行拷貝,既然須要CPU參與,也就涉及到了內核態和用戶態的相互切換。

NIO的零拷貝

  零拷貝的數據拷貝以下圖:

  

  改進的地方:

    咱們已經將上下文切換次數從4次減小到了2次;

    將數據拷貝次數從4次減小到了3次(其中只有1次涉及了CPU,另外2次是DMA直接存取)。

  但這尚未達到咱們零拷貝的目標。若是底層NIC(網絡接口卡)支持gather操做,咱們能進一步減小內核中的數據拷貝。在Linux 2.4以及更高版本的內核中,socket緩衝區描述符已被修改用來適應這個需求。這種方式不但減小屢次的上下文切換,同時消除了須要CPU參與的重複的數據拷貝。用戶這邊的使用方式不變,而內部已經有了質的改變:

   

  NIO的零拷貝由transferTo()方法實現。transferTo()方法將數據從FileChannel對象傳送到可寫的字節通道(如Socket Channel等)。在內部實現中,由native方法transferTo0()來實現,它依賴底層操做系統的支持。在UNIX和Linux系統中,調用這個方法將會引發sendfile()系統調用。

使用場景通常是:

文件較大,讀寫較慢,追求速度
JVM內存不足,不能加載太大數據
內存帶寬不夠,即存在其餘程序或線程存在大量的IO操做,致使帶寬原本就小

  以上都創建在不須要進行數據文件操做的狀況下,若是既須要這樣的速度,也須要進行數據操做怎麼辦?
  那麼使用NIO的直接內存!

NIO的直接內存

  首先,它的做用位置處於傳統IO(BIO)與零拷貝之間,爲什麼這麼說?

  傳統IO,能夠把磁盤的文件通過內核空間,讀到JVM空間,而後進行各類操做,最後再寫到磁盤或是發送到網絡,效率較慢但支持數據文件操做。

  零拷貝則是直接在內核空間完成文件讀取並轉到磁盤(或發送到網絡)。因爲它沒有讀取文件數據到JVM這一環,所以程序沒法操做該文件數據,儘管效率很高!

  而直接內存則介於二者之間,效率通常且可操做文件數據

  直接內存(mmap技術)將文件直接映射到內核空間的內存,返回一個操做地址(address),它解決了文件數據須要拷貝到JVM才能進行操做的窘境。而是直接在內核空間直接進行操做,省去了內核空間拷貝到用戶空間這一步操做。

  NIO的直接內存是由MappedByteBuffer實現的。核心便是map()方法,該方法把文件映射到內存中,得到內存地址addr,而後經過這個addr構造MappedByteBuffer類,以暴露各類文件操做API。

  因爲MappedByteBuffer申請的是堆外內存,所以不受Minor GC控制,只能在發生Full GC時才能被回收。而DirectByteBuffer改善了這一狀況,它是MappedByteBuffer類的子類,同時它實現了DirectBuffer接口,維護一個Cleaner對象來完成內存回收。所以它既能夠經過Full GC來回收內存,也能夠調用clean()方法來進行回收。

  另外,直接內存的大小可經過jvm參數來設置:-XX:MaxDirectMemorySize

  NIO的MappedByteBuffer還有一個兄弟叫作HeapByteBuffer。顧名思義,它用來在堆中申請內存,本質是一個數組。因爲它位於堆中,所以可受GC管控,易於回收。

參考:

一、Java NIO?看這一篇就夠了!  https://mp.weixin.qq.com/s/c9tkrokcDQR375kiwCeV9w?

二、NIO相關基礎篇  https://mp.weixin.qq.com/s/ln5YR__A0RPpvqTVbj3o-g

三、IO多路複用之select、poll、epoll詳解  https://www.cnblogs.com/jeakeven/p/5435916.html

四、select、poll、epoll之間的區別總結[整理]  https://www.cnblogs.com/Anker/p/3265058.html  https://www.cnblogs.com/aspirant/p/9166944.html

五、NIO技術概覽  http://www.ideabuffer.cn/2017/08/13/NIO%E6%8A%80%E6%9C%AF%E6%A6%82%E8%A7%88/

六、淺談NIO與零拷貝 https://blog.csdn.net/localhost01/article/details/83422888

相關文章
相關標籤/搜索