Netty學習(7)——AIO

 

1. NIO2.0——AIO

    1. 從某種程度上來說,NIO依然是同步阻塞的

雖然NIO中Channel(網絡Channel)和Buffer可以實現非阻塞的read/write操作,而且Selector提供了多路複用的功能,使得可以在一個線程中管理使用多個IO通道,避免了傳統IO的存在的問題。但是,NIO中在Selector進行調用select()方法進行通道選擇時,其依舊是同步阻塞的,而且由於多個Channel註冊於Selector上,這個方法會同時阻塞多個IO請求操作,儘管select()方法可以設置超時返回,但依舊是不利的。

換句話說,雖然NIO在網絡操作中,提供了非阻塞的read/write方法,但是NIO的IO行爲還是同步的。對於NIO來說,我們的業務線程是在IO操作準備好時(調用Selector中的select()方法),得到通知(select()方法返回,表示有準備好的Channel),接着就由這個線程自行進行IO操作(通過Channel進行read/write操作),在第一代NIO中,每個線程可以持有多個IO通道並選擇使用,但實際上一個線程還是隻能選擇操作一個IO,IO操作本身是同步的。

    2. NIO改進
爲了真正實現異步非阻塞的IO操作,在NIO的基礎上進行改進,升級爲2代NIO——即AIO機制。

AIO相比於NIO,則更加進了一步,它不是在IO準備好時再通知線程,而是在IO操作已經完成後,再給線程發出通知。因此AIO是不會阻塞的,此時我們的業務邏輯將變成一個回調函數,等待IO操作完成後,由系統自動觸發。也就是相當於

在AIO中,當進行讀寫操作時,只須直接調用API的read或write方法即可。這兩種方法均爲異步的,對於讀操作而言,當有流可讀取時,操作系統會將可讀的流傳入read方法的緩衝區,並通知應用程序;對於寫操作而言,當操作系統將write方法傳遞的流寫入完畢時,操作系統主動通知應用程序。 即可以理解爲,read/write方法都是異步的,完成後會主動調用回調函數。

主要在Java.nio.channels包下增加了下面四個異步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

在AIO socket編程中,服務端通道是AsynchronousServerSocketChannel,這個類提供了一個open()靜態工廠,一個bind()方法用於綁定服務端IP地址(還有端口號),另外還提供了accept()用於接收用戶連接請求。在客戶端使用的通道是AsynchronousSocketChannel,這個通道處理提供open靜態工廠方法外,還提供了read和write方法。

在AIO編程中,發出一個事件(accept read write等)之後要指定事件處理類(回調函數),AIO中的事件處理類是CompletionHandler<V,A>,這個接口定義瞭如下兩個方法,分別在異步操作成功和失敗時被回調。

void completed(V result, A attachment);

void failed(Throwable exc, A attachment);

    3. AIO與NIO的實際區別

    在JAVA NIO框架中,我們說到了一個重要概念“selector”(選擇器)。它負責代替應用查詢中所有已註冊的通道到操作系統中進行IO事件輪詢、管理當前註冊的通道集合,定位發生事件的通道等操操作。

    但是在JAVA AIO框架中,由於應用程序不是“輪詢”方式,而是訂閱-通知方式,所以不再需要“selector”(選擇器)了,改由channel通道直接到操作系統註冊監聽。異步IO則採用“訂閱-通知”模式:即應用程序向操作系統註冊IO監聽,然後繼續做自己的事情。當操作系統發生IO事件,並且準備好數據後,在主動通知應用程序,觸發相應的函數。這就使得AIO真正意義上實現了異步阻塞模式。(AIO是依賴於操作系統的實現的)

和同步IO一樣,異步IO也是由操作系統進行支持的。微軟的windows系統提供了一種異步IO技術:IOCP(I/O CompletionPort,I/O完成端口);
Linux下由於沒有這種異步IO技術,所以使用的是epoll(類似於Selector的一種多路複用IO技術的實現)對異步IO進行模擬。

è¿éåå¾çæè¿°

 

2. AIO中的API使用

