Java中BIO,NIO和AIO使用樣例
上文中分析了阻塞,非阻塞,同步和非同步概念上的區別以及各種IO模型的操作流程,本篇文章將主要介紹Java中BIO,NIO和AIO三種IO模型如何使用。需要注意的是,本文中所提到的所有樣例都是在一個server對應一個client的情況下工作的,如果你想擴充套件為一個server服務多個client,那麼程式碼需要做相應的修改才能使用。另外,本文只會講解server端如何處理,客戶端的操作流程可以仿照服務端進行程式設計,大同小異。文章最後給出了原始碼的下載地址。
BIO(Blocking I/O)
在Java中,BIO是基於流的,這個流包括位元組流或者字元流,但是細心的同學可能會發現基本上所有的流都是單向的,要麼只讀,要麼只寫。在實際上程式設計時,在對IO操作之前,要先獲取輸入流或輸出流,然後對輸入流讀或對輸出流寫即完成實際的IO讀寫操作。 首先需要新建一個ServerSocket物件監聽特定埠,然後當有客戶端的連線請求到來時,在伺服器端獲取一個Socket物件,用來進行實際的通訊。
ServerSocket serverSocket = new ServerSocket(PORT);
Socket socket = serverSocket.accept();
獲取到Socket物件後,通過這個Socket物件拿到輸入流和輸出流就可以進行相應的讀寫操作了。
DataInputStream in = new DataInputStream(socket.getInputStream());
DataOutputStream out = new DataOutputStream(socket.getOutputStream());
由於BIO的程式設計的模型比較簡單,這裡就寫這麼多,需要下載原始碼的可以到文章末尾。
NIO(New I/O, or Nonblocking I/O)
BIO的程式設計模型簡單易行,但是缺點也很明顯。由於採用的是同步阻塞IO的模式,所以server端要為每一個連線建立一個執行緒,一方面,執行緒之間在進行上下文切換的時候會造成比較大的開銷,另一方面,當連線數過多時,可能會造成伺服器崩潰的現象產生。
為了解決這個問題,在JDK 1.4的時候,引入了NIO(New IO)的概念。NIO主要由三個部分組成,即Channel,Buffer和Selector。Channel可以跟BIO中的Stream類比,不同的是Channel是可讀可寫的。當和Channel進行互動的時候需要Buffer
// Read data from channel to buffer
SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (socketChannel.read(byteBuffer) > 0) {
byteBuffer.flip();
while(byteBuffer.hasRemaining()){
System.out.print((char) byteBuffer.get());
}
byteBuffer.clear();
}
// Write data to channel from buffer
socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
NIO中另一個重要的元件是Selector,Selector可以用來檢查一個或多個Channel是否有新的資料到來,這種方式可以實現在一個執行緒中管理多個Channel的目的,示意圖如下。
在使用selector之前,一定要注意把對應的Channel配置為非阻塞。否則在註冊的時候會拋異常。
serverSocketChannel.configureBlocking(false);
然後呼叫select函式,select是個阻塞函式,它會阻塞直到某一個操作被啟用。這個時候可以獲取一系列的SelectionKey,通過這個SelectionKey可以判斷其對應的Channel可進行的操作(可讀,可寫或者可接受連線),然後進行相應的操作即可。這裡還要注意一個問題就是在判斷完可執行的操作後,需要將這個SelectionKey從集合中移除。
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (!selectionKey.isValid())
continue;
if (selectionKey.isAcceptable()) {
// ready for accepting
} else if (selectionKey.isReadable()) {
// ready for reading
} else if (selectionKey.isWritable()) {
// ready for writing
}
iterator.remove();
}
NIO這裡最後一個問題是,什麼時候Channel可寫,這個問題困擾了我很久,經過從網上查資料最後得出的結論是,只要這個Channel處於空閒狀態,都是可寫的。這個我也從實際的程式中論證了。
AIO(Asynchronous I/O)
在JDK 1.7時,Java引入了AIO的概念,AIO還是基於Channel和Buffer的,不同的是它是非同步的。使用者執行緒把實際的IO操作以及資料拷貝全部委託給核心來做,使用者只要傳遞給核心一個用於儲存資料的地址空間即可。核心處理的結果通過兩種方式返回給使用者執行緒。一是通過Future物件,另外一種是通過回撥函式的方式,回撥函式需要實現CompletionHandler介面。這裡只給出通過回撥方式處理資料的樣例,其中關鍵的步驟已經在程式中添加了註釋。
// 建立AsynchronousServerSocketChannel監聽特定埠,並設定回撥AcceptCompletionHandler
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(PORT));
serverSocketChannel.accept(serverSocketChannel, new AcceptCompletionHandler());
// 監聽回撥,當用連線時會觸發該回調
private static class AcceptCompletionHandler implements CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel> {
@Override
public void completed(AsynchronousSocketChannel result, AsynchronousServerSocketChannel attachment) {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 註冊read請求以及回撥ReadCompletionHandler
result.read(byteBuffer, result, new ReadCompletionHandler(byteBuffer, "client"));
// 遞迴監聽
attachment.accept(attachment, this);
}
@Override
public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
// 遞迴監聽
attachment.accept(attachment, this);
}
}
// 讀取資料回撥,當有資料可讀時觸發該回調
public class ReadCompletionHandler implements CompletionHandler<Integer, AsynchronousSocketChannel> {
private ByteBuffer byteBuffer;
private String remoteName;
public ReadCompletionHandler(ByteBuffer byteBuffer, String remoteName) {
this.byteBuffer = byteBuffer;
this.remoteName = remoteName;
}
@Override
public void completed(Integer result, AsynchronousSocketChannel attachment) {
if (result <= 0)
return;
byteBuffer.flip();
System.out.println("[" + this.remoteName + "] " + new String(byteBuffer.array()));
byteBuffer.clear();
// 遞迴監聽資料
attachment.read(byteBuffer, attachment, this);
}
@Override
public void failed(Throwable exc, AsynchronousSocketChannel attachment) {
byteBuffer.clear();
// 遞迴監聽資料
attachment.read(byteBuffer, attachment, this);
}
}
上面給出了BIO,NIO以及AIO在Java中的使用的部分程式,並且分析了其中關鍵步驟的使用及其需要注意的事項。
需要原始碼的同學可以到這裡下載。