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

  在上篇《Java IO(2)阻塞式輸入輸出(BIO)》的末尾談到了什麼是阻塞式輸入輸出,經過Socket編程對其有了大體瞭解。如今再從新回顧梳理一下,對於只有一個「客戶端」和一個「服務器端」來說,服務器端須要阻塞式接收客戶端的請求,這裏的阻塞式表示服務器端的應用代碼會被掛起直到客戶端有請求過來,在高併發的應用場景有多個客戶端發起鏈接下非阻塞式IO(NIO)是不二之選(且只須要在服務器端使用1個線程來管理,並不須要多個線程來處理多個鏈接)。在現實狀況下,Tomcat、Jetty等不少Web服務器均使用了NIO技術。html

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

Channel(通道)

  在NIO中,你須要忘掉「流」這個概念,取而代之的是「通道」。舉例在網絡應用程序中有多個客戶端鏈接,此時數據傳輸的概念並非「流」而「通道」,通道與流最大的不一樣就是,通道是雙向的,而流是單向的(例如InputStream、OutputStream)。程序員

Buffer(緩衝區)

  在NIO中並非簡單的將流的概念替換爲了通道,與通道搭配的是緩衝區。在BIO的字節流中並不會使用到緩衝區,而是直接操做文件經過字節方式直接讀取,而NIO則不一樣,它會將通道中的數據讀入緩存區,或者將緩存區的數據寫入通道。編程

Selector(選擇器)

  若是使用NIO的應用程序中只有一個Channel,選擇器則是能夠不須要的,而若是有多個Channel,換言之有多個鏈接時,此時經過選擇器,在服務器端的應用程序中就只須要1個線程對多個鏈接進行管理。json

  固然從最開始就說到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的公衆號 

相關文章
相關標籤/搜索