Java NIO.2系列文章之非同步通道API入門

pjmike_pj發表於2019-03-01

NIO.2概覽

NIO.2也就是人們常說的 AIO,在Java 7中引入了NIO的改進版NIO 2,它是非同步非阻塞的IO方式。

AIO的核心概念就是發起非阻塞方式的I/O操作,立即響應,卻不立即返回結果,當I/O操作完成時通知。

這篇文章主要介紹NIO 2的非同步通道API的一些內容,後續文章再分析NIO.2的其他特性

非同步通道API

從Java 7開始,java.nio.channel包中新增加4個非同步通道:

  • AsynchronousSocketChannel
  • AsynchronousServerSocketChannel
  • AsynchronousFileChannel
  • AsynchronousDatagramChannel

這些類在風格上與NIO 通道API相似,他們共享相同的方法與引數結構體,並且大多數對於NIO通道類可用的引數,對於新的非同步版本仍然可用。

非同步通道API提供了兩種對已啟動非同步操作的監測與控制機制:

  • 第一種通過返回一個 java.util.concurrent.Future物件來表示非同步操作的結果
  • 第二種是通過傳遞給操作一個新類的物件java.nio.channels.CompletionHandler來完成,它會定義在操作完畢後所執行的處理程式方法。

Future

從Java 1.5開始,引入了Future介面,使用該介面可以在任務執行完畢之後得到任務執行結果。在NIO 2中,Future物件表示非同步操作的結果,假設我們要建立一個伺服器來監聽客戶端連線,開啟AsynchronousServerSocketChannel 並將其繫結到類似於 ServerSocketChannel的地址:

AsynchronousServerSocketChannel server 
  = AsynchronousServerSocketChannel.open().bind(null);
複製程式碼

方法bind() 將一個套接字地址作為其引數,這裡傳遞了一個Null地址,它會自動將套接字繫結到本地主機地址,並使用空閒的臨時埠,就像傳統的 ServerSocket設定 0埠一樣,也是使用作業系統隨機分配的臨時埠。
然後呼叫伺服器的accept()方法:

Future<AsynchronousSocketChannel> future = server.accept();

複製程式碼

當我們在NIO中呼叫 ServerSocketChannel的accept()方法時,它會阻塞,直到從客戶端收到傳入連線。但是AsynchronousServerSocketChannel 的accept() 方法會立即返回 Future 物件。

Future物件的泛型型別是操作的返回型別,在上面的例子,它是 AsynchronousSocketChannel ,但它也可以是Integer或String ,具體取決於操作的最終返回型別。

我們可以使用Future物件來查詢操作的狀態

future.isDone();
複製程式碼

如果基礎操作已經完成,則此API返回 true,請注意,在這種情況下,完成可能意味著正常終止,異常,或者取消。

我們還可以明確檢查操作是否被取消,如果操作在正常完成之前被取消,則它返回true。如下:

future.isCancelled();
複製程式碼

實際的取消操作,如下:

future.cancel(true)
複製程式碼

cancel()方法可利用一個布林標誌來指出執行接受的執行緒是否可被中斷。

要檢索操作結果,我們使用get()方法,該方法將阻塞等待結果的返回:

AsynchronousSocketChannel client= future.get();
複製程式碼

另外,我們也可以設定阻塞時間,下例設定為10s:

AsynchronousSocketChannel worker = future.get(10, TimeUnit.SECONDS);
複製程式碼

CompletionHandler

使用 Future 來處理操作的替代方法是使用 CompletionHandler 類的回撥機制。非同步通道允許指定完成處理程式以使用操作的結果:

AsynchronousServerSocketChannel listener
  = AsynchronousServerSocketChannel.open().bind(null);
 
listener.accept(
  attachment, new CompletionHandler<AsynchronousSocketChannel, Object>() {
    public void completed(
      AsynchronousSocketChannel client, Object attachment) {
          // do whatever with client
      }
    public void failed(Throwable exc, Object attachment) {
          // handle failure
      }
  });
