JAVA BIO,NIO,AIO詳解(附程式碼實現)
這幾天在看面試的東西,可能是自己比較笨,花了快兩天的時間才理清楚。特此記錄一下。
首先我們要理解的一個很重要概念是,客戶端連線和傳送資料是分開的,連線不代表立馬會傳輸資料。
說說BIO,NIO,AIO到底是什麼東西
BIO:同步堵塞
NIO:非同步堵塞
AIO:非同步非堵塞
看到這裡你肯定一臉懵逼,這到底是什麼意思,別急,慢慢看。
在JAVA裡面IO分兩塊,一塊是操作檔案的,一塊是操作網路的。下面先講網路
網路:
一個通俗易懂的例子
下面先舉一個通俗的例子加深理解,參演人員:你(客戶端),酒吧(服務端),服務員(服務端執行緒)
BIO:你去酒吧喝酒,酒吧立馬給你安排一個專門的服務員小姐姐只為你服務。你是不是覺得美滋滋,但是人家酒吧的壓力就大了,那麼多咱們這種屌絲,哪裡有那麼多小姐姐?(客戶端會一直佔用服務端的執行緒
NIO:你去酒吧喝酒,酒吧就TM一個小姐姐,你要喝酒是吧,好啊,坐吧。小姐姐會一會兒過來問你,先生,要酒嗎?你說要的話她就給你滿上。倒完就跑,跑去問別人。你肯定覺得,難受啊兄嘚。但是對人家酒吧來說就舒服了,人家只要一個服務員就搞定了。(客戶端只有在需要的時候才佔用服務端的執行緒,但同時服務端會不停的問你是否需要)
AIO:與NIO的不同在於,連問你的小姐姐都木有了(≧ ﹏ ≦),你要酒了,自己喊,小姐姐,小姐姐,來幫我倒個酒唄。(客戶端需要的時候會主動通知服務端)
關鍵程式碼實現
篇幅有限,此處只列舉關鍵程式碼,完整程式碼請下載後面的專案,自己執行一遍,什麼都懂了。
BIO:
ServerNormal:
public final class ServerNormal {
public synchronized static void start(int port) throws IOException{
if(server != null) return;
try{
//通過建構函式建立ServerSocket
//如果埠合法且空閒,服務端就監聽成功
server = new ServerSocket(port);
System.out.println("伺服器已啟動,埠號:" + port);
//通過無線迴圈監聽客戶端連線
//如果沒有客戶端接入,將阻塞在accept操作上。
while(true){
Socket socket = server.accept();
//當有新的客戶端接入時,會執行下面的程式碼
//然後建立一個新的執行緒處理這條Socket鏈路
new Thread(new ServerHandler(socket)).start();
}
}finally{
//一些必要的清理工作
if(server != null){
System.out.println("伺服器已關閉。");
server.close();
server = null;
}
}
}
}
ServerHandler:
public class ServerHandler implements Runnable{
@Override
public void run() {
BufferedReader in = null;
PrintWriter out = null;
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(),true);
String expression;
String result;
while(true){
//主要原因酒在於(expression = in.readLine())==null這句程式碼是堵塞的,時間都花在了等待上。
if((expression = in.readLine())==null) break;
System.out.println("伺服器收到訊息:" + expression);
}
}
可以看出來服務端是在客戶端每次有新請求時都去啟動了一個新執行緒的去接收客戶端傳送的資料的,BufferedReader.readLine()會讀滿緩衝區或者在讀到檔案末尾(遇到:”/r”、”/n”、”/r/n”)才返回,這樣就會導致網路很慢的適合執行緒可能會卡很久,從而導致服務端積聚大量的執行緒,大量的執行緒會嚴重影響伺服器效能,甚至罷工。如何解決這個問題呢?NIO橫空出世。
NIO
ServerHandle
public class ServerHandle implements Runnable {
@Override
public void run() {
//迴圈遍歷selector
while (started) {
try {
//無論是否有讀寫事件發生,selector每隔1s被喚醒一次
selector.select(10000);
//阻塞,只有當至少一個註冊的事件發生的時候才會繼續.
Set<SelectionKey> keys = selector.selectedKeys();
if (keys.size()>0)
System.out.println("哈哈哈,我接收到客戶端的請求了"+keys.size());
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (Throwable t) {
t.printStackTrace();
}
}
}
}
注意看selector.select方法,該方法在客戶端沒有資料傳輸的時候是堵塞的,有資料傳輸的時候才會往下走,具體關於API請自行百度。這個selector就類使用我們前面舉例中的服務員小姐姐,哪裡客人需要倒酒她就過去給人家倒,沒有需要倒酒的時候她就在休息(堵塞)。注意:BIO堵塞的是有沒有客戶端的連線,有客戶端連線就啟動一個執行緒去讀取客戶端傳送過來的資料,而NIO堵塞的是客戶端有沒有資料傳輸。
列舉NIO和BIO適合的場景。
BIO:你有少量的連線使用非常高的頻寬,一次傳送大量的資料,也許典型的IO伺服器實現可能非常契合。(因為這種方式服務端不需要啟動太多的執行緒且設計簡單,用NIO設計過於複雜)(就好像你是個酒神,喝酒是真的快,你讓小姐姐總就跑來給你倒酒,人家也煩是不是?所有還不如專門給你一個小姐姐呢?)
NIO:如果需要管理同時開啟的成千上萬個連線,這些連線每次只是傳送少量的資料,例如聊天伺服器,實現NIO的伺服器可能是一個優勢。(因為這種方式用BIO實現會造出服務端執行緒啟動太多且因為資料較少不能得到有效的利用,而使用NIO則很適合這種場景)
用人可能會說,那你這個什麼NIO不也還是堵塞的嗎?你只是避免了服務端有過多的執行緒再等等啊,並沒有實現非同步啊。是的,本身上來說NIO並不是真正意義上的非同步,稱之為new io更合適。那要實現真正的非同步怎麼玩呢?那請看下面的AIO
AIO
注意:aio是JDK1.7裡面才新增的。aio即非同步通知,也就是你寫完資料了會非同步呼叫一個方法,與NIO的區別在於NIO你需要等到服務端輪詢到你,而AIO是你寫完了你立馬通知到客戶端處理你的資訊。
AsyncServerHandler
public class AsyncServerHandler implements Runnable {
public CountDownLatch latch;
public AsynchronousServerSocketChannel channel;
@Override
public void run() {
latch = new CountDownLatch(1);
//用於接收客戶端的連線,有客戶端連線後會回撥AcceptHandler裡面的方法
channel.accept(this,new AcceptHandler());
//此處,讓現場在此阻塞,防止服務端執行完成後退出
latch.await();
}
}
AcceptHandler
public class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, AsyncServerHandler> {
@Override
public void completed(AsynchronousSocketChannel channel,AsyncServerHandler serverHandler) {
System.out.println("AcceptHandler.completed()");
//繼續接受其他客戶端的請求
Server.clientCount++;
System.out.println("連線的客戶端數:" + Server.clientCount);
serverHandler.channel.accept(serverHandler, this);
//建立新的Buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
//非同步讀 第三個引數為接收訊息回撥的業務Handler
channel.read(buffer, buffer, new ReadHandler(channel));
}
@Override
public void failed(Throwable exc, AsyncServerHandler serverHandler) {
exc.printStackTrace();
serverHandler.latch.countDown();
}
}
ReadHandler
public class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {
//用於讀取半包訊息和傳送應答
private AsynchronousSocketChannel channel;
public ReadHandler(AsynchronousSocketChannel channel) {
this.channel = channel;
}
//讀取到訊息後的處理
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("ReadHandler.completed()");
//flip操作
attachment.flip();
//根據
byte[] message = new byte[attachment.remaining()];
attachment.get(message);
try {
String expression = new String(message, "UTF-8");
System.out.println("伺服器收到訊息: " + expression);
String calrResult = "傳送給客戶端的訊息";
//向客戶端傳送訊息
doWrite(calrResult);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
//傳送訊息
private void doWrite(String result) {
byte[] bytes = result.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
//非同步寫資料 引數與前面的read一樣
channel.write(writeBuffer, writeBuffer,new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
//如果沒有傳送完,就繼續傳送直到完成
if (buffer.hasRemaining())
channel.write(buffer, buffer, this);
else{
//建立新的Buffer
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
//非同步讀 第三個引數為接收訊息回撥的業務Handler
channel.read(readBuffer, readBuffer, new ReadHandler(channel));
}
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
channel.close();
} catch (IOException e) {
}
}
});
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
try {
this.channel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
回撥的類需要實現CompletionHandler介面的兩個方法。completed():在成功適合呼叫,failed():在失敗的適合呼叫.
可以看到所謂AIO關鍵的兩個類是AsynchronousServerSocketChannel和AsynchronousSocketChannel。前者用於服務端,有個accept()方法,該方法用於接受客戶端的連線,客戶端有連線後會呼叫引數中的Handler類。注意這個方法是非堵塞的,只是在成功之後會呼叫。而AsynchronousSocketChannel的read(),write()均是非堵塞的,會在讀取完了之後呼叫引數中的Handler類。基於這兩個類就是真正意義上的非同步了。
檔案
後續再討論
關於本文的java原始碼下載:https://gitee.com/nuoqian1/IoTest
參考文章:
https://ifeve.com/java-nio-vs-io/
https://blog.csdn.net/anxpp/article/details/51512200
https://blog.csdn.net/caolipeng_918/article/details/49534113
https://blog.csdn.net/happyzwh/article/details/53437570