NIO教程 ——檢視閱讀(上)

NIO教程 ——檢視閱讀

簡介

NIO中的N可以理解為Non-blocking ,不單純是New 。

不同點:
  • 標(biāo)準(zhǔn)的IO編程接口是面向字節(jié)流和字符流的。而NIO是面向通道和緩沖區(qū)的,數(shù)據(jù)總是從通道中讀到buffer緩沖區(qū)內(nèi),或者從buffer寫入到通道中。
  • Java NIO使我們可以進行非阻塞IO操作。比如說,單線程中從通道讀取數(shù)據(jù)到buffer,同時可以繼續(xù)做別的事情,當(dāng)數(shù)據(jù)讀取到buffer中后,線程再繼續(xù)處理數(shù)據(jù)。寫數(shù)據(jù)也是一樣的。
  • NIO中有一個“slectors”的概念。selector可以檢測多個通道的事件狀態(tài)(例如:鏈接打開,數(shù)據(jù)到達)這樣單線程就可以操作多個通道的數(shù)據(jù)。

概覽

NIO包含下面3個核心的組件,Channel,Buffer和Selector組成了這個核心的API:

  • Channels ——通道
  • Buffers ——緩沖區(qū)
  • Selectors ——選擇器

通常來說NIO中的所有IO都是從Channel開始的。Channel和流有點類似。通過Channel,我們即可以從Channel把數(shù)據(jù)寫到Buffer中,也可以把數(shù)據(jù)沖Buffer寫入到Channel 。

http://tutorials.jenkov.com/images/java-nio/overview-channels-buffers.png

有很多的Channel,Buffer類型。下面列舉了主要的幾種:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

正如你看到的,這些channel基于于UDP和TCP的網(wǎng)絡(luò)IO,以及文件IO。 和這些類一起的還有其他一些比較有趣的接口,在本節(jié)中暫時不多介紹。為了簡潔起見,我們會在必要的時候引入這些概念。 下面是核心的Buffer實現(xiàn)類的列表:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

這些Buffer涵蓋了可以通過IO操作的基礎(chǔ)類型:byte,short,int,long,float,double以及characters. NIO實際上還包含一種MappedBytesBuffer,一般用于和內(nèi)存映射的文件。

選擇器允許單線程操作多個通道。如果你的程序中有大量的鏈接,同時每個鏈接的IO帶寬不高的話,這個特性將會非常有幫助。比如聊天服務(wù)器。 下面是一個單線程中Slector維護3個Channel的示意圖:

http://tutorials.jenkov.com/images/java-nio/overview-selectors.png

要使用Selector的話,我們必須把Channel注冊到Selector上,然后就可以調(diào)用Selector的select()方法。這個方法會進入阻塞,直到有一個channel的狀態(tài)符合條件。當(dāng)方法返回后,線程可以處理這些事件。

Java NIO Channel通道

Java NIO Channel通道和流非常相似,主要有以下3點區(qū)別:

  • 通道可以讀也可以寫,流一般來說是單向的(只能讀或者寫)。
  • 通道可以異步讀寫。
  • 通道總是基于緩沖區(qū)Buffer來讀寫。

Channel的實現(xiàn)

下面列出Java NIO中最重要的集中Channel的實現(xiàn):

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

FileChannel用于文件的數(shù)據(jù)讀寫。 DatagramChannel用于UDP的數(shù)據(jù)讀寫。 SocketChannel用于TCP的數(shù)據(jù)讀寫。 ServerSocketChannel允許我們監(jiān)聽TCP鏈接請求,每個請求會創(chuàng)建會一個SocketChannel.

RandomAccessFile擴展:

RandomAccessFile(隨機訪問文件)類。該類是Java語言中功能最為豐富的文件訪問類 。RandomAccessFile類支持“隨機訪問”方式,這里“隨機”是指可以跳轉(zhuǎn)到文件的任意位置處讀寫數(shù)據(jù)。在訪問一個文件的時候,不必把文件從頭讀到尾,而是希望像訪問一個數(shù)據(jù)庫一樣“隨心所欲”地訪問一個文件的某個部分,這時使用RandomAccessFile類就是最佳選擇。

四種模式:R RW RWD RWS

r 以只讀的方式打開文本,也就意味著不能用write來操作文件
rw 讀操作和寫操作都是允許的
rws 每當(dāng)進行寫操作,同步的刷新到磁盤,刷新內(nèi)容和元數(shù)據(jù)
rwd 每當(dāng)進行寫操作,同步的刷新到磁盤,刷新內(nèi)容
RandomAccessFile的用處:

1、大型文本日志類文件的快速定位獲取數(shù)據(jù):

得益于seek的巧妙設(shè)計,我認(rèn)為我們可以從超大的文本中快速定位我們的游標(biāo),例如每次存日志的時候,我們可以建立一個索引緩存,索引是日志的起始日期,value是文本的poiniter 也就是光標(biāo),這樣我們可以快速定位某一個時間段的文本內(nèi)容

2、并發(fā)讀寫

