Java IO(3)非阻塞式輸入輸出(NIO)

来源:http://www.cnblogs.com/yulinfeng/archive/2017/12/24/8095326.html
-Advertisement-
Play Games

在上篇《Java IO(2)阻塞式輸入輸出(BIO)》的末尾談到了什麼是阻塞式輸入輸出,通過Socket編程對其有了大致瞭解。現在再重新回顧梳理一下,對於只有一個“客戶端”和一個“伺服器端”來講,伺服器端需要阻塞式接收客戶端的請求,這裡的阻塞式表示伺服器端的應用代碼會被掛起直到客戶端有請求過來,在高 ...


  在上篇《Java IO(2)阻塞式輸入輸出(BIO)》的末尾談到了什麼是阻塞式輸入輸出,通過Socket編程對其有了大致瞭解。現在再重新回顧梳理一下,對於只有一個“客戶端”和一個“伺服器端”來講,伺服器端需要阻塞式接收客戶端的請求,這裡的阻塞式表示伺服器端的應用代碼會被掛起直到客戶端有請求過來,在高併發的應用場景有多個客戶端發起連接下非阻塞式IO(NIO)是不二之選(且只需要在伺服器端使用1個線程來管理,並不需要多個線程來處理多個連接)。在現實情況下,Tomcat、Jetty等很多Web伺服器均使用了NIO技術。

  接下來對於非阻塞式輸入輸出(NIO)的學習以及理解首先從它的三個基礎概念講起。

Channel(通道)

  在NIO中,你需要忘掉“流”這個概念,取而代之的是“通道”。舉例在網路應用程式中有多個客戶端連接,此時數據傳輸的概念並不是“流”而“通道”,通道與流最大的不同就是,通道是雙向的,而流是單向的(例如InputStream、OutputStream)。

Buffer(緩衝區)

  在NIO中並不是簡單的將流的概念替換為了通道,與通道搭配的是緩衝區。在BIO的位元組流中並不會使用到緩衝區,而是直接操作文件通過位元組方式直接讀取,而NIO則不同,它會將通道中的數據讀入緩存區,或者將緩存區的數據寫入通道。

Selector(選擇器)

  如果使用NIO的應用程式中只有一個Channel,選擇器則是可以不需要的,而如果有多個Channel,換言之有多個連接時,此時通過選擇器,在伺服器端的應用程式中就只需要1個線程對多個連接進行管理。

  當然從最開始就說到Channel是雙向的,所以在最終圖的示例為下圖所示:

  下麵再重新回到這三個概念,詳細解釋它們是如何協同工作的。

Channel & Buffer

  通常情況下Channel會和Buffer配合使用,但可以不使用Channel。首先需要明確的是,應用程式不管是從文件(包括網路或者其他什麼地方)中讀取數據,還是寫入數據到文件(包括網路或者其他什麼地方)都需要Buffer。

1. 直接將數據寫入Buffer,應用程式從Buffer中獲取數據

1 ByteBuffer buffer = ByteBuffer.allocate(1024);
2 byte b = 121;
3 buffer.put(b);
4 buffer.flip();    //讀寫轉換,由“寫模式”轉換為“讀模式”
5 System.out.println((char)buffer.get());    

  第1行,分配一個1KB大小的Buffer緩衝區,ByteBuffer.allcoate返回HeapByteBuffer實例。

  第3行,向Buffer中寫入一個位元組。

  第4行,Buffer由“寫模式”轉換為“讀模式”。

  第5行,ByteBuffer.get方法讀取Buffer中的數據,並且position索引+1。 在上面的代碼中有一個重點——flip方法,這個方法的存在是由於Buffer兼顧了讀和寫的操作,在ByteBuffer的實現中有三個重要的成員變數需要註意: capacity——Buffer容量 position——索引位置 limit——讀時表示最大容量,即limit = capacity;寫時表示最後一個數據所在的索引位置。 用圖例來說明上面代碼的執行過程。

  從上圖可以清晰的看到Buffer內部是如何進行讀寫操作的,其中調用flip方法是很關鍵且重要的一個步驟,試想如果不調用flip進行讀寫轉換,此時position、limit、capacity的索引位置將會如下圖所示。

  此時進行讀的操作將會得到一個錯誤數據(0)。 儘管在講這個小標題“直接將數據寫入Buffer,應用程式從Buffer中獲取數據”,但實際上已經簡要介紹了Buffer的內部實現原理。

  通過上面的例子可以看到,Channel和Buffer並不一定要在一起,單獨使用Buffer也是可以的,但要使用Chnnel那就必須得配合Buffer。

