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

Java3y發表於2018-05-14

前言

只有光頭才能變強

回顧前面:

本來我預想是先來回顧一下傳統的IO模式的,將傳統的IO模式的相關類理清楚(因為IO的類很多)。

但是,發現在整理的過程已經有很多優秀的文章了,而我自己來整理的話可能達不到他們的水平。並且傳統的IO估計大家都會用,而NIO就不一定了

下面我就貼幾張我認為整理比較優秀的思維導圖(下面會給出圖片來源地址,大家可前往閱讀):

按操作方式分類結構圖:

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

位元組流的輸入和輸出對照圖:

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

字元流的輸入和輸出對照圖:

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

按操作物件分類結構圖:

JDK10都發布了,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/pselectfd這些關鍵字,沒有相關基礎的人看起來簡直是天書
  • 這就導致了在初學時認為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的:

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

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

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

1.1為什麼要使用NIO

可以看到使用過NIO重新實現過的傳統IO根本不虛,在大檔案下效果還比NIO要好(當然了,個人幾次的測試,或許不是很準)

  • 而NIO要有一定的學習成本,也沒有傳統IO那麼好理解。

那這意味著我們可以不使用/學習NIO了嗎

答案是否定的,IO操作往往在兩個場景下會用到:

  • 檔案IO
  • 網路IO

NIO的魅力:在網路中使用IO就可以體現出來了

  • 後面會說到網路中使用NIO,不急哈~

二、NIO快速入門

首先我們來看看IO和NIO的區別

JDK10都發布了,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是緩衝區的抽象類:

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

其中ByteBuffer是用得最多的實現類(在管道中讀寫位元組資料)。

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

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

  • put()
  • get()

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

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

Buffer類維護了4個核心變數屬性來提供關於其所包含的陣列的資訊。它們是:

  • 容量Capacity
    • 緩衝區能夠容納的資料元素的最大數量。容量在緩衝區建立時被設定,並且永遠不能被改變。(不能被改變的原因也很簡單,底層是陣列嘛)
  • 上界Limit
    • 緩衝區裡的資料的總數,代表了當前緩衝區中一共有多少資料。
  • 位置Position
    • 下一個要被讀或寫的元素的位置。Position會自動由相應的 get( )put( )函式更新。
  • 標記Mark
    • 一個備忘位置。用於記錄上一次讀寫的位置

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

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());
    }
複製程式碼

執行結果:

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

現在我想要從快取區拿資料,怎麼拿呀??NIO給了我們一個flip()方法。這個方法可以改動position和limit的位置

還是上面的程式碼,我們flip()一下後,再看看4個核心屬性的值會發生什麼變化:

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

很明顯的是:

  • limit變成了position的位置了
  • 而position變成了0

看到這裡的同學可能就會想到了:當呼叫完filp()時:limit是限制讀到哪裡,而position是從哪裡讀

一般我們稱filp()為**“切換成讀模式”**

  • 每當要從快取區的時候讀取資料時,就呼叫filp()“切換成讀模式”

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

切換成讀模式之後,我們就可以讀取緩衝區的資料了:


        // 建立一個limit()大小的位元組陣列(因為就只有limit這麼多個資料可讀)
        byte[] bytes = new byte[byteBuffer.limit()];

        // 將讀取的資料裝進我們的位元組陣列中
        byteBuffer.get(bytes);

        // 輸出資料
        System.out.println(new String(bytes, 0, bytes.length));

複製程式碼

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

隨後輸出一下核心變數的值看看:

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

讀完我們還想寫資料到緩衝區,那就使用clear()函式,這個函式會“清空”緩衝區:

  • 資料沒有真正被清空,只是被遺忘掉了

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

2.1.3FileChannel通道核心要點

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

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配合緩衝區實現檔案複製的功能:

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

使用記憶體對映檔案的方式實現檔案複製的功能(直接操作緩衝區):

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

通道之間通過transfer()實現資料的傳輸(直接操作緩衝區):

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

2.1.4直接與非直接緩衝區

  • 非直接緩衝區是需要經過一個:copy的階段的(從核心空間copy到使用者空間)
  • 直接緩衝區不需要經過copy階段,也可以理解成--->記憶體對映檔案,(上面的圖片也有過例子)。

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

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

