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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...