上一篇博客中我們介紹了Java中的NIO模型,而JDK1.7之後升級NIO類庫,也就是NIO2.0.Java正式提供了異步IO操作,同時提供了與UNIX網絡編程事件驅動IO相對應的AIO。NIO(non-block IO)指的是同步非阻塞IO,AIO(Asynchronous IO)則是異步非阻塞IO。我們先來了解一些基礎知識,最後再用AIO來設計一個服務器。
一、IO處理模型
服務器往客戶端發送數據的過程主要有以下四步:
1、服務器調用read()方法,由用戶態轉爲內核態,把磁盤的數據讀到Read Buffer(內核緩存區)上;
2、read()方法返回,把Read Buffer(內核緩存區)的數據讀到Buffer上,服務器由內核態轉爲用戶態;之後對Buffer中的數據做進一步處理;
3、數據處理完之後,服務器調用send()方法,由用戶態轉爲內核態,把Buffer中的數據讀到Socket Buffer上;
4、將數據讀到NIC Buffer(NetWork Interface Card,網卡的緩存區),send()方法返回,服務器由內核態轉爲用戶態;
如果想更詳細地瞭解IO的知識,可以看下這篇博文(https://www.jianshu.com/p/9b2922c10129)
二、NIO和AIO的區別
1、同步與異步
NIO,又稱同步IO,當應用程序發出一個IO請求後,它需要主動去檢查這個IO請求是否完成。
AIO,又稱異步IO,當應用程序發出一個IO請求後,就不再管它了,當這個IO請求完成之後,操作系統主動通知應用程序來做後續的處理。
舉個例子:你在家裏燒開水,你把水放進去燒以後就去看電視了。如果是普通的燒水壺,你每隔一段時間都要去看一眼水燒開了沒有,這就是同步IO;但是如果是響水壺,你只要安安靜靜地看你的電視,等水壺一響,你就知道水燒開了,這就是異步IO。
2、設計模式
NIO採用Reactor的設計模式,而AIO採用Proactor的設計模式。
Reactor和Proactor最主要的區別就是數據的讀取和寫入Buffer的操作由誰來完成。
對於Reactor而言,它被激活時僅表示當前SocketChannel中有數據來了,應用程序需要自己把SocketChannel中的數據搬到Buffer裏面;
而Proactor被激活時則表示SocketChannel中有數據來了,並且我已經幫你把它搬到Buffer裏面了,應用程序可以直接對Buffer中的數據進行處理。
3、適用場景
NIO採用了Reactor的模式,讀寫由應用程序自己進行,適合用於連接數目多且連接短的場景中
AIO採用了Proactor,讀寫操作由內核完成,適合用於連接數目多且連接長的場景中
三、AIO原理
1、Java回調模式
設想這麼一個場景,我們現在需要往磁盤中取數據進行處理,並把處理後的結果發送給客戶端。
(1)、傳統方法
定義一個類A,實現read()、process()和send()三個方法,在線程中一次調用這三個方法。但是計算很複雜,主線程就會一直阻塞在process()方法中。
(2)、異步調用
我們定義了兩個類,其中類A包含read()和send()方法,類B包含process()方法。首先我們啓動主線程讀取數據,數據讀取結束後我們就啓動新線程實例化類B,調用它的process()處理數據,主線程就可以做其他事了。等到b.process()方法執行結束後,再主動回調a的send()方法即可。
關於Java回調模式,奉上一篇通俗易懂的博文(《java回調函數詳解》)
2、AIO原理圖
我們根據上面的原理來分析一下AIO的過程
(1)、客戶端
A、首先建立一個AsynchronusSocketChannel,綁定端口號,並調用它的connect()方法嘗試連接服務器,並指明connect()方法的回調類爲ConnectCompletionHandler;
B、連接服務器得到兩種返回值。如果連接失敗則調用AcceptCompletionHandler類的failed()方法進行後續處理,如果成功,則調用ConnectCompletionHandler類的completion()方法進行後續處理。
(2)、服務端
A、建立一個AsynchronousServerSocketChannel,綁定端口號,調用accept()方法等待客戶端連接,並指明accept()方法的回調類爲AcceptCompletionHandler;
B、判斷連接是否成功,如果失敗就調用AcceptCompletionHandler的failed()方法,如果成功就調用AcceptCompletionHandler的completed()方法;
C、如果調用了AcceptCompletionHandler的completed()方法,即連接成功時。服務端總共進行了三個操作。一個是建立AsynchronousSocketChannel與當前的客戶端建立連接(調用completed()方法之前),一個是調用AsynchronousServerSocketChannel.accept()繼續處理其他的客戶端連接(調用completed()方法時),最後一個是調用AsychronousSocketChannel.read()進行客戶端消息的讀操作,並指明回調類爲ReadCompletionHandler
D、如果AsynchronousSocketChannel接收到數據,在接收完數據後,會調用ReadCompletionHandler.completed()方法對消息進行處理
(3)注意點
A、所有方法的回調結果都由相應的CompletionHandler類的進行處理,eg.accept()方法由AcceptCompletionHandler類(實現了CompletionHandler接口的類)進行處理。
四、代碼實現
1、服務器
(1)、TimeServer類
package aioserver;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class TimeServer {
public static void main(String[] args) throws IOException {
int port = 8080;
if (args != null && args.length > 0) {
try{
port = Integer.valueOf(args[0]);
}catch(NumberFormatException e){
}
}
/*
*創建異步的時間服務器處理類,並啓動線程將其拉起
*/
AsyncTimeServerHandler timeServer = new AsyncTimeServerHandler(port);
new Thread(timeServer,"AIO-AsyncTimeServerHandler-001").start();
}
}
(2)、AsyncTimeServerHandler類
package aioserver;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.CountDownLatch;
public class AsyncTimeServerHandler implements Runnable{
private int port;
/*
* CountDownLatch類位於java.util.concurrent包下,
* 利用它可以實現類似計數器的功能。比如有一個任務A,
* 它要等待其他4個任務執行完畢之後才能執行,
* 此時就可以利用CountDownLatch來實現這種功能了。
*/
CountDownLatch latch;
AsynchronousServerSocketChannel asychronousServerSocketChannel;
public AsyncTimeServerHandler(int port) {
this.port = port;
try {
//1、創建一個異步的服務器通道AsynchronousServerSocketChannel,並綁定端口號
asychronousServerSocketChannel = AsynchronousServerSocketChannel.open();
asychronousServerSocketChannel.bind(new InetSocketAddress(port));
System.out.println("The time server is start in port : " + port);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void run() {
//初始化CountDownLantch爲1,在完成一組正在執行的操作之前,允許當前線程一直阻塞
//這裏是爲了防止服務端執行完成退出
//在實際的項目應用中,不需要啓動獨立的線程來處理AsynchronousServerSocketChannel?
latch = new CountDownLatch(1);
doAccept();
//等待線程執行完畢
try {
//await()和wait()的區別
latch.await();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
private void doAccept() {
//1、調用accept()方法,並指明回調類爲AcceptCompletionHandler,不過這裏用了匿名內部類,沒有定義出具體的類名
asychronousServerSocketChannel.accept(this, new CompletionHandler<AsynchronousSocketChannel, AsyncTimeServerHandler>() {
//2、判斷是否連接成功,成功就調用completed()方法
@Override
public void completed(AsynchronousSocketChannel result,
AsyncTimeServerHandler attachment) {
//3、調用accept()方法,監聽其他的客戶端連接
attachment.asychronousServerSocketChannel.accept(attachment, this);
//鏈路建立成功之後,服務端需要接收客戶端的請求消息,
//創建新的ByteBuffer,預分配1M的緩衝區。
ByteBuffer buffer = ByteBuffer.allocate(1024);
//3、調用AsynchronousSocketChannel的read方法進行異步讀操作,並指明回調類爲ReadCompletionHandler
result.read(buffer, buffer, new ReadCompletionHandler(result));
}
@Override
public void failed(Throwable exc, AsyncTimeServerHandler attachment) {
exc.printStackTrace();
attachment.latch.countDown();
}
});
}
}
(3)、ReadCompletionHandler類
package aioserver;
import java.nio.ByteBuffer;
import java.nio.channels.CompletionHandler;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.channels.AsynchronousSocketChannel;
public class ReadCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
private AsynchronousSocketChannel channel;
public ReadCompletionHandler(AsynchronousSocketChannel channel) {
//將AsynchronousSocketChannel通過參數傳遞到ReadCompletion Handler中當作成員變量來使用
//主要用於讀取半包消息和發送應答。本例程不對半包讀寫進行具體說明
if (this.channel == null)
this.channel = channel;
}
//4、讀取消息結束,調用ReadCompletionHandler.completed()方法進行處理
@Override
public void completed(Integer result, ByteBuffer attachment) {
//讀取到消息後的處理,首先對attachment進行flip操作,爲後續從緩衝區讀取數據做準備。
attachment.flip();
//根據緩衝區的可讀字節數創建byte數組
byte[] body = new byte[attachment.remaining()];
attachment.get(body);
try {
//通過new String方法創建請求消息,對請求消息進行判斷,
//如果是"QUERY TIME ORDER"則獲取當前系統服務器的時間,
String req = new String(body, "UTF-8");
System.out.println("The time server receive order : " + req);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(req) ? new java.util.Date(
System.currentTimeMillis()).toString() : "BAD ORDER";
//調用doWrite方法發送給客戶端。
doWrite(currentTime);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
private void doWrite(String currentTime) {
if (currentTime != null && currentTime.trim().length() > 0) {
//首先對當前時間進行合法性校驗,如果合法,調用字符串的解碼方法將應答消息編碼成字節數組,
//然後將它複製到發送緩衝區writeBuffer中,
byte[] bytes = (currentTime).getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
//最後調用AsynchronousSocketChannel的異步write方法。
//正如前面介紹的異步read方法一樣,它也有三個與read方法相同的參數,
//在本例程中我們直接實現write方法的異步回調接口CompletionHandler。
channel.write(writeBuffer, writeBuffer,
new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
//對發送的writeBuffer進行判斷,如果還有剩餘的字節可寫,說明沒有發送完成,需要繼續發送,直到發送成功。
if (buffer.hasRemaining())
channel.write(buffer, buffer, this);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
//關注下failed方法,它的實現很簡單,就是當發生異常的時候,對異常Throwable進行判斷,
//如果是I/O異常,就關閉鏈路,釋放資源,
//如果是其他異常,按照業務自己的邏輯進行處理,如果沒有發送完成,繼續發送.
//本例程作爲簡單demo,沒有對異常進行分類判斷,只要發生了讀寫異常,就關閉鏈路,釋放資源。
try {
channel.close();
} catch (IOException e) {
// ingnore on close
}
}
});
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
this.channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
說明:本文代碼部分主要來自《netty權威指南一書》
C10k系列文章: