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
物件,利用Future
的get
方法去獲取連線結果,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進行學習,它們還是有很多相似的地方。