1. 程式人生 > >Java中BIO,NIO和AIO使用樣例

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主要由三個部分組成,即ChannelBufferSelectorChannel可以跟BIO中的Stream類比,不同的是Channel是可讀可寫的。當和Channel進行互動的時候需要Buffer

的支援,資料可以從Buffer寫到Channel中,也可以從Channel中讀到Buffer中,他們的關係如下圖。channel&buffer以SocketChannel為例,Channel和Buffer互動的例子如下。ByteBuffer是Buffer的一種實現,在使用ByteBuffer之前,需要為其分配空間,然後呼叫Channel的read方法將資料寫入Buffer中,在完成後,在使用Buffer中的資料之前需要呼叫Buffer的flip方法。Buffer中有個position常量,記錄當前操作資料的位置,當向Buffer中寫資料時,position會記錄當前寫的位置,當寫操作完成後,flip會把position至為0,這樣讀取Buffer中的資料時,就會從0開始了。另外需要注意的是處理完Buffer中的資料後需要呼叫clear方法將Buffer清空。向Channel中寫資料的操作比較簡單,這裡不再贅述。

// 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在使用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中的使用的部分程式,並且分析了其中關鍵步驟的使用及其需要注意的事項。

需要原始碼的同學可以到這裡下載

參考