2. 從文件中讀取數據寫入Buffer,應用程式從Buffer中獲取數據

  此時的數據來源是文件,開頭提過在NIO中忘掉“流”,記住“通道”。在NIO中可以通過傳統的流獲取通道。例如從輸入流FileInputSteram中調用getChannel,或者從輸出流FileOutputStream中調用getChannel,當然還有兼顧輸入和輸出的RandomAccessFile類從中調用getChannel。

  BIO中首先獲取流,NIO中首先獲取通道。

1 RandomAccessFile file = new RandomAccessFile("/Users/yulinfeng/Documents/Coding/Idea/simplenio/src/main/java/com/demo/test.json", "rw");
2 FileChannel channel = file.getChannel();
3 ByteBuffer buffer = ByteBuffer.allocate(1024);
4 channel.read(buffer);
5 buffer.flip();
6 System.out.println(new String(buffer.array()));

  看到這段NIO讀取文件數據的代碼,心中默寫傳統的BIO是如何讀取文件數據的。

1 InputStream in = new FileInputStream("/Users/yulinfeng/Documents/Coding/Idea/simplenio/src/main/java/com/demo/test.json");
2 byte[] bytes = new byte[1024];
3 in.read(bytes);
4 System.out.println(new String(bytes));
View Code

  展開代碼可以看到,基本上如出一轍,在NIO中就是多了Buffer這個媒介來讀取數據。

  回到NIO讀取文件數據的代碼。 第1行,獲取文件流。 第2行,獲取Channel通道。 第3-6行,創建Buffer緩衝區,並將數據讀取從通道讀取到緩衝區。 同樣還是用圖例來說明上面代碼的執行過程。

  最後調用ByteBuffer.array方法返回緩衝區中的值,此時並未移動position的數組下標。這個例子結合圖例我相信能很清楚地看到NIO是如何從文件中讀取數據的,下麵這個例子將輸出數據到文件。

3. 從應用程式中將數據輸出到文件中

  前面都是應用程式從Buffer中獲取數據並且用圖例的方式瞭解了它的內部運行原理。本例將把數據通過Buffer寫到文件中,當然得記住還需要通過Channel才能寫入文件。

1 RandomAccessFile file = new RandomAccessFile("E:\\IdeaProjects\\simplenio\\src\\main\\java\\com\\demo\\out\\test.json", "rw");
2 FileChannel channel = file.getChannel();
3 ByteBuffer buffer = Charset.forName("utf-8").encode("{\"name\": \"Kevin\"}");    //這裡會自動進行讀寫轉換,第1個例子需要手動調用flip方法進行讀寫模式的轉換

  通過上面的例子很容易想到,首先需要通道,那麼就利用可讀可寫的RandomAccessFile獲取通道;其次需要緩衝區;最後將緩衝區的數據寫入到通道中即可。這段代碼其實可以把重點放到是如何從緩衝區寫到管道的。

  第1-2行,通過可讀可寫的RandomAccessFile類獲取Channel通道。(要是只需要寫文件,也可以通過FileOutputStream.getChannel獲得)

  第3行,將字元串{“name”: “Kevin”}通過UTF-8編碼寫入Buffer緩衝區,NIO會對自動對其進行讀寫模式的轉換,不需要手動調用flip方法。

  第4行,將Buffer中的數據寫入通道。

4. 從一個文件讀數據,再寫到另一個文件

  NIO不易掌握,需要反覆練習,所以本文會給出多個例子反覆操練並領會NIO的設計哲學。

  這個例子有兩種實現方式,第一種基於上面的例子就能拼湊出來,第二種則需要掌握一個新的API——transferFrom / transferTo

4.1通過上面的知識讀文件再寫文件

