JDK10都發布了,nio你了解多少?
前言
只有光頭才能變強
回顧前面:
本來我預想是先來回顧一下傳統的IO模式的,將傳統的IO模式的相關類理清楚(因為IO的類很多)。
但是,發現在整理的過程已經有很多優秀的文章了,而我自己來整理的話可能達不到他們的水平。並且傳統的IO估計大家都會用,而NIO就不一定了。
下面我就貼幾張我認為整理比較優秀的思維導圖(下面會給出圖片來源地址,大家可前往閱讀):
按操作方式分類結構圖:
字節流的輸入和輸出對照圖:
字符流的輸入和輸出對照圖:
按操作對象分類結構圖:
**上述圖片原文地址,知乎作者@小明**:
還有閱讀傳統IO源碼的優秀文章:
相信大家看完上面兩個給出的鏈接+理解了包裝模式就是這么簡單啦,傳統的IO應該就沒什么事啦~~
而NIO對於我來說可以說是挺陌生的,在當初學的時候是接觸過的。但是一直沒有用它,所以停留認知:nio是jdk1.4開始有的,比傳統IO高級。
相信很多初學者都跟我一樣,對NIO是不太了解的。而我們現在jdk10都已經發布了,jdk1.4的nio都不知道,這有點說不過去了。
所以我花了幾天去了解NIO的核心知識點,期間看了《Java 編程思想》和《瘋狂Java 講義》的nio模塊。但是,會發現看完了之后還是很迷,不知道NIO這是干嘛用的,而網上的資料與書上的知識點沒有很好地對應。
- 網上的資料很多都以IO的五種模型為基礎來講解NIO,而IO這五種模型其中又涉及到了很多概念:
同步/異步/阻塞/非阻塞/多路復用
,而不同的人又有不同的理解方式。 - 還有涉及到了unix的
select/epoll/poll/pselect
,fd
這些關鍵字,沒有相關基礎的人看起來簡直是天書 - 這就導致了在初學時認為nio遠不可及
我在找資料的過程中也收藏了好多講解NIO的資料,這篇文章就是以初學的角度來理解NIO。也算是我這兩天看NIO的一個總結吧。
- 希望大家可以看了之后知道什么是NIO,NIO的核心知識點是什么,會使用NIO~
那么接下來就開始吧,如果文章有錯誤的地方請大家多多包涵,不吝在評論區指正哦~
聲明:本文使用JDK1.8
一、NIO的概述
JDK 1.4中的java.nio.*包
中引入新的Java I/O庫,其目的是提高速度。實際上,“舊”的I/O包已經使用NIO重新實現過,即使我們不顯式的使用NIO編程,也能從中受益。
- nio翻譯成 no-blocking io 或者 new io 都無所謂啦,都說得通~
在《Java編程思想》讀到“即使我們不顯式的使用NIO編程,也能從中受益”的時候,我是挺在意的,所以:我們測試一下使用NIO復制文件和傳統IO復制文件的性能:
import java.io.*; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class SimpleFileTransferTest { private long transferFile(File source, File des) throws IOException { long startTime = System.currentTimeMillis(); if (!des.exists()) des.createNewFile(); BufferedInputStream bis = new BufferedInputStream(new FileInputStream(source)); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(des)); //將數據源讀到的內容寫入目的地--使用數組 byte[] bytes = new byte[1024 * 1024]; int len; while ((len = bis.read(bytes)) != -1) { bos.write(bytes, 0, len); } long endTime = System.currentTimeMillis(); return endTime - startTime; } private long transferFileWithNIO(File source, File des) throws IOException { long startTime = System.currentTimeMillis(); if (!des.exists()) des.createNewFile(); RandomAccessFile read = new RandomAccessFile(source, "rw"); RandomAccessFile write = new RandomAccessFile(des, "rw"); FileChannel readChannel = read.getChannel(); FileChannel writeChannel = write.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024 * 1024);//1M緩沖區 while (readChannel.read(byteBuffer) > 0) { byteBuffer.flip(); writeChannel.write(byteBuffer); byteBuffer.clear(); } writeChannel.close(); readChannel.close(); long endTime = System.currentTimeMillis(); return endTime - startTime; } public static void main(String[] args) throws IOException { SimpleFileTransferTest simpleFileTransferTest = new SimpleFileTransferTest(); File sourse = new File("F:\\電影\\[電影天堂www.dygod.cn]猜火車-cd1.rmvb"); File des = new File("X:\\Users\\ozc\\Desktop\\io.avi"); File nio = new File("X:\\Users\\ozc\\Desktop\\nio.avi"); long time = simpleFileTransferTest.transferFile(sourse, des); System.out.println(time + ":普通字節流時間"); long timeNio = simpleFileTransferTest.transferFileWithNIO(sourse, nio); System.out.println(timeNio + ":NIO時間"); } }
我分別測試了文件大小為13M,40M,200M的:
1.1為什么要使用NIO
可以看到使用過NIO重新實現過的傳統IO根本不虛,在大文件下效果還比NIO要好(當然了,個人幾次的測試,或許不是很准)
- 而NIO要有一定的學習成本,也沒有傳統IO那么好理解。
那這意味着我們可以不使用/學習NIO了嗎?
答案是否定的,IO操作往往在兩個場景下會用到:
- 文件IO
- 網絡IO
NIO的魅力:在網絡中使用IO就可以體現出來了!
- 后面會說到網絡中使用NIO,不急哈~
二、NIO快速入門
首先我們來看看IO和NIO的區別:
- 可簡單認為:IO是面向流的處理,NIO是面向塊(緩沖區)的處理
- 面向流的I/O 系統一次一個字節地處理數據。
- 一個面向塊(緩沖區)的I/O系統以塊的形式處理數據。
NIO主要有三個核心部分組成:
- buffer緩沖區
- Channel管道
- Selector選擇器
2.1buffer緩沖區和Channel管道
在NIO中並不是以流的方式來處理數據的,而是以buffer緩沖區和Channel管道配合使用來處理數據。
簡單理解一下:
- Channel管道比作成鐵路,buffer緩沖區比作成火車(運載着貨物)
而我們的NIO就是通過Channel管道運輸着存儲數據的Buffer緩沖區的來實現數據的處理!
- 要時刻記住:Channel不與數據打交道,它只負責運輸數據。與數據打交道的是Buffer緩沖區
- Channel-->運輸
- Buffer-->數據
相對於傳統IO而言,流是單向的。對於NIO而言,有了Channel管道這個概念,我們的讀寫都是雙向的(鐵路上的火車能從廣州去北京、自然就能從北京返還到廣州)!
2.1.1buffer緩沖區核心要點
我們來看看Buffer緩沖區有什么值得我們注意的地方。
Buffer是緩沖區的抽象類:
其中ByteBuffer是用得最多的實現類(在管道中讀寫字節數據)。
拿到一個緩沖區我們往往會做什么?很簡單,就是讀取緩沖區的數據/寫數據到緩沖區中。所以,緩沖區的核心方法就是:
put()
get()
Buffer類維護了4個核心變量屬性來提供關於其所包含的數組的信息。它們是:
- 容量Capacity
- 緩沖區能夠容納的數據元素的最大數量。容量在緩沖區創建時被設定,並且永遠不能被改變。(不能被改變的原因也很簡單,底層是數組嘛)
- 上界Limit
- 緩沖區里的數據的總數,代表了當前緩沖區中一共有多少數據。
- 位置Position
- 下一個要被讀或寫的元素的位置。Position會自動由相應的
get( )
和put( )
函數更新。
- 下一個要被讀或寫的元素的位置。Position會自動由相應的
- 標記Mark
- 一個備忘位置。用於記錄上一次讀寫的位置。
2.1.2buffer代碼演示
首先展示一下是如何創建緩沖區的,核心變量的值是怎么變化的。
public static void main(String[] args) { // 創建一個緩沖區 ByteBuffer byteBuffer = ByteBuffer.allocate(1024); // 看一下初始時4個核心變量的值 System.out.println("初始時-->limit--->"+byteBuffer.limit()); System.out.println("初始時-->position--->"+byteBuffer.position()); System.out.println("初始時-->capacity--->"+byteBuffer.capacity()); System.out.println("初始時-->mark--->" + byteBuffer.mark()); System.out.println("--------------------------------------"); // 添加一些數據到緩沖區中 String s = "Java3y"; byteBuffer.put(s.getBytes()); // 看一下初始時4個核心變量的值 System.out.println("put完之后-->limit--->"+byteBuffer.limit()); System.out.println("put完之后-->position--->"+byteBuffer.position()); System.out.println("put完之后-->capacity--->"+byteBuffer.capacity()); System.out.println("put完之后-->mark--->" + byteBuffer.mark()); }
運行結果:
現在我想要從緩存區拿數據,怎么拿呀??NIO給了我們一個flip()
方法。這個方法可以改動position和limit的位置!
還是上面的代碼,我們flip()
一下后,再看看4個核心屬性的值會發生什么變化:
很明顯的是:
- limit變成了position的位置了
- 而position變成了0
看到這里的同學可能就會想到了:當調用完filp()
時:limit是限制讀到哪里,而position是從哪里讀
一般我們稱filp()
為“切換成讀模式”
- 每當要從緩存區的時候讀取數據時,就調用
filp()
“切換成讀模式”。
切換成讀模式之后,我們就可以讀取緩沖區的數據了:
// 創建一個limit()大小的字節數組(因為就只有limit這么多個數據可讀) byte[] bytes = new byte[byteBuffer.limit()]; // 將讀取的數據裝進我們的字節數組中 byteBuffer.get(bytes); // 輸出數據 System.out.println(new String(bytes, 0, bytes.length));
隨后輸出一下核心變量的值看看:
讀完我們還想寫數據到緩沖區,那就使用clear()
函數,這個函數會“清空”緩沖區:
- 數據沒有真正被清空,只是被遺忘掉了
2.1.3FileChannel通道核心要點
Channel通道只負責傳輸數據、不直接操作數據的。操作數據都是通過Buffer緩沖區來進行操作!
// 1. 通過本地IO的方式來獲取通道 FileInputStream fileInputStream = new FileInputStream("F:\\3yBlog\\JavaEE常用框架\\Elasticsearch就是這么簡單.md"); // 得到文件的輸入通道 FileChannel inchannel = fileInputStream.getChannel(); // 2. jdk1.7后通過靜態方法.open()獲取通道 FileChannel.open(Paths.get("F:\\3yBlog\\JavaEE常用框架\\Elasticsearch就是這么簡單2.md"), StandardOpenOption.WRITE);
使用FileChannel配合緩沖區實現文件復制的功能:
使用內存映射文件的方式實現文件復制的功能(直接操作緩沖區):
通道之間通過transfer()
實現數據的傳輸(直接操作緩沖區):
2.1.4直接與非直接緩沖區
- 非直接緩沖區是需要經過一個:copy的階段的(從內核空間copy到用戶空間)
- 直接緩沖區不需要經過copy階段,也可以理解成--->內存映射文件,(上面的圖片也有過例子)。
使用直接緩沖區有兩種方式:
- 緩沖區創建的時候分配的是直接緩沖區
- 在FileChannel上調用
map()
方法,將文件直接映射到內存中創建
2.1.5scatter和gather、字符集
這個知識點我感覺用得挺少的,不過很多教程都有說這個知識點,我也拿過來說說吧:
- 分散讀取(scatter):將一個通道中的數據分散讀取到多個緩沖區中
- 聚集寫入(gather):將多個緩沖區中的數據集中寫入到一個通道中
分散讀取
聚集寫入
字符集(只要編碼格式和解碼格式一致,就沒問題了)
三、IO模型理解
文件的IO就告一段落了,我們來學習網絡中的IO~~~為了更好地理解NIO,我們先來學習一下IO的模型~
根據UNIX網絡編程對I/O模型的分類,在UNIX可以歸納成5種I/O模型:
- 阻塞I/O
- 非阻塞I/O
- I/O多路復用
- 信號驅動I/O
- 異步I/O
3.0學習I/O模型需要的基礎
3.0.1文件描述符
Linux 的內核將所有外部設備都看做一個文件來操作,對一個文件的讀寫操作會調用內核提供的系統命令(api),返回一個file descriptor
(fd,文件描述符)。而對一個socket的讀寫也會有響應的描述符,稱為socket fd
(socket文件描述符),描述符就是一個數字,指向內核中的一個結構體(文件路徑,數據區等一些屬性)。
- 所以說:在Linux下對文件的操作是利用文件描述符(file descriptor)來實現的。
3.0.2用戶空間和內核空間
為了保證用戶進程不能直接操作內核(kernel),保證內核的安全,操心系統將虛擬空間划分為兩部分
- 一部分為內核空間。
- 一部分為用戶空間。
3.0.3I/O運行過程
我們來看看IO在系統中的運行是怎么樣的(我們以read為例)
可以發現的是:當應用程序調用read方法時,是需要等待的--->從內核空間中找數據,再將內核空間的數據拷貝到用戶空間的。
- 這個等待是必要的過程!
下面只講解用得最多的3個I/0模型:
- 阻塞I/O
- 非阻塞I/O
- I/O多路復用
3.1阻塞I/O模型
在進程(用戶)空間中調用recvfrom
,其系統調用直到數據包到達且被復制到應用進程的緩沖區中或者發生錯誤時才返回,在此期間一直等待。
3.2非阻塞I/O模型
recvfrom
從應用層到內核的時候,如果沒有數據就直接返回一個EWOULDBLOCK錯誤,一般都對非阻塞I/O模型進行輪詢檢查這個狀態,看內核是不是有數據到來。
3.3I/O復用模型
前面也已經說了:在Linux下對文件的操作是利用文件描述符(file descriptor)來實現的。
在Linux下它是這樣子實現I/O復用模型的:
- 調用
select/poll/epoll/pselect
其中一個函數,傳入多個文件描述符,如果有一個文件描述符就緒,則返回,否則阻塞直到超時。
比如poll()
函數是這樣子的:int poll(struct pollfd *fds,nfds_t nfds, int timeout);
其中 pollfd
結構定義如下:
struct pollfd { int fd; /* 文件描述符 */ short events; /* 等待的事件 */ short revents; /* 實際發生了的事件 */ };
- (1)當用戶進程調用了select,那么整個進程會被block;
- (2)而同時,kernel會“監視”所有select負責的socket;
- (3)當任何一個socket中的數據准備好了,select就會返回;
- (4)這個時候用戶進程再調用read操作,將數據從kernel拷貝到用戶進程(空間)。
- 所以,I/O 多路復用的特點是通過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符其中的任意一個進入讀就緒狀態,select()函數就可以返回。
select/epoll的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。
3.4I/O模型總結
正經的描述都在上面給出了,不知道大家理解了沒有。下面我舉幾個例子總結一下這三種模型:
阻塞I/O:
- Java3y跟女朋友去買喜茶,排了很久的隊終於可以點飲料了。我要綠研,謝謝。可是喜茶不是點了單就能立即拿,於是我在喜茶門口等了一小時才拿到綠研。
- 在門口干等一小時
非阻塞I/O:
- Java3y跟女朋友去買一點點,排了很久的隊終於可以點飲料了。我要波霸奶茶,謝謝。可是一點點不是點了單就能立即拿,同時服務員告訴我:你大概要等半小時哦。你們先去逛逛吧~於是Java3y跟女朋友去玩了幾把斗地主,感覺時間差不多了。於是又去一點點問:請問到我了嗎?我的單號是xxx。服務員告訴Java3y:還沒到呢,現在的單號是XXX,你還要等一會,可以去附近耍耍。問了好幾次后,終於拿到我的波霸奶茶了。
- 去逛了下街、斗了下地主,時不時問問到我了沒有
I/O復用模型:
- Java3y跟女朋友去麥當勞吃漢堡包,現在就厲害了可以使用微信小程序點餐了。於是跟女朋友找了個地方坐下就用小程序點餐了。點餐了之后玩玩斗地主、聊聊天什么的。時不時聽到廣播在復述XXX請取餐,反正我的單號還沒到,就繼續玩唄。~~等聽到廣播的時候再取餐就是了。時間過得挺快的,此時傳來:Java3y請過來取餐。於是我就能拿到我的麥辣雞翅漢堡了。
- 聽廣播取餐,廣播不是為我一個人服務。廣播喊到我了,我過去取就Ok了。
四、使用NIO完成網絡通信
4.1NIO基礎繼續講解
回到我們最開始的圖:
NIO被叫為 no-blocking io
,其實是在網絡這個層次中理解的,對於FileChannel來說一樣是阻塞。
我們前面也僅僅講解了FileChannel,對於我們網絡通信是還有幾個Channel的~
所以說:我們通常使用NIO是在網絡中使用的,網上大部分討論NIO都是在網絡通信的基礎之上的!說NIO是非阻塞的NIO也是網絡中體現的!
從上面的圖我們可以發現還有一個Selector
選擇器這么一個東東。從一開始我們就說過了,nio的核心要素有:
- Buffer緩沖區
- Channel通道
- Selector選擇器
我們在網絡中使用NIO往往是I/O模型的多路復用模型!
- Selector選擇器就可以比喻成麥當勞的廣播。
- 一個線程能夠管理多個Channel的狀態
4.2NIO阻塞形態
為了更好地理解,我們先來寫一下NIO在網絡中是阻塞的狀態代碼,隨后看看非阻塞是怎么寫的就更容易理解了。
- 是阻塞的就沒有Selector選擇器了,就直接使用Channel和Buffer就完事了。
客戶端:
public class BlockClient { public static void main(String[] args) throws IOException { // 1. 獲取通道 SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666)); // 2. 發送一張圖片給服務端吧 FileChannel fileChannel = FileChannel.open(Paths.get("X:\\Users\\ozc\\Desktop\\新建文件夾\\1.png"), StandardOpenOption.READ); // 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是與數據打交道的呢 ByteBuffer buffer = ByteBuffer.allocate(1024); // 4.讀取本地文件(圖片),發送到服務器 while (fileChannel.read(buffer) != -1) { // 在讀之前都要切換成讀模式 buffer.flip(); socketChannel.write(buffer); // 讀完切換成寫模式,能讓管道繼續讀取文件的數據 buffer.clear(); } // 5. 關閉流 fileChannel.close(); socketChannel.close(); } }
服務端:
public class BlockServer { public static void main(String[] args) throws IOException { // 1.獲取通道 ServerSocketChannel server = ServerSocketChannel.open(); // 2.得到文件通道,將客戶端傳遞過來的圖片寫到本地項目下(寫模式、沒有則創建) FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); // 3. 綁定鏈接 server.bind(new InetSocketAddress(6666)); // 4. 獲取客戶端的連接(阻塞的) SocketChannel client = server.accept(); // 5. 要使用NIO,有了Channel,就必然要有Buffer,Buffer是與數據打交道的呢 ByteBuffer buffer = ByteBuffer.allocate(1024); // 6.將客戶端傳遞過來的圖片保存在本地中 while (client.read(buffer) != -1) { // 在讀之前都要切換成讀模式 buffer.flip(); outChannel.write(buffer); // 讀完切換成寫模式,能讓管道繼續讀取文件的數據 buffer.clear(); } // 7.關閉通道 outChannel.close(); client.close(); server.close(); } }
結果就可以將客戶端傳遞過來的圖片保存在本地了:
此時服務端保存完圖片想要告訴客戶端已經收到圖片啦:
客戶端接收服務端帶過來的數據:
如果僅僅是上面的代碼是不行的!這個程序會阻塞起來!
- 因為服務端不知道客戶端還有沒有數據要發過來(與剛開始不一樣,客戶端發完數據就將流關閉了,服務端可以知道客戶端沒數據發過來了),導致服務端一直在讀取客戶端發過來的數據。
- 進而導致了阻塞!
於是客戶端在寫完數據給服務端時,顯式告訴服務端已經發完數據了!
4.3NIO非阻塞形態
如果使用非阻塞模式的話,那么我們就可以不顯式告訴服務器已經發完數據了。我們下面來看看怎么寫:
客戶端:
public class NoBlockClient { public static void main(String[] args) throws IOException { // 1. 獲取通道 SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666)); // 1.1切換成非阻塞模式 socketChannel.configureBlocking(false); // 2. 發送一張圖片給服務端吧 FileChannel fileChannel = FileChannel.open(Paths.get("X:\\Users\\ozc\\Desktop\\新建文件夾\\1.png"), StandardOpenOption.READ); // 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是與數據打交道的呢 ByteBuffer buffer = ByteBuffer.allocate(1024); // 4.讀取本地文件(圖片),發送到服務器 while (fileChannel.read(buffer) != -1) { // 在讀之前都要切換成讀模式 buffer.flip(); socketChannel.write(buffer); // 讀完切換成寫模式,能讓管道繼續讀取文件的數據 buffer.clear(); } // 5. 關閉流 fileChannel.close(); socketChannel.close(); } }
服務端:
public class NoBlockServer { public static void main(String[] args) throws IOException { // 1.獲取通道 ServerSocketChannel server = ServerSocketChannel.open(); // 2.切換成非阻塞模式 server.configureBlocking(false); // 3. 綁定連接 server.bind(new InetSocketAddress(6666)); // 4. 獲取選擇器 Selector selector = Selector.open(); // 4.1將通道注冊到選擇器上,指定接收“監聽通道”事件 server.register(selector, SelectionKey.OP_ACCEPT); // 5. 輪訓地獲取選擇器上已“就緒”的事件--->只要select()>0,說明已就緒 while (selector.select() > 0) { // 6. 獲取當前選擇器所有注冊的“選擇鍵”(已就緒的監聽事件) Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); // 7. 獲取已“就緒”的事件,(不同的事件做不同的事) while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); // 接收事件就緒 if (selectionKey.isAcceptable()) { // 8. 獲取客戶端的鏈接 SocketChannel client = server.accept(); // 8.1 切換成非阻塞狀態 client.configureBlocking(false); // 8.2 注冊到選擇器上-->拿到客戶端的連接為了讀取通道的數據(監聽讀就緒事件) client.register(selector, SelectionKey.OP_READ); } else if (selectionKey.isReadable()) { // 讀事件就緒 // 9. 獲取當前選擇器讀就緒狀態的通道 SocketChannel client = (SocketChannel) selectionKey.channel(); // 9.1讀取數據 ByteBuffer buffer = ByteBuffer.allocate(1024); // 9.2得到文件通道,將客戶端傳遞過來的圖片寫到本地項目下(寫模式、沒有則創建) FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); while (client.read(buffer) > 0) { // 在讀之前都要切換成讀模式 buffer.flip(); outChannel.write(buffer); // 讀完切換成寫模式,能讓管道繼續讀取文件的數據 buffer.clear(); } } // 10. 取消選擇鍵(已經處理過的事件,就應該取消掉了) iterator.remove(); } } } }
還是剛才的需求:服務端保存了圖片以后,告訴客戶端已經收到圖片了。
在服務端上只要在后面寫些數據給客戶端就好了:
在客戶端上要想獲取得到服務端的數據,也需要注冊在register上(監聽讀事件)!
public class NoBlockClient2 { public static void main(String[] args) throws IOException { // 1. 獲取通道 SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666)); // 1.1切換成非阻塞模式 socketChannel.configureBlocking(false); // 1.2獲取選擇器 Selector selector = Selector.open(); // 1.3將通道注冊到選擇器中,獲取服務端返回的數據 socketChannel.register(selector, SelectionKey.OP_READ); // 2. 發送一張圖片給服務端吧 FileChannel fileChannel = FileChannel.open(Paths.get("X:\\Users\\ozc\\Desktop\\新建文件夾\\1.png"), StandardOpenOption.READ); // 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是與數據打交道的呢 ByteBuffer buffer = ByteBuffer.allocate(1024); // 4.讀取本地文件(圖片),發送到服務器 while (fileChannel.read(buffer) != -1) { // 在讀之前都要切換成讀模式 buffer.flip(); socketChannel.write(buffer); // 讀完切換成寫模式,能讓管道繼續讀取文件的數據 buffer.clear(); } // 5. 輪訓地獲取選擇器上已“就緒”的事件--->只要select()>0,說明已就緒 while (selector.select() > 0) { // 6. 獲取當前選擇器所有注冊的“選擇鍵”(已就緒的監聽事件) Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); // 7. 獲取已“就緒”的事件,(不同的事件做不同的事) while (iterator.hasNext()) { SelectionKey selectionKey = iterator.next(); // 8. 讀事件就緒 if (selectionKey.isReadable()) { // 8.1得到對應的通道 SocketChannel channel = (SocketChannel) selectionKey.channel(); ByteBuffer responseBuffer = ByteBuffer.allocate(1024); // 9. 知道服務端要返回響應的數據給客戶端,客戶端在這里接收 int readBytes = channel.read(responseBuffer); if (readBytes > 0) { // 切換讀模式 responseBuffer.flip(); System.out.println(new String(responseBuffer.array(), 0, readBytes)); } } // 10. 取消選擇鍵(已經處理過的事件,就應該取消掉了) iterator.remove(); } } } }
測試結果:
下面就簡單總結一下使用NIO時的要點:
- 將Socket通道注冊到Selector中,監聽感興趣的事件
- 當感興趣的時間就緒時,則會進去我們處理的方法進行處理
- 每處理完一次就緒事件,刪除該選擇鍵(因為我們已經處理完了)
4.4管道和DataGramChannel
這里我就不再講述了,最難的TCP都講了,UDP就很簡單了。
UDP:
管道:
總的來說NIO也是一個比較重要的知識點,因為它是學習netty的基礎~
想以一篇來完全講解NIO顯然是不可能的啦,想要更加深入了解NIO可以往下面的鏈接繼續學習~
參考資料:
- https://www.zhihu.com/question/29005375---如何學習Java的NIO?
- http://ifeve.com/java-nio-all/---Java NIO 系列教程
- https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html-----NIO 入門
- https://blog.csdn.net/anxpp/article/details/51503329-----Linux 網絡 I/O 模型簡介(圖文)
- https://wangjingxin.top/2016/10/21/decoration/-----談談java的NIO和AIO
- https://www.yiibai.com/java_nio/-----Java NIO教程
- https://blog.csdn.net/cowthan/article/details/53563206------Java 8:Java 的新IO (nio)
- https://blog.csdn.net/youyou1543724847/article/details/52748785-------JAVA NIO(1.基本概念,基本類)
- https://www.cnblogs.com/zingp/p/6863170.html-----IO模式和IO多路復用
- https://www.cnblogs.com/Evsward/p/nio.html----掌握NIO,程序人生
- https://blog.csdn.net/anxpp/article/details/51512200----Java 網絡IO編程總結(BIO、NIO、AIO均含完整實例代碼)
- https://zhuanlan.zhihu.com/p/24393775?refer=hinus---進擊的Java新人
- 《Java編程思想》
- 《瘋狂Java 講義》
轉自:https://www.cnblogs.com/Java3y/archive/2018/05/14/9036728.html