複製程式碼

I/O操作成功完成時,將呼叫已完成的回撥 API。如果操作失敗,則呼叫失敗的API.

非同步通道API例項

服務端 (with Future)

下面是使用 Future的方式構建服務端。

public class AsyncEchoServer {
    private AsynchronousServerSocketChannel server;
    private Future<AsynchronousSocketChannel> future;
    private AsynchronousSocketChannel worker;

    public AsyncEchoServer() throws IOException, ExecutionException, InterruptedException {
        System.out.println("Open Server Channel");
        server = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress("127.0.0.1", 9090));
        future = server.accept();
    }

    public void runServer() throws ExecutionException, InterruptedException, IOException, TimeoutException {
        //獲取操作結果
        worker = future.get();
        if (worker != null && worker.isOpen()) {
            ByteBuffer buffer = ByteBuffer.allocate(100);
            //將通道中的資料寫入緩衝區
            worker.read(buffer).get(10,TimeUnit.SECONDS);
            System.out.println("received from client: " + new String(buffer.array()));
        }
        server.close();
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException, IOException, TimeoutException {
        AsyncEchoServer server = new AsyncEchoServer();
        server.runServer();
    }
}

複製程式碼

服務端(With CompletionHandler)

下面我們將瞭解如何使用 CompletionHandler 方法而不是 Future 方法實現相同的服務端程式碼。

public class AsyncEchoServerWithCallBack {
    private AsynchronousServerSocketChannel server;
    private AsynchronousSocketChannel worker;
    private AsynchronousChannelGroup group;
    public AsyncEchoServerWithCallBack() throws IOException, ExecutionException, InterruptedException {
        System.out.println("Open Server Channel");
        group = AsynchronousChannelGroup.withFixedThreadPool(10, Executors.defaultThreadFactory());
        server = AsynchronousServerSocketChannel.open(group).bind(new InetSocketAddress("127.0.0.1", 9090));
        //當有新連線建立時會呼叫 CompletionHandler介面實現物件中的 completed()方法
        server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
            @Override
            public void completed(AsynchronousSocketChannel result, Object attachment) {
                if (server.isOpen()) {
                    server.accept(null, this);
                }
                worker = result;
                if ((worker != null) && (worker.isOpen())) {
                    ByteBuffer byteBuffer = ByteBuffer.allocate(100);
                    worker.read(byteBuffer);
                    System.out.println("received the client: "+new String(byteBuffer.array()));
                }
            }

            @Override
            public void failed(Throwable exc, Object attachment) {
                //TODO
            }
        });
    }
    public static void main(String[] args) throws InterruptedException, ExecutionException, IOException, TimeoutException {
        AsyncEchoServerWithCallBack server = new AsyncEchoServerWithCallBack();
    }
}

複製程式碼

當有新連線建立時會呼叫 CompletionHandler介面實現物件中的 completed()方法,當出現錯誤時,會呼叫 failed方法。

accept方法的第一個引數可以是一個任意型別的物件,稱為呼叫時的” 附加物件”。附件物件在 accept()方法呼叫時傳入,可以在 CompletionHandler 介面的實現物件中從 completed 和 failed 方法的引數(attachment)中獲取,這樣就可以進行資料的傳遞。使用 CompletionHandler介面的方法都支援使用附件物件來傳遞資料。

AsynchronousChannelGroup類

非同步通道在處理 I/O請求時,需要使用一個AsynchronousChannelGroup類,該類的物件表示的是一個非同步通道的分組,每一個分組都有一個執行緒池與之對應,需要使用AsynchronousChannelGroup類的靜態工廠方法 withFixedThreadPool,withCachedThreadPool或者 withThreaPool設定執行緒池。這個執行緒池中的執行緒用來處理 I/O 事件。多個非同步通道可以共用一個分組的執行緒池資源。