也是得益于seek的設(shè)計,我認(rèn)為多線程可以輪流操作seek控制光標(biāo)的位置,從未達到不同線程的并發(fā)寫操作。

3、更方便的獲取二進制文件

通過自帶的讀寫轉(zhuǎn)碼(readDouble、writeLong等),我認(rèn)為可以快速的完成字節(jié)碼到字符的轉(zhuǎn)換功能,對使用者來說比較友好。
RandomAccessFile參考

實例:

public class FileChannelTest {

    public static void main(String[] args) throws IOException {
        RandomAccessFile file = new RandomAccessFile("D:\\text\\1_loan.sql", "r");
        //mode只有4中,如果不是讀寫的mode或者給的不是4種中的,就會報錯。
        RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "r");
        try {
            FileChannel fileChannel = file.getChannel();
            FileChannel copyFileChannel = copyFile.getChannel();
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            int read = fileChannel.read(byteBuffer);
            while (read != -1) {
                System.out.println("read:" + read);
                //byteBuffer緩沖區(qū)切換為讀模式
                byteBuffer.flip();
                copyFileChannel.write(byteBuffer);
                //“清空”byteBuffer緩沖區(qū),以滿足后續(xù)寫入操作
                byteBuffer.clear();
                //注意,每次讀時都要返回讀后的狀態(tài)read值賦值給循環(huán)判斷體read,否則會陷入死循環(huán)true
                read = fileChannel.read(byteBuffer);
            }
        } finally {
            file.close();
            copyFile.close();
        }
    }
}

報錯:

RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "w");
//因為沒有"w"的mode
Exception in thread "main" java.lang.IllegalArgumentException: Illegal mode "w" must be one of "r", "rw", "rws", or "rwd"
    at java.io.RandomAccessFile.<init>(RandomAccessFile.java:221)
    
RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "r");
//因為沒有"w"的權(quán)限
Exception in thread "main" java.nio.channels.NonWritableChannelException
    at sun.nio.ch.FileChannelImpl.write(FileChannelImpl.java:194)
    at com.niotest.FileChannelTest.main(FileChannelTest.java:33)    

NIO Buffer緩沖區(qū)

Java NIO Buffers用于和NIO Channel交互。正如你已經(jīng)知道的,我們從channel中讀取數(shù)據(jù)到buffers里,從buffer把數(shù)據(jù)寫入到channels.

buffer本質(zhì)上就是一塊內(nèi)存區(qū),可以用來寫入數(shù)據(jù),并在稍后讀取出來。這塊內(nèi)存被NIO Buffer包裹起來,對外提供一系列的讀寫方便開發(fā)的接口。

Buffer基本用法

利用Buffer讀寫數(shù)據(jù),通常遵循四個步驟

  • 把數(shù)據(jù)寫入buffer;
  • 調(diào)用flip;
  • 從Buffer中讀取數(shù)據(jù);
  • 調(diào)用buffer.clear()或者buffer.compact()

當(dāng)寫入數(shù)據(jù)到buffer中時,buffer會記錄已經(jīng)寫入的數(shù)據(jù)大小。當(dāng)需要讀數(shù)據(jù)時,通過flip()方法把buffer從寫模式調(diào)整為讀模式;在讀模式下,可以讀取所有已經(jīng)寫入的數(shù)據(jù)。

當(dāng)讀取完數(shù)據(jù)后,需要清空buffer,以滿足后續(xù)寫入操作。清空buffer有兩種方式:調(diào)用clear()或compact()方法。clear會清空整個buffer,compact則只清空已讀取的數(shù)據(jù),未被讀取的數(shù)據(jù)會被移動到buffer的開始位置,寫入位置則緊跟著未讀數(shù)據(jù)之后。

Buffer的容量,位置,上限(Buffer Capacity, Position and Limit)

buffer緩沖區(qū)實質(zhì)上就是一塊內(nèi)存,用于寫入數(shù)據(jù),也供后續(xù)再次讀取數(shù)據(jù)。這塊內(nèi)存被NIO Buffer管理,并提供一系列的方法用于更簡單的操作這塊內(nèi)存。

一個Buffer有三個屬性是必須掌握的,分別是:

  • capacity容量
  • position位置
  • limit限制

position和limit的具體含義取決于當(dāng)前buffer的模式。capacity在兩種模式下都表示容量。

下面有張示例圖,描訴了不同模式下position和limit的含義:

buffers-modes.png
容量(Capacity)

作為一塊內(nèi)存,buffer有一個固定的大小,叫做capacity容量。也就是最多只能寫入容量值的字節(jié),整形等數(shù)據(jù)。一旦buffer寫滿了就需要清空已讀數(shù)據(jù)以便下次繼續(xù)寫入新的數(shù)據(jù)。

位置(Position)

當(dāng)寫入數(shù)據(jù)到Buffer的時候需要中一個確定的位置開始,默認(rèn)初始化時這個位置position為0,一旦寫入了數(shù)據(jù)比如一個字節(jié),整形數(shù)據(jù),那么position的值就會指向數(shù)據(jù)之后的一個單元,position最大可以到capacity-1.

