Netty基礎------NIO 和 AIO

Netty基礎------NIO 和 AIO

目錄

Netty基礎------NIO 和 AIO

1、基本概念

2、 Java NIO 核心組件

2.1 緩衝區Buffer

2.2 選擇器Selector

2.3 通道Channel

3、NIO實例

4、總結(AIO)

5、補充NIO的三種模型



1、基本概念

java.nio全稱java non-blocking IO(實際上是 new io),是指JDK 1.4 及以上版本里提供的新api(New IO) ,爲所有的原始類型(boolean類型除外)提供緩存支持的數據容器,使用它可以提供非阻塞式的高伸縮性網絡。

NIO採用內存映射文件的方式來處理輸入輸出,NIO將文件或文件的一段區域映射到內存中,這樣就可以像訪問內存一樣訪問文件了。NIO與原來的IO有同樣的作用和目的,但是使用的方式完全不同, NIO支持面向緩衝區(Buffer)的、基於通道(Channel)的IO操作。NIO將以更加高效的方式進行文件的讀寫操作。

從對比中更加了解NIO:

NIO和BIO的區別:

  • 面向流與面向緩衝

       Java NIO 和 BIO 之間第一個最大的區別是,BIO 是面向流的,NIO 是面向緩衝區的

      Java BIO 面向流意味着每次從流中讀一個或多個字節,直至讀取所有字節,它們沒有被緩存在任何地方。此外,它不能前後移動流中的數據。如果需要前後移動從流中讀取的數據,需要先將它緩存到一個緩衝區。

      Java NIO 的緩衝導向方法略有不同。數據讀取到一個它稍後處理的緩衝區,需要時可在緩衝區中前後移動。這就增加了處理過程中的靈活性。但是,還需要檢查是否該緩衝區中包含所有您需要處理的數據。而且,需確保當更多的數據讀入緩衝區時,不要覆蓋緩衝區裏尚未處理的 數據。

  • 阻塞與非阻塞

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

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

  • NIO 的選擇器(Selector)

       Java NIO 的選擇器(Selector)允許一個單獨的線程來監視多個輸入通道,你可以註冊多個通道使用一個選擇器,然 後使用一個單獨的線程來“選擇”通道:這些通道里已經有可以處理的輸入,或者選擇已準備寫入的通道。這種選擇機制, 使得一個單獨的線程很容易來管理多個通道。

       而BIO並沒有選擇器。

2、 Java NIO 核心組件

  在NIO 中有幾個核心對象需要掌握:緩衝區(Buffer)、選擇器(Selector)、通道(Channel)。

2.1 緩衝區Buffer

1、什麼是Buffer緩存區

緩衝區Buffer是一塊連續的內存塊,是 NIO 數據讀或寫的中轉地

爲什麼說NIO是基於緩衝區的IO方式呢?

因爲,當一個鏈接建立完成後,IO的數據未必會馬上到達,爲了當數據到達時能夠正確完成IO操作,在BIO(阻塞IO)中,等待IO的線程必須被阻塞,以全天候地執行IO操作。爲了解決這種IO方式低效的問題,引入了緩衝區的概念,當數據到達時,可以預先被寫入緩衝區,再由緩衝區交給線程,因此線程無需阻塞地等待IO

  緩衝區實際上是一個容器對象,更直接的說,其實就是一個數組,在NIO 庫中,所有數據都是用緩衝區處理的。在讀取數據時,它是直接讀到緩衝區中的; 在寫入數據時,它也是寫入到緩衝區中的;任何時候訪問NIO 中的數據,都是將它放到緩衝區中。而在面向流I/O 系統中,所有數據都是直接寫入或者直接將數據讀取到Stream 對象中。在NIO 中,所有的緩衝區類型都繼承於抽象類Buffer,最常用的就是ByteBuffer,對於Java 中的基本類型,基本都有一個具體Buffer 類型與之相對應,它們之間的繼承關係如下圖所示:

