java NIO模型和三大核心原理
1.NIO
(1)基本介紹
1)Java NIO全程 java non-blocking IO,是指JDK提供的新API。從JDK1.4開始,Java提供了一系列改進的輸入/輸出的新特性,被統稱為NIO,是同步非阻塞的
2)NIO相關類都被放在java.nio包及子包下,并且對原java.io包中的很多類進行改寫
3)NIO有三大核心部分:Channel(通道),Buffer(緩沖區),Selector(選擇器)
4)NIO是面向緩沖區,或者面向塊編程的。數據讀取到一個它稍后處理的緩沖區,需要時可在緩沖區中前后移動,這就增加了處理過程中的靈活性,使用它可以提供非阻塞式高伸縮性網絡
5)Java NIO的非阻塞模式,使一個線程從某通道發送請求或者讀取數據,但是它僅能得到目前可用的數據,如果目前沒有數據可用時,就什么都不會獲取,而不是保持線程阻塞,所以直至數據變的可以讀取之前,該線程可以繼續做其它的事情。非阻塞也是如此,一個線程請求寫入一些數據到某通道,但不需要等待它完全寫入,這個線程同時可以去做別的事情
6)HTTP2.0使用了多路復用的技術,做到同一個連接并發處理多個請求,并且并發請求的數量比HTTP1.1大了好幾個數量級
(2)NIO和BIO的比較
1)BIO以流的方式處理數據,而NIO以塊的方式處理數據,塊I/O的效率比流I/O高很多
2)BIO是阻塞的,NIO是非阻塞的
3)BIO基于字節流和字符流進行操作,而NIO基于Channel(通道)和Buffer(緩沖區)進行操作,數據總是從通道讀取到緩沖區中,或者從緩沖區寫入到通道中。Selector(選擇器)用于監聽多個通道的時間(比如:連接請求,數據到達等),因此使用單個線程就可以監聽多個客戶端通道
(3)NIO三大核心原理
?
1)每個channel都會對應一個Buffer
2)Selector 對應一個線程, 一個線程對應多個channel(連接)
3)該圖反應了有三個channel注冊到該selector//程序
4)程序切換到哪個channel是由事件決定的,Event就是一個重要的概念
5)Selector 會根據不同的事件,在各個通道上切換
6)Buffer 就是一個內存塊 , 底層是有一個數組
7)數據的讀取寫入是通過Buffer,這個和BIO,BIO中或者是輸入流,或者是輸出流, 不能雙向,但是 NIO 的 Buffer 是可以讀也可以寫, 需要 flip 方法切換。channel 是雙向的, 可以返回底層操作系統的情況, 比如 Linux ,底層的操作系統通道就是雙向的
2.緩沖區(Buffer)
(1)基本介紹
緩沖區(Buffer):緩沖區本質上是一個可以讀寫數據的內存塊,可以理解成是一個容器對象(含數組),該對象提供了一組方法,可以更輕松地使用內存塊,緩沖區對象內置了一些機制,能夠跟蹤和記錄緩沖區的狀態變化情況。Channel 提供從文件、網絡讀取數據的渠道,但是讀取或寫入的數據都必須經由 Buffer
?
(2)Buffer及其子類
1)在 NIO 中,Buffer 是一個頂層父類,它是一個抽象類, 類的層級關系圖:
?
| ByteBuffer | 存儲字節數據到緩沖區 |
| ShortBuffer | 存儲字符串數據到緩沖區 |
| CharBuffer | 存儲字符數據到緩沖區 |
| IntBuffer | 存儲整數數據到緩沖區 |
| LongBuffer | 存儲長整型數據到緩沖區 |
| DoubleBuffer | 存儲高精度小數到緩沖區 |
| FloatBuffer | 存儲小數到緩沖區 |
2)Buffer類定義了所有的緩沖區都具有的四個屬性來提供關于其所包含的數據元素的信息:
?
| mark | 標記 |
| position | 位置,下一個要被讀寫的元素的索引,每次讀取緩沖區數據都會改變該值,為下次讀寫做準備 |
| limit | 表示緩沖區的當前終點,不能對緩沖區超過極限的位置進行讀寫操作 |
| capacity | 容量,即可以容納的最大數據量。在緩沖區被創建時被設定并且不能修改 |
(3)常用方法
?
3.通道(Channel)
(1)基本介紹
-
NIO的通道類似于流,但有些區別如下:
-
通道可以同時進行讀寫,而流只能讀或者寫
-
通道可以實現異步讀寫數據
-
通道可以從緩沖讀數據,也可以寫數據到緩沖
-
-
BIO中的stream是單向的,例如FileInputStream對象只能進行讀取數據的操作,而NIO中的通道(Channel)是雙向的,可以讀操作,也可以寫操作
-
常見的Channel類有:FileChannel、DatagramChannel、ServerSocketChannel和SocketChannel。
-
FileChannel用于文件的數據讀寫,DatagramChannel用于UDP的數據讀寫,ServerSocketChannel和SocketChannel用于TCP的數據讀寫
(2)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) | 把數據從當前通道復制給目標通道 |
(3)案例1-本地文件寫數據
public class NIOFileChannel01 {public static void main(String[] args) throws Exception{ ?String str = "hello,NIO";//創建一個輸出流->channelFileOutputStream fileOutputStream = new FileOutputStream("/Users/apple/學習/study/test01.txt"); ?//通過 fileOutputStream 獲取 對應的 FileChannel//這個 fileChannel 真實 類型是 FileChannelImplFileChannel fileChannel = fileOutputStream.getChannel(); ?//創建一個緩沖區 ByteBufferByteBuffer byteBuffer = ByteBuffer.allocate(1024); ?//將 str 放入 byteBufferbyteBuffer.put(str.getBytes()); ? ?//對byteBuffer 進行flipbyteBuffer.flip(); ?//將byteBuffer 數據寫入到 fileChannelfileChannel.write(byteBuffer);fileOutputStream.close(); ?} }(4)案例2-本地文件讀數據
public class NIOFileChannel02 {public static void main(String[] args) throws Exception { ?//創建文件的輸入流File file = new File("/Users/apple/學習/study/test01.txt");FileInputStream fileInputStream = new FileInputStream(file); ?//通過fileInputStream 獲取對應的FileChannel -> 實際類型 FileChannelImplFileChannel fileChannel = fileInputStream.getChannel(); ?//創建緩沖區ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length()); ?//將通道的數據讀入到BufferfileChannel.read(byteBuffer); ?//將byteBuffer 的 字節數據 轉成StringSystem.out.println(new String(byteBuffer.array()));fileInputStream.close(); ?} }(5)案例3-使用Buffer完成文件的讀取、寫入
public class NIOFileChannel03 {public static void main(String[] args) throws Exception { ?FileInputStream fileInputStream = new FileInputStream("1.txt");FileChannel fileChannel01 = fileInputStream.getChannel(); ?FileOutputStream fileOutputStream = new FileOutputStream("2.txt");FileChannel fileChannel02 = fileOutputStream.getChannel(); ?ByteBuffer byteBuffer = ByteBuffer.allocate(512); ?//循環讀取while (true) { ?//這里有一個重要的操作,一定不要忘了/*public final Buffer clear() {position = 0;limit = capacity;mark = -1;return this;}*/byteBuffer.clear(); //清空bufferint read = fileChannel01.read(byteBuffer);System.out.println("read =" + read);//表示讀完if (read == -1) {break;}//將buffer 中的數據寫入到 fileChannel02 -- 2.txtbyteBuffer.flip();fileChannel02.write(byteBuffer);} ?//關閉相關的流fileInputStream.close();fileOutputStream.close();} }(6)案例4-拷貝文件 transferFrom 方法
public class NIOFileChannel04 {public static void main(String[] args) ?throws Exception { ?//創建相關流FileInputStream fileInputStream = new FileInputStream("a.jpg");FileOutputStream fileOutputStream = new FileOutputStream("a1.jpg"); ?//獲取各個流對應的fileChannelFileChannel sourceCh = fileInputStream.getChannel();FileChannel destCh = fileOutputStream.getChannel(); ?//使用transferForm完成拷貝destCh.transferFrom(sourceCh,0,sourceCh.size());//關閉相關通道和流sourceCh.close();destCh.close();fileInputStream.close();fileOutputStream.close();} }(7)案例5-零拷貝文件-transferTo文件
零拷貝參考資料:
https://www.cnblogs.com/yibutian/p/9482640.html
http://www.360doc.com/content/19/0528/13/99071_838741319.shtml
public class NewIOClient {public static void main(String[] args) throws Exception {String filename = "/Users/apple/password.txt"; ?//得到一個文件channelFileChannel fileChannel = new FileInputStream(filename).getChannel();FileChannel fileChannel1 = new FileOutputStream("/Users/apple/password1.txt").getChannel(); ?//準備發送long startTime = System.currentTimeMillis(); ?//在linux下一個transferTo 方法就可以完成傳輸//在windows 下 一次調用 transferTo 只能發送8m,就需要分段傳輸文件//transferTo 底層使用到零拷貝long transferCount = fileChannel.transferTo(0, fileChannel.size(), fileChannel1); ?System.out.println("發送的總的字節數 =" + transferCount + " 耗時:" + (System.currentTimeMillis() - startTime)); ?//關閉fileChannel.close(); ?} } ?(8)注意事項和細節
1)ByteBuffer 支持類型化的 put 和 get, put 放入的是什么數據類型,get 就應該使用相應的數據類型來取出,否則可能有 BufferUnderflowException 異常
public class NIOByteBufferPutGet {public static void main(String[] args) { ?//創建一個BufferByteBuffer buffer = ByteBuffer.allocate(64); ?//類型化方式放入數據buffer.putInt(100);buffer.putLong(9);buffer.putChar('a');buffer.putShort((short) 4); ?//取出buffer.flip(); ?System.out.println(); ?System.out.println(buffer.getInt());System.out.println(buffer.getLong());System.out.println(buffer.getChar());System.out.println(buffer.getLong());} }2)可以將一個普通Buffer轉成只讀Buffer
public class ReadOnlyBuffer {public static void main(String[] args) { ?//創建一個bufferByteBuffer buffer = ByteBuffer.allocate(64); ?for(int i = 0; i < 64; i++) {buffer.put((byte)i);} ?//讀取buffer.flip(); ?//得到一個只讀的BufferByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();System.out.println(readOnlyBuffer.getClass()); ?//讀取while (readOnlyBuffer.hasRemaining()) {System.out.println(readOnlyBuffer.get());} ?readOnlyBuffer.put((byte)100); //ReadOnlyBufferException} }3)NIO 還提供了 MappedByteBuffer, 可以讓文件直接在內存(堆外的內存)中進行修改
public class MappedByteBufferTest {public static void main(String[] args) throws Exception { ?RandomAccessFile randomAccessFile = new RandomAccessFile("/Users/apple/學習/study/test01.txt", "rw");//獲取對應的通道FileChannel channel = randomAccessFile.getChannel(); ?/*** 參數1: FileChannel.MapMode.READ_WRITE 使用的讀寫模式* 參數2: 0 : 可以直接修改的起始位置* 參數3: 5: 是映射到內存的大小(不是索引位置) ,即將 1.txt 的多少個字節映射到內存* 可以直接修改的范圍就是 0-5* 實際類型 DirectByteBuffer*/MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5); ?mappedByteBuffer.put(0, (byte) 'H');mappedByteBuffer.put(3, (byte) '9'); // ? ? ? mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException ?randomAccessFile.close();System.out.println("修改成功~~");} }4)NIO 還支持 通過多個 Buffer (即 Buffer 數組) 完成讀寫操作,即 Scattering 和 Gathering
/*** Scattering:將數據寫入到buffer時,可以采用buffer數組,依次寫入 [分散]* Gathering: 從buffer讀取數據時,可以采用buffer數組,依次讀*/ public class ScatteringAndGatheringTest {public static void main(String[] args) throws Exception { ?//使用 ServerSocketChannel 和 SocketChannel 網絡 ?ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();InetSocketAddress inetSocketAddress = new InetSocketAddress(7000); ?//綁定端口到socket ,并啟動serverSocketChannel.socket().bind(inetSocketAddress); ?//創建buffer數組ByteBuffer[] byteBuffers = new ByteBuffer[2];byteBuffers[0] = ByteBuffer.allocate(5);byteBuffers[1] = ByteBuffer.allocate(3); ?//等客戶端連接(telnet)SocketChannel socketChannel = serverSocketChannel.accept();//假定從客戶端接收8個字節int messageLength = 8;//循環的讀取while (true) { ?int byteRead = 0; ?while (byteRead < messageLength) {long l = socketChannel.read(byteBuffers);//累計讀取的字節數byteRead += l;System.out.println("byteRead=" + byteRead);//使用流打印, 看看當前的這個buffer的position 和 limitArrays.stream(byteBuffers).map(buffer -> "position=" + buffer.position() + ", limit=" + buffer.limit()).forEach(System.out::println);} ?//將所有的buffer進行flipArrays.asList(byteBuffers).forEach(Buffer::flip); ?//將數據讀出顯示到客戶端long byteWirte = 0;while (byteWirte < messageLength) {long l = socketChannel.write(byteBuffers);byteWirte += l;} ?//將所有的buffer 進行clearArrays.asList(byteBuffers).forEach(Buffer::clear); ?System.out.println("byteRead:=" + byteRead + " byteWrite=" + byteWirte + ", messageLength" + messageLength);} ?} }7.選擇器(Selector)
(1)基本介紹
1)Java的NIO,用非阻塞的IO方式。可以用一個線程,處理多個的客戶端連接,就會用到選擇器
2)Selector能夠檢測多個注冊的通道上是否有事件發生。如果有事件發生,便獲取事件然后針對每個事件進行相應的處理。這樣就可以只用一個單線程去管理多個通道,也就是管理多個連接和請求
3)只有在連接/通道真正有讀寫事件發生時,才會進行讀寫,就大大地減少了系統開銷,并且不必為每個連接都創建一個線程,不用去維護多個線程
4)避免了多線程之間的上下文切換導致的開銷
(2)常用方法
| open() | 得到一個選擇器對象 |
| Select() | 監控所有注冊的通道,當其中有IO操作可以進行時,將對應的SelectionKey加入到內部集合并返回 |
| select(long timeout) | 阻塞timeout毫秒 |
| selectedKeys() | 從內部集合中得到所有的selectionKeys |
| selectNow() | 不阻塞,立馬返回 |
| wakeup() | 喚醒 selector |
(3)代碼示例
NIOServer.java
public class NIOServer {public static void main(String[] args) throws Exception { ?//創建ServerSocketChannel -> ServerSocket ?ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); ?//得到一個Selector對象Selector selector = Selector.open(); ?//綁定一個端口6666, 在服務器端監聽serverSocketChannel.socket().bind(new InetSocketAddress(6666));//設置為非阻塞serverSocketChannel.configureBlocking(false); ?//把 serverSocketChannel 注冊到 selector 關心 事件為 OP_ACCEPTserverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); ?System.out.println("注冊后的selectionkey 數量=" + selector.keys().size()); ? ?//循環等待客戶端連接while (true) { ?//這里我們等待1秒,如果沒有事件發生, 返回if (selector.select(1000) == 0) {System.out.println("服務器等待了1秒,無連接");continue;} ?//如果返回的>0, 就獲取到相關的 selectionKey集合//1.如果返回的>0, 表示已經獲取到關注的事件//2. selector.selectedKeys() 返回關注事件的集合// ? 通過 selectionKeys 反向獲取通道Set<SelectionKey> selectionKeys = selector.selectedKeys();System.out.println("selectionKeys 數量 = " + selectionKeys.size()); ?//遍歷 Set<SelectionKey>, 使用迭代器遍歷Iterator<SelectionKey> keyIterator = selectionKeys.iterator(); ?while (keyIterator.hasNext()) {//獲取到SelectionKeySelectionKey key = keyIterator.next();//根據key 對應的通道發生的事件做相應處理//如果是 OP_ACCEPT, 有新的客戶端連接if (key.isAcceptable()) {//該該客戶端生成一個 SocketChannelSocketChannel socketChannel = serverSocketChannel.accept();System.out.println("客戶端連接成功 生成了一個 socketChannel " + socketChannel.hashCode());//將 SocketChannel 設置為非阻塞socketChannel.configureBlocking(false);//將socketChannel 注冊到selector, 關注事件為 OP_READ, 同時給socketChannel//關聯一個BuffersocketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); ?System.out.println("客戶端連接后 ,注冊的selectionkey 數量=" + selector.keys().size()); //2,3,4.. ? ?}//發生 OP_READif (key.isReadable()) { ?//通過key 反向獲取到對應channelSocketChannel channel = (SocketChannel) key.channel(); ?//獲取到該channel關聯的bufferByteBuffer buffer = (ByteBuffer) key.attachment();channel.read(buffer);System.out.println("form 客戶端 " + new String(buffer.array())); ?} ?//手動從集合中移動當前的selectionKey, 防止重復操作keyIterator.remove(); ?} ?} ?} }NIOClient.java
public class NIOClient {public static void main(String[] args) throws Exception{ ?//得到一個網絡通道SocketChannel socketChannel = SocketChannel.open();//設置非阻塞socketChannel.configureBlocking(false);//提供服務器端的ip 和 端口InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);//連接服務器if (!socketChannel.connect(inetSocketAddress)) { ?while (!socketChannel.finishConnect()) {System.out.println("因為連接需要時間,客戶端不會阻塞,可以做其它工作..");}} ?//...如果連接成功,就發送數據String str = "hello, Selector~";//Wraps a byte array into a bufferByteBuffer buffer = ByteBuffer.wrap(str.getBytes());//發送數據,將 buffer 數據寫入 channelsocketChannel.write(buffer);System.in.read(); ?} }(4)SelectionKey
SelectionKey,表示 Selector 和網絡通道的注冊關系, 共四種:
-
int OP_ACCEPT:有新的網絡連接可以 accept,值為 16
-
int OP_CONNECT:代表連接已經建立,值為 8
-
int OP_READ:代表讀操作,值為 1
-
int OP_WRITE:代表寫操作,值為 4
相關方法:
| selector() | 得到與之關聯的Selector對象 |
| channel() | 得到與之關聯的通道 |
| attachment() | 得到與之關聯的共享數據 |
| interestOps(int ops) | 設置或改變監聽事件 |
| isAcceptable() | 是否可以accept |
| isReadable() | 是否可以讀 |
| isWritable() | 是否可以寫 |
總結
以上是生活随笔為你收集整理的java NIO模型和三大核心原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Netty入门之Netty的基本介绍和I
- 下一篇: Netty之十大核心模块组件介绍