當(dāng)從Buffer讀取數(shù)據(jù)時,也需要從一個確定的位置開始。buffer從寫入模式變?yōu)樽x取模式時,position會歸零,每次讀取后,position向后移動。

上限(Limit)

寫模式,limit的含義是我們所能寫入的最大數(shù)據(jù)量。它等同于buffer的容量。

一旦切換到讀模式limit則代表我們所能讀取的最大數(shù)據(jù)量,他的值等同于寫模式下position的位置。

數(shù)據(jù)讀取的上限時buffer中已有的數(shù)據(jù),也就是limit的位置(原寫模式下position所指的位置)。

Buffer Types

Java NIO有如下具體的Buffer類型:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

正如你看到的,Buffer的類型代表了不同數(shù)據(jù)類型,換句話說,Buffer中的數(shù)據(jù)可以是上述的基本類型;

分配一個Buffer(Allocating a Buffer)

為了獲取一個Buffer對象,你必須先分配。每個Buffer實現(xiàn)類都有一個allocate()方法用于分配內(nèi)存

    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    CharBuffer charBuffer = CharBuffer.allocate(48);

寫入數(shù)據(jù)到Buffer(Writing Data to a Buffer)

寫數(shù)據(jù)到Buffer有兩種方法:

  • 從Channel中寫數(shù)據(jù)到Buffer
  • 手動寫數(shù)據(jù)到Buffer,調(diào)用put方法
//從Channel中寫數(shù)據(jù)到Buffer
int read = fileChannel.read(byteBuffer);
//調(diào)用put方法寫
buf.put(3);  
//把數(shù)據(jù)寫到特定的位置
public ByteBuffer put(int i, byte x);
//把一個具體類型數(shù)據(jù)寫入buffer
public ByteBuffer putInt(int x);

flip()——翻轉(zhuǎn)

flip()方法可以把Buffer從寫模式切換到讀模式。調(diào)用flip方法會把position歸零,并設(shè)置limit為之前的position的值。 也就是說,現(xiàn)在position代表的是讀取位置,limit標(biāo)示的是已寫入的數(shù)據(jù)位置。

從Buffer讀取數(shù)據(jù)(Reading Data from a Buffer)

從Buffer讀數(shù)據(jù)也有兩種方式。

  • 從buffer讀數(shù)據(jù)到channel。
  • 從buffer直接讀取數(shù)據(jù),調(diào)用get方法。
//讀取數(shù)據(jù)到channel的例子:
int bytesWritten = inChannel.write(buf);
//調(diào)用get讀取數(shù)據(jù)的例子:
byte aByte = buf.get(); 

rewind()——倒帶

Buffer.rewind()方法將position置為0,這樣我們可以重復(fù)讀取buffer中的數(shù)據(jù)。limit保持不變。

clear() and compact()

一旦我們從buffer中讀取完數(shù)據(jù),需要復(fù)用buffer為下次寫數(shù)據(jù)做準(zhǔn)備。只需要調(diào)用clear或compact方法。

clear方法會重置position為0,limit為capacity,也就是整個Buffer清空。實際上Buffer中數(shù)據(jù)并沒有清空,我們只是把標(biāo)記為修改了。(重新寫入的時候這些存在的數(shù)據(jù)就會被新的數(shù)據(jù)覆蓋)

如果Buffer還有一些數(shù)據(jù)沒有讀取完,調(diào)用clear就會導(dǎo)致這部分?jǐn)?shù)據(jù)被“遺忘”,因為我們沒有標(biāo)記這部分?jǐn)?shù)據(jù)未讀。

針對這種情況,如果需要保留未讀數(shù)據(jù),那么可以使用compact()。 因此compact()和clear()的區(qū)別就在于對未讀數(shù)據(jù)的處理,是保留這部分?jǐn)?shù)據(jù)還是一起清空。

mark() and reset()

通過mark方法可以標(biāo)記當(dāng)前的position,通過reset來恢復(fù)mark的位置,這個非常像canva的save和restore:

buffer.mark();

//call buffer.get() a couple of times, e.g. during parsing.

buffer.reset();  //set position back to mark.

equals() and compareTo()

可以用eqauls和compareTo比較兩個buffer

equals()

判斷兩個buffer相對,需滿足:

  • 類型相同
  • buffer中剩余字節(jié)數(shù)相同
  • 所有剩余字節(jié)相等

從上面的三個條件可以看出,equals只比較buffer中的部分內(nèi)容,并不會去比較每一個元素。

compareTo()

compareTo也是比較buffer中的剩余元素,只不過這個方法適用于比較排序的:

NIO Scatter (分散)/ Gather(聚集)

——分散讀和聚集寫的場景。

Java NIO發(fā)布時內(nèi)置了對scatter / gather的支持。scatter / gather是通過通道讀寫數(shù)據(jù)的兩個概念。

Scattering read指的是從通道讀取的操作能把數(shù)據(jù)寫入多個buffer,也就是scatters代表了數(shù)據(jù)從一個channel到多個buffer的過程。

gathering write則正好相反,表示的是從多個buffer把數(shù)據(jù)寫入到一個channel中。