20180209170523764.jpg

    1. java.nio.channels.AsynchronousChannel:這是一個接口,用來標記一個channel支持異步IO操作。有主要的三個子類AsynchronousFileChannel、AsynchronousSocketChannel和AsynchronousServerSocketChannel,分別對應FileChannel、SocketChannel以及ServerSocketChannel。(很奇怪爲什麼沒有AsynchronousDatagramChannel)

    2. AsynchronousChannelGroup:異步channel的分組管理,目的是爲了資源共享。一個AsynchronousChannelGroup綁定一個線程池,這個線程池執行三個任務:等待IO事件、處理IO數據和派發CompletionHandler。AsynchronousServerSocketChannel創建的時候可以傳入一個AsynchronousChannelGroup,那麼通過AsynchronousServerSocketChannel創建的 AsynchronousSocketChannel將同屬於一個組,共享資源,(可以理解爲相當於Selector)。AsynchronousChannelGroup需要綁定線程池來創建,通過三個靜態方法來創建,可以需要根據具體應用相應調整。

public abstract class AsynchronousChannelGroup {
    public static AsynchronousChannelGroup withFixedThreadPool(int nThreads, ThreadFactory threadFactory);
    public static AsynchronousChannelGroup withCachedThreadPool(ExecutorService executor,int initialSize);
    public static AsynchronousChannelGroup withThreadPool(ExecutorService executor)​​​;
}

    3. CompletionHandler:異步IO操作結果的回調接口,用於定義在IO操作完成後所作的回調工作。AIO的API允許兩種方式來處理異步操作的結果,返回的Future模式或者註冊CompletionHandler,常用CompletionHandler的方式,這些handler的調用是由AsynchronousChannelGroup的線程池派發的。顯然,線程池的大小是性能的關鍵因素。

CompletionHandler接口有兩個個方法,分別對應於處理成功、失敗、被取消(通過返回的Future)情況下的回調處理:

public interface CompletionHandler<V,A> {  
  
    void completed(V result, A attachment);  
  
    void failed(Throwable exc, A attachment);  
}  

    4. ByteBuffer:負責承載通信過程中需要讀、寫的消息。

 

AsynchronousServerSocketChannel

    使用方式主要爲三步:打開通道、綁定監聽端口、接收客戶端連接請求。

    1. 打開(創建)通道

可以通過調用AsynchronousServerSocketChannel的靜態方法open()來創建AsynchronousServerSocketChannel實例

        try {
            AsynchronousServerSocketChannel serverSocket = AsynchronousServerSocketChannel.open();
        }catch (IOException e) {
            e.printStackTrace();
        }

或者在open()方法傳入AsynchronousChannelGroup參數,設置通道分組,以實現組內通道資源共享。如果通道打開失敗,就會拋出IOException

        try {
            ExecutorService pool = Executors.newCachedThreadPool();
            AsynchronousChannelGroup group = AsynchronousChannelGroup.withCachedThreadPool(pool, 1024);
            AsynchronousServerSocketChannel serverSocket = AsynchronousServerSocketChannel.open(group);
        }catch (IOException e) {
            e.printStackTrace();
        }

AsynchronousChannelGroup封裝了處理由綁定到組的異步通道所觸發的I/O操作完成所需的機制。每個AsynchronousChannelGroup關聯了一個被用於提交處理I/O事件和分發消費在組內通道上執行的異步操作結果的completion-handlers的線程池。除了處理I/O事件,該線程池還有可能處理其他一些用於支持完成異步I/O操作的任務。從上面例子可以看到,通過指定AsynchronousChannelGroup的方式打開AsynchronousServerSocketChannel,可以定製server channel執行的線程池。如果不指定AsynchronousChannelGroup,則AsynchronousServerSocketChannel會歸類到一個默認的分組中。

    2. 綁定監聽端口和地址

通過調用bind()方法來綁定要監聽的端口。


        try {
            ExecutorService pool = Executors.newCachedThreadPool();
            AsynchronousChannelGroup group = AsynchronousChannelGroup.withCachedThreadPool(pool, 1024);
            AsynchronousServerSocketChannel serverSocket = AsynchronousServerSocketChannel.open(group);
            int port = 8888;
            serverSocket.bind(new InetSocketAddress(port));
        }catch (IOException e) {
            e.printStackTrace();
        }

    3. 監聽和接收客戶端連接請求

