深入學習Netty(3)——傳統AIO程式設計

JJian發表於2021-07-07

前言

  之前已經整理過了BIO、NIO兩種I/O的相關博文,每一種I/O都有其特點,但相對開發而言,肯定是要又高效又簡單的I/O程式設計才是真正需要的,在之前的NIO博文(深入學習Netty(2)——傳統NIO程式設計)中就已經介紹過NIO程式設計的缺點(相比較而言的缺點:同步非阻塞,需要單獨開啟執行緒不斷輪詢),所以才會有真正的非同步非阻塞I/O出現,這就是此篇博文需要介紹的AIO程式設計。

  參考資料《Netty In Action》、《Netty權威指南》(有需要的小夥伴可以評論或者私信我)

  博文中所有的程式碼都已上傳到Github,歡迎Star、Fork

  感興趣可以先學習相關博文:

 


 

一、NIO 2.0與AIO程式設計

  JDK 1.7升級了NIO類庫,升級後的NIO類庫稱之為NIO 2.0,Java提供了非同步檔案I/O操作,同時提供了與UNIX網路程式設計事件驅動I/O對應的AIO

  NIO 2.0的非同步套接字通道是真正的非同步非阻塞I/O,對應有UNIX網路程式設計中的事件驅動I/O(AIO),相比較NIO,它不需要通過Selector對註冊的通道進行輪詢操作即可實現非同步讀寫,簡化了NIO的程式設計模型。

  NIO 2.0提供了新的非同步通道的概念,非同步通道提供了以下兩種方式獲取操作結果:

  • 通過juc.Futrue類來表示非同步操作的結果
AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8080);
Future<Void> connect = socketChannel.connect(inetSocketAddress);
while (!connect.isDone()) {
  Thread.sleep(
10); }
  • 在非同步操作的時候傳入java.nio.channels。實現CompletionHandler介面complete()的方法作為操作完成回撥
private class MyCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
 
  @Override
  public void completed(Integer result, ByteBuffer attachment) {
    // TODO 回撥後業務操作
  }
  @Override
  public void failed(Throwable t, ByteBuffer attachment) {
    t.printStackTrace();
  }

二、AIO服務端

(1)服務端AIO非同步處理任務AsyncTimeServerHandler

  • 建立非同步服務通道並監聽埠
  • 非同步監聽客戶端連線
/**
 * 服務端AIO非同步處理任務
 * -建立非同步服務通道監聽埠
 * -監聽客戶端連線
 */
public class AsyncTimeServerHandler implements Runnable{

    private int port;

    CountDownLatch latch;
    AsynchronousServerSocketChannel asynchronousServerSocketChannel;