Scatter/gather在有些場景下會非常有用,比如需要處理多份分開傳輸?shù)臄?shù)據(jù)。舉例來說,假設(shè)一個消息包含了header和body,我們可能會把header和body保存在不同獨立buffer中,這種分開處理header與body的做法會使開發(fā)更簡明。

Scattering Reads

"scattering read"是把數(shù)據(jù)從單個Channel寫入到多個buffer,下面是示意圖:

scatter.png

觀察代碼可以發(fā)現(xiàn),我們把多個buffer寫在了一個數(shù)組中,然后把數(shù)組傳遞給channel.read()方法。read()方法內(nèi)部會負(fù)責(zé)把數(shù)據(jù)按順序?qū)戇M傳入的buffer數(shù)組內(nèi)。一個buffer寫滿后,接著寫到下一個buffer中。

實際上,scattering read內(nèi)部必須寫滿一個buffer后才會向后移動到下一個buffer,因此這并不適合消息大小會動態(tài)改變的部分,也就是說,如果你有一個header和body,并且header有一個固定的大小(比如128字節(jié)),這種情形下可以正常工作。

gathering Writes

"gathering write"把多個buffer的數(shù)據(jù)寫入到同一個channel中.

gather.png

傳入一個buffer數(shù)組給write,內(nèi)部會按順序?qū)?shù)組內(nèi)的內(nèi)容寫進channel,這里需要注意,寫入的時候針對的是buffer中position到limit之間的數(shù)據(jù)。也就是如果buffer的容量是128字節(jié),但它只包含了58字節(jié)數(shù)據(jù),那么寫入的時候只有58字節(jié)會真正寫入。因此gathering write是可以適用于可變大小的message的,這和scattering reads不同。

NIO Channel to Channel Transfers通道傳輸接口

在Java NIO中如果一個channel是FileChannel類型的,那么他可以直接把數(shù)據(jù)傳輸?shù)搅硪粋€channel。這個特性得益于FileChannel包含的transferTo和transferFrom兩個方法。

transferFrom()——目標(biāo)channel用,參數(shù)為源數(shù)據(jù)channel。

transferFrom的參數(shù)position和count表示目標(biāo)文件的寫入位置和最多寫入的數(shù)據(jù)量。如果通道源的數(shù)據(jù)小于count那么就傳實際有的數(shù)據(jù)量。 另外,有些SocketChannel的實現(xiàn)在傳輸時只會傳輸哪些處于就緒狀態(tài)的數(shù)據(jù),即使SocketChannel后續(xù)會有更多可用數(shù)據(jù)。因此,這個傳輸過程可能不會傳輸整個的數(shù)據(jù)。

transferTo()——源數(shù)據(jù)用,參數(shù)為目標(biāo)channel

SocketChannel的問題也存在于transferTo.SocketChannel的實現(xiàn)可能只在發(fā)送的buffer填充滿后才發(fā)送,并結(jié)束。

實例:

public class ChannelTransferTest {

    public static void main(String[] args) throws IOException {
        RandomAccessFile fromfile = new RandomAccessFile("D:\\text\\1_loan.sql", "rw");
        //mode只有4中,如果不是讀寫的mode或者給的不是4種中的,就會報錯。
        RandomAccessFile toFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "rw");

        FileChannel fromfileChannel = fromfile.getChannel();
        FileChannel toFileChannel = toFile.getChannel();
        //==========================transferTo=================================
        //transferTo方法把fromfileChannel數(shù)據(jù)傳輸?shù)搅硪粋€toFileChannel
        //long transferSize = fromfileChannel.transferTo(0, fromfileChannel.size(), toFileChannel);
        //System.out.println(transferSize);

        //=============================transferFrom==============================
        //把數(shù)據(jù)從通道源傳輸?shù)絫oFileChannel,相比通過buffer讀寫更加的便捷
        long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size());
        //參數(shù)position和count表示目標(biāo)文件的寫入位置和最多寫入的數(shù)據(jù)量
        //long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size()-1000);
        //如果通道源的數(shù)據(jù)小于count那么就傳實際有的數(shù)據(jù)量。
        //long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size()+1000);
        System.out.println(transferSize1);
    }
}

NIO Selector選擇器

Selector是Java NIO中的一個組件,用于檢查一個或多個NIO Channel的狀態(tài)是否處于可讀、可寫。如此可以實現(xiàn)單線程管理多個channels,也就是可以管理多個網(wǎng)絡(luò)鏈接

為什么使用Selector

用單線程處理多個channels的好處是我需要更少的線程來處理channel。實際上,你甚至可以用一個線程來處理所有的channels。從操作系統(tǒng)的角度來看,切換線程開銷是比較昂貴的,并且每個線程都需要占用系統(tǒng)資源,因此暫用線程越少越好。

需要留意的是,現(xiàn)代操作系統(tǒng)和CPU在多任務(wù)處理上已經(jīng)變得越來越好,所以多線程帶來的影響也越來越小。如果一個CPU是多核的,如果不執(zhí)行多任務(wù)反而是浪費了機器的性能。不過這些設(shè)計討論是另外的話題了。簡而言之,通過Selector我們可以實現(xiàn)單線程操作多個channel。