監聽客戶端連接請求,主要通過調用accept()方法完成。accept()有兩個重載方法:

public abstract <A> void accept(A,CompletionHandler<AsynchronousSocketChannel,? super A>);
public abstract Future<AsynchronousSocketChannel> accept();

這兩個重載方法的行爲方式完全相同,提供CompletionHandler回調參數或者返回一個Future<T>類型變量。

Future版本的accept方法通過Future接口可以調用Future.get()方法阻塞等待調用結果,返回一個AsynchronousSocketChannel對象。

        try {
            ExecutorService pool = Executors.newCachedThreadPool();
            AsynchronousChannelGroup group = AsynchronousChannelGroup.withCachedThreadPool(pool, 1024);
            AsynchronousServerSocketChannel serverSocket = AsynchronousServerSocketChannel.open(group);
            int port = 8888;
            serverSocket.bind(new InetSocketAddress(port));
            while(true) {
                Future<AsynchronousSocketChannel> accept = serverSocket.accept();
                AsynchronousSocketChannel socket = accept.get();//阻塞方法,獲取AsynchronousSocketChannel
                //通過獲取的Socket來進行網絡IO操作
                //但一般不這樣使用,因爲這樣就會導致變得和第一代NIO一樣了,所以基本都是使用另一種CompletionHandler的重載方法
            }


        }catch (IOException e1) {
            e1.printStackTrace();
        }catch (InterruptedException e2) {
            e2.printStackTrace();
        } catch (ExecutionException e3) {
            e3.printStackTrace();
        }

而CompletionHandler回調參數版本則相反,真正的數據IO處理並不會放在當前線程中,而是通過一個回調方法處理,處理邏輯代碼就寫在CompletionHandler中的completed方法中,因爲該方法會在AsynchronousServerSocketChannel成功接收到一個AsynchronousSocketChannel,回調執行,而如果AsynchronousServerSocketChannel接受AsynchronousSocketChannel失敗,就會回調failed方法。

serverSocketChannel
.accept(serverSocketChannel, new CompletionHandler<AsynchronousSocketChannel,
        AsynchronousServerSocketChannel>() {
          @Override
          public void completed(final AsynchronousSocketChannel result,
                                final AsynchronousServerSocketChannel attachment) {
            // 接收到新的客戶端連接,此時本次accept已經完成
            // 繼續監聽下一個客戶端連接到來
            serverSocketChannel.accept(serverSocketChannel,this);
            // result即和該客戶端的連接會話
            // 此時可以通過result與客戶端進行交互
          }
          ...
        });

爲什麼會在completed方法中調用accept方法:因爲當一個新的客戶端建立連接之後,就會回調completed方法,一個AsynchronousServerSocketChannel會與多個客戶端建立連接,此時就需要繼續調用accept方法來接受更多的客戶端連接。

    4. 設置TCP連接屬性:通過一個AsynchronousServerSocketChannel建立的連接肯定是TCP連接了,所以通過該對象我們可以設置TCP連接的一些屬性。

// 設置socket選項,比如設置保持TCP連接,也就是TCP長連接
serverSocketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE,true);
// 獲取socket選項設置
boolean keepAlive = serverSocketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);

獲取本地IP地址

InetSocketAddress address = (InetSocketAddress) serverSocketChannel.getLocalAddress();

 

AsynchronousSocketChannel

    1. 創建連接

首先需要調用open方法創建一個AsynchronousSocketChannel對象,然後通過connect方法與服務端建立連接。connect方法也有兩個重載版本

一個版本是返回Future對象,另一種是傳入CompletionHandler參數對象

            AsynchronousSocketChannel socket = AsynchronousSocketChannel.open();