    public AsyncTimeServerHandler(int port) {
        this.port = port;
        try {
            // 建立非同步的服務通道asynchronousServerSocketChannel, 並bind監聽埠
            asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open();
            asynchronousServerSocketChannel.bind(new InetSocketAddress(port));
            System.out.println("The time server is start in port : " + port);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        // countDownLatch沒有count減一,所以導致一直阻塞
        latch = new CountDownLatch(1);
        doAccept();
        try {
            // 防止執行操作執行緒還未結束,服務端執行緒就退出,程式不退出的前提下,才能夠讓accept繼續可以回撥接受來自客戶端的連線
            // 實際開發過程中不需要單獨開啟執行緒去處理AsynchronousServerSocketChannel
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 接收客戶端的連線
     * 引數CompletionHandler型別的handler例項來接收accept操作成功的通知訊息
     */
    public void doAccept() {
        asynchronousServerSocketChannel.accept(this, new AcceptCompletionHandler());
    }
}

(2)服務端連線非同步回撥處理器AcceptCompletionHandler:非同步處理客戶端連線完成後的操作

/**
 * 客戶端連線非同步處理器
 * completed()方法完成回撥logic
 * failed()方法完成失敗回撥logic
 */
public class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, AsyncTimeServerHandler> {

    /**
     * 呼叫該方法表示客戶端已經介接入成功
     * 同時再accept接收新的客戶端連線
     * @param result
     * @param attachment
     */
    @Override
    public void completed(AsynchronousSocketChannel result,
                          AsyncTimeServerHandler attachment) {
        // 此時還要繼續呼叫accept方法是因為,completed方法表示上一個客戶端連線完成,而下一個新的客戶端需要連線
        // 如此形成新的迴圈:每接收一個客戶端的成功連線之後,再非同步接收新的客戶端連線
        attachment.asynchronousServerSocketChannel.accept(attachment, this);
        // 預分配1M的緩衝區
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 呼叫read方法非同步讀,傳入CompletionHandler型別引數非同步回撥讀事件
        result.read(buffer, buffer, new ReadCompletionHandler(result));
    }

    @Override
    public void failed(Throwable exc, AsyncTimeServerHandler attachment) {
        exc.printStackTrace();
        // 讓服務執行緒不再阻塞
        attachment.latch.countDown();
    }
}

(3)服務端read事件非同步回撥處理器ReadCompletionHandler:非同步回撥處理客戶端請求資料

/**
 * 服務端read事件非同步處理器
 *  completed非同步回撥處理客戶端請求資料
 */
public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
    private AsynchronousSocketChannel channel;

    public ReadCompletionHandler(AsynchronousSocketChannel channel) {
        if (this.channel == null) {
            this.channel = channel;
        }
    }

    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        attachment.flip();
        // 根據緩衝區的可讀位元組建立byte陣列
        byte[] body = new byte[attachment.remaining()];
        attachment.get(body);
        try {
            // 解析請求命令
            String req = new String(body, "UTF-8");
            System.out.println("The time server receive order : " + req);
            String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(req) ? new java.util.Date(
                    System.currentTimeMillis()).toString() : "BAD ORDER";
            // 傳送當前時間給客戶端
            doWrite(currentTime);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    private void doWrite(String currentTime) {
        if (currentTime != null && currentTime.trim().length() > 0) {
            byte[] bytes = (currentTime).getBytes();
            ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
            writeBuffer.put(bytes);
            writeBuffer.flip();
            // write非同步回撥,傳入CompletionHandler型別引數
            channel.write(writeBuffer, writeBuffer,
                    new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer buffer) {
                            // 如果沒有傳送完成,繼續傳送
                            if (buffer.hasRemaining()) {
                                channel.write(buffer, buffer, this);
                            }
                        }

                        @Override
                        public void failed(Throwable exc, ByteBuffer attachment) {
                            try {
                                channel.close();
                            } catch (IOException e) {
                                // TODO 只要是I/O異常就需要關閉鏈路,釋放資源

                            }
                        }
                    });
        }
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        try {
            this.channel.close();
        } catch (IOException e) {
            e.printStackTrace();
            // TODO 只要是I/O異常就需要關閉鏈路,釋放資源
        }
    }
}

(4)服務端啟動TimeServer

/**
 * AIO 非同步非阻塞服務端
 * 不需要單獨開執行緒去處理read、write等事件
 * 只需要關注complete-handlers中的回撥completed方法
 */
public class TimeServer {

    public static void main(String[] args) throws IOException {
        int port = 8086;
        AsyncTimeServerHandler timeServer = new AsyncTimeServerHandler(port);
        new Thread(timeServer, "AIO-AsyncTimeServerHandler").start();
    }
}

(5)啟動服務端

服務端Console:

使用命令netstat檢視8086埠是否監聽

三、AIO客戶端

(1)客戶端AIO非同步回撥處理任務

  • 開啟AsynchronousSocketChannel通道,連線服務端
  • 傳送服務端指令
  • 回撥處理服務端應答
/**
 * 客戶端AIO非同步回撥處理任務
 * -開啟AsynchronousSocketChannel通道,連線服務端
 * -傳送服務端指令
 * -回撥處理服務端應答
 */
public class AsyncTimeClientHandler implements CompletionHandler<Void, AsyncTimeClientHandler>, Runnable {

    private AsynchronousSocketChannel client;
    private String host;
    private int port;
    private CountDownLatch latch;