2、緩衝區的基本實現

緩衝區對象本質上是一個數組,但它其實是一個特殊的數組,緩衝區對象內置了一些機制,能夠跟蹤和記錄緩衝區的狀態變化情況,如果我們使用get()方法從緩衝區獲取數據或者使用put()方法把數據寫入緩衝區,都會引起緩衝區狀態的變化。在緩衝區中,最重要的屬性有下面三個,它們一起合作完成對緩衝區內部狀態的變化跟蹤:

  • position:指定下一個將要被寫入或者讀取的元素索引,它的值由get()/put()方法自動更新,在新創建一個Buffer 對象時,position 被初始化爲0。
  • limit:指定還有多少數據需要取出(在從緩衝區寫入通道時),或者還有多少空間可以放入數據(在從通道讀入緩衝區時)。
  • capacity:指定了可以存儲在緩衝區中的最大數據容量,實際上,它指定了底層數組的大小,或者至少是指定了准許我們使用的底層數組的容量。

在對Buffer進行讀/寫操作前,我們可以調用Buffer類提供的一些輔助方法來正確設置 position 和 limit 的值,主要有如下幾個:

  • flip(): 設置 limit 爲 position 的值,然後 position 置爲0。對Buffer進行讀取操作前調用
  • rewind(): 僅僅將 position 置0。一般是在重新讀取Buffer數據前調用,比如要讀取同一個Buffer的數據寫入多個通道時會用到。
  • clear(): 回到初始狀態,即 limit 等於 capacity,position 置0。重新對Buffer進行寫入操作前調用。
  • compact(): 將未讀取完的數據(position 與 limit 之間的數據)移動到緩衝區開頭,並將 position 設置爲這段數據末尾的下一個位置。其實就等價於重新向緩衝區中寫入了這麼一段數據。

來看一幅圖:

假如我們創建了一個數組大小爲10的緩衝區,那麼剛開始時,position指向0,limit和capatity指向10;

當我們進行put進去4個大小的數據後,調用flip方法後,就會變成上圖,position和limit之間的數據代表着還沒有讀,讀數據時,position向limit移動。

2.2 選擇器Selector

Selector(選擇器)是一個特殊的組件,用於採集各個通道的狀態(或者說事件)。我們先將通道註冊到選擇器,並設置好關心的事件,然後就可以通過調用select()方法,靜靜地等待事件發生。通道有如下4個事件可供我們監聽:

  • Accept:有可以接受的連接
  • Connect:連接成功
  • Read:有數據可讀
  • Write:可以寫入數據了

  由於如果用阻塞I/O,需要多線程(浪費內存),如果用非阻塞I/O,需要不斷重試(耗費CPU)。Selector的出現解決了這尷尬的問題,非阻塞模式下,通過Selector,我們的線程只爲已就緒的通道工作,不用盲目的重試了。比如,當所有通道都沒有數據到達時,也就沒有Read事件發生,我們的線程會在select()方法處被掛起,從而讓出了CPU資源。

  傳統的Server/Client 模式會基於TPR(Thread per Request),服務器會爲每個客戶端請求建立一個線程,由該線程單獨負責處理一個客戶請求。這種模式帶來的一個問題就是線程數量的劇增,大量的線程會增大服務器的開銷。大多數的實現爲了避免這個問題,都採用了線程池模型,並設置線程池線程的最大數量,這又帶來了新的問題,如果線程池中有200 個線程,而有200 個用戶都在進行大文件下載,會導致第201個用戶的請求無法及時處理,即便第201 個用戶只想請求一個幾KB 大小的頁面。

  NIO 中非阻塞I/O 採用了基於Reactor 模式的工作方式,I/O 調用不會被阻塞,相反是註冊感興趣的特定I/O 事件,如可讀數據到達,新的套接字連接等等,在發生特定事件時,系統再通知我們。NIO 中實現非阻塞I/O 的核心對象就是Selector,Selector 就是註冊各種I/O 事件地方,而且當那些事件發生時,就是這個對象告訴我們所發生的事件,如下圖所示:

 

  從圖中可以看出,當有讀或寫等任何註冊的事件發生時,可以從Selector 中獲得相應的SelectionKey,同時從SelectionKey 中可以找到發生的事件和該事件所發生的具體的SelectableChannel,以獲得客戶端發送過來的數據。使用NIO 中非阻塞I/O 編寫服務器處理程序,大體上可以分爲下面三個步驟:

  1. 向Selector 對象註冊感興趣的事件。
  2. 從Selector 中獲取感興趣的事件。
  3. 根據不同的事件進行相應的處理。