//            Future future = socket.connect(new InetSocketAddress("localhost",8888));
//            future.get();

            socket.connect(new InetSocketAddress("localhost", 8888),
                    socket, new CompletionHandler<Void, AsynchronousSocketChannel>() {
                        @Override
                        public void completed(Void result, AsynchronousSocketChannel attachment) {

                        }

                        @Override
                        public void failed(Throwable exc, AsynchronousSocketChannel attachment) {

                        }
                    });

    2. 寫數據

    構建一個ByteBuffer對象並調用socketChannel.write(ByteBuffer)方法異步發送消息,並通過CompletionHandler回調接收處理髮送結果:

ByteBuffer writeBuf = ByteBuffer.wrap("From socketChannel:Hello i am socketChannel".getBytes());
socketChannel.write(writeBuf, null, new CompletionHandler<Integer, Object>() {
  @Override
  public void completed(final Integer result, final Object attachment) {
    // 發送完成,result:總共寫入的字節數
  }

  @Override
  public void failed(final Throwable exc, final Object attachment) {
    // 發送失敗
  }
});

    3. 讀數據

    構建一個指定接收長度的ByteBuffer用於接收數據,調用socketChannel.read()方法讀取消息並通過CompletionHandler處理讀取結果:

ByteBuffer readBuffer = ByteBuffer.allocate(128);
socketChannel.read(readBuffer, null, new CompletionHandler<Integer, Object>() {
  @Override
  public void completed(final Integer result, final Object attachment) {
    // 讀取完成,result:實際讀取的字節數。如果通道中沒有數據可讀則result=-1。
  }

  @Override
  public void failed(final Throwable exc, final Object attachment) {
    // 讀取失敗
  }
});

    4. 通過AsynchronousSocketChannel也可以設置設置/獲取socket選項(TCP連接屬性)

// 設置socket選項
socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE,true);
// 獲取socket選項設置
boolean keepAlive = socketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);

 

CompletionHandler

    1. AIO中定義的異步通道允許指定一個CompletionHandler處理器消費一個異步操作的結果(也就是當準備好IO數據通道後,就回調CompletionHandler中的方法,使用IO數據通道進行IO處理,這也就導致了異步操作,不在等候IO通道的就緒,也不用將IO操作在當前線程中執行,而是採用回調的方式)。從上文中也可以看到,AIO中大部分的異步I/O操作接口都封裝了一個帶CompletionHandler類型參數的重載方法,使用CompletionHandler可以很方便地處理AIO中的異步I/O操作結果。CompletionHandler是一個具有兩個泛型類型參數的接口,聲明瞭兩個接口方法:

public interface CompletionHandler<V,A> {
    void completed(V result, A attachment);
    void failed(Throwable exc, A attachment);
}

 

AIO以及NIO存在的問題

    NIO以及AIOU雖然實現了異步非阻塞網絡IO操作,但是,其依舊具有一些缺點:

  1. 雖然JAVA NIO 和 JAVA AIO框架提供了多路複用IO/異步IO的支持,但是並沒有提供上層“信息格式”的良好封裝。例如前兩者並沒有提供針對 ProtocolBuffer、JSON這些信息格式的封裝,但是Netty框架提供了這些數據格式封裝(基於責任鏈模式的編碼和解碼功能)
  2. 要編寫一個可靠的、易維護的、高性能的(注意它們的排序)NIO/AIO服務器應用。除了框架本身要兼容實現各類操作系統的實現外。更重要的是它應該還要處理很多上層特有服務,例如:客戶端的權限、還有上面提到的信息格式封裝、簡單的數據讀取。這些Netty框架都提供了響應的支持。
  3. JAVA NIO框架存在一個poll/epoll bug:Selector doesn’t block on Selector.select(timeout),不能block意味着CPU的使用率會變成100%(這是底層JNI的問題,上層要處理這個異常實際上也好辦)。當然這個bug只有在Linux內核上才能重現。這個問題在JDK 1.7版本中還沒有被完全解決:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=2147719。雖然Netty 4.0中也是基於JAVA NIO框架進行封裝的(上文中已經給出了Netty中NioServerSocketChannel類的介紹),但是Netty已經將這個bug進行了處理。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章