java架構之路-(netty專題)初步認識BIO、NIO、AIO
本次我們主要來說一下我們的IO阻塞模型,只是不多,但是一定要理解,對於後面理解netty很重要的
IO模型精講
IO模型就是說用什麼樣的通道進行資料的傳送和接收,Java共支援3種網路程式設計IO模式:BIO,NIO,AIO。
BIO
BIO(Blocking IO) 同步阻塞模型,一個客戶端連線對應一個處理執行緒。也是我們熟悉的同步阻塞模型,先別管那個同步的概念,我們先來看一下什麼是阻塞,簡單來一段程式碼。
服務端:
package com.xiaocai.bio; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class SocketServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9000); while (true) { System.out.println("等待連線。。"); //阻塞方法 Socket socket = serverSocket.accept(); System.out.println("有客戶端連線了。。"); handler(socket); } } private static void handler(Socket socket) throws IOException { System.out.println("thread id = " + Thread.currentThread().getId()); byte[] bytes = new byte[1024]; System.out.println("準備read。。"); //接收客戶端的資料,阻塞方法,沒有資料可讀時就阻塞 int read = socket.getInputStream().read(bytes); System.out.println("read完畢。。"); if (read != -1) { System.out.println("接收到客戶端的資料:" + new String(bytes, 0, read)); System.out.println("thread id = " + Thread.currentThread().getId()); } socket.getOutputStream().write("HelloClient".getBytes()); socket.getOutputStream().flush(); } }
客戶端
package com.xiaocai.bio; import java.io.IOException; import java.net.Socket; public class SocketClient { public static void main(String[] args) throws IOException { Socket socket = new Socket("127.0.0.1", 9000); //向服務端傳送資料 socket.getOutputStream().write("HelloServer".getBytes()); socket.getOutputStream().flush(); System.out.println("向服務端傳送資料結束"); byte[] bytes = new byte[1024]; //接收服務端回傳的資料 socket.getInputStream().read(bytes); System.out.println("接收到服務端的資料:" + new String(bytes)); socket.close(); } }
這個就是一個簡單的BIO服務端程式碼,就是要準備接受執行緒訪問的程式碼段。這一個單執行緒版本什麼意思呢?
我們先開啟一個埠為9000的socket服務,然後執行Socket socket = serverSocket.accept();意思就是等待執行緒的出現,我們來接收客戶端的請求,這個方法時阻塞的,也是隻有在阻塞狀態才可以接收到我們的請求。當有請求進來時,執行handler(socket);方法,中間是列印執行緒ID的方法不解釋,int read = socket.getInputStream().read(bytes);準備讀取我們的客戶端傳送資料。read和write可能會混淆,我畫個圖來說一下。
我們也可以看到我們的客戶端也是先拿到socket連線(Socket socket = new Socket("127.0.0.1", 9000)),然後要往服務端寫入資料(socket.getOutputStream().write("HelloServer".getBytes());)以byte位元組形式寫入。這時我們的服務端等待read我們的客戶端weite的資料,會進入阻塞狀態,如果我們的客戶端遲遲不寫資料,我們的客戶端一直是阻塞狀態,也就無法接收到新的請求,因為阻塞了,沒法回到我們的Socket socket = serverSocket.accept();去等待客戶端請求,只要在serverSocket.accept阻塞時才可以接收新的請求。於是我們採取了多執行緒的方式來解決這個問題,我們來看一下程式碼。
package com.xiaocai.bio; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; public class SocketServer { public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(9000); while (true) { System.out.println("等待連線。。"); //阻塞方法 Socket socket = serverSocket.accept(); System.out.println("有客戶端連線了。。"); new Thread(new Runnable() { @Override public void run() { try { handler(socket); } catch (IOException e) { e.printStackTrace(); } } }).start(); } } private static void handler(Socket socket) throws IOException { System.out.println("thread id = " + Thread.currentThread().getId()); byte[] bytes = new byte[1024]; System.out.println("準備read。。"); //接收客戶端的資料,阻塞方法,沒有資料可讀時就阻塞 int read = socket.getInputStream().read(bytes); System.out.println("read完畢。。"); if (read != -1) { System.out.println("接收到客戶端的資料:" + new String(bytes, 0, read)); System.out.println("thread id = " + Thread.currentThread().getId()); } socket.getOutputStream().write("HelloClient".getBytes()); socket.getOutputStream().flush(); } }
我們這時每次有客戶端來新的請求時,我們就會開啟一個執行緒來處理這個請求,及時你的客戶端沒有及時的write資料,雖然我們的服務端read進行了阻塞,也只是阻塞了你自己的執行緒,不會造成其它請求無法接收到。
這樣的處理方式貌似好了很多很多,其實不然,想一個例項,我們的看小妹直播時,一句歡迎榜一大哥,彈幕很多,加入一次性來了100彈幕還好,我們開啟100個執行緒來處理,如果一起來了十萬彈幕呢?難道你要開啟十萬個執行緒來處理這些彈幕嘛?很顯然BIO還是有弊端的,BIO還是有優點的(程式碼少,不容易出錯)。
NIO
NIO(Non Blocking IO) 同步非阻塞,伺服器實現模式為一個執行緒可以處理多個請求(連線),客戶端傳送的連線請求都會註冊到多路複用器selector上,多路複用器輪詢到連線有IO請求就進行處理。 可能概念太抽象了,我來舉個例子吧,現在有兩個小區都有很多的房子出租,BIO小區和NIO小區,都有一個門衛,BIO小區,來了一個租客,門衛大爺就拿著鑰匙,帶這個租客去看房子了,後面來的租客都暫時無法看房子了,尷尬...想同時多人看房子,必須增加門衛大爺的數量,而我們的NIO小區就很聰明,還是一個門衛大媽,來了一個租客要看房子,門衛大媽,給了那個租客一把鑰匙,並且告訴他哪房間是空的,你自己進去看吧,及時這個租客看房子慢,耽誤了很多時間也不怕了,因為門衛大媽一直在門衛室,即使又來了新的租客,門衛大媽也是如此,只給鑰匙和空房間地址就可以了。這個例子反正我記得很清楚,也覺得很貼切,這裡提到了一個鑰匙的概念,一會告訴你們是做什麼的,我們先看一下程式碼。
服務端
package com.xiaocai.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.Iterator; public class NIOServer { //public static ExecutorService pool = Executors.newFixedThreadPool(10); public static void main(String[] args) throws IOException { // 建立一個在本地埠進行監聽的服務Socket通道.並設定為非阻塞方式 ServerSocketChannel ssc = ServerSocketChannel.open(); //必須配置為非阻塞才能往selector上註冊,否則會報錯,selector模式本身就是非阻塞模式 ssc.configureBlocking(false); ssc.socket().bind(new InetSocketAddress(9000)); // 建立一個選擇器selector Selector selector = Selector.open(); // 把ServerSocketChannel註冊到selector上,並且selector對客戶端accept連線操作感興趣 ssc.register(selector, SelectionKey.OP_ACCEPT); while (true) { System.out.println("等待事件發生。。"); // 輪詢監聽channel裡的key,select是阻塞的,accept()也是阻塞的 int select = selector.select(); System.out.println("有事件發生了。。"); // 有客戶端請求,被輪詢監聽到 Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); //刪除本次已處理的key,防止下次select重複處理 it.remove(); handle(key); } } } private static void handle(SelectionKey key) throws IOException { if (key.isAcceptable()) { System.out.println("有客戶端連線事件發生了。。"); ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); //NIO非阻塞體現:此處accept方法是阻塞的,但是這裡因為是發生了連線事件,所以這個方法會馬上執行完,不會阻塞 //處理完連線請求不會繼續等待客戶端的資料傳送 SocketChannel sc = ssc.accept(); sc.configureBlocking(false); //通過Selector監聽Channel時對讀事件感興趣 sc.register(key.selector(), SelectionKey.OP_READ); } else if (key.isReadable()) { System.out.println("有客戶端資料可讀事件發生了。。"); SocketChannel sc = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); //NIO非阻塞體現:首先read方法不會阻塞,其次這種事件響應模型,當呼叫到read方法時肯定是發生了客戶端傳送資料的事件 int len = sc.read(buffer); if (len != -1) { System.out.println("讀取到客戶端傳送的資料:" + new String(buffer.array(), 0, len)); } ByteBuffer bufferToWrite = ByteBuffer.wrap("HelloClient".getBytes()); sc.write(bufferToWrite); key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE); } else if (key.isWritable()) { SocketChannel sc = (SocketChannel) key.channel(); System.out.println("write事件"); // NIO事件觸發是水平觸發 // 使用Java的NIO程式設計的時候,在沒有資料可以往外寫的時候要取消寫事件, // 在有資料往外寫的時候再註冊寫事件 key.interestOps(SelectionKey.OP_READ); //sc.close(); } } }
客戶端
package com.xiaocai.nio; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.Iterator; public class NioClient { //通道管理器 private Selector selector; /** * 啟動客戶端測試 * * @throws IOException */ public static void main(String[] args) throws IOException { NioClient client = new NioClient(); client.initClient("127.0.0.1", 9000); client.connect(); } /** * 獲得一個Socket通道,並對該通道做一些初始化的工作 * * @param ip 連線的伺服器的ip * @param port 連線的伺服器的埠號 * @throws IOException */ public void initClient(String ip, int port) throws IOException { // 獲得一個Socket通道 SocketChannel channel = SocketChannel.open(); // 設定通道為非阻塞 channel.configureBlocking(false); // 獲得一個通道管理器 this.selector = Selector.open(); // 客戶端連線伺服器,其實方法執行並沒有實現連線,需要在listen()方法中調 //用channel.finishConnect() 才能完成連線 channel.connect(new InetSocketAddress(ip, port)); //將通道管理器和該通道繫結,併為該通道註冊SelectionKey.OP_CONNECT事件。 channel.register(selector, SelectionKey.OP_CONNECT); } /** * 採用輪詢的方式監聽selector上是否有需要處理的事件,如果有,則進行處理 * * @throws IOException */ public void connect() throws IOException { // 輪詢訪問selector while (true) { selector.select(); // 獲得selector中選中的項的迭代器 Iterator<SelectionKey> it = this.selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = (SelectionKey) it.next(); // 刪除已選的key,以防重複處理 it.remove(); // 連線事件發生 if (key.isConnectable()) { SocketChannel channel = (SocketChannel) key.channel(); // 如果正在連線,則完成連線 if (channel.isConnectionPending()) { channel.finishConnect(); } // 設定成非阻塞 channel.configureBlocking(false); //在這裡可以給服務端傳送資訊哦 ByteBuffer buffer = ByteBuffer.wrap("HelloServer".getBytes()); channel.write(buffer); //在和服務端連線成功之後,為了可以接收到服務端的資訊,需要給通道設定讀的許可權。 channel.register(this.selector, SelectionKey.OP_READ); // 獲得了可讀的事件 } else if (key.isReadable()) { read(key); } } } } /** * 處理讀取服務端發來的資訊 的事件 * * @param key * @throws IOException */ public void read(SelectionKey key) throws IOException { //和服務端的read方法一樣 // 伺服器可讀取訊息:得到事件發生的Socket通道 SocketChannel channel = (SocketChannel) key.channel(); // 建立讀取的緩衝區 ByteBuffer buffer = ByteBuffer.allocate(1024); int len = channel.read(buffer); if (len != -1) { System.out.println("客戶端收到資訊:" + new String(buffer.array(), 0, len)); } } }
程式碼看到了很多很多,我來解釋一下大概什麼意思吧,這個NIO超級重要後面的netty就是基於這個寫的,一定要搞懂,首先我們建立了一個ServerSocketChannel和一個選擇器selector,設定為非阻塞的(固定寫法,沒有為什麼),將我們的 selector繫結到我們的ServerSocketChannel上,然後執行selector.select();進入阻塞狀態,別擔心,這個阻塞沒影響,為我們提供了接收客戶端的請求,你沒有請求,我阻塞著,不會耽誤你們什麼的。
回到我們的客戶端,還是差不多的樣子,拿到我們的NioClient開始連線我們的服務端,這個時候,我們的服務端接收到了我們的客戶端請求,阻塞狀態的selector.select()繼續執行,並且給予了一個SelectionKey(Iterator<SelectionKey> it = selector.selectedKeys().iterator())也就是我們剛才的小例子中提到的鑰匙,key=鑰匙,還算是靠譜吧~!開始執行我們的handle方法,有個if else,這個是說,你是第一次請求要建立通道,還是要寫資料,還是要讀取資料,記住啊,讀寫都是相對的,自己多琢磨幾次就可以轉過圈來了,就是我上面畫圖說的read和write。拿我們的建立通道來說,通過我們的鑰匙key你就可以得到ServerSocketChannel,然後進行設定下次可能會發生的讀寫事件,然後看我們的讀事件,我們看到了int len = sc.read(buffer)這個讀在我們的BIO中是阻塞的,而我們的NIO這個方法不是阻塞的,這也就體現出來了我們的BIO同步阻塞和NIO同步非阻塞,阻塞和非阻塞的區別也就說完了。畫個圖,我們來看一下我們的NIO模型。
NIO 有三大核心元件: Channel(通道), Buffer(緩衝區),Selector(選擇器)
這裡我們的Buffer沒有去說,到netty會說的, Channel(通道), Buffer(緩衝區)都是雙向的,現在回過頭來想想我舉的小例子,selector門衛大媽,SelectionKey鑰匙。對於NIO有了一些理解了吧,NIO看著很棒的,但是你有想過寫上述程式碼的痛苦嗎?
AIO
AIO(NIO 2.0) 非同步非阻塞, 由作業系統完成後回撥通知服務端程式啟動執行緒去處理, 一般適用於連線數較多且連線時間較長的應用。其實AIO就是對於NIO的二次封裝,要不怎麼叫做NIO2.0呢,我們來簡單看一下程式碼。
服務端:
package com.xiaocai.aio; 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; public class AIOServer { public static void main(String[] args) throws Exception { final AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000)); serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() { @Override public void completed(AsynchronousSocketChannel socketChannel, Object attachment) { try { // 再此接收客戶端連線,如果不寫這行程式碼後面的客戶端連線連不上服務端 serverChannel.accept(attachment, this); System.out.println(socketChannel.getRemoteAddress()); ByteBuffer buffer = ByteBuffer.allocate(1024); socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() { @Override public void completed(Integer result, ByteBuffer buffer) { buffer.flip(); System.out.println(new String(buffer.array(), 0, result)); socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes())); } @Override public void failed(Throwable exc, ByteBuffer buffer) { exc.printStackTrace(); } }); } catch (IOException e) { e.printStackTrace(); } } @Override public void failed(Throwable exc, Object attachment) { exc.printStackTrace(); } }); Thread.sleep(Integer.MAX_VALUE); } }
客戶端:
package com.xiaocai.aio; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousSocketChannel; public class AIOClient { public static void main(String... args) throws Exception { AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open(); socketChannel.connect(new InetSocketAddress("127.0.0.1", 9000)).get(); socketChannel.write(ByteBuffer.wrap("HelloServer".getBytes())); ByteBuffer buffer = ByteBuffer.allocate(512); Integer len = socketChannel.read(buffer).get(); if (len != -1) { System.out.println("客戶端收到資訊:" + new String(buffer.array(), 0, len)); } } }
阻塞非阻塞都明白了,這裡來解釋一下同步,我們看到我們的AIO在serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {}直接開啟了執行緒,也就是說accept直接以後,我不再需要考慮阻塞情況,可以繼續執行下面的程式碼了,也就是我們說到的非同步執行,內部還是我們的NIO,不要覺得AIO多麼的6B,內部就是封裝了我們的NIO,效能和NIO其實差不多的,可能有些時候還不如NIO(未實測)。
遺漏一個知識點,NIO的多路複用器是如何工作的,在我們的JDK1.5以前的,多路複用器是陣列和連結串列的方式來遍歷的,到了我們的JDK1.5採用hash來回調的。
總結:
我們這次主要說了BIO、NIO、AIO三個網路程式設計IO模式,最重要的就是我們的NIO,一張圖來總結一下三個IO的差別吧。
最進弄了一個公眾號,小菜技術,歡迎大家的加入