image.png

創(chuàng)建Selector

創(chuàng)建一個Selector可以通過Selector.open()方法

Selector selector = Selector.open();

注冊Channel到Selector上

先把Channel注冊到Selector上,這個操作使用SelectableChannel的register()。SocketChannel等都有繼承此抽象類。

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

Channel必須是非阻塞的。所以FileChannel不適用Selector,因為FileChannel不能切換為非阻塞模式。Socket channel可以正常使用。

注意register的第二個參數(shù),這個參數(shù)是一個“關(guān)注集合”,代表我們關(guān)注的channel狀態(tài),有四種基礎(chǔ)類型可供監(jiān)聽:

  1. Connect——連接就緒(連接成功后)
  2. Accept——可連接就緒(接受請求連接時)
  3. Read——讀就緒
  4. Write——寫就緒

一個channel觸發(fā)了一個事件也可視作該事件處于就緒狀態(tài)。因此當(dāng)channel與server連接成功后,那么就是“連接就緒”狀態(tài)。server channel接收請求連接時處于“可連接就緒”狀態(tài)。channel有數(shù)據(jù)可讀時處于“讀就緒”狀態(tài)。channel可以進行數(shù)據(jù)寫入時處于“寫就緒”狀態(tài)。

上述的四種就緒狀態(tài)用SelectionKey中的常量表示如下:

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

如果對多個事件感興趣可利用位的或運算結(jié)合多個常量,比如:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;  

SelectionKey's

在上一小節(jié)中,我們利用register方法把Channel注冊到了Selectors上,這個方法的返回值是SelectionKeys,這個返回的對象包含了一些比較有價值的屬性:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object (optional)
Interest Set

這個“關(guān)注集合”實際上就是我們希望處理的事件的集合,它的值就是注冊時傳入的參數(shù),我們可以用按為與運算把每個事件取出來:

int interestSet = selectionKey.interestOps();

boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE; 
Ready Set

"就緒集合"中的值是當(dāng)前channel處于就緒的值,一般來說在調(diào)用了select方法后都會需要用到就緒狀態(tài)

int readySet = selectionKey.readyOps();

從“就緒集合”中取值的操作類似于“關(guān)注集合”的操作,當(dāng)然還有更簡單的方法,SelectionKey提供了一系列返回值為boolean的的方法:

selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Channel + Selector

從SelectionKey操作Channel和Selector非常簡單:

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector();  
Attaching Objects

我們可以給一個SelectionKey附加一個Object,這樣做一方面可以方便我們識別某個特定的channel,同時也增加了channel相關(guān)的附加信息。例如,可以把用于channel的buffer附加到SelectionKey上:

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

附加對象的操作也可以在register的時候就執(zhí)行:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

從Selector中選擇channel

一旦我們向Selector注冊了一個或多個channel后,就可以調(diào)用select來獲取channel。select方法會返回所有處于就緒狀態(tài)的channel。 select方法具體如下:

  • int select()
  • int select(long timeout)
  • int selectNow()

select()方法在返回channel之前處于阻塞狀態(tài)。 select(long timeout)和select做的事一樣,不過他的阻塞有一個超時限制。

selectNow()不會阻塞,根據(jù)當(dāng)前狀態(tài)立刻返回合適的channel。

select()方法的返回值是一個int整形,代表有多少channel處于就緒了。也就是自上一次select后有多少channel進入就緒。舉例來說,假設(shè)第一次調(diào)用select時正好有一個channel就緒,那么返回值是1,并且對這個channel做任何處理,接著再次調(diào)用select,此時恰好又有一個新的channel就緒,那么返回值還是1,現(xiàn)在我們一共有兩個channel處于就緒,但是在每次調(diào)用select時只有一個channel是就緒的。

selectedKeys()

在調(diào)用select并返回了有channel就緒之后,可以通過選中的key集合來獲取channel,這個操作通過調(diào)用selectedKeys()方法:

Set<SelectionKey> selectedKeys = selector.selectedKeys();    

還記得在register時的操作吧,我們register后的返回值就是SelectionKey實例,也就是我們現(xiàn)在通過selectedKeys()方法所返回的SelectionKey。

遍歷這些SelectionKey可以通過如下方法:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while(keyIterator.hasNext()) {

    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // a connection was accepted by a ServerSocketChannel.

    } else if (key.isConnectable()) {
        // a connection was established with a remote server.

    } else if (key.isReadable()) {
        // a channel is ready for reading

    } else if (key.isWritable()) {
        // a channel is ready for writing
    }

    keyIterator.remove();
}

上述循環(huán)會迭代key集合,針對每個key我們單獨判斷他是處于何種就緒狀態(tài)。

注意:keyIterater.remove()方法的調(diào)用,Selector本身并不會移除SelectionKey對象,這個操作需要我們手動執(zhí)行。當(dāng)下次channel處于就緒是,Selector仍然會把這些key再次加入進來。