    public AsyncTimeClientHandler(String host, int port) {
        this.host = host;
        this.port = port;
        try {
            client = AsynchronousSocketChannel.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        latch = new CountDownLatch(1);
        client.connect(new InetSocketAddress(host, port), this, this);
        try {
            // 防止非同步操作都沒完成,連線執行緒就結束退出
            latch.await();
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
        try {
            client.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 傳送請求完成非同步回撥
     * @param result
     * @param attachment
     */
    @Override
    public void completed(Void result, AsyncTimeClientHandler attachment) {
        byte[] req = "QUERY TIME ORDER".getBytes();
        ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
        writeBuffer.put(req);
        writeBuffer.flip();
        client.write(writeBuffer, writeBuffer,
                new CompletionHandler<Integer, ByteBuffer>() {
                    @Override
                    public void completed(Integer result, ByteBuffer buffer) {
                        if (buffer.hasRemaining()) {
                            client.write(buffer, buffer, this);
                        } else {
                            ByteBuffer readBuffer = ByteBuffer.allocate(1024);
                            // 回撥服務端應答訊息
                            client.read(readBuffer, readBuffer,
                                    new CompletionHandler<Integer, ByteBuffer>() {
                                        @Override
                                        public void completed(Integer result, ByteBuffer buffer) {
                                            buffer.flip();
                                            byte[] bytes = new byte[buffer.remaining()];
                                            buffer.get(bytes);
                                            String body;
                                            try {
                                                body = new String(bytes, "UTF-8");
                                                System.out.println("Now is : " + body);
                                                // 服務端應答完成後,連線執行緒退出
                                                latch.countDown();
                                            } catch (UnsupportedEncodingException e) {
                                                e.printStackTrace();
                                            }
                                        }

                                        @Override
                                        public void failed(Throwable exc, ByteBuffer attachment) {
                                            try {
                                                client.close();
                                                // 防止執行緒一直阻塞
                                                latch.countDown();
                                            } catch (IOException e) {
                                                // ingnore on close
                                            }
                                        }
                                    });
                        }
                    }

                    @Override
                    public void failed(Throwable exc, ByteBuffer attachment) {
                        try {
                            client.close();
                            latch.countDown();
                        } catch (IOException e) {
                            // ingnore on close
                        }
                    }
                });
    }

    @Override
    public void failed(Throwable exc, AsyncTimeClientHandler attachment) {
        exc.printStackTrace();
        try {
            client.close();
            latch.countDown();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

(2)客戶端TimeClient

/**
 * AIO 非同步非阻塞 客戶端
 * 不需要單獨開執行緒去處理read、write等事件
 * 只需要關注complete-handlers中的回撥completed方法
 */
public class TimeClient {
    public static void main(String[] args) {
        int port = 8086;
        new Thread(new AsyncTimeClientHandler("127.0.0.1", port), "AIO-AsyncTimeClientHandler").start();

    }
}

(3)啟動客戶端

客戶端Console:

服務端Console:

四、總結

服務端通過countDownLatch一直阻塞

由程式碼實踐我們可知:

  JDK底層通過ThreadPoolExecutor執行回撥通知,非同步回撥通知類由sun.nio.ch.AsynchronousChannelGroupImpl實現,然後將任務提交到該執行緒池以處理I/O事件,並分派給completion-handlers ,該佇列消耗對組中通道執行的非同步操作的結果

  非同步SocketChannel是被動執行,不需要單獨像NIO程式設計那樣單獨建立一個獨立的I/O執行緒處理讀寫操作,都是由JDK底層的執行緒池負責回撥並驅動讀寫操作的。所以基於NIO 2.0的新的非同步非阻塞相比較NIO程式設計要簡單,這兩區別在於:

  • 在NIO中等待IO事件由我們註冊的selector來完成,在感興趣的事情來了,我們的執行緒來accept.read.write.connect...解析,解析完後再交由業務邏輯處理。
  • 而在在非同步IO(AIO、NIO 2.0)中等待IO事件同樣為accept,read,write,connect,但資料處理交由系統完成,我們需要做的就是在completionHandlers中處理業務邏輯回撥即可

相關文章