2.3 通道Channel

Channel: 數據的源頭或者數據的目的地 ,用於向 buffer 提供數據或者讀取 buffer 數據 ,buffer 對象的唯一接口,異步 I/O 支持。

  Buffer作爲IO流中數據的緩衝區,而Channel則作爲socket的IO流與Buffer的傳輸通道。客戶端socket與服務端socket之間的IO傳輸不直接把數據交給CPU使用,而是先經過Channel通道把數據保存到Buffer,然後CPU直接從Buffer區讀寫數據,一次可以讀寫更多的內容。使用Buffer提高IO效率的原因(這裏與IO流裏面的BufferedXXStream、BufferedReader、BufferedWriter提高性能的原理一樣):IO的耗時主要花在數據傳輸的路上,普通的IO是一個字節一個字節地傳輸,而採用了Buffer的話,通過Buffer封裝的方法(比如一次讀一行,則以行爲單位傳輸而不是一個字節一次進行傳輸)就可以實現“一大塊字節”的傳輸。比如:IO就是送快遞,普通IO是一個快遞跑一趟,採用了Buffer的IO就是一車跑一趟。很明顯,buffer效率更高,花在傳輸路上的時間大大縮短。

  面向buffer的通道,一個Channel(通道)代表和某一實體的連接,這個實體可以是文件、網絡套接字等。也就是說,通道是Java NIO提供的一座橋樑,用於我們的程序和操作系統底層I/O服務進行交互。通道是一種很基本很抽象的描述,和不同的I/O服務交互,執行不同的I/O操作,實現不一樣,因此具體的有FileChannel、SocketChannel,ServerSocketChannel,DatagramChannel等。通道使用起來跟Stream比較像,可以讀取數據到Buffer中,也可以把Buffer中的數據寫入通道。但是channel是雙向的,而stream是單向的。

  通道是一個對象,通過它可以讀取和寫入數據,當然了所有數據都通過Buffer 對象來處理。我們永遠不會將字節直接寫入通道中,相反是將數據寫入包含一個或者多個字節的緩衝區。同樣不會直接從通道中讀取字節,而是將數據從通道讀入緩衝區,再從緩衝區獲取這個字節。在NIO 中,提供了多種通道對象,而所有的通道對象都實現了Channel 接口。

3、NIO實例

  服務端:

package com.NIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

/**
 * @author: zps
 **/
public class NIOServer {