使用直接緩衝區有兩種方式:

  • 緩衝區建立的時候分配的是直接緩衝區
  • 在FileChannel上呼叫map()方法,將檔案直接對映到記憶體中建立

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

2.1.5scatter和gather、字符集

這個知識點我感覺用得挺少的,不過很多教程都有說這個知識點,我也拿過來說說吧:

  • 分散讀取(scatter):將一個通道中的資料分散讀取到多個緩衝區中
  • 聚集寫入(gather):將多個緩衝區中的資料集中寫入到一個通道中

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

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

分散讀取

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

聚集寫入

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

字符集(只要編碼格式和解碼格式一致,就沒問題了)

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

三、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為例)

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

可以發現的是:當應用程式呼叫read方法時,是需要等待的--->從核心空間中找資料,再將核心空間的資料拷貝到使用者空間的。

  • 這個等待是必要的過程

下面只講解用得最多的3個I/0模型:

  • 阻塞I/O
  • 非阻塞I/O
  • I/O多路複用

3.1阻塞I/O模型

在程式(使用者)空間中呼叫recvfrom,其系統呼叫直到資料包到達且被複制到應用程式的緩衝區中或者發生錯誤時才返回,在此期間一直等待

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

3.2非阻塞I/O模型

recvfrom從應用層到核心的時候,如果沒有資料就直接返回一個EWOULDBLOCK錯誤,一般都對非阻塞I/O模型進行輪詢檢查這個狀態,看核心是不是有資料到來。

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

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;       /* 實際發生了的事件 */
};

複製程式碼

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

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

  • (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基礎繼續講解

回到我們最開始的圖:

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

NIO被叫為 no-blocking io,其實是在網路這個層次中理解的,對於FileChannel來說一樣是阻塞

我們前面也僅僅講解了FileChannel,對於我們網路通訊是還有幾個Channel的~

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

所以說:我們通常使用NIO是在網路中使用的,網上大部分討論NIO都是在網路通訊的基礎之上的!說NIO是非阻塞的NIO也是網路中體現的!

從上面的圖我們可以發現還有一個Selector選擇器這麼一個東東。從一開始我們就說過了,nio的核心要素有:

  • Buffer緩衝區
  • Channel通道
  • Selector選擇器

我們在網路中使用NIO往往是I/O模型的多路複用模型

  • Selector選擇器就可以比喻成麥當勞的廣播
  • 一個執行緒能夠管理多個Channel的狀態

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

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

複製程式碼

結果就可以將客戶端傳遞過來的圖片儲存在本地了:

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

此時服務端儲存完圖片想要告訴客戶端已經收到圖片啦:

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

客戶端接收服務端帶過來的資料:

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

如果僅僅是上面的程式碼是不行的!這個程式會阻塞起來!

  • 因為服務端不知道客戶端還有沒有資料要發過來(與剛開始不一樣,客戶端發完資料就將流關閉了,服務端可以知道客戶端沒資料發過來了),導致服務端一直在讀取客戶端發過來的資料。
  • 進而導致了阻塞!

於是客戶端在寫完資料給服務端時,顯式告訴服務端已經發完資料了!

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

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

    }
}

複製程式碼

還是剛才的需求:服務端儲存了圖片以後,告訴客戶端已經收到圖片了

在服務端上只要在後面寫些資料給客戶端就好了:

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

在客戶端上要想獲取得到服務端的資料,也需要註冊在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();
            }
        }
    }


}
複製程式碼

測試結果:

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

下面就簡單總結一下使用NIO時的要點:

  • 將Socket通道註冊到Selector中,監聽感興趣的事件
  • 當感興趣的時間就緒時,則會進去我們處理的方法進行處理
  • 每處理完一次就緒事件,刪除該選擇鍵(因為我們已經處理完了)

4.4管道和DataGramChannel

這裡我就不再講述了,最難的TCP都講了,UDP就很簡單了。

UDP:

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

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

管道:

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

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

五、總結

總的來說NIO也是一個比較重要的知識點,因為它是學習netty的基礎~

想以一篇來完全講解NIO顯然是不可能的啦,想要更加深入瞭解NIO可以往下面的連結繼續學習~

參考資料:

如果文章有錯的地方歡迎指正,大家互相交流。習慣在微信看技術文章,想要獲取更多的Java資源的同學,可以關注微信公眾號:Java3y

文章的目錄導航

相關文章