SelectionKey.channel返回的channel實例需要強轉(zhuǎn)為我們實際使用的具體的channel類型,例如ServerSocketChannel或SocketChannel.

wakeUp()

由于調(diào)用select而被阻塞的線程,可以通過調(diào)用Selector.wakeup()來喚醒即便此時已然沒有channel處于就緒狀態(tài)。具體操作是,在另外一個線程調(diào)用wakeup,被阻塞與select方法的線程就會立刻返回。

close()

當(dāng)操作Selector完畢后,需要調(diào)用close方法。close的調(diào)用會關(guān)閉Selector并使相關(guān)的SelectionKey都無效。channel本身不會被關(guān)閉。

示例:首先打開一個Selector,然后注冊channel,最后監(jiān)聽Selector的狀態(tài)。

public class NIOServer {

    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將通道注冊到選擇器上,指定接收“監(jiān)聽通道”事件
        server.register(selector, SelectionKey.OP_ACCEPT);

        // 5. 輪訓(xùn)地獲取選擇器上已“就緒”的事件--->只要select()>0,說明已就緒
        while (selector.select() > 0) {
            // 6. 獲取當(dāng)前選擇器所有注冊的“選擇鍵”(已就緒的監(jiān)聽事件)
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

            // 7. 獲取已“就緒”的事件,(不同的事件做不同的事)
            while (iterator.hasNext()) {

                SelectionKey selectionKey = iterator.next();

                // 接收事件就緒
                if (selectionKey.isAcceptable()) {

                    // 8. 獲取客戶端的鏈接
                    SocketChannel client = server.accept();

                    // 8.1 切換成非阻塞狀態(tài)
                    client.configureBlocking(false);

                    // 8.2 注冊到選擇器上-->拿到客戶端的連接為了讀取通道的數(shù)據(jù)(監(jiān)聽讀就緒事件)
                    client.register(selector, SelectionKey.OP_READ);

                } else if (selectionKey.isReadable()) { // 讀事件就緒

                    // 9. 獲取當(dāng)前選擇器讀就緒狀態(tài)的通道
                    SocketChannel client = (SocketChannel) selectionKey.channel();

                    // 9.1讀取數(shù)據(jù)
                    ByteBuffer buffer = ByteBuffer.allocate(1024);

                    // 9.2得到文件通道,將客戶端傳遞過來的圖片寫到本地項目下(寫模式、沒有則創(chuàng)建)
                    FileChannel outChannel = FileChannel.open(Paths.get("2_loan.sql"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);

                    while (client.read(buffer) > 0) {
                        // 在讀之前都要切換成讀模式
                        buffer.flip();

                        outChannel.write(buffer);

                        // 讀完切換成寫模式,能讓管道繼續(xù)讀取文件的數(shù)據(jù)
                        buffer.clear();
                    }
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    byteBuffer.put("yeah,i know,i got your message!".getBytes());
                    byteBuffer.flip();
                    client.write(byteBuffer);
                }
                // 10. 取消選擇鍵(已經(jīng)處理過的事件,就應(yīng)該取消掉了)
                iterator.remove();
            }
        }
    }
}
public class NIOClientTwo {

    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將通道注冊到選擇器中,獲取服務(wù)端返回的數(shù)據(jù)
        socketChannel.register(selector, SelectionKey.OP_READ);

        // 2. 發(fā)送一張圖片給服務(wù)端吧
        FileChannel fileChannel = FileChannel.open(Paths.get("D:\\text\\1_loan.sql"), StandardOpenOption.READ);

        // 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是與數(shù)據(jù)打交道的呢
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        // 4.讀取本地文件(圖片),發(fā)送到服務(wù)器
        while (fileChannel.read(buffer) != -1) {

            // 在讀之前都要切換成讀模式
            buffer.flip();

            socketChannel.write(buffer);

            // 讀完切換成寫模式,能讓管道繼續(xù)讀取文件的數(shù)據(jù)
            buffer.clear();
        }


        // 5. 輪訓(xùn)地獲取選擇器上已“就緒”的事件--->只要select()>0,說明已就緒
        while (selector.select() > 0) {
            // 6. 獲取當(dāng)前選擇器所有注冊的“選擇鍵”(已就緒的監(jiān)聽事件)
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

            // 7. 獲取已“就緒”的事件,(不同的事件做不同的事)
            while (iterator.hasNext()) {

                SelectionKey selectionKey = iterator.next();

                // 8. 讀事件就緒
                if (selectionKey.isReadable()) {

                    // 8.1得到對應(yīng)的通道
                    SocketChannel channel = (SocketChannel) selectionKey.channel();

                    ByteBuffer responseBuffer = ByteBuffer.allocate(1024);

                    // 9. 知道服務(wù)端要返回響應(yīng)的數(shù)據(jù)給客戶端,客戶端在這里接收
                    int readBytes = channel.read(responseBuffer);

                    if (readBytes > 0) {
                        // 切換讀模式
                        responseBuffer.flip();
                        System.out.println(new String(responseBuffer.array(), 0, readBytes));
                    }
                }
                // 10. 取消選擇鍵(已經(jīng)處理過的事件,就應(yīng)該取消掉了)
                iterator.remove();
            }
        }
    }
}

