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