1 RandomAccessFile readFile = new RandomAccessFile("E:\\IdeaProjects\\simplenio\\src\\main\\java\\com\\demo\\inout\\from.json", "rw");
2 FileChannel readChannel = readFile.getChannel();
3 ByteBuffer buffer = ByteBuffer.allocate(1024);
4 readChannel.read(buffer);
5 buffer.flip();      //讀寫轉換
6 RandomAccessFile writeFile = new RandomAccessFile("E:\\IdeaProjects\\simplenio\\src\\main\\java\\com\\demo\\inout\\to.json", "rw");
7 FileChannel writeChannel = writeFile.getChannel();
8 writeChannel.write(buffer);

  經過上面的幾個例子寫出這個示例應該沒什麼問題,需要註意的是第x行的buffer.flip方法是讀寫轉換,這在上面有提到過。

4.2 通過新的API——transferFrom讀文件並寫文件

1 RandomAccessFile fromFile = new RandomAccessFile("E:\\IdeaProjects\\simplenio\\src\\main\\java\\com\\demo\\inout\\from.json", "rw");
2 FileChannel fromChannel = fromFile.getChannel();
3 RandomAccessFile toFile = new RandomAccessFile("E:\\IdeaProjects\\simplenio\\src\\main\\java\\com\\demo\\inout\\to.json", "rw");
4 FileChannel toChannel = toFile.getChannel();
5 
6 toChannel.transferFrom(fromChannel, 0, fromChannel.size());

  通過transferFrom就能將一個通道直接輸出到另一個通道而不需要緩衝區做中轉。

5. Socket網路應用程式是如何使用NIO的

  前面的例子全是有關本地文件的讀寫操作,在一個應用程式中有可能免不了通過網路來傳輸數據,傳統的Socket編程利用的是BIO,也就是阻塞式輸入輸出。而NIO同樣也可應用到Socket網路編程中。下麵兩個例子均是1個客戶端對應1個伺服器端。此時並不能很好的體會BIO和NIO的區別,若多個客戶端對應1個伺服器端,此時NIO的優點便很快顯現,不過要實現多個客戶端對應1個伺服器端則需要Selector(選擇器),由於現在還並未詳細認識它所以將“多個客戶端對應1個伺服器端”放置在後面提及。  

5.1 阻塞式網路編程(BIO Socket)

  BIO Socket是我取的名字,意思是利用傳統的阻塞式IO來進行Socket編程,本文雖主講NIO,但也需要瞭解並熟練掌握BIO。故,在此先使用傳統的IO來進行Socket編程以便能對下文的NIO Socket有一個類比。在本例中使用UDP協議傳輸數據。

 1 /**
 2  * BIO客戶端
 3  * Created by Kevin on 2017/12/18.
 4  */
 5 public class Client {
 6     public static void main(String[] args) throws Exception{
 7         String data = "this is Client.";
 8         DatagramSocket socket = new DatagramSocket();
 9         DatagramPacket packet = new DatagramPacket(data.getBytes(), data.getBytes().length, InetAddress.getByName("127.0.0.1"), 8989);
10         socket.send(packet);
11     }
12 }
 1 /**
 2  * 伺服器端
 3  * BIO Created by Kevin on 2017/12/18.
 4  */
 5 public class Server {
 6     public static void main(String[] args) throws Exception{
 7         DatagramSocket socket = new DatagramSocket(8989);
 8         byte[] data = new byte[1024];
 9         DatagramPacket packet = new DatagramPacket(data, data.length);
10         socket.receive(packet);    //伺服器端在未收到數據時,會在此處被阻塞掛起
11         System.out.println(new String(packet.getData()));
12     }
13 }

  這是我們比較熟悉的Socket編程,其中有特點的就是在伺服器端的第x行代碼,此處若未收到來自客戶端的數據,伺服器端將會被阻塞。

