上篇帶大家瞭解了IO的概念,同步非同步,阻塞非阻塞的區別,沒有看過的小夥伴可以去看下哦
本篇是Netty系列的第二篇,帶大家來著重解析NIO,作為Netty的核心,它到底有什麼特別的地方呢?
跟著狼王往下看....
前言我們先來想一個問題,為什麼Netty使用NIO,而不是AIO呢?
我想各位心中肯定有自己的答案了,讓我們帶著問題往下看吧
Netty為什麼選擇NIO我們先來重溫下這兩個的區別:
NIO模型同步非阻塞NIO有同步阻塞和同步非阻塞兩種模式,一般講的是同步非阻塞,伺服器實現模式為一個請求一個執行緒,但客戶端傳送的連線請求都會註冊到多路複用器上,多路複用器輪詢到連線有I/O請求時才啟動一個執行緒進行處理。
AIO模型非同步非阻塞伺服器實現模式為一個有效請求一個執行緒,客戶端的I/O請求都是由OS先完成了再通知伺服器應用去啟動執行緒進行處理,注:AIO又稱為NIO2.0,在JDK7才開始支援。
然後看下Netty作者在這個問題上的原話:
Not faster than NIO (epoll) on unix systems (which is true)There is no daragram suppportUnnecessary threading model (too much abstraction without usage)
不比nio快在Unix系統上
不支援資料報
不必要的執行緒模型(太多沒什麼用的抽象化)
所以我們可以總結出以下四點:
Netty不看重Windows上的使用,在Linux系統上,AIO的底層實現仍使用EPOLL,沒有很好實現AIO,因此在效能上沒有明顯的優勢,而且被JDK封裝了一層不容易深度最佳化Netty整體架構是reactor模型, 而AIO是proactor模型, 混合在一起會非常混亂,把AIO也改造成reactor模型看起來是把epoll繞個彎又繞回來AIO還有個缺點是接收資料需要預先分配快取, 而不是NIO那種需要接收時才需要分配快取, 所以對連線數量非常大但流量小的情況, 記憶體浪費很多Linux上AIO不夠成熟,處理回撥結果速度跟不到處理需求,比如外賣員太少,顧客太多,供不應求,造成處理速度有瓶頸(待驗證)NIO簡介Java NIO 是 java 1.4 之後新出的一套IO介面,這裡的的新是相對於原有標準的Java IO和Java Networking介面。NIO提供了一種完全不同的操作方式。
NIO中的N可以理解為Non-blocking,不單純是New。
它支援面向緩衝的,基於通道的I/O操作方法。 隨著JDK 7的推出,NIO系統得到了擴充套件,為檔案系統功能和檔案處理提供了增強的支援。由於NIO檔案類支援的這些新的功能,NIO被廣泛應用於檔案處理。
NIO與IO的區別1 Channels and Buffers(通道和緩衝區)
IO是面向流的,NIO是面向緩衝區的
標準的IO程式設計介面是面向位元組流和字元流的。而NIO是面向通道和緩衝區的,資料總是從通道中讀到buffer緩衝區內,或者從buffer緩衝區寫入到通道中;( NIO中的所有I/O操作都是透過一個通道開始的。)Java IO面向流意味著每次從流中讀一個或多個位元組,直至讀取所有位元組,它們沒有被快取在任何地方;Java NIO是面向快取的I/O方法。將資料讀入緩衝器,使用通道進一步處理資料。在NIO中,使用通道和緩衝區來處理I/O操作。2 Non-blocking IO(非阻塞IO)
IO流是阻塞的,NIO流是不阻塞的。
Java NIO使我們可以進行非阻塞IO操作。比如說,當執行緒中從通道讀取資料到buffer,同時可以繼續做別的事情,當資料讀取到buffer中後,執行緒再繼續處理資料。寫資料也是一樣的。另外,非阻塞寫也是如此。一個執行緒請求寫入一些資料到某通道,但不需要等待它完全寫入,這個執行緒同時可以去做別的事情。Java IO的各種流是阻塞的。這意味著,當一個執行緒呼叫read() 或 write()時,該執行緒被阻塞,直到有一些資料被讀取,或資料完全寫入。該執行緒在此期間不能再幹任何事情了3 Selectors(選擇器)
NIO有選擇器,而IO沒有。
選擇器用於使用單個執行緒處理多個通道。因此,它需要較少的執行緒來處理這些通道。執行緒之間的切換對於作業系統來說是昂貴的。因此,為了提高系統效率選擇器是有用的。NIO三大核心元件NIO有3個實體:Buffer(緩衝區),Channel(通道),Selector(多路複用器)。
Buffer是客戶端存放服務端資訊的一個容器,服務端如果把資料準備好了,就會透過Channel往Buffer裡面傳。Buffer有7個型別:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer。Channel是客戶端與服務端之間的雙工連線通道。所以在請求的過程中,客戶端與服務端中間的Channel就在不停的執行“連線、詢問、斷開”的過程。直到資料準備好,再透過Channel傳回來。Channel主要有4個型別:FileChannel(從檔案讀取資料)、DatagramChannel(讀寫UDP網路協議資料)、SocketChannel(讀寫TCP網路協議資料)、ServerSocketChannel(可以監聽TCP連線)Selector是服務端選擇Channel的一個複用器。Seletor有兩個核心任務:監控資料是否準備好,應答Channel。具體說來,多個Channel反覆輪詢時,Selector就看該Channel所需的資料是否準備好了;如果準備好了,則將資料透過Channel返回給該客戶端的Buffer,該客戶端再進行後續其他操作;如果沒準備好,則告訴Channel還需要繼續輪詢;多個Channel反覆詢問Selector,Selector為這些Channel一一解答。BufferBuffer常見子類
ByteBuffer,儲存位元組資料到緩衝區,進行網路通訊使用最頻繁 ShortBuffer,儲存字串資料到緩衝區 CharBuffer,儲存字元資料到緩衝區 IntBuffer,儲存整數資料到緩衝區 LongBuffer,儲存長整型資料到緩衝區 DoubleBuffer,儲存小數到緩衝區 FloatBuffer,儲存小數到緩衝區
Buffer類屬性解析
常見方法:
public final int capacity( )//返回此緩衝區的容量public final int position( )//返回此緩衝區的位置public final Buffer position (int newPositio)//設定此緩衝區的位置public final int limit( )//返回此緩衝區的限制public final Buffer limit (int newLimit)//設定此緩衝區的限制public final Buffer mark( )//在此緩衝區的位置設定標記public final Buffer reset( )//將此緩衝區的位置重置為以前標記的位置public final Buffer clear( )//清除此緩衝區, 即將各個標記恢復到初始狀態,但是資料並沒有真正擦除, 後面操作會覆蓋public final Buffer flip( )//反轉此緩衝區public final Buffer rewind( )//重繞此緩衝區public final int remaining( )//返回當前位置與限制之間的元素數public final boolean hasRemaining( )//告知在當前位置和限制之間是否有元素public abstract boolean isReadOnly( );//告知此緩衝區是否為只讀緩衝區 public abstract boolean hasArray();//告知此緩衝區是否具有可訪問的底層實現陣列public abstract Object array();//返回此緩衝區的底層實現陣列public abstract int arrayOffset();//返回此緩衝區的底層實現陣列中第一個緩衝區元素的偏移量public abstract boolean isDirect();//告知此緩衝區是否為直接緩衝區
ByteBuffer常用方法
public abstract class ByteBuffer { //緩衝區建立相關api public static ByteBuffer allocateDirect(int capacity)//建立直接緩衝區 public static ByteBuffer allocate(int capacity)//設定緩衝區的初始容量 public static ByteBuffer wrap(byte[] array)//把一個數組放到緩衝區中使用 //構造初始化位置offset和上界length的緩衝區 public static ByteBuffer wrap(byte[] array,int offset, int length) //快取區存取相關API public abstract byte get( );//從當前位置position上get,get之後,position會自動+1 public abstract byte get (int index);//從絕對位置get public abstract ByteBuffer put (byte b);//從當前位置上新增,put之後,position會自動+1 public abstract ByteBuffer put (int index, byte b);//從絕對位置上put }
Channel常見channel類
1.FileChannel //檔案io操作
2.DatagramChannel //UDP資料讀寫
3.ServerSocketChannel和SocketChannel //TCP資料讀寫
FileChannel 常用方法
public int read(ByteBuffer dst) ,從通道讀取資料並放到緩衝區中 public int write(ByteBuffer src) ,把緩衝區的資料寫到通道中 public long transferFrom(ReadableByteChannel src, long position, long count),從目標通道中複製資料到當前通道 public long transferTo(long position, long count, WritableByteChannel target),把資料從當前通道複製給目標通道程式碼實踐:
透過FileChannel和ByteBuffer讀寫檔案
public void writeToFile() { try { //1.建立一個輸出流,並透過輸出流獲取channel FileOutputStream out = new FileOutputStream("D:\\fileChannelTest.txt"); final FileChannel channel = out.getChannel(); //2.透過byteBuffer讀取字串並寫入到channel中 ByteBuffer buffer = ByteBuffer.allocate(1024); buffer.put(("hello,world!").getBytes()); buffer.flip(); //反轉buffer的流向 channel.write(buffer); channel.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } public void readFromFile() { try { //1.獲取輸入流,並轉化成channel File file = new File("D:\\fileChannelTest.txt"); FileInputStream inputStream = new FileInputStream(file); final FileChannel channel = inputStream.getChannel(); //2.從通道中讀取資料到buffer,並輸出到控制檯 ByteBuffer buffer = ByteBuffer.allocate(1024); while(true) { //迴圈讀取直到全部讀取到buffer中 buffer.clear(); //清空快取區,只是把標記初始化,資料不會清楚 int read = channel.read(buffer); if (read == -1) { //讀取完畢,退出迴圈 break; } } System.out.println("content is " + new String(buffer.array())); channel.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
檔案複製
public void readFromFile() { try { //1.獲取輸入流,並獲取對應的FileChannel File file = new File("D:\\fileChannelTest.txt"); FileInputStream inputStream = new FileInputStream(file); final FileChannel channel = inputStream.getChannel(); //2.從通道中讀取資料到buffer,並輸出到控制檯 ByteBuffer buffer = ByteBuffer.allocate(1024); while(true) { //迴圈讀取直到全部讀取到buffer中 buffer.clear(); //清空快取區,只是把標記初始化,資料不會清楚 int read = channel.read(buffer); if (read == -1) { //讀取完畢,退出迴圈 break; } } System.out.println("content is " + new String(buffer.array())); channel.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }
注意事項:透過ByteBuffer進行物件的傳輸時,寫入的型別和讀取的型別必須一致,否則可能會出現BufferUnderFlowException異常
SelectorSelector 能夠檢測多個註冊的通道上是否有事件發生(注意:多個Channel以事件的方式可以註冊到同一個Selector),如果有事件發生,便獲取事件然後針對每個事件進行相應的處理,如果沒有事件發生時,當前執行緒可以處理其他事情
常見方法
public abstract class Selector implements Closeable { public static Selector open();//得到一個選擇器物件public int select(long timeout);//監控所有註冊的通道,當其中有 IO 操作可以進行時,將對應的 SelectionKey 加入到內部集合中並返回,引數用來設定超時時間public Set<SelectionKey> selectedKeys();//從內部集合中得到所有的 SelectionKey }
NIO客戶端和服務端程式碼實現服務端實現流程
構建NIO服務端
1.建立ServerSocketChannel,並繫結5555埠
2.建立selector物件,並將ServerSocketChannel註冊到seletor中,監聽accept事件
3.透過selectKey.isAcceptable判斷是否有客戶端建立連線,並註冊連線的SocketChannel到selector,監聽對應的read事件
4.透過selectKey.isReadable判斷通道是否發生讀事件,並獲取對應的socketChannel讀到緩衝區中,並輸出資料
程式碼實現:
public static void main(String[] args) throws Exception{ //建立ServerSocketChannel,-->> ServerSocket ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); InetSocketAddress inetSocketAddress = new InetSocketAddress(5555); serverSocketChannel.socket().bind(inetSocketAddress); serverSocketChannel.configureBlocking(false); //設定成非阻塞 //開啟selector,並註冊accept事件 Selector selector = Selector.open(); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); while(true) { selector.select(2000); //監聽所有通道 //遍歷selectionKeys Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); if(key.isAcceptable()) { //處理連線事件 SocketChannel socketChannel = serverSocketChannel.accept(); socketChannel.configureBlocking(false); //設定為非阻塞 System.out.println("client:" + socketChannel.getLocalAddress() + " is connect"); socketChannel.register(selector, SelectionKey.OP_READ); //註冊客戶端讀取事件到selector } else if (key.isReadable()) { //處理讀取事件 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); SocketChannel channel = (SocketChannel) key.channel(); channel.read(byteBuffer); System.out.println("client:" + channel.getLocalAddress() + " send " + new String(byteBuffer.array())); } iterator.remove(); //事件處理完畢,要記得清除 } } }
客戶端1.建立客戶端SocketChannel,並繫結ip和埠號
2.透過ByteBuffer和SocketChannel傳送訊息到服務端
程式碼實現:
總結本文狼王帶你瞭解了NIO,瞭解了為什麼Netty選擇NIO,解析了NIO三大核心元件:Buffer(緩衝區),Channel(通道),Selector(多路複用器)
從程式碼層面更直觀的展示,並提供了相應的程式碼實現思路
Netty系列的第二篇也結束了,透過這兩篇的鋪墊,下篇將會正式開始講Netty,後續我會不斷更新該系列文章,由淺至深,從簡到難,多方位多角度的帶你認識Netty這個網路框架!希望你們是我最好的觀眾!
假如面試中你被問到這些,我相信你看了這篇一定能撥動面試官的心!
原文連結:https://mp.weixin.qq.com/s/Qm0kbgYU7bv3vPSE4AGxdg