呼叫AsynchronousSocketChannel 和 AsynchronousServerSocketChannel 類的 open 方法 開啟非同步套接字通道時,可以傳入一個AsynchronousChannelGroup類的物件。如果呼叫的 open 方法沒有傳入 AsynchronousChannelGroup 類的物件,預設使用系統提供的分組,系統分組對應的執行緒池中的執行緒是守護執行緒,如果使用預設分組,程式啟動之後很快就退出了,因為系統分組使用的守護執行緒不會阻止虛擬機器的退出。

客戶端

public class AsyncEchoClient {
    private AsynchronousSocketChannel client;
    private Future<Void> future;

    public AsyncEchoClient() throws IOException {
        //開啟一個非同步channel
        System.out.println("Open client channel");
        client = AsynchronousSocketChannel.open();
        //連線本地埠和地址,在連線成功後不返回任何內容,但是,我們仍然可以使用Future物件來監視非同步操作的狀態
        System.out.println("Connect to server");
        future = client.connect(new InetSocketAddress("127.0.0.1", 9090));
    }

    /**
     * 向服務端傳送訊息
     *
     * @param message
     * @return
     */
    public void sendMessage(String message) throws ExecutionException, InterruptedException {
        if (!future.isDone()) {
            future.cancel(true);
            return;
        }
        //將一個位元組陣列封裝到ByteBuffer中
        ByteBuffer byteBuffer = ByteBuffer.wrap(message.getBytes());
        System.out.println("Sending message to the server");
        //將資料寫入通道
        int numberBytes = client.write(byteBuffer).get();
        byteBuffer.clear();
    }

    public static void main(String[] args) throws IOException, ExecutionException, InterruptedException {
        AsyncEchoClient client = new AsyncEchoClient();
        client.sendMessage("hello world");
    }
}

複製程式碼

測試結果

客戶端:

Open client channel
Connect to server
複製程式碼

服務端:

Open Server Channel
received the client: hello world
複製程式碼

Java NIO 2非同步IO的體現

我們都知道由 JDK 1.7提供的NIO 2.0 新增了非同步的套接字通道,它是真正的非同步 IO,在非同步IO操作的時候可以傳遞變數,當操作完成之後會回撥相關的方法。那麼NIO 2的非同步非阻塞特性是如何體現的呢?從之前的描述就可以窺見很多細節:

非同步的體現

AsynchronousServerSocketChannel為例,當呼叫該類的物件的 accept()方法時,其返回了一個 Future<AsynchronousSocketChannel>物件,呼叫accept()方法就像呼叫傳統I/O中的ServerSocket的accept()一樣,本質上都是接收客戶端連線請求,只不過AsynchronousServerSocketChannel物件沒有一直阻塞等待,而是立馬返回一個Future物件,利用Futureget方法去獲取連線結果,Future物件就是非同步操作的結果,我們還可以利用Future的isDone方法查詢操作完成的狀態,這就是非同步的體現。

當然在使用 CompletionHandler 方法中一樣的道理,有新連線建立時會回撥 CompletionHandler介面實現物件中的 completed()方法,當出現錯誤時,會呼叫 failed方法。

非阻塞的體現

當呼叫AsynchronousServerSocketChannel物件的 accept()方法後,返回Future物件,此時執行緒可以接著幹其他事情,這是非阻塞的,要想獲得操作結果,就呼叫 Future的 isDone方法查詢操作是否完畢,使用 get()去獲取結果,典型的非阻塞操作。而在傳統 I/O模型中,套接字類物件的 accept方法會一直阻塞等待,直到有新連線接入進來才停止阻塞。

小結

NIO.2,也叫AIO,瞭解其非同步通道API,也能更好地幫助我們去理解非同步IO操作。當我們學習NIO2的API時,也可以對照NIO中的通道API進行學習,它們還是有很多相似的地方。

參考資料 & 鳴謝

相關文章