5.2 非阻塞式網路編程(NIO Socket)

  在通常情況下,對於網路編程用的比較多的還是阻塞式。非阻塞式在應用程式中並不是特別常見,但它在Tomcat等Web伺服器中卻很常見。這是因為對於非阻塞式的網路編程其最大的優點或者說是最大的使用場景就是面對多個客戶端時良好的性能表現。

  此處我們還是在單一的客戶端場景下使用非阻塞式網路編程(多個客戶端就會使用到Selector選擇器,下文會展開)。同樣在本例中使用UDP協議傳輸數據。

 1 /**
 2  * NIO客戶端
 3  * Created by Kevin on 2017/12/18.
 4  */
 5 public class Client {
 6     public static void main(String[] args) throws Exception{
 7         DatagramChannel channel = DatagramChannel.open();       //類似讀取本地文件,首先都需要建立一個通道
 8         ByteBuffer buffer = Charset.forName("utf-8").encode("this is Client.");     //其次建立一個緩衝區
 9         channel.send(buffer, new InetSocketAddress("127.0.0.1", 8989));
10     }
11 }
 1 /**
 2  *NIO 伺服器端
 3  * Created by Kevin on 2017/12/18.
 4  */
 5 public class Server {
 6     public static void main(String[] args) throws Exception{
 7         DatagramChannel channel = DatagramChannel.open();
 8         channel.socket().bind(new InetSocketAddress("127.0.0.1", 8989));
 9         ByteBuffer buffer = ByteBuffer.allocate(1024);
10         channel.receive(buffer);    //伺服器端沒有收到來自客戶端的數據,會在這裡和BIO Socket一樣被阻塞
11         System.out.println(new String(buffer.array()));
12     }
13 }

  對於NIO Socket的伺服器端第10行可能會感到疑惑,既然是非阻塞的那麼為什麼在這個地方還是被阻塞了呢?在未收到客戶端的數據時為什麼還是被阻塞掛起了呢?這就需要用開頭提到的這是1個客戶端對應1個伺服器端的場景,BIO和NIO並無明顯區別,對於BIO或許更有優勢,因為它的API相對來說更簡單一些。而如果是多個客戶端,如果使用NIO,伺服器端會利用Selector(選擇器)來選擇準備好了的數據,而不會想此例一樣一直等待一個客戶端傳輸數據。接下來就是對Selector選擇器的進一步認識。

Selector

  看到這裡對於NIO似乎還只有一個認識,API變得負責了,莫名其妙地從“流”的概念轉換為了“通道”“+“緩衝區”,並且似乎和BIO並無多大區別。要我說,最大的區別和改進莫過於徹底理解NIO中的Selector(選擇器)。 在《Java IO(2)阻塞式輸入輸出(BIO)》一文的末尾提到了在伺服器端利用線程來處理數據以便使得程式能擁有更大的吞吐量,這種利用新開一個線程來處理接收到的數據不失為一種常用的計策。但是,在程式中,我個人認為還是要謹慎使用多線程,畢竟線程的上下文切換是有一定的開銷的,況且線程如果過多還有可能造成Java虛擬機的棧溢出。Selector選擇器的出現就可以使用1個線程來管理。

  上面的示常式序都只有一個通道,也就是說同時只會讀取或寫入一個文件,如果現在有多個客戶端,此時也就有多個通道,Selector選擇器將會選擇已經準備好了的通道讀取數據。

  要使用Selector選擇器,免不了大致會經過以下幾個流程:創建Selector選擇器;將Channel通道修改為非阻塞模式(只有Socket才能修改為非阻塞模式,FileChannel不能修改),並將通道註冊至Selector;Selector調用select方法對通道進行選擇。

 1 /**
 2  * NIO 客戶端,此處只有一個客戶端連接
 3  * Created by Kevin on 2017/12/24.
 4  */
 5 public class Client {
 6     public static void main(String[] args) throws Exception{
 7         DatagramChannel channel = DatagramChannel.open();
 8         ByteBuffer buffer = Charset.forName("utf-8").encode("this is Client.");
 9         channel.send(buffer, new InetSocketAddress("127.0.0.1", 8989));
10     }
11 }

  如上註釋所說,此處的示例仍然是只有一個客戶端連接,對於伺服器端的連接下麵將會使用Selector選擇器,重要部分在註釋中已說明。

 1 /**
 2  * NIO 伺服器端
 3  * Created by Kevin on 2017/12/23.
 4  */
 5 public class Server {
 6     public static void main(String[] args) throws Exception{
 7         Selector selector = Selector.open();    //Selector選擇器
 8         DatagramChannel channel = DatagramChannel.open();   //Channel通道
 9         channel.configureBlocking(false);
10         channel.bind(new InetSocketAddress("127.0.0.1", 8989));
11         channel.register(selector, SelectionKey.OP_READ);   //此通道註冊在Selector時關註是否可讀
12         while (true) {
13             selector.select();  //如果沒有一個註冊到此Selector上的通道就緒,則阻塞;反之,只要有一個通道就緒則不會被阻塞。selectNow方法不論是否有通道就緒,都不會阻塞。
14             Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();   //選擇就緒的通道
15             while (iterator.hasNext()) {
16                 SelectionKey key = iterator.next();
17                 iterator.remove();
18                 if (key.isReadable()) {     //收到客戶端數據
19                     receive(key);
20                 }
21                 if (key.isWritable()) {     //伺服器端通道準備好向客戶端發送數據
22                     send(key);
23                 }
24             }
25         }
26     }
27 
28     /**
29      * 伺服器端收到客戶端數據,並做處理
30      * @param key
31      */
32     private static void receive(SelectionKey key) throws Exception{
33         DatagramChannel channel = (DatagramChannel) key.channel();
34         ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
35         channel.receive(byteBuffer);
36         System.out.println(new String(byteBuffer.array()));
37     }
38     /**
39      * 伺服器端通道已準備好向客戶端發送數據
40      * @param key
41      */
42     private static void send(SelectionKey key) {
43 
44     }
45 }

  對於使用Selector選擇器,可以使得伺服器端只使用1個線程來管理多個連接,儘管在上面的例子沒有給出示例代碼,但這種場景在Web應用中可以說是必然的,因為對於客戶端(瀏覽器)一定是很多的,而伺服器就只有一個,此時正是NIO場景的最大使用,當然上面的例子也可以看到JDK原生NIO編程相比於BIO是略微有點複雜的,市面上也有很多優秀的第三方NIO框架——Netty、Mina均是對NIO的再次封裝,這在以後也會提到,此篇關於NIO的瞭解暫到此處,以後將會在對此有更深刻的理解時再次講解。下篇將介紹——AIO(非同步輸入輸出)。 

 

 

