JDK10都發布了,nio你了解多少?

Hmily25 6年前發布 | 51K 次閱讀 NIO JDK Java開發

前言

只有光頭才能變強

回顧前面:

本來我預想是先來回顧一下傳統的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時間");


}

}</code></pre>

我分別測試了文件大小為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系統 以塊的形式處理數據
    </li> </ul>

    NIO主要有 三個核心部分組成

    • buffer緩沖區
    • Channel管道
    • Selector選擇器

    2.1buffer緩沖區和Channel管道

    在NIO中并不是以流的方式來處理數據的,而是以buffer緩沖區和Channel管道 配合使用 來處理數據。

    簡單理解一下:

    • Channel管道比作成鐵路,buffer緩沖區比作成火車(運載著貨物)

    而我們的NIO就是 通過Channel管道運輸著存儲數據的Buffer緩沖區的來實現數據的處理 !

    • 要時刻記住:Channel不與數據打交道,它只負責運輸數據。與數據打交道的是Buffer緩沖區
      • Channel-->運輸
      • Buffer-->數據
      </li> </ul>

      相對于傳統IO而言, 流是單向的 。對于NIO而言,有了Channel管道這個概念,我們的 讀寫都是雙向 的(鐵路上的火車能從廣州去北京、自然就能從北京返還到廣州)!

      2.1.1buffer緩沖區核心要點

      我們來看看Buffer緩沖區有什么值得我們注意的地方。

      Buffer是緩沖區的抽象類:

      其中ByteBuffer是 用得最多的實現類 (在管道中讀寫字節數據)。

      拿到一個緩沖區我們往往會做什么?很簡單,就是 讀取緩沖區的數據/寫數據到緩沖區中 。所以,緩沖區的核心方法就是:

      • put()
      • get()

      Buffer類維護了4個核心變量屬性來提供 關于其所包含的數組的信息 。它們是:

      • 容量Capacity
        • 緩沖區能夠容納的數據元素的最大數量 。容量在緩沖區創建時被設定,并且永遠不能被改變。(不能被改變的原因也很簡單,底層是數組嘛)
        </li>
      • 上界Limit
        • 緩沖區里的數據的總數 ,代表了當前緩沖區中一共有多少數據。
        • </ul> </li>
        • 位置Position
          • 下一個要被讀或寫的元素的位置 。Position會自動由相應的 get( ) 和 put( ) 函數更新。
          • </ul> </li>
          • 標記Mark
            • 一個備忘位置。 用于記錄上一次讀寫的位置
            • </ul> </li> </ul>

              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());
              }</code></pre> 
              

              運行結果:

              現在 我想要從緩存區拿數據 ,怎么拿呀??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可以往下面的鏈接繼續學習~

              參考資料:

              如果文章有錯的地方歡迎指正,大家互相交流。

              來自:http://zhongfucheng.bitcron.com/post/javaji-chu/jdk10du-fa-bu-liao-nioni-liao-jie-duo-shao

               

 本文由用戶 Hmily25 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!