NIO FileChannel文件通道

Java NIO中的FileChannel是用于連接文件的通道。通過文件通道可以讀、寫文件的數(shù)據(jù)。Java NIO的FileChannel是相對標(biāo)準(zhǔn)Java IO API的可選接口。

FileChannel不可以設(shè)置為非阻塞模式,他只能在阻塞模式下運行。

打開文件通道

在使用FileChannel前必須打開通道,打開一個文件通道需要通過輸入/輸出流或者RandomAccessFile,下面是通過RandomAccessFile打開文件通道的案例:

RandomAccessFile aFile = new RandomAccessFile("D:\text\1_loan.sql", "rw");
FileChannel inChannel = aFile.getChannel();
從文件通道內(nèi)讀取數(shù)據(jù)

讀取文件通道的數(shù)據(jù)可以通過read方法:

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);

首先開辟一個Buffer,從通道中讀取的數(shù)據(jù)會寫入Buffer內(nèi)。接著就可以調(diào)用read方法,read的返回值代表有多少字節(jié)被寫入了Buffer,返回-1則表示已經(jīng)讀取到文件結(jié)尾了。

向文件通道寫入數(shù)據(jù)

寫數(shù)據(jù)用write方法,入?yún)⑹荁uffer:

String newData = "New String to write to file..." + System.currentTimeMillis();

ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());

buf.flip();

while(buf.hasRemaining()) {
    channel.write(buf);
}

注意這里的write調(diào)用寫在了wihle循環(huán)匯總,這是因為write不能保證有多少數(shù)據(jù)真實被寫入,因此需要循環(huán)寫入直到?jīng)]有更多數(shù)據(jù)。

關(guān)閉通道

操作完畢后,需要把通道關(guān)閉:

channel.close();    

FileChannel Position

當(dāng)操作FileChannel的時候讀和寫都是基于特定起始位置的(position),獲取當(dāng)前的位置可以用FileChannel的position()方法,設(shè)置當(dāng)前位置可以用帶參數(shù)的position(long pos)方法。

//獲取當(dāng)前的位置
long position = fileChannel.position();
//設(shè)置當(dāng)前位置為pos +123
fileChannel.position(pos +123);

假設(shè)我們把當(dāng)前位置設(shè)置為文件結(jié)尾之后,那么當(dāng)我們視圖從通道中讀取數(shù)據(jù)時就會發(fā)現(xiàn)返回值是-1,表示已經(jīng)到達文件結(jié)尾了。 如果把當(dāng)前位置設(shè)置為文件結(jié)尾之后,再向通道中寫入數(shù)據(jù),文件會自動擴展以便寫入數(shù)據(jù),但是這樣會導(dǎo)致文件中出現(xiàn)類似空洞,即文件的一些位置是沒有數(shù)據(jù)的。

FileChannel Size

size()方法可以返回FileChannel對應(yīng)的文件的文件大?。?/p>

long fileSize = channel.size();    

FileChannel Truncate

利用truncate方法可以截取指定長度的文件

FileChannel truncateFile = fileChannel.truncate(1024);

FileChannel Force

force方法會把所有未寫磁盤的數(shù)據(jù)都強制寫入磁盤。這是因為在操作系統(tǒng)中出于性能考慮回把數(shù)據(jù)放入緩沖區(qū),所以不能保證數(shù)據(jù)在調(diào)用write寫入文件通道后就及時寫到磁盤上了,除非手動調(diào)用force方法。 force方法需要一個布爾參數(shù),代表是否把meta data也一并強制寫入。

channel.force(true);

NIO SocketChannel套接字通道

在Java NIO體系中,SocketChannel是用于TCP網(wǎng)絡(luò)連接的套接字接口,相當(dāng)于Java網(wǎng)絡(luò)編程中的Socket套接字接口。創(chuàng)建SocketChannel主要有兩種方式,如下:

  1. 打開一個SocketChannel并連接網(wǎng)絡(luò)上的一臺服務(wù)器。
  2. 當(dāng)ServerSocketChannel接收到一個連接請求時,會創(chuàng)建一個SocketChannel。
建立一個SocketChannel連接

打開一個SocketChannel可以這樣操作:

SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://www.google.com", 80));  
關(guān)閉一個SocketChannel連接

關(guān)閉一個SocketChannel只需要調(diào)用他的close方法,如下:

socketChannel.close();
從SocketChannel中讀數(shù)據(jù)

從一個SocketChannel連接中讀取數(shù)據(jù),可以通過read()方法,如下:

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = socketChannel.read(buf);

首先需要開辟一個Buffer。從SocketChannel中讀取的數(shù)據(jù)將放到Buffer中。

接下來就是調(diào)用SocketChannel的read()方法.這個read()會把通道中的數(shù)據(jù)讀到Buffer中。read()方法的返回值是一個int數(shù)據(jù),代表此次有多少字節(jié)的數(shù)據(jù)被寫入了Buffer中。如果返回的是-1,那么意味著通道內(nèi)的數(shù)據(jù)已經(jīng)讀取完畢,到底了(鏈接關(guān)閉)。

