段楠
(吉林大學軟件學院,長春130000)
在網絡通訊的開發中,存在著同步/異步、阻塞/非阻塞的多種方式,不同方式的軟件性能與開發成本各不相同,可根據軟件需求進行選擇。以下介紹同步/異步、阻塞/非阻塞在網絡通訊中的概念,和應用程序與操作系統內核之間同步/異步、阻塞/非阻塞的概念不盡相同。
同步與異步在網絡通訊環境下更傾向于對客戶端通訊方式的描述。
同步是指客戶端向服務器發送請求后,必須等到服務器的響應到來才可進行下一步操作。
異步是指客戶端向服務器發送請求后,不必等待服務器響應并可繼續其他操作,待服務器發來響應后,客戶端會得到I/O 操作完成的通知,只需對結果進行處理即可。
阻塞與非阻塞在網絡通訊環境下更傾向于對服務器通訊方式的描述。
阻塞是指服務器在接受某個客戶端的請求后,觸發相應函數,無論能不能執行都會一直等待,直到當前函數執行結束后才能進行下一步操作。
非阻塞是指服務器在接收客戶端的請求后,向操作系統注冊I/O 監聽,如果當前不能讀寫會立刻返回并執行其他操作。當讀寫可執行時,操作系統會通知服務器應用程序執行讀寫操作,觸發相應函數。
Java 在網絡通訊方面最早只有實現同步阻塞方式的BIO 組件,BIO 服務器的實現方式為給每一個客戶端的連接提供一個進程。即使當前客戶端沒有執行任何操作,該線程也會一直阻塞等待。這種方式顯然會帶來很大的資源浪費。在這種方式下實現并發連接需要對服務器使用多線程技術,為每個客戶端連接增加一個新的線程。可見,這種方式會消耗大量的資源。
在JDK 1.4 之后,Java 開始支持同步非阻塞的通訊方式,即NIO 組件。NIO 服務器的實現方式為給每個客戶端的請求提供一個線程。客戶端的連接會注冊到多路復用器上,多路復用器進行輪詢,當某個連接有I/O請求時啟動一個線程進行處理。
在JDK 7 之后,Java 推出了具有異步非阻塞通訊功能的AIO 組件。AIO 服務器的實現方式為給每個有效請求提供一個進程。即操作系統對I/O 請求執行完之后,再通知服務器應用程序啟動線程進行處理。
以上三種通訊方式可根據軟件應用的具體需求進行選擇。BIO 適合并發量較小的應用,且對服務器的資源要求較高。但實現方式簡單,可快速開發。NIO適合并發量較大且多數為短連接的應用,實現方式較為復雜。AIO 適合并發量較大且多數為長連接的應用,會調動操作系統實現并發操作,實現方式較為復雜。
同步/異步、阻塞/非阻塞原本是應用程序與操作系統交互時的一組概念。同步與異步指的是應用程序的方法調用是否需要等待結果的返回,才能進行其他操作。阻塞與非阻塞指的是一個讀寫操作是否需要等待操作系統內核的所有讀寫完成,才算完成。而在網絡通訊中這組概念使用的也是系統內核中的基本原理,因此了解系統內核中異步非阻塞的原理是十分有必要的。
在應用程序與系統內核交互中,異步指的是,應用程序調用讀寫操作方法后,不須一直等待結果的返回,而是可以繼續進行其他的操作。當讀寫完成后,會由操作系統通知到應用程序,再由應用程序對結果進行處理。
非阻塞是指在系統底層,進行一個讀寫操作時CPU 無須等待當前操作的所有內核I/O 全部完成,而是每個內核I/O 完成之后會立刻返回一個狀態,此時CPU 就可以繼續執行其他任務。當某個讀寫操作的內核I/O 全部完成之后,該讀寫操作完成。而CPU 需要判斷某個讀寫操作當前是否有內核I/O 請求,以及確認讀寫操作是否完成并取得數據,這就需要CPU 對讀寫操作的控制機制。主要有“輪詢”和“中斷”兩種機制。“輪詢”是指CPU 通過循環對所有I/O 訪問,得知當前讀寫操作的狀態。輪詢過程中應用程序需要等待CPU的詢問,因此是一種同步非阻塞的方式。“中斷”是指讀寫操作有I/O 請求時主動請求CPU 為其分配內核資源。而應用程序在發送讀寫請求后只需等待系統將結果返回即可,此時可執行其他操作,因此時一種異步非阻塞的方式。
系統內核層面的異步非阻塞,與網絡通訊層面的異步非阻塞原理大致相同,只是描述對象有所差別。上述“輪詢”方式類似于Java NIO 的實現方式,服務器對客戶端的請求進行輪詢處理,來查找某個客戶端是
否有數據請求。上述“中斷”方式類似于Java AIO 的實現方式,客戶端的請求到來后,首先由操作系統進行I/O操作,然后將結果通知給服務器應用程序,進行一些處理后再返回響應給客戶端。此時客戶端不需要等待服務器的輪詢,只需等待結果即可。Java AIO 顯然是一種異步非阻塞的通訊方式,以下詳細闡述該技術。
Java 異步非阻塞通訊組件AIO 使用“訂閱-通知”方式進行實現。即服務器應用程序向操作系統注冊I/O監聽,當操作系統完成I/O 操作之后,通知服務器應用程序進行處理,觸發相應函數。
在服務器應用程序中,當需要讀寫時只需調用read 和write 方法。這兩種方法都是非阻塞的。進行read 操作時,操作系統將客戶端的I/O 請求處理完成后,將數據放入read 的緩沖區,并通知服務器應用程序對數據進行處理。進行write 操作時,服務器應用程序將數據寫入write 緩沖區,操作系統從緩沖區取得數據并進行I/O 操作,操作完成后通知服務器應用程序進行回調操作。read 和write 可在讀寫操作完成后通過回調函數的方式進行回調操作。回調方法包括completed方法和failed 方法。completed 方法在讀寫操作成功后回調執行,一般會在其中對ByteBuffer 數據進行業務邏輯處理,以及遞歸調用下一個讀寫操作。failed 方法在讀寫操作失敗后回調執行,一般向控制臺或日志文件打印異常信息。
AIO 中有如下幾個重要元素:
Channel:是應用程序與操作系統之間的通道,通過該通道可實現應用程序與操作系統之間的數據傳輸。Channel 包括:AsynchronousServerSocketChannel,實現服務器應用程序對操作系統的監聽與操作系統對服務器應用程序的通知。AsynchronousSocketChannel,實現服務器對TCP 套接字的監聽。DatagramChannel,實現服務器對UDP 套接字的監聽。AsynchronousFileChannel,實現服務器應用程序對文件數據I/O 的監聽。
ByteBuffer:為每一種Channel 提供的數據緩存區,用于Channel 中數據的交換。ByteBuffer 中有一個指向當前讀寫位置的索引,每次讀寫結束后會停留在最后數據的位置。因此每個ByteBuffer 對應的通道在每次讀操作前,需要使用flip 方法將該索引回到初始位置。每次讀操作后,需要使用clear 方法將ByteBuffer清空,以便下次讀入。
Attachment:通道的附件,在嵌套或遞歸的讀寫操作中起到上下文的作用。
以時間發送程序為例,給出Java AIO 實現的一個簡單示范。服務器每收到一個客戶端請求,就發送系統當前時間響應給客戶端。
首先創建AsynchronousServerSocketChannel,可以用線程池的方式創建。
ExecutorService executor=Executors.newFixedThreadPool(20);
AsynchronousChannelGroup group=AsynchronousChannel-Group.withThreadPool(executor);
AsynchronousServerSocketChannel serverSocketChannel=AsynchronousServerSocketChannel.open(group);
然后對指定的主機及端口進行綁定,并監聽發來的連接請求。監聽方法中一定要綁定回調方法來執行監聽成功后的下一步操作。需要自己編寫Completion-Handler 接口的實現類,并重寫completed 與failed方法。
serverSocketChannel.bind (new InetSocketAddress ("0.0.0.0",33335));
serverSocketChannel.accept(null,new AcceptHandler(serverSocketChannel));
以下為CompletionHandler 接口的實現類AcceptHandler 的completed 方法,參數需要Asynchronous-SocketChannel 作為當前連接客戶端的通道,attachment可根據需要決定是否傳入有效數據。
public void completed(AsynchronousSocketChannel socketChannel,Object attachment)
遞歸進行客戶端連接監聽。
serverSocketChannel.accept(attachment,this);
讀取客戶端發來的數據。將需要發送的數據用ByteBuffer 封裝并傳入第一個參數。第二個參數為attachment,如不需要上下文對象可傳入null。第三個參數即回調方法類,可使用匿名內部類的方式實現。
ByteBuffer buffer=ByteBuffer.allocate(1024);
socketChannel.read(buffer,null,new CompletionHandler
讀取客戶端發來的消息并解碼。
buffer.flip();
String request=StandardCharsets.UTF_8.decode(buffer).toString();
處理業務邏輯。write 方法向客戶端寫入響應數據,由于不需要遞歸調用所以只傳入數據緩沖參數即可。
if(request.equals("time")){
SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date=sdf.format(new Date());
socketChannel.write(ByteBuffer.wrap(date.getBytes("utf-8"))).get();
}else{
socketChannel.write(ByteBuffer.wrap("非法輸入".get-Bytes("utf-8"))).get();
}
遞歸進行下次數據讀取。
buffer.clear();
socketChannel.read(buffer,null,this);
客戶端首先要打開AsynchronousSocketChannel 并連接服務器。
AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
socketChannel.connect (new InetSocketAddress ("127.0.0.1",33335)).get();
執行寫操作,給服務器發送請求。
socketChannel.write (ByteBuffer.wrap (request.getBytes("utf-8")),null,newCompletionHandler
讀取服務器發來的響應。
ByteBuffer buffer=ByteBuffer.allocate(1024);
socketChannel.read(buffer,null,new CompletionHandler
進行輸出,之后清空緩沖區。
buffer.flip();
String response=StandardCharsets.UTF_8.decode(buffer).toString();
System.out.println(response);
buffer.clear();
遞歸調用下次寫操作,實現客戶端可重復向服務器發送數據。
String request=in.readLine();
socketChannel.write (ByteBuffer.wrap (request.getBytes("utf-8")),null,this);
本文首先介紹了同步與異步、阻塞與非阻塞的概念,從操作系統內核的原始原理到網絡通訊的具體情形對異步非阻塞進行了具體闡述。然后以Java 開發為例,給出了異步非阻塞網絡通訊的具體實例。闡述了Java AIO 的基本原理,示范了其實現的具體方式。