    public static void main(String[] args) throws IOException {

        //這個方法就相當於創建了一個ServerSocket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        //創建一個selector 選擇器
        Selector selector = Selector.open();
        //這裏是需要注意的地方,默認是不阻塞的,所以需要手動設置
        serverSocketChannel.configureBlocking(false);
        //在選擇器中進行註冊
        serverSocketChannel.register(selector , SelectionKey.OP_ACCEPT);

        while (true){
             if(selector.select(1000) == 0){
                System.out.println("服務器等待了1s,無連接!");
                continue;
            }
             //通過key獲取註冊到selector的通道
             Set<SelectionKey> selectionKeys = selector.selectedKeys();

            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()){
                 SelectionKey key = iterator.next();
                 //根據key的類型做相應的處理
                 if(key.isAcceptable()){
                     SocketChannel socketChannel = serverSocketChannel.accept();
                     System.out.println("客戶端連接成功!");
                     socketChannel.configureBlocking(false);
                     //將客戶端的通道註冊到選擇器上,並將狀態設置爲有數據可讀
                     socketChannel.register(selector , SelectionKey.OP_READ , ByteBuffer.allocate(1024));
                 //繼續監聽
                 key.interestOps(SelectionKey.OP_ACCEPT);
                 }
                 if(key.isReadable()){
                     SocketChannel channel = (SocketChannel) key.channel();
                     //獲取該channel關聯的buffer
                     ByteBuffer buffer = (ByteBuffer) key.attachment();
                     channel.read(buffer);
                     System.out.println("客戶端傳來消息:" + new String(buffer.array()));

                     //當然,下面可以繼續設置事件的狀態
                     // key.interestOps(SelectionKey.OP_WRITE);
                 }
                 iterator.remove();
            }
        }
    }


}

客戶端: 

package com.NIO;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;

/**
 * @author: zps
 **/
public class NIOClient {
    public static void main(String[] args) throws IOException {
        //這個就相當於傳統的創建一個Socket
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
        InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost" , 8080);
        //連接服務器
        if(!socketChannel.connect(inetSocketAddress)){
            while (!socketChannel.finishConnect()){
                System.out.println("等待連接中。。。。。");
            }
        }
        ByteBuffer buffer = null;
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            String msg = scanner.nextLine();
            buffer = ByteBuffer.wrap(msg.getBytes());
            //發送消息
            socketChannel.write(buffer);
            buffer.clear();
        }
    }
}

4、總結(AIO)

這裏主要是對BIO 和 NIO 和 AIO之間的聯繫與區別做一些小總結:

  • 進程中的 IO 調用步驟大致可以分爲以下四步:

    • 進程向操作系統請求數據 ;
    • 操作系統把外部數據加載到內核的緩衝區中;
    • 操作系統把內核的緩衝區拷貝到進程的緩衝區 ;
    • 進程獲得數據完成自己的功能 ;
  • Java I/O 的基本概念:

    • 阻塞/非阻塞,針對的對象爲 調用者

    • 同步/異步,針對的對象爲 被調用者,同步指的是被調用方做完事情之後再返回,異步指的是被調用方先返回,然後再做事情,做完之後再想辦法通知調用方;

BIO

同步阻塞 I/O 模式

底層:服務端應用進程通過調用底層操作系統命令 recvfrom 去接收數據,而由於內核數據沒有準備好,應用進程就會阻塞,直到內核準備好數據並將其從內核複製到應用進程的緩衝區中或者發生錯誤才返回;

阻塞體現:若多個客戶端依此發送 socket 連接請求到服務器,那在服務端處理的時候,只能在處理完第一個 socket 請求後,才能處理下一個客戶端的請求,期間下一個客戶端只能掛起等待;

同步體現:此時服務端應用進程阻塞在當前連接請求操作上,當處理完第一個客戶端的請求後,再返回結果給客戶端;

場景:適用於,連接數少的場景下;

優勢:實現上比較簡單;

劣勢:吞吐量低,耗資源,效率不高;

NIO

同步非阻塞 I/O 模式,多路複用,引入了 channel(通道),selector(選擇器),buff(緩衝區) 的概念

底層:服務端應用進程通過調用底層操作系統命令 select 去接收數據,多個進程的 I/O 都可以註冊在同一個 select 上,當用戶進程調用該 select 時,select 去監聽所有註冊好的 I/O,如果所有被監聽的 I/O 需要的數據都沒有準備好,那麼 select 調用進程會被阻塞;如果有一個進程的 I/O 數據準備好了,select 就返回可讀條件,然後調用 recvfrom 把所讀數據複製到到應用進程緩衝區。

非阻塞體現:一個 select 處理多個客戶應用進程的 I/O,如果第一個 I/O 數據沒有準備好,那麼就去處理第二個客戶端的 I/O,依此類推,客戶端之間誰的數據先準備好就先處理誰的,不存在第二個要等第一個處理完才能開始處理的情況;