向SocketChannel寫數(shù)據(jù)

向SocketChannel中寫入數(shù)據(jù)是通過write()方法,write也需要一個Buffer作為參數(shù)。下面看一下具體的示例:

String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
    channel.write(buf);
}

非阻塞模式

我們可以把SocketChannel設(shè)置為non-blocking(非阻塞)模式。這樣的話在調(diào)用connect(), read(), write()時都是異步的。

socketChannel.configureBlocking(false);
connect()

如果我們設(shè)置了一個SocketChannel是非阻塞的,那么調(diào)用connect()后,方法會在鏈接建立前就直接返回。為了檢查當(dāng)前鏈接是否建立成功,我們可以調(diào)用finishConnect(),如下:

socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://www.google.com", 80));

while(! socketChannel.finishConnect() ){
    //wait, or do something else...    
}
write()

在非阻塞模式下,調(diào)用write()方法不能確保方法返回后寫入操作一定得到了執(zhí)行。因此我們需要把write()調(diào)用放到循環(huán)內(nèi)。這和前面在講write()時是一樣的,此處就不在代碼演示。

read()

在非阻塞模式下,調(diào)用read()方法也不能確保方法返回后,確實讀到了數(shù)據(jù)。因此我們需要自己檢查的整型返回值,這個返回值會告訴我們實際讀取了多少字節(jié)的數(shù)據(jù)。

Selector結(jié)合非阻塞模式

SocketChannel的非阻塞模式可以和Selector很好的協(xié)同工作。把一個或多個SocketChannel注冊到一個Selector后,我們可以通過Selector指導(dǎo)哪些channels通道是處于可讀,可寫等等狀態(tài)的。

NIO ServerSocketChannel服務(wù)端套接字通道

在Java NIO中,ServerSocketChannel是用于監(jiān)聽TCP鏈接請求的通道,正如Java網(wǎng)絡(luò)編程中的ServerSocket一樣。

ServerSocketChannel實現(xiàn)類位于java.nio.channels包下面。

void test() throws IOException {
    //打開一個ServerSocketChannel我們需要調(diào)用他的open()方法
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.socket().bind(new InetSocketAddress(9999));
    while(true) {
        SocketChannel socketChannel = serverSocketChannel.accept();
        //do something with socketChannel...
        if (socketChannel.isConnected()) {
            break;
        }
    }
    //關(guān)閉一個ServerSocketChannel我們需要調(diào)用close()方法
    serverSocketChannel.close();
}

監(jiān)聽鏈接

通過調(diào)用accept()方法,我們就開始監(jiān)聽端口上的請求連接。當(dāng)accept()返回時,他會返回一個SocketChannel連接實例,實際上accept()是阻塞操作,他會阻塞帶去線程知道返回一個連接; 很多時候我們是不滿足于監(jiān)聽一個連接的,因此我們會把accept()的調(diào)用放到循環(huán)中,就像這樣:

while(true){
    SocketChannel socketChannel = serverSocketChannel.accept();
    //do something with socketChannel...
}

當(dāng)然我們可以在循環(huán)體內(nèi)加上合適的中斷邏輯,而不是單純的在while循環(huán)中寫true,以此來結(jié)束循環(huán)監(jiān)聽;

非阻塞模式

實際上ServerSocketChannel是可以設(shè)置為非阻塞模式的。在非阻塞模式下,調(diào)用accept()函數(shù)會立刻返回,如果當(dāng)前沒有請求的鏈接,那么返回值為空null。因此我們需要手動檢查返回的SocketChannel是否為空,例如:

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

serverSocketChannel.socket().bind(new InetSocketAddress(9999));
//設(shè)置為非阻塞模式
serverSocketChannel.configureBlocking(false);
while(true){
    SocketChannel socketChannel = serverSocketChannel.accept();
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Non-blocking Server非阻塞服務(wù)器 非阻塞服務(wù)器代碼[https://github.com/jje...
    卡斯特梅的雨傘閱讀 704評論 0 1
  • Java NIO(New IO)是從Java 1.4版本開始引入的一個新的IO API,可以替代標(biāo)準(zhǔn)的Java I...
    zhisheng_blog閱讀 1,200評論 0 7
  • Java NIO(New IO)是從Java 1.4版本開始引入的一個新的IO API,可以替代標(biāo)準(zhǔn)的Java I...
    JackChen1024閱讀 7,969評論 1 143
  • Java NIO(New IO)是從Java 1.4版本開始引入的一個新的IO API,可以替代標(biāo)準(zhǔn)的Java I...
    編碼前線閱讀 2,355評論 0 5
  • 16宿命:用概率思維提高你的勝算 以前的我是風(fēng)險厭惡者,不喜歡去冒險,但是人生放棄了冒險,也就放棄了無數(shù)的可能。 ...
    yichen大刀閱讀 8,244評論 0 4

友情鏈接更多精彩內(nèi)容