前言
之前已經整理過了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中處理業務邏輯回撥即可。