同步的體現:此時服務端應用進程也阻塞在當前連接請求操作,當處理完第一個客戶端的請求後,再返回結果給客戶端;

場景:適用於連接數目多,但短的場景;

優勢:可靠性高,性能也高;

劣勢:

  • 每個進程 select 能監聽的 I/O 數目是有限的,默認是 1024 個,故單機處理請求的能力有限;
  • 由於採用輪詢去監聽請求的狀態,當請求增多時,輪詢過程性能開銷就會變多;
  • 代碼實現上也比較複雜

AIO

異步非阻塞 I/O

底層:底層過程同 NIO,區別在於,AIO 使用的命令是 epoll ,使用事件驅動的方式來代替輪詢的方式,當監聽的 I/O 準備好了,採用事件驅動(事件回調)的方式通知進程去獲取數據

非阻塞體現:一個 epoll 監聽多個客戶應用進程的 I/O,當某個 I/O 準備好時,通過事件驅動的方式告知進程可以處理數據了,不存在第二個要等第一個處理完才能開始處理的情況;

異步的體現:服務端會先將 I/O 處理結果返回給客戶端(完成或尚未完成),如果尚未完成,服務端會接着處理,而客戶端可以做其他的業務處理,在事件回調中再來處理請求業務;

場景:適用於連接數目多且長的場景;

優勢:可靠性高,性能高;

5、補充NIO的三種模型

1、Reactor模式思想:分而治之+事件驅動

1)分而治之

一個連接裏完整的網絡處理過程一般分爲accept、read、decode、process、encode、send這幾步。

Reactor模式將每個步驟映射爲一個Task,服務端線程執行的最小邏輯單元不再是一次完整的網絡請求,而是Task,且採用非阻塞方式執行。

2)事件驅動

每個Task對應特定網絡事件。當Task準備就緒時,Reactor收到對應的網絡事件通知,並將Task分發給綁定了對應網絡事件的Handler執行。

3)幾個角色

Reactor:負責響應事件,將事件分發給綁定了該事件的Handler處理;

Handler:事件處理器,綁定了某類事件,負責執行對應事件的Task對事件進行處理;

Acceptor:Handler的一種,綁定了connect事件。當客戶端發起connect請求時,Reactor會將accept事件分發給Acceptor處理。

2、單線程Reactor

單線程reactor

1)優點:

不需要做併發控制,代碼實現簡單清晰。

2)缺點:

a)不能利用多核CPU;

b)一個線程需要執行處理所有的accept、read、decode、process、encode、send事件,處理成百上千的鏈路時性能上無法支撐;

c)一旦reactor線程意外跑飛或者進入死循環,會導致整個系統通信模塊不可用。

3、多線程Reactor

多線程reactor

特點:

a)有專門一個reactor線程用於監聽服務端ServerSocketChannel,接收客戶端的TCP連接請求;

b)網絡IO的讀/寫操作等由一個worker reactor線程池負責,由線程池中的NIO線程負責監聽SocketChannel事件,進行消息的讀取、解碼、編碼和發送。

c)一個NIO線程可以同時處理N條鏈路,但是一個鏈路只註冊在一個NIO線程上處理,防止發生併發操作問題。

4、主從多線程

主從多線程reactor

在絕大多數場景下,Reactor多線程模型都可以滿足性能需求;但是在極個別特殊場景中,一個NIO線程負責監聽和處理所有的客戶端連接可能會存在性能問題。

特點:

a)服務端用於接收客戶端連接的不再是個1個單獨的reactor線程,而是一個boss reactor線程池;

b)服務端啓用多個ServerSocketChannel監聽不同端口時,每個ServerSocketChannel的監聽工作可以由線程池中的一個NIO線程完成。



參考:https://www.jianshu.com/p/38b56531565d

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章