這是一個能給程式員加buff的公眾號 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 8月初離職,來到現在的新東家負責一個新的項目。而我最近開發的兩個webapp一直都是以Vue為主,這也是這篇文章的由來。 正文前的胡侃&一點點吐槽 在經歷了兩個公司不同的項目後,發現都存在一個很致命卻又如此相似的問題。就是領導層的決策,導致項目的開發後期加班嚴重。領導們普遍都是先DIY,然後等到項目 ...
  • 整體思路 代碼部分 2.對左屏(相機偏移的場景)重新進行渲染(暫時解決方案,對相機外的場景同樣進行渲染,存在的問題:效率太低) 有待解決的問題 相機偏移後(左屏),應當對場景(左屏)重新進行渲染。具體指 ...
  • for(var i=0;i<as.length;i++){ as[i].onmouseover=function(){this.style.backgroundColor='grey';} as[i].onmouseout=function(){this.style.backgroundColor= ...
  • <script>window.onload = function(){ var u = navigator.userAgent; var isiOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); if(isiOS){ if(location.search ...
  • spring mvc簡介與運行原理 Spring的模型-視圖-控制器(MVC)框架是圍繞一個DispatcherServlet來設計的,這個Servlet會把請求分發給各個處理器,並支持可配置的處理器映射、視圖渲染、本地化、時區與主題渲染等,甚至還能支持文件上傳。 原理.png (1) Http請求 ...
  • 題目描述 如題,給定一棵有根多叉樹,請求出指定兩個點直接最近的公共祖先。 輸入輸出格式 輸入格式: 第一行包含三個正整數N、M、S,分別表示樹的結點個數、詢問的個數和樹根結點的序號。 接下來N-1行每行包含兩個正整數x、y,表示x結點和y結點之間有一條直接連接的邊(數據保證可以構成樹)。 接下來M行 ...
  • 1507: [NOI2003]Editor Description Input 輸入文件editor.in的第一行是指令條數t,以下是需要執行的t個操作。其中: 為了使輸入文件便於閱讀,Insert操作的字元串中可能會插入一些回車符,請忽略掉它們(如果難以理解這句話,可以參考樣例)。 除了回車符之外 ...
  • 前言 樹鏈剖分是什麼? 樹鏈剖分,說白了就是一種讓你代碼不得不強行增加1k的數據結構-dms 個人理解:+1:joy: 有什麼用? 證明出題人非常毒瘤 可以非常友(bao)好(li)的解決一些樹上問題:grimacing: (友情提示:學樹鏈剖分之前請先掌握線段樹) 核心思想 樹鏈剖分的思想比較神奇 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...