高性能的Java通信。絕對離不開Java NIO技術(shù),現(xiàn)在主流的技術(shù)框架或中間件服務(wù)器,都使用了JavaNIO技術(shù),譬如Tomcat、Jetty. Netty.。學(xué)習(xí)和享握NIO技術(shù),已經(jīng)不是一項加分能,而是一項必備技能。不管是面試,還是實際開發(fā),作為Java的“攻城師”,都必須掌握NIO的原理和開發(fā)實踐技能。
3.1 Java NIO簡介
在1.4版本之前,JavaIO類庫是阻塞1O;從1.4版本開始,引進(jìn)了新的異步IO庫,被稱為JavaNew IO類庫,簡稱為JAVA NIO。New IO類庫的目標(biāo),就是要讓Java支持非阻塞IO?;谶@個原因,更多的人喜歡稱Java NIO為非阻塞IO (Non-Block IO),稱“老的"阻塞式Java IO為OIO(Old 10)。總體上說,NIO彌補了原來面向流的OIO同步阻塞的不足,它為標(biāo)準(zhǔn)Java代碼提供了高速的、面向緩沖區(qū)的IO.
Java NIO由以下三個核心組件組成:
- Channel (通道)
- Buffer (緩沖區(qū))
- Selector (選擇器)
如果理解了第1章的四種IO模型,大家一眼就能識別出來,Java NIO屬于第三種模型-- IO多路復(fù)用模型。當(dāng)然, Java NIO組件,提供了統(tǒng)一的API,為大家屏蔽了底層的不同操作系統(tǒng)的差異。
后面的章節(jié),我們會對以上的三個Java NIO的核心組件,展開詳細(xì)介紹。 先來看看Java的NIO和OIO的簡單對比。
3.1.1 NIO 和OIO的對比
在Java中,NIO和OIO的區(qū)別,主要體現(xiàn)在三個方面:
- (1) OIO是面向流(Stream Oriented)的,NIO 是面向緩沖區(qū)( Buffer Oriented)的。
何謂面向流,何謂面向緩沖區(qū)呢?
OIO是面向字節(jié)流或字符流的,在一般的OIO操作中,我們以流式的方式順序地從一個流(Stream)中讀取一個或多個字節(jié),因此,我們不能隨意地改變讀取指針的位置。而在NIO操作中則不同,NIO 中引入了Channel (通道)和Buffer (緩沖區(qū))的概念。讀取和寫入,只需要從通道中讀取數(shù)據(jù)到緩沖區(qū)中,或?qū)?shù)據(jù)從緩沖區(qū)中寫入到通道中。NIO不像OIO那樣是順序操作,可以隨意地讀取Buffer中任意位置的數(shù)據(jù)。
- (2) OIO的操作是阻塞的,而NIO的操作是非阻塞的。
NIO如何做到非阻塞的呢?大家都知道,OIO操作都是阻塞的,例如,我們調(diào)用一個read 方法讀取一個文件的內(nèi)容, 那么調(diào)用read的線程會被阻塞住,直到read 操作完成。
而在NIO的非阻塞模式中,當(dāng)我們調(diào)用read方法時,如果此時有數(shù)據(jù),則read讀取數(shù)據(jù)并返回;如果此時沒有數(shù)據(jù),則read直接返回,而不會阻塞當(dāng)前線程。NIO的非阻塞,是如何做到的呢?其實在上一章,答案已經(jīng)揭曉了,NIO 使用了通道和通道的多路復(fù)用技術(shù)。
- (3) OIO沒有選擇器(Selector) 概念,而NIO有選擇器的概念。
NIO的實現(xiàn),是基于底層的選擇器的系統(tǒng)調(diào)用。NIO的選擇器,需要底層操作系統(tǒng)提供支持。而OIO不需要用到選擇器。
3.1.2 通道(Channel)
在OIO中,同一個網(wǎng)絡(luò)連接會關(guān)聯(lián)到兩個流,一個是輸入流( Input Stream),另一個輸出流(Output Stream)。通過這兩個流,不斷地進(jìn)行輸入和輸出。
在NIO中,同一個網(wǎng)絡(luò)連接使用一個通道表示,所有的NIO的IO操作都是從通道開始的。一個通道類似于OIO中的兩個流的結(jié)合體,既可以從通道讀取,也可以向通道寫入。
3.1.3 Selector 選擇器
首先,回顧一個基礎(chǔ)的問題,什么是IO多路復(fù)用?指的是一個進(jìn)程/線程可以同時監(jiān)視多個文件描述符(一個網(wǎng)絡(luò)連接,操作系統(tǒng)底層使用個 文件描述符來表示), 一且其中的一個或者多個文件描述符可讀或者可寫,系統(tǒng)內(nèi)核就通知該進(jìn)程/線程。在Java應(yīng)用層面,如何實現(xiàn)對多個文件描述符的監(jiān)視呢?需要用到一個非常重要的Java NIO組件一 Selector選擇器。
選擇器的神奇功能是什么呢?它一個IO事件的查詢器 。通過選擇器,一 個線程可以查詢多個通道的I0事件的就緒狀態(tài)。
實現(xiàn)IO多路復(fù)用,從具體的開發(fā)層面來說,首先把通道注冊到選擇器中,然后通過選擇器內(nèi)部的機制,可以查詢(select) 這些注冊的通道是否有已經(jīng)就緒的IO事件(例如可讀、可寫、網(wǎng)絡(luò)連接完成等)。
一個選擇器只需要一個線程進(jìn)行監(jiān)控,換句話說,我們可以很簡單地使用一個線程,通過選擇器去管理多個通道。這是非常高效的,這種高效來自于Java的選擇器組件Selector,以及其背后的操作系統(tǒng)底層的IO多路復(fù)用的支持。
與OIO相比,使用選擇器的最大優(yōu)勢:系統(tǒng)開銷小,系統(tǒng)不必為每一 個網(wǎng)絡(luò)連接(文件描述符)創(chuàng)建進(jìn)程/線程,從而大大減小了系統(tǒng)的開銷。
3.1.4 緩沖區(qū)(Buffer)
應(yīng)用程序與通道(Channel) 主要的交互操作,就是進(jìn)行數(shù)據(jù)的read 讀取和write 寫入。為完成如此大任,NIO為大家準(zhǔn)備了第三個重要的組件一NIO Buffer (NIO緩沖區(qū))。通道的讀取就是將數(shù)據(jù)從通道讀取到緩沖區(qū)中;通道的寫入,就是將數(shù)據(jù)從緩沖區(qū)中寫入到通道中。
緩沖區(qū)的使用,是面向流的OIO所沒有的,也是NIO非阻塞的重要前提和基礎(chǔ)之一。
下面從緩沖區(qū)開始,詳細(xì)介紹NIO的Buffer (緩沖區(qū))、Channel (通道)、 Selector (選擇三大核心組件。
3.2 詳解NIO Buffer類及其屬性
NIO的Buffer (緩沖區(qū))本質(zhì)上是一個內(nèi)存塊,既可以寫入數(shù)據(jù),也可以從中讀取數(shù)據(jù)。NIO的Buffer類,是一個抽象類,位于java.nio包中,其內(nèi)部是一個內(nèi)存塊(數(shù)組)。
NIO的Buf與普通的內(nèi)存塊(Java數(shù)組)不同的是:NIO Buffer對象,提供了一組更加有效的方法,用來進(jìn)行寫入和讀取的交替訪問。
需要強調(diào)的是: Bufier 類是個非線程安全類。
3.2.1 Buffer 類
Buffer類是一一個抽象類,對應(yīng)于Java的主要數(shù)據(jù)類型,在NIO中有8種緩沖區(qū)類,分別如下:ByteBuffer、CharBuffer 、DoubleBuffer 、FloatBuffer 、IntBuffer、 LongBuffer 、ShortBuffer 、MappedByteBuffer。
前7種Buffer 類型,覆蓋了能在IO中傳輸?shù)乃械腏ava基本數(shù)據(jù)類型。第8種類型MappedByteBuffer是專門用于內(nèi)存映射的一種ByteBuffer類型。
實際上,使用最多的還是ByteBuffer二進(jìn)制字節(jié)緩沖區(qū)類型,后面會看到。
3.2.2 Buffer類的重要屬性
Buffer類在其內(nèi)部,有一個byte[]數(shù)組內(nèi)存塊,作為內(nèi)存緩沖區(qū)。為了記錄讀寫的狀態(tài)和位置,Buffer類提供了一些重要的屬性。 其中,有三個重要的成員屬性:capacity (容量)、position (讀寫位置)、limit (讀寫的限制)。
除此之外,還有一個標(biāo)記屬性: mark (標(biāo)記),可以將當(dāng)前的position臨時存入mark中:需要的時候,可以再從mark標(biāo)記恢復(fù)到position位置。
- capacity 屬性
Buffer類的capacity屬性,表示內(nèi)部容量的大小。一且寫入的對象數(shù)量超過 了capacity 容量,緩沖區(qū)就滿了,不能再寫入了。
Buffer 類的capacity屬性一旦初始化,就不能再改變。原因是什么呢? Buffer類的對象在初始化時,會按照capacity 分配內(nèi)部的內(nèi)存。在內(nèi)存分配好之后,它的大小當(dāng)然就不能改變了。
再強調(diào)一下, capacity 容量不是指內(nèi)存塊byte[]數(shù)組的字節(jié)的數(shù)量。capacity 容量指的是寫入的數(shù)據(jù)對象的數(shù)量。
前面講到,Buffer類是一個抽象類, Java不能直接用來新建對象。使用的時候,必須使用Buffer的某個子類,例如使用DoubleBuffer,則寫入的數(shù)據(jù)是double類型,如果其capacity是100,那么我們最多可以寫入100個double數(shù)據(jù)。
- position屬性
Buffer類的position屬性,表示當(dāng)前的位置。position 屬性與緩沖區(qū)的讀寫模式有關(guān)。在不同的模式下,position 屬性的值是不同的。當(dāng)緩沖區(qū)進(jìn)行讀寫的模式改變時,position 會進(jìn)行調(diào)整。
在寫入模式下,position 的值變化規(guī)則如下: (1) 在剛進(jìn)入到寫模式時,position 值為0,表示當(dāng)前的寫入位置為從頭開始。(2) 每當(dāng)一個數(shù)據(jù)寫到緩沖區(qū)之后,position 會向后移動到下一個可寫的位置。(3) 初始的position值為0,最大可寫值position 為limit - 1。 當(dāng)position值達(dá)到limitt, 緩沖區(qū)就已經(jīng)無空間可寫了。
在讀模式下,position 的值變化規(guī)則如下: (1) 當(dāng)緩沖區(qū)剛開始進(jìn)入到讀模式時,position 會重置為0。(2) 當(dāng)從緩沖區(qū)讀取時,也是從poitionin位置開始讀。讀取數(shù)據(jù)后, position向前移到下一個可讀的位置。(3) position 最大的值為最大可讀上限limit,當(dāng)position達(dá)到limit時,表明緩沖區(qū)已經(jīng)無數(shù)據(jù)可讀。
起點在哪里呢?當(dāng)新建一個緩沖區(qū)時,緩沖區(qū)處于寫入模式,這時是可以寫數(shù)據(jù)的。數(shù)據(jù)寫入后,如果要從緩沖區(qū)讀取數(shù)據(jù),這就要進(jìn)行模式的切換,可以使用(即調(diào)用) flip 翻轉(zhuǎn)方法,將緩沖區(qū)變成讀取模式。
這個flip翻轉(zhuǎn)過程中,poitioion會進(jìn)行非常巨大的調(diào)整。具體的規(guī)則是:position由原來的寫入位置,變成新的可讀位置,也就是0,表示可以從頭開始讀,flip翻轉(zhuǎn)的另外一半工作,就是要調(diào)整limit屬性。
- limit 屬性
limit 屬性,表示讀寫的最大上限。limit 屬性,也與緩沖區(qū)的讀寫模式有關(guān)。在不間的模式下,limit 的值的含義是不同的。
在寫模式下,limit 屬性值的含義為可以寫入的數(shù)據(jù)最大上限。在剛進(jìn)入到寫模式時,limit 的值會被設(shè)置成緩沖區(qū)的capacity容量值,表示可以一直將緩沖區(qū)的容量寫滿。
在讀模式下,limit 的值含義為最多能從緩沖區(qū)中讀取到多少數(shù)據(jù)。
一般來說, 是先寫入再讀取。當(dāng)緩沖區(qū)寫入完成后,就可以開始從Buffer 讀取數(shù)據(jù),可以使用flip翻轉(zhuǎn)方法,這時,limit的值也會進(jìn)行非常大的調(diào)整。
具體如何調(diào)整呢?將寫模式下的position值,設(shè)置成讀模式下的limit值,也就是說,將之前寫入的最大數(shù)量,作為可以讀取的上限值。
在flip翻轉(zhuǎn)時,屬性的調(diào)整,將沙及position、limit 兩個屬性,這種調(diào)整比較微妙,不是太好理解,舉一個簡單例子:
首先,創(chuàng)建緩沖區(qū)。剛開始,緩沖區(qū)處于寫模式。position 為0,limit 為最大容量。
然后,向緩沖區(qū)寫數(shù)據(jù)。每寫入一個數(shù)據(jù),position 向后面移動一個位置, 也就是position的值加1。假定寫入了5個數(shù),當(dāng)寫入完成后,position 的值為5。
這時,使用(即調(diào)用) fip方法,將緩沖區(qū)切換到讀模式。limit 的值,先會被設(shè)置成寫模式時的position值。這里新的limit是5,表示可以讀取的最大上限是5個數(shù)。同時,新的position會被重置為0,表示可以從0開始讀。
3.2.3 4 個屬性的小結(jié)
除了前面的3個屬性,第4個屬性mark (標(biāo)記)比較簡單。就是相當(dāng)一個暫存屬性,暫時保存position的值,方便后面的重復(fù)使用position值。
下面用一個表格總結(jié)一下Buffer 類的4個重要屬性,參見表3-1。
表3-1 Buffer 四個重要屬性的取值說明
| 屬性 | 說明 |
|---|---|
| capacity | 容量,即可以容納的最大數(shù)據(jù)量:在緩沖區(qū)創(chuàng)建時設(shè)置并且不能改變 |
| limit | 上限,緩沖區(qū)中當(dāng)前的數(shù)據(jù)量 |
| position | 位置,緩沖區(qū)中下一個要被讀或?qū)懙脑氐乃饕?/td> |
| mark | 調(diào)用mark()方法來設(shè)復(fù)mark=position,再調(diào)用reset()可以讓postion復(fù)到mark標(biāo)記的位置,即position=mark |
3.3 詳解NIO Buffer類的重要方法
本小節(jié)將詳細(xì)介紹Buffer類使用中常用的幾個方法,包含Buffer實例的獲取、對Buffer實例的寫入、讀取、重復(fù)讀、標(biāo)記和重置等。
3.3.1 allocate()創(chuàng)建緩沖區(qū)
在使用Buffer (緩沖區(qū))之前,我們首先需要獲取Buffer 子類的實例對象,并且分配內(nèi)存空間。
為了獲取一個Buffer實例對象,這里并不是使用子類的構(gòu)造器new來創(chuàng)建一個實例對象,而是調(diào)用子類的allocate0方法。
下面的程序片段就是用來獲取一個整型Buffer 類的緩沖區(qū)實例對象,代碼如下:
import java.nio.IntBuffer;
public class UseBuffer {
static IntBuffer intBuffer = null;
public static void allocatTest() {
intBuffer = IntBuffer.allocate(20);
System.out.println("------------after allocate------------------");
System.out.println("position=" + intBuffer.position());
System.out.println("limit=" + intBuffer.limit());
System.out.println("capacity=" + intBuffer.capacity());
}
public static void main(String[] args) {
allocatTest();
}
}
例子中,IntBufe 是具體的Bufter子類,通過調(diào)用IntBuffer.allocate(20),創(chuàng)建另一個IntBuffer實例對象,并且分配了20*4個字節(jié)的內(nèi)存空間。
通過程序的輸出結(jié)果,我們可以查看一個新建緩沖[區(qū)實例對象的主要屬性值,如下所示:
------------after allocate------------------
position=0
limit=20
capacity=20
從上面的運行結(jié)果,可以看出:
一個緩沖區(qū)在新建后,處于寫入的模式,position 寫入位置為0,最大可寫上限limit的初始化值(這里是20),而緩沖區(qū)的容量capacity也是初始化值。
3.3.2 put()寫入到緩沖區(qū)
在調(diào)用allocate 方法分配內(nèi)存、返回了實例對象后,緩沖區(qū)實例對象處于寫模式,可以寫入對象。要寫入緩沖區(qū),需要調(diào)用put方法。put方法很簡單,只有一個參數(shù),即為所需要寫入的對象。不過,寫入的數(shù)據(jù)類型要求與緩沖區(qū)的類型保持一致。
接著前面的例子,向剛剛創(chuàng)建的intBuffer緩存實例對象中,寫入的5個整數(shù),代碼如下:
import java.nio.IntBuffer;
public class UseBuffer {
static IntBuffer intBuffer = null;
public static void allocatTest() {
intBuffer = IntBuffer.allocate(20);
System.out.println("------------after allocate------------------");
System.out.println("position=" + intBuffer.position());
System.out.println("limit=" + intBuffer.limit());
System.out.println("capacity=" + intBuffer.capacity());
}
public static void putTest() {
for (int i = 0; i < 5; i++) {
intBuffer.put(i);
}
System.out.println("------------after putTest------------------");
System.out.println("position=" + intBuffer.position());
System.out.println("limit=" + intBuffer.limit());
System.out.println("capacity=" + intBuffer.capacity());
}
public static void main(String[] args) {
System.out.println("分配內(nèi)存");
allocatTest();
System.out.println("寫入");
putTest();
}
}
寫入5個元素后,同樣輸出緩沖區(qū)的主要屬性值,輸出的結(jié)果如下:
分配內(nèi)存
------------after allocate------------------
position=0
limit=20
capacity=20
寫入
------------after putTest------------------
position=5
limit=20
capacity=20
從結(jié)果可以看到,position 變成了5,指向了第6個可以寫入的元素位置。而limit最大寫入元素的上限、capacity 最大容量的值,并沒有發(fā)生變化。
3.3.3 flip()翻轉(zhuǎn)
向緩沖區(qū)寫入數(shù)據(jù)之后,是否可以直接從緩沖區(qū)中讀取數(shù)據(jù)呢?呵呵,不能。
這時緩沖區(qū)還處于寫模式,如果需要讀取數(shù)據(jù),還需要將緩沖區(qū)轉(zhuǎn)換成讀模式。flip()翻轉(zhuǎn)方法是Buffer類提供的一個模式轉(zhuǎn)變的重要方法,它的作用就是將寫入模式翻轉(zhuǎn)成讀取模式。
接著前面的例子,演示一下flip0方法的使用:
import java.nio.IntBuffer;
public class UseBuffer {
static IntBuffer intBuffer = null;
public static void allocatTest() {
intBuffer = IntBuffer.allocate(20);
System.out.println("------------after allocate------------------");
System.out.println("position=" + intBuffer.position());
System.out.println("limit=" + intBuffer.limit());
System.out.println("capacity=" + intBuffer.capacity());
}
public static void putTest() {
for (int i = 0; i < 5; i++) {
intBuffer.put(i);
}
System.out.println("------------after putTest------------------");
System.out.println("position=" + intBuffer.position());
System.out.println("limit=" + intBuffer.limit());
System.out.println("capacity=" + intBuffer.capacity());
}
public static void flipTest() {
intBuffer.flip();
System.out.println("------------after flipTest ------------------");
System.out.println("position=" + intBuffer.position());
System.out.println("limit=" + intBuffer.limit());
System.out.println("capacity=" + intBuffer.capacity());
}
public static void main(String[] args) {
System.out.println("分配內(nèi)存");
allocatTest();
System.out.println("寫入");
putTest();
System.out.println("翻轉(zhuǎn)");
flipTest();
}
}
在調(diào)用flip進(jìn)行模式翻轉(zhuǎn)之后, 緩沖區(qū)的屬性有了奇妙的變化,輸出如下:
分配內(nèi)存
------------after allocate------------------
position=0
limit=20
capacity=20
寫入
------------after putTest------------------
position=5
limit=20
capacity=20
翻轉(zhuǎn)
------------after flipTest ------------------
position=0
limit=5
capacity=20
請用flip方法后,之前寫入模式下的position值5,變成了可讀上限limit值5;而新的讀取模下的poition值,簡單粗暴地變成了0,表示從頭開始讀取。
對flip方法的從寫入到讀取轉(zhuǎn)換的規(guī)則,詳細(xì)的介紹如下:
首先,設(shè)置可讀的長度上限limit。將寫模式下的緩沖區(qū)中內(nèi)容的最后寫入位置position值,作為讀模式下的limit 上限值。
其次,把讀的起始位置position的值設(shè)為0,表示從頭開始讀。
最后,清除之前的mark標(biāo)記,因為mark保存的是寫模式F的臨時為止,繼續(xù)使用舊的mark標(biāo)記,會造成位置混亂。
有關(guān)上面的三步,其實可以查看flip方法的源代碼,Buffer flip0方法的源代碼如下:
public final Buffer flip() {
limit = position; //設(shè)置可讀的長度上限limit,為寫入的positon
position = 0;//把讀的起始位置position的值設(shè)為0,表示從頭開始讀
mark = -1;// 清除之前的mark標(biāo)記
return this;
}
至此,大家都知道了,如何將緩沖區(qū)切換成讀取模式。
新的問題來了,在讀取完成后,如何再一次將緩沖區(qū)切換成寫入模式呢?可以調(diào)用Buffer.clear()清空或者Buffer. compact()壓縮方法,它們可以將緩沖區(qū)轉(zhuǎn)換為寫模式。
Buffer的模式轉(zhuǎn)換,大致如圖3-1所示。

3.3.4 get()從緩沖區(qū)讀取
調(diào)用flip方法,將緩沖區(qū)切換成讀取模式。這時,可以開始從緩沖區(qū)中進(jìn)行數(shù)據(jù)讀取了。讀數(shù)據(jù)很簡單,調(diào)用get方法,每次從position 的位置讀取一一個數(shù)據(jù),并且進(jìn)行相應(yīng)的緩沖區(qū)屬性的調(diào)整。
接著前面flip 的使用實例,演示一下緩沖區(qū) 的讀取操作,代碼如下:
import java.nio.IntBuffer;
public class UseBuffer {
static IntBuffer intBuffer = null;
public static void getTest() {
for (int i = 0; i < 2; i++) {
int j = intBuffer.get();
System.out.println("j = " + j);
}
System.out.println("------------after get 2 int ------------------");
System.out.println("position=" + intBuffer.position());
System.out.println("limit=" + intBuffer.limit());
System.out.println("capacity=" + intBuffer.capacity());
for (int i = 0; i < 3; i++) {
int j = intBuffer.get();
System.out.println("j = " + j);
}
System.out.println("------------after get 3 int ------------------");
System.out.println("position=" + intBuffer.position());
System.out.println("limit=" + intBuffer.limit());
System.out.println("capacity=" + intBuffer.capacity());
}
}
先讀兩個,再讀3個,運行后,輸出的結(jié)果如下:
------------after get 2 int ------------------
position=2
limit=5
capacity=20
j = 2
j = 3
j = 4
------------after get 3 int ------------------
position=5
limit=5
capacity=20
從程序的輸出結(jié)果,我們可以看到,讀取操作會改變可讀位置position 的值,而Iimit值不會改變、如果position值和limit的值相等,表示所有數(shù)據(jù)讀取完成,position 指向了一個沒有數(shù)據(jù)的元素位置,已經(jīng)不能再讀了。此時再讀,會拋出BufferUnderflowException異常。
這里強調(diào)一下,在讀完之后, 是否可以立即進(jìn)行寫入模式呢?不能?,F(xiàn)在還處于讀取模式,我們必須調(diào)用Buffer clear()或Buffer compact(),即清空或者壓縮緩沖區(qū),才能變成寫入模式,讓其重新可寫。
另外,還有一個問題:緩沖區(qū)是不是可以重復(fù)讀呢?答案是可以的。
3.3.5 rewind()倒帶
已經(jīng)讀完的數(shù)據(jù),如果需要再讀一遍,可以調(diào)用rewind0方法。 rewind()也叫倒帶,就像播放磁帶一樣倒回去,再重新播放。
接著前面的代碼,繼續(xù)rewind方法使用的演示,示例代碼如下:
import java.nio.IntBuffer;
public class UseBuffer {
static IntBuffer intBuffer = null;
public static void rewindTest() {
intBuffer.rewind();
System.out.println("------------after rewind ------------------");
System.out.println("position=" + intBuffer.position());
System.out.println("limit=" + intBuffer.limit());
System.out.println("capacity=" + intBuffer.capacity());
}
}
執(zhí)行結(jié)果如下:
------------after rewind ------------------
position=0
limit=5
capacity=20
rewind0方法,主要是調(diào)整了緩沖區(qū)的position屬性,具體的調(diào)整規(guī)則如下:
(1)position重置為0,所以可以重讀緩沖區(qū)中的所有數(shù)據(jù)。
(2) limit保持不變,數(shù)據(jù)量還是一 樣的, 仍然表示能從緩沖區(qū)中讀取多少個元素。
(3)mark標(biāo)記被清理, 表示之前的臨時位置不能再用了。
Buffer.rewind()方法的源代碼如下:
public final Buffer rewind() {
position = 0;
mark = -1;
return this;
}
通過源代碼,我們可以看到rewind()方法與flip()很相似,區(qū)別在于: rewind()不會影值limit值;而flip()會重設(shè)limit屬性值。
在rewind倒帶之后,就可以再一次讀取,重復(fù)讀取的示例代碼如下:
import java.nio.IntBuffer;
public class UseBuffer {
static IntBuffer intBuffer = null;
/**
* rewind之后,重復(fù)讀
* 并且演示 mark 標(biāo)記方法
*/
public static void reRead() {
for (int i = 0; i < 5; i++) {
if (i == 2) {
intBuffer.mark();
}
int j = intBuffer.get();
System.out.println("j = " + j);
}
System.out.println("------------after reRead------------------");
System.out.println("position=" + intBuffer.position());
System.out.println("limit=" + intBuffer.limit());
System.out.println("capacity=" + intBuffer.capacity());
}
}
這段代碼,和前面的讀取示例代碼基本相同,只是增加了一個 mark調(diào)用。
3.3.6 mark( )和reset( )
Buffer.mark()方法的作用是將當(dāng)前position的值保存起來,放在mark屬性中,讓mark屬性記住這個臨時位置;之后, 可以調(diào)用Buffer.reset(方法將mark的值恢復(fù)到position中。
也就是說,Buffer.mark()和 Buffer.reset()方法是配套使用的。兩種方法都需要內(nèi)部mark屬性的支持。
在前面重復(fù)讀取緩沖區(qū)的示例代碼中,讀到第3個元素(i==2時),調(diào)用mark0方法,把當(dāng)前位置position的值保存到mark屬性中,這時mark屬性的值為2。
接下來,就可以調(diào)用reset方法,將mark屬性的值恢復(fù)到position中。然后可以從位置2 (第三個元素)開始讀。
繼續(xù)接著前面的重復(fù)讀取的代碼,進(jìn)行reset的示例演示,代碼如下:
import java.nio.IntBuffer;
public class UseBuffer {
static IntBuffer intBuffer = null;
public static void afterReset() {
System.out.println("------------after reset------------------");
intBuffer.reset();
System.out.println("position=" + intBuffer.position());
System.out.println("limit=" + intBuffer.limit());
System.out.println("capacity=" + intBuffer.capacity());
for (int i =2; i < 5; i++) {
int j = intBuffer.get();
System.out.println("j = " + j);
}
}
}
在上面的代碼中,首先調(diào)用reset()把mark中的值恢復(fù)到position 中,因此讀取的位置position是2,表示可以再次開始從第3個元素開始讀取數(shù)據(jù)。上面的程序代碼的輸出結(jié)果是:
------------after reset------------------
position=2
limit=5
capacity=20
j = 2
j = 3
j = 4
調(diào)用reset方法之后,position 的值為2。此時去讀取緩沖區(qū),輸出后面的三個元素為2、3、4。
3.3.7 clear( )清空緩沖區(qū)
在讀取模式下,調(diào)用clear(方法將緩沖區(qū)切換為寫入模式。此方法會將position 清零,limit :設(shè)置為capacity最大容量值,可以一直寫入,直到緩沖區(qū)寫滿。
接著上面的實例,演示一下clear方法。代碼如下:
import java.nio.IntBuffer;
public class UseBuffer {
static IntBuffer intBuffer = null;
public static void clearDemo() {
System.out.println("------------after clear------------------");
intBuffer.clear();
System.out.println("position=" + intBuffer.position());
System.out.println("limit=" + intBuffer.limit());
System.out.println("capacity=" + intBuffer.capacity());
}
}
運行結(jié)果:
清空
------------after clear------------------
position=0
limit=20
capacity=20
在緩沖區(qū)處于讀取模式時,調(diào)用clear(),緩沖區(qū)會被切換成寫入模式。調(diào)用clear()之后,我們可以看到清空了position 的值,即設(shè)置寫入的起始位置為0,并且寫入的上限為最大容量。
3.3.8 使用Buffer 類的基本步驟
總體來說,使用Java NIO Buffer類的基本步驟如下:
(1)使用創(chuàng)建子類實例對象的allocate()方法, 創(chuàng)建一個Buffer類的實例對象。
(2)調(diào)用put方法,將數(shù)據(jù)寫入到緩沖區(qū)中。
(3)寫入完成后,在開始讀取數(shù)據(jù)前,調(diào)用Buffer.flip(方法, 將緩沖區(qū)轉(zhuǎn)換為讀模式。
(4)調(diào)用get方法,從緩沖區(qū)中讀取數(shù)據(jù)。
(5)讀取完成后,調(diào)用Buffer.clear() 或Buffer. compact()方法,將緩沖區(qū)轉(zhuǎn)換為寫入模式。
3.4 詳解NIO Channel(通道)類
前面講到,NIO中一個連接就是用一個Channel (通道)來表示。大家知道,從更廣泛的層面來說,一個通道可以表示一個底層的文件描述符,例如硬件設(shè)備、文件、網(wǎng)絡(luò)連接等。然而,遠(yuǎn)遠(yuǎn)不止如此,除了可以對應(yīng)到底層文件描述符,JavaNIO的通道還可以更加細(xì)化。例如,對應(yīng)不同的網(wǎng)絡(luò)傳輸協(xié)議類型,在Java中都有不同的NIO Channel (通道)實現(xiàn)。
3.4.1 Channel (通道)的主要類型
這里不對紛繁復(fù)雜的Java NIO通道類型進(jìn)行過多的描述,僅僅聚焦于介紹其中最為重要的四種Channel (通道)實現(xiàn): FileChannel、 SocketChannel、 ServerSocketChannel、 DatagramChannel。
對于以上四種通道,說明如下:
(1) FileChannel文件通道,用于文件的數(shù)據(jù)讀寫。
(2) SocketChannel 套接字通道,用于Socket套接字TCP連接的數(shù)據(jù)讀寫。.
(3) ServerSocketChannel 服務(wù)器嵌套字通道(或服務(wù)器監(jiān)聽通道),允許我們監(jiān)聽TCP連接請求,為每個監(jiān)聽到的請求,創(chuàng)建一個SocketChannel套接字通道。
(4) DatagramChannel數(shù)據(jù)報通道,用于UDP協(xié)議的數(shù)據(jù)讀寫。
這個四種通道,涵蓋了文件IO、TCP網(wǎng)絡(luò)、UDP IO基礎(chǔ)IO。下面從Channel (通道)的獲取、讀取、寫入、關(guān)閉四個重要的操作,來對四種通道進(jìn)行簡單的介紹。
3.4.2 FileChannel 文件通道
FileChannel是專門操作文件的通道。通過FileChannel, 既可以從一個文件中讀取數(shù)據(jù),也可以將數(shù)據(jù)寫入到文件中。特別申明一下,F(xiàn)ileChannel 為阻塞模式,不能設(shè)置為非阻塞模式。
下面分別介紹: FileChannel 的獲取、讀取、寫入、關(guān)閉四個操作。
1.獲取FileChannel通道
可以通過文件的輸入流、輸出流獲取FileChannel 文件通道,示例如下:
//創(chuàng)建一 條文件輸入流
FileInputStream fis = new FileInputStream(srcFile) ;
//獲取文件流的通道
FileChannel inChannel = fis. getChannel() ;
//創(chuàng)建條文件輸出流
FileOutputStream fos = new FileOutputStream(destFile) ;
//獲取文件流的通道
FileChannel outchannel = fos. getChannel() ;
也可以通過RandomAccessFile文件隨機訪問類,獲取FileChannel文件通道:
//創(chuàng)建RandomAccessFile隨機訪問對象
RandomAccessFile aFile = new RandomAccessFile ("filename. txt", "rw") ;
//獲取文件流的通道
FileChannel inChannel = aFile . getChannel () ;
2.讀取FileChannel通道
在大部分應(yīng)用場景,從通道讀取數(shù)據(jù)都會調(diào)用通道的int read (ByteBufferbuf) 方法,它從通道讀取到數(shù)據(jù)寫入到ByteBuffer緩沖區(qū),并且返回讀取到的數(shù)據(jù)量。
RandomAccessFile aFile = new RandomAccessFile (fileName, "rw") ;
//獲取通道
FileChannel inChannel=aFile.getChannel() ;
//獲取一個字節(jié)緩沖區(qū)
ByteBufferbuf = ByteBuf fer.allocate (CAPACITY) ;
int length = -1;
//調(diào)用通道的read方法,讀取數(shù)據(jù)并買入字節(jié)類型的緩沖區(qū)
while ( (length = inChannel. read (buf) ) != -1) {
//省..處理讀取到的buf中的數(shù)據(jù)
}
注意:雖然對于通道來說是讀取數(shù)據(jù),但是對于ByteBuffer緩沖區(qū)來說是寫入數(shù)據(jù),這時,ByteBuffer緩沖區(qū)處于寫入模式。
3.寫入FileChannel通道
寫入數(shù)據(jù)到通道,在大部分應(yīng)用場景,都會調(diào)用通道的int write (ByteBuffer buf) 方法。此方法的參數(shù)一一ByteBuffer 緩沖區(qū),是數(shù)據(jù)的來源。write 方法的作用,是從ByteBuffer 緩沖區(qū)中讀取數(shù)據(jù),然后寫入到通道自身,而返回值是寫入成功的字節(jié)數(shù)。
//如果buf剛寫完數(shù)據(jù),需要flip翻轉(zhuǎn)bug,使其變成讀取模式
buf.flip();
int outlength = 0;
//調(diào)用write方法,將buf得數(shù)據(jù)寫入通道
while ( (outlength = outChannel.write(buf) ) != -1) {
System.out.println("寫入得字節(jié)數(shù)" + outlength);
}
注意:此時的ByteBuffer 緩沖區(qū)要求是可讀的,處于讀模式下。
4.關(guān)閉通道
當(dāng)通道使用完成后,必須將其關(guān)閉。關(guān)閉非常簡單,調(diào)用close方法即可。
/ /關(guān)閉通道
channel.close() ;
5.強制刷新到磁盤
在將緩沖區(qū)寫入通道時,出于性能原因,操作系統(tǒng)不可能每次都實時將數(shù)據(jù)寫入磁盤。如果需要保證寫入通道的緩沖數(shù)據(jù),最終都真正地寫入磁盤,可以調(diào)用FileChannel的force()方法。
// 制刷新到磁盤
channel. force (true) ;
3.4.3 使用FileChannel完成文件復(fù)制的實踐案例
下面是一個簡單的實戰(zhàn)案例:使用文件通道復(fù)制文件。其功能是:使用FileChannel文件通道,.將原文件復(fù)制一份,也就是把原文中的數(shù)據(jù)都復(fù)制到目標(biāo)文件中。完整代碼如下:
import cc.gongchang.cc.gongchang.util.IOUtil;
import cc.gongchang.config.NioDemoConfig;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileNIOCopyDemo {
/**
* 演示程序的入口函數(shù)
*
* @param args
*/
public static void main(String[] args) {
//演示復(fù)制資源文件
nioCopyResouceFile();
}
/**
* 復(fù)制兩個資源目錄下的文件
*/
public static void nioCopyResouceFile() {
String sourcePath = NioDemoConfig.FILE_RESOURCE_SRC_PATH;
String srcPath = IOUtil.getResourcePath(sourcePath);
System.out.println("srcPath=" + srcPath);
String destShortPath = NioDemoConfig.FILE_RESOURCE_DEST_PATH;
String destdePath = IOUtil.builderResourcePath(destShortPath);
System.out.println("destdePath=" + destdePath);
nioCopyFile(srcPath, destdePath);
}
/**
* 復(fù)制文件
*
* @param srcPath
* @param destPath
*/
public static void nioCopyFile(String srcPath, String destPath) {
File srcFile = new File(srcPath);
File destFile = new File(destPath);
try {
//如果目標(biāo)文件不存在,則新建
if (!destFile.exists()) {
destFile.createNewFile();
}
long startTime = System.currentTimeMillis();
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel inChannel = null;
FileChannel outchannel = null;
try {
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);
inChannel = fis.getChannel();
outchannel = fos.getChannel();
int length = -1;
ByteBuffer buf = ByteBuffer.allocate(1024);
//從輸入通道讀取到buf
while ((length = inChannel.read(buf)) != -1) {
//翻轉(zhuǎn)buf,變成成讀模式
buf.flip();
int outlength = 0;
//將buf寫入到輸出的通道
while ((outlength = outchannel.write(buf)) != 0) {
System.out.println("寫入字節(jié)數(shù):" + outlength);
}
//清除buf,變成寫入模式
buf.clear();
}
//強制刷新磁盤
outchannel.force(true);
} finally {
IOUtil.closeQuietly(outchannel);
IOUtil.closeQuietly(fos);
IOUtil.closeQuietly(inChannel);
IOUtil.closeQuietly(fis);
}
long endTime = System.currentTimeMillis();
System.out.println("base 復(fù)制毫秒數(shù):" + (endTime - startTime));
} catch (IOException e) {
e.printStackTrace();
}
}
}
特別強調(diào)一下, 除了FileChannel的通道操作外,還需要注意ByteBuffer的模式切換。新建的ByteBuffer,默認(rèn)是寫入模式,可以作為inChannel.read ( ByteBuffer)的參數(shù)。inChannel.read 方法將從通道inChannel讀到的數(shù)據(jù)寫入到ByteBuffer。
此后,需要調(diào)用緩沖區(qū)的flip方法,將ByteBuffer切換成讀取模式,才能作為outchannel. write(ByteBuffer)方法的參數(shù),從ByteBuffer讀取數(shù)據(jù),再寫入到outchannel輸出通道。
如此,便是完成一次復(fù)制。在進(jìn)入下一次復(fù)制前,還要進(jìn)行- -次緩沖區(qū)的模式切換。ByteBuffer數(shù)據(jù)讀完之后,需要將通過clear方法切換成寫入模式,才能進(jìn)入下一次的復(fù)制。
在示例代碼中,外層的每一輪while 循環(huán),都需要兩次模式ByteBuffer切換:第一次切換時,翻轉(zhuǎn)buf,變成讀取模式;第二次切換時,清除buf,變成寫入模式。
上面的示例代碼,主要的目的在于:演示文件通道以及字節(jié)緩沖區(qū)的使用。作為文件復(fù)制的程序來說,實戰(zhàn)代碼的效率不是最高的。
更高效的文件復(fù)制,可以調(diào)用文件通道的transferFrom方法。具體的代碼,可以參見FileNIOFastCopyDemo類。如下所示:
public class FileNIOFastCopyDemo {
public static void main(String[] args) {
//演示復(fù)制資源文件
nioCopyResouceFile();
}
/**
* 復(fù)制兩個資源目錄下的文件
*/
public static void nioCopyResouceFile() {
String sourcePath = NioDemoConfig.FILE_RESOURCE_SRC_PATH;
String srcDecodePath = IOUtil.getResourcePath(sourcePath);
Logger.debug("srcDecodePath=" + srcDecodePath);
String destPath = NioDemoConfig.FILE_RESOURCE_DEST_PATH;
String destDecodePath = IOUtil.builderResourcePath(destPath);
Logger.debug("destDecodePath=" + destDecodePath);
nioCopyFile(srcDecodePath, destDecodePath);
}
/**
* 復(fù)制文件
*
* @param srcPath
* @param destPath
*/
public static void nioCopyFile(String srcPath, String destPath) {
File srcFile = new File(srcPath);
File destFile = new File(destPath);
try {
//如果目標(biāo)文件不存在,則新建
if (!destFile.exists()) {
destFile.createNewFile();
}
long startTime = System.currentTimeMillis();
FileInputStream fis = null;
FileOutputStream fos = null;
FileChannel inChannel = null;
FileChannel outChannel = null;
try {
fis = new FileInputStream(srcFile);
fos = new FileOutputStream(destFile);
inChannel = fis.getChannel();
outChannel = fos.getChannel();
long size = inChannel.size();
long pos = 0;
long count = 0;
while (pos < size) {
//每次復(fù)制最多1024個字節(jié),沒有就復(fù)制剩余的
count = size - pos > 1024 ? 1024 : size - pos;
//復(fù)制內(nèi)存,偏移量pos + count長度
pos += outChannel.transferFrom(inChannel, pos, count);
}
//強制刷新磁盤
outChannel.force(true);
} finally {
IOUtil.closeQuietly(outChannel);
IOUtil.closeQuietly(fos);
IOUtil.closeQuietly(inChannel);
IOUtil.closeQuietly(fis);
}
long endTime = System.currentTimeMillis();
Logger.debug("base 復(fù)制毫秒數(shù):" + (endTime - startTime));
} catch (IOException e) {
e.printStackTrace();
}
}
}
3.4.4 SocketChannel 套接字通道
在NIO中,涉及網(wǎng)絡(luò)連接的通道有兩個,一個是SocketChannel 負(fù)責(zé)連接傳輸,另一個是ServerSocketChannel負(fù)責(zé)連接的監(jiān)聽。
NIO中的SocketChannel傳輸通道,與OIO中的Socket類對應(yīng)。
NIO中的ServerSocketChannel監(jiān)聽通道,對應(yīng)于OIO中的ServerSocket 類。
ServerSocketChannel應(yīng)用于服務(wù)器端,而SocketChannel同時處于服務(wù)器端和客戶端。換句話說,對應(yīng)于一個連接,兩端都有-一個負(fù)責(zé)傳輸?shù)腟ocketChannel傳輸通道。
無論是ServerSocketChannel,還是SocketChannel,都支持阻塞和非阻塞兩種模式。如何進(jìn)行模式的設(shè)置呢?調(diào)用configureBlocking方法,具體如下:
- (1) socketChannel.configureBlocking ( false)設(shè)置為非阻塞模式。
- (2) socketChannel.configureBlocking (true) 設(shè)置為阻塞模式。
在阻塞模式下,SocketChannel 通道的connect連接、read 讀、write 寫操作,都是同步的和阻塞式的,在效率上與Java舊的OIO的面向流的阻塞式讀寫操作相同。因此,在這里不介紹阻塞模式下的通道的具體操作。在非阻塞模式下,通道的操作是異步、高效率的,這也是相對于傳統(tǒng)的OIO的優(yōu)勢所在。下面詳細(xì)介紹在非阻塞模式下通道的打開、讀寫和關(guān)閉操作等操作。
1.獲取SocketChannel傳輸通道
在客戶端,先通過SocketChannel靜態(tài)方法open()獲得一個套接字傳輸通道;然后,將socket套接字設(shè)置為非阻塞模式;最后,通過connect()實例方法,對服務(wù)器的IP和端口發(fā)起連接。
/ /獲得一個套接字傳輸通道
SocketChannelsocketChannel = SocketChannel.open() ;
//設(shè)置為非阻塞模式
socketChannel.configureBlocking (false) ;
/ /對服務(wù)器的IP和端口發(fā)起連接
socketChannel.connect (new InetSocketAddress("127.0.0.1",80)) ;
非阻塞情況下,與服務(wù)器的連接可能還沒有真正建立,socketChannel.connect 方法就返回了,因此需要不斷地自旋,檢查當(dāng)前是否是連接到了主機:
while(! socketChannel. finishConnect() ) {
//不斷地自旋、等待,或者做-些其他的事情.....
}
在服務(wù)器端,如何獲取傳輸套接字呢?
當(dāng)新連接事件到來時,在服務(wù)器端的ServerSocketChannel 能成功地查詢出一個新連接事件,并且通過調(diào)用服務(wù)器端ServerSocketChannel監(jiān)聽套接字的accept()方法,來獲取新連接的套接字通道:
//新連接事件到來,首先通過事件,獲取服務(wù)器監(jiān)聽通道
ServerSocketChannel server = (Server SocketChannel) key. channel() ;
//獲取新連接的套接字通道
SocketChannel socketChannel = server.accept() ;
//設(shè)置為非阻塞模式
socketChannel.configureBlocking (false) ;
強調(diào)一下,NIO套接字通道,主要用于非阻塞應(yīng)用場景。所以,需要調(diào)用configureBlocking(false),從阻塞模式設(shè)置為非阻塞模式。
2.讀取SocketChannel傳輸通道
當(dāng)SocketChannel 通道可讀時,可以從SocketChannel讀取數(shù)據(jù),具體方法與前面的文件通道讀取方法是相同的。調(diào)用read方法,將數(shù)據(jù)讀入緩沖區(qū)ByteBuffer.
ByteBufferbuf = ByteBuffer.allocate(1024) ;
int bytesRead = socketChannel.read(buf) ;
在讀取時,因為是異步的,因此我們必須檢查read的返回值,以便判斷當(dāng)前是否讀取到了數(shù)據(jù)。read()方法的返回值,是讀取的字節(jié)數(shù)。如果返回-1,那么表示讀取到對方的輸出結(jié)束標(biāo)志,對方已經(jīng)輸出結(jié)束,準(zhǔn)備關(guān)閉連接。實際上,通過read方法讀數(shù)據(jù),本身是很簡單的,比較困難的是,在非阻塞模式下,如何知道通道何時是可讀的呢?這就需要用到NIO的新組件一一Selector通道選擇器,稍后介紹。
3.寫入到SocketChannel傳輸通道
和前面的把數(shù)據(jù)寫入到FileChannel文件通道一樣,大部分應(yīng)用場景都會調(diào)用通道的int write(ByteBuffer buf)方法。
//寫入前需要讀取緩沖區(qū),要求ByteBuffer是讀取模式
buffer.flip() ;
socketChannel.write(buffer) ;
4.關(guān)閉SocketChannel傳輸通道
在關(guān)閉SocketChannel傳輸通道前,如果傳輸通道用來寫入數(shù)據(jù),則建議調(diào)用一次shutdownOutput()終止輸出方法,向?qū)Ψ桨l(fā)送一個輸出的結(jié)束標(biāo)志(-1)。然后調(diào)用socketChannel.close(方法,關(guān)閉套接字連接。
//終止輸出方法,向?qū)Ψ桨l(fā)送一個輸出的結(jié)束標(biāo)志
socketChannel. shutdownOutput () ;
//關(guān)閉套接字連接
IOutil. closeQuietly(socketChannel) ;
3.4.5 使用SocketChannel發(fā)送文件的實踐案例
下面的實踐案例是使用FileChannel 文件通道讀取本地文件內(nèi)容,然后在客戶端使用SocketChannel套接字通道,把文件信息和文件內(nèi)容發(fā)送到服務(wù)器。
客戶端的完整代碼如下:
import cc.gongchang.cc.gongchang.util.IOUtil;
import cc.gongchang.config.NioDemoConfig;
import java.io.File;
import java.io.FileInputStream;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
public class NioSendClient {
/**
* 構(gòu)造函數(shù)
* 與服務(wù)器建立連接
*
* @throws Exception
*/
public NioSendClient() {
}
private Charset charset = Charset.forName("UTF-8");
/**
* 向服務(wù)端傳輸文件
*
* @throws Exception
*/
public void sendFile() {
try {
String sourcePath = NioDemoConfig.SOCKET_SEND_FILE;
String srcPath = IOUtil.getResourcePath(sourcePath);
System.out.println("srcPath=" + srcPath);
String destFile = NioDemoConfig.SOCKET_RECEIVE_FILE;
System.out.println("destFile=" + destFile);
File file = new File(srcPath);
if (!file.exists()) {
System.out.println("文件不存在");
return;
}
FileChannel fileChannel = new FileInputStream(file).getChannel();
SocketChannel socketChannel = SocketChannel.open();
socketChannel.socket().connect(
new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_IP
, NioDemoConfig.SOCKET_SERVER_PORT));
socketChannel.configureBlocking(false);
System.out.println("Cliect 成功連接服務(wù)端");
while (!socketChannel.finishConnect()) {
//不斷的自旋、等待,或者做一些其他的事情
}
//發(fā)送文件名稱
ByteBuffer fileNameByteBuffer = charset.encode(destFile);
socketChannel.write(fileNameByteBuffer);
//發(fā)送文件長度
ByteBuffer buffer = ByteBuffer.allocate(NioDemoConfig.SEND_BUFFER_SIZE);
buffer.putLong(file.length());
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
//發(fā)送文件內(nèi)容
System.out.println("開始傳輸文件");
int length = 0;
long progress = 0;
while ((length = fileChannel.read(buffer)) > 0) {
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
progress += length;
System.out.println("| " + (100 * progress / file.length()) + "% |");
}
if (length == -1) {
IOUtil.closeQuietly(fileChannel);
socketChannel.shutdownOutput();
IOUtil.closeQuietly(socketChannel);
}
System.out.println("======== 文件傳輸成功 ========");
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 入口
*
* @param args
*/
public static void main(String[] args) {
NioSendClient client = new NioSendClient(); // 啟動客戶端連接
client.sendFile(); // 傳輸文件
}
}
以上代碼中的文件發(fā)送過程:首先發(fā)送目標(biāo)文件名稱(不帶路徑),然后發(fā)送文件長度,最后是發(fā)送文件內(nèi)容。代碼中的配置項,如服務(wù)器的IP、 服務(wù)器端口、待發(fā)送的源文件名稱(帶路至)、遠(yuǎn)程的目標(biāo)文件名稱等配置信息,都是從system.properties配置文件中讀取的,通過自定義的NioDemoConfig配置類來完成配置。
在運行以上客戶端的程序之前,需要先運行服務(wù)器端的程序。服務(wù)器端的類與客戶端的源代馬在同一個包下,類名為NioReceiveServer, 具體參見源代碼工程,我們稍后再詳細(xì)介紹這個類。
3.4.6 DatagramChannel 數(shù)據(jù)報通道
和Socket套接字的TCP傳輸協(xié)議不同,UDP協(xié)議不是面向連接的協(xié)議。使用UDP協(xié)議時,只要知道服務(wù)器的IP和端口,就可以直接向?qū)Ψ桨l(fā)送數(shù)據(jù)。在Java中使用UDP協(xié)議傳輸數(shù)據(jù),比TCP協(xié)議更加簡單。在Java NIO中,使用DatagramChannel數(shù)據(jù)報通道來處理UDP協(xié)議的數(shù)據(jù)傳輸。
1.獲取DatagramChannel數(shù)據(jù)報通道
獲取數(shù)據(jù)報通道的方式很簡單,調(diào)用DatagramChannel 類的open 靜態(tài)方法即可。然后調(diào)用configureBlocking (false) 方法,設(shè)置成非阻塞模式。
//獲取Datagr amChannel數(shù)據(jù)報通道
DatagramChannel channel = DatagramChannel.open() ;
//設(shè)置為非阻塞模式
datagramChannel .configureBlocking(false) ;
如果需要接收數(shù)據(jù),還需要調(diào)用bind方法綁定一個數(shù)據(jù)報的監(jiān)聽端口,具體如下:
//調(diào)用bind方法綁定-個數(shù)據(jù)報的監(jiān)聽端口
channel.socket().bind(new InetSocketAddress (18080) ) ;
2.讀取DatagramChannel數(shù)據(jù)報通道數(shù)據(jù)
當(dāng)DatagramChannel通道可讀時,可以從DatagramChannel讀取數(shù)據(jù)。和前面的SocketChannel的讀取方式不同,不是調(diào)用read 方法,而是調(diào)用 receive ( ByteBufferbuf)方法將數(shù)據(jù)從DatagramChannel讀入,再寫入到ByteBuffer緩沖區(qū)中。
/ /創(chuàng)建緩沖區(qū)
ByteBufferbuf = ByteBuffer.allocate(1024) ;
/ /從DatagramChannel讀入,再寫入到ByteBuffer緩沖區(qū)
SocketAddres sclientAddr = datagramChannel.receive(buffer) ;
通道讀取receive ( ByteBuffrbuf)方法的返回值,是SocketAddress類型,表示返回發(fā)送端的連接地址(包括IP和端口)。通過receive方法讀數(shù)據(jù)非常簡單,但是,在非阻塞模式下,如何知道DatagramChannel通道何時是可讀的呢?和SocketChannel 一樣,同樣需要用到NIO的新組件-Selector 通道選擇器,稍后介紹。
3.寫入DatagramChannel數(shù)據(jù)報通道
向DatagramChannel發(fā)送數(shù)據(jù),和向SocketChannel通道發(fā)送數(shù)據(jù)的方法也是不同的。這里不是調(diào)用write 方法,而是調(diào)用send方法。示例代碼如下:
//把緩沖區(qū)翻轉(zhuǎn)到讀取模式
buffer. flip() ;
//調(diào)用send方法,把數(shù)據(jù)發(fā)送到目標(biāo)IP+端口
dChannel. send (buffer,
new InetSocketAddress (
NioDemoConfig.SOCKET_SERVER_IP,
NioDemoConfig.SOCKETSERVER_PORT
)
) ;
//清空緩沖區(qū),切換到寫入模式
buffer.clear () ;
由于UDP是面向非連接的協(xié)議,因此,在調(diào)用send方法發(fā)送數(shù)據(jù)的時候,需要指定接收方的地址(IP 和端口)。
4.關(guān)閉DatagramChannel數(shù)據(jù)報通道
這個比較簡單,直接調(diào)用close(方法,即可關(guān)閉數(shù)據(jù)報通道。
//簡單關(guān)閉即可
dChannel. close() ;
3.4.7 使用 DatagramChannel數(shù)據(jù)包通道發(fā)送數(shù)據(jù)的實踐案例
下面是一個使用DatagramChannel數(shù)據(jù)包通到發(fā)送數(shù)據(jù)的客戶端示例程序代碼。其功能是:獲取用戶的輸入數(shù)據(jù),通過DatagramChannel數(shù)據(jù)報通道,將數(shù)據(jù)發(fā)送到遠(yuǎn)程的服務(wù)器??蛻舳说耐暾绦虼a如下:
public class UDPClient {
public void send() throws IOException {
//操作一:獲取DatagramChannel數(shù)據(jù)報通道
DatagramChannel dChannel = DatagramChannel.open();
dChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(NioDemoConfig.SEND_BUFFER_SIZE);
Scanner scanner = new Scanner(System.in);
Print.tcfo("UDP 客戶端啟動成功!");
Print.tcfo("請輸入發(fā)送內(nèi)容:");
while (scanner.hasNext()) {
String next = scanner.next();
buffer.put((Dateutil.getNow() + " >>" + next).getBytes());
buffer.flip();
// 操作三:通過DatagramChannel數(shù)據(jù)報通道發(fā)送數(shù)據(jù)
dChannel.send(buffer,
new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_IP
, NioDemoConfig.SOCKET_SERVER_PORT));
buffer.clear();
}
//操作四:關(guān)閉DatagramChannel數(shù)據(jù)報通道
dChannel.close();
}
public static void main(String[] args) throws IOException {
new UDPClient().send();
}
}
通過示例程序代碼可以看出,在客戶端使DatagramChannel數(shù)據(jù)報通道發(fā)送數(shù)據(jù),比起在客戶端使用套接字SocketChannel發(fā)送數(shù)據(jù),簡單很多。
接下來看看在服務(wù)器端應(yīng)該如何使用DatagramChannel數(shù)據(jù)包通道接收數(shù)據(jù)呢?
下面貼出服務(wù)器端通過DatagramChannel數(shù)據(jù)包通道接收數(shù)據(jù)的程序代碼,可能大家目前不一定可以看懂,因為代碼中用到了Selector 選擇器,但是不要緊,下一個小節(jié)就介紹它。
服務(wù)器端的接收功能是:通過DatagramChannel數(shù)據(jù)報通道,綁定一個服務(wù)器地址(IP+端口),接收客戶端發(fā)送過來的UDP數(shù)據(jù)報。服務(wù)器端的完整代碼如下:
public class UDPServer {
public void receive() throws IOException {
//操作一:獲取DatagramChannel數(shù)據(jù)報通道
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.configureBlocking(false);
datagramChannel.bind(new InetSocketAddress(
NioDemoConfig.SOCKET_SERVER_IP
, NioDemoConfig.SOCKET_SERVER_PORT));
Print.tcfo("UDP 服務(wù)器啟動成功!");
Selector selector = Selector.open();
datagramChannel.register(selector, SelectionKey.OP_READ);
while (selector.select() > 0) {
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
ByteBuffer buffer = ByteBuffer.allocate(NioDemoConfig.SEND_BUFFER_SIZE);
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
if (selectionKey.isReadable()) {
//操作二:讀取DatagramChannel數(shù)據(jù)報通道數(shù)據(jù)
SocketAddress client = datagramChannel.receive(buffer);
buffer.flip();
Print.tcfo(new String(buffer.array(), 0, buffer.limit()));
buffer.clear();
}
}
iterator.remove();
}
selector.close();
datagramChannel.close();
}
public static void main(String[] args) throws IOException {
new UDPServer().receive();
}
}
在服務(wù)器端,首先調(diào)用了bind方法綁定datagramChannel的監(jiān)聽端口。當(dāng)數(shù)據(jù)到來后,調(diào)用了receive方法,從datagramChannel數(shù)據(jù)包通道接收數(shù)據(jù),再寫入到ByteBuffer緩沖區(qū)中。
除此之外,在服務(wù)器端代碼中,為了監(jiān)控數(shù)據(jù)的到來,使用了Selector選擇器。什么是選擇器?如何使用選擇器呢?欲知后事如何,請聽下節(jié)分解。
3.5 詳解NIO Selector選擇器
Java NIO的三大核心組件: Channel (通道)、Buffer (緩沖區(qū))、Selector (選擇器)。其中通道和緩沖區(qū),二者的聯(lián)系也比較密切:數(shù)據(jù)總是從通道讀到緩沖區(qū)內(nèi),或者從緩沖區(qū)寫入到通道中。
至此,前面兩個組件已經(jīng)介紹完畢,下面迎來了最后一個非常重要的角色一選擇器( Selector)。
3.5.1 選擇器以及注冊
選擇器(Selector) 是什么呢?選擇器和通道的關(guān)系又是什么?
簡單地說:選擇器的使命是完成IO的多路復(fù)用。一個通道代表一條連接通路, 通過選擇器可以同時監(jiān)控多個通道的IO (輸入輸出)狀況。選擇器和通道的關(guān)系,是監(jiān)控和被監(jiān)控的關(guān)系。
選擇器提供了獨特的API方法,能夠選出(select) 所監(jiān)控的通道擁有哪些已經(jīng)準(zhǔn)備好的、就緒的I0操作事件。
一般來說,一個單線程處理一個選擇器,一個選擇器可以監(jiān)控很多通道。通過選擇器,一個單線程可以處理數(shù)百、數(shù)千、數(shù)萬、甚至更多的通道。在極端情況下(數(shù)萬個連接),只用一個線程就可以處理所有的通道,這樣會大量地減少線程之間上下文切換的開銷。
通道和選擇器之間的關(guān)系,通過register (注冊)的方式完成。調(diào)用通道的Channel.register(Selector sel,int ops)方法,可以將通道實例注冊到一個選擇器中。register 方法有兩個參數(shù):第一個參數(shù),指定通道注冊到的選擇器實例;第二個參數(shù),指定選擇器要監(jiān)控的IO事件類型。
可供選擇器監(jiān)控的通道IO事件類型,包括以下四種:
(1)可讀: SelectionKey.OP_READ
(2)可寫: SelectionKey.OP_WRITE
(3)連接: SelectionKey.OP_CONNECT
(4)接收: SelectionKey.OP_ACCEPT
事件類型的定義在SelectionKey類中。如果選擇器要監(jiān)控通道的多種事件,可以用“按位或”運算符來實現(xiàn)。例如,同時監(jiān)控可讀和可寫IO事件:
/ /監(jiān)控通道的多種事件,用“按位或”運算符來實現(xiàn)
int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;
什么是IO事件呢?這個概念容易混淆,這里特別說明一下。這里的IO事件不是對通道的IO操作,而是通道的某個IO操作的一種就緒狀態(tài),表示通道具備完成某個IO操作的條件。
比方說,某個SocketChannel通道,完成了和對端的握手連接,則處于“連接就緒”(OP_ CONNECT)狀態(tài)。
再比方說,某個ServerSocketChannel服務(wù)器通道,監(jiān)聽到一個新連接的到來,則處于“接收就緒”(OP_ ACCEPT)狀態(tài)。
還比方說,一個有數(shù)據(jù)可讀的SocketChannel通道,處于“讀就緒”(OP_ READ)狀態(tài);一個等待寫入數(shù)據(jù)的,處于“寫就緒”(OP_ WRITE)狀態(tài)。
3.5.2 SelectableChannel 可選擇通道
并不是所有的通道,都是可以被選擇器監(jiān)控或選擇的。比方說,F(xiàn)ileChannel 文件通道就不能被選擇器復(fù)用。判斷一個通道能否被選擇器監(jiān)控或選擇,有一個前提:判斷它是否繼承了抽象類SelectableChannel (可選擇通道)。如果繼承了SelectableChannel, 則可以被選擇,否則不能。
簡單地說,一條通道若能被選擇,必須繼承SelectableChannel類。
SelectableChannel類,是何方神圣呢?它提供了實現(xiàn)通道的可選擇性所需要的公共方法。Java NIO中所有網(wǎng)絡(luò)鏈接Socket 套接字通道,都繼承了SelectableChannel 類,都是可選擇的。而FileChannel文件通道,并沒有繼承SelectableChannel,因此不是可選擇通道。
3.5.3 SelectionKey 選擇鍵
通道和選擇器的監(jiān)控關(guān)系注冊成功后,就可以選擇就緒事件。具體的選擇工作,和調(diào)用選擇器Selector的select(方法來完成。通過select 方法,選擇器可以不斷地選擇通道中所發(fā)生操作的就緒狀態(tài),返回注冊過的感興趣的那些IO事件。換句話說,-旦在通道中發(fā)生了某些I0事件(就緒狀態(tài)達(dá)成),并且是在選擇器中注冊過的IO事件,就會被選擇器選中,并放入SelectionKey選擇鍵的集合中。
這里出現(xiàn)一個新的概念:SelectionKey 選擇鍵。SelectionKey 選擇鍵是什么呢?簡單地說,SelectionKey選擇鍵就是那些被選擇器選中的IO事件。前面講到,一個IO事件發(fā)生(就緒狀態(tài)達(dá)成)后,如果之前在選擇器中注冊過,就會被選擇器選中,并放入SelectionKey選擇鍵集合中;如果之前沒有注冊過,即使發(fā)生了IO 事件,也不會被選擇器選中。SelectionKey 選擇鍵和I0的關(guān)系,可以簡單地理解為:選擇鍵,就是被選中了的IO事件。
在編程時,選擇鍵的功能是很強大的。通過SelectionKey選擇鍵,不僅僅可以獲得通道的IO事件類型,比方說SelectionKey.OP_READ;還可以獲得發(fā)生IO事件所在的通道;另外,也可以獲得選出選擇鍵的選擇器實例。
3.5.4 選擇器使用流程
使用選擇器,主要有以下三步:
(1)獲取選擇器實例; (2)將通道注冊到選擇器中; (3)輪詢感興趣的IO就緒事件(選擇鍵集合)。
第一步:獲取選擇器實例
選擇器實例是通過調(diào)用靜態(tài)工廠方法open()來獲取的,具體如下:
//調(diào)用靜態(tài)工廠方法open ()來獲取Selector實例
Selector selector = Selector.open() ;
Selector選擇器的類方法open()的內(nèi)部,是向選擇器SPI (SelectorProvider)發(fā)出請求,通過默認(rèn)的SelectorProvider (選擇器提供者)對象,獲取一個新的選擇器實例。Java中SPI全稱為(Service Provider Interface,服務(wù)提供者接口),是JDK的一種可以擴(kuò)展的服務(wù)提供和發(fā)現(xiàn)機制。Java 通過SPI的方式,提供選擇器的默認(rèn)實現(xiàn)版本。也就是說,其他的服務(wù)提供商可以通過SPI的方式,提供定制化版本的選擇器的動態(tài)替換或者擴(kuò)展。
第二步:將通道注冊到選擇器實例
要實現(xiàn)選擇器管理通道,需要將通道注冊到相應(yīng)的選擇器上,簡單的示例代碼如下:
// 2.獲取通道
ServerSocketChannelserver SocketChannel = Server SocketChannel.open() ;
//3.設(shè)置為非阻塞
serverSocketChannel.configureBlocking(false) ;
// 4.綁定連接
serverSocketChannel.bind (new
Ine tSocketAddress (SystemConfig. SOCKET SERVER PORT)) ;
// 5.將通道注冊到選擇器上,并制定監(jiān)聽事件為:“接收連接”事件
serverSocketChannel.register(selector, SelectionKey.OP ACCEPT) ;
上面通過調(diào)用通道的register)方法,將ServerSocketChannel 通道注冊到了一個選擇器上。當(dāng)然,在注冊之前,首先要準(zhǔn)備好通道。
這里需要注意:注冊到選擇器的通道,必須處于非阻塞模式下,否則將拋出IllegalBlockingModeException異常。這意味著,F(xiàn)ileChannel 文件通道不能與選擇器一起使用, 因為FileChannel文件通道只有阻塞模式,不能切換到非阻塞模式;而Socket套接字相關(guān)的所有通道都可以。
其次,還需要注意: 一個通道,并不一定要支持所有的四種IO事件。例如服務(wù)器監(jiān)聽通道ServerSocketChannel,僅僅支持Accept ( 接收到新連接) IO事件;而SocketChannel傳輸通道,則不支持Accept (接收到新連接) IO事件。
如何判斷通道支持哪些事件呢?可以在注冊之前,可以通過通道的validOps0方法,來獲取該通道所有支持的IO事件集合。
第三步:選出感興趣的IO就緒事件(選擇鍵集合)
通過Selector選擇器的select()方法,選出已經(jīng)注冊的、已經(jīng)就緒的IO事件,保存到SelectionKey選擇鍵集合中。SelectionKey 集合保存在選擇器實例內(nèi)部,是一個元素為SelectionKey類型的集合(Set)。調(diào)用選擇器的selectedKeys(方法,可以取得選擇鍵集合。
接下來,需要迭代集合的每一個選擇鍵,根據(jù)具體IO事件類型,執(zhí)行對應(yīng)的業(yè)務(wù)操作。大致的處理流程如下:
/ /輪詢,選擇感興趣的IO就緒事件(選擇鍵集合)
while (selector.select() > 0) {
Set selectedKeys = selector.selectedKeys () ;
Iterator keyIterator = selectedKeys.iterator() ;
while (keyIterator.hasNext() ) {
SelectionKey key = keyIterator.next() ;
//根據(jù)具體的Io事件類型,執(zhí)行對應(yīng)的業(yè)務(wù)操作
if (key.isAcceptable()) {
//IO事件: ServerSocketChannel服務(wù)器監(jiān)聽通道有新連接
} else if (key. isConnectable()) {
// IO事件:傳輸通道連接成功
else if (key. isReadable()) {
// IO事件:傳輸通道可讀
else if (key. isWritable()) {
// I0事件:傳輸通道可寫
}
//處理完成后,移除選擇鍵
keyIterator. remove () ;
}
}
處理完成后,需要將選擇鍵從這個SelectionKey集合中移除,防止下一次循環(huán)的時候,被重復(fù)的處理。SelectionKey 集合不能添加元素,如果試圖向SelectionKey 選擇鍵集合中添加元素,則將拋出java.lang.UnsupportedOperationException異常。
用于選擇就緒的IO事件的select()方法,有多個重載的實現(xiàn)版本,具體如下:
(1) select): 阻塞調(diào)用,一直到至少有一個通道發(fā)生了注冊的I0事件。
(2) select(long timeout);和select()-樣,但最長阻塞時間為timeout指定的毫秒數(shù)。
(3) selectNow():非阻塞,不管有沒有I0事件,都會立刻返回。
select()方法返回的整數(shù)值(int 整數(shù)類型),表示發(fā)生了IO事件的通道數(shù)量。更準(zhǔn)確地說,是從上一次select到這一次select 之間,有多少通道發(fā)生了IO事件。強調(diào)一.下,select()方法返回的數(shù)量,指的是通道數(shù),而不是IO事件數(shù),準(zhǔn)確地說,是指發(fā)生了選擇器感興趣的IO事件的通道數(shù)。
3.5.5 使用NIO實現(xiàn)Discard服務(wù)器的實踐案例
Discard服務(wù)器的功能很簡單:僅僅讀取客戶端通道的輸入數(shù)據(jù),讀取完成后直接關(guān)閉客戶端通道;并且讀取到的數(shù)據(jù)直接拋棄掉( Discard)。Discard 服務(wù)器足夠簡單明了,作為第一個學(xué)習(xí)NIO的通信實例,較有參考價值。;
下面的Discard服務(wù)器代碼,將選擇器使用流程中的步驟進(jìn)行了細(xì)化:
import cc.gongchang.cc.gongchang.util.Logger;
import cc.gongchang.config.NioDemoConfig;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
public class NioDiscardServer {
public static void startServer() throws IOException {
// 1、獲取Selector選擇器
Selector selector = Selector.open();
// 2、獲取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3.設(shè)置為非阻塞
serverSocketChannel.configureBlocking(false);
// 4、綁定連接
serverSocketChannel.bind(new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_PORT));
Logger.info("服務(wù)器啟動成功");
// 5、將通道注冊到選擇器上,并注冊的IO事件為:“接收新連接”
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6、輪詢感興趣的I/O就緒事件(選擇鍵集合)
while (selector.select() > 0) {
// 7、獲取選擇鍵集合
Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
while (selectedKeys.hasNext()) {
// 8、獲取單個的選擇鍵,并處理
SelectionKey selectedKey = selectedKeys.next();
// 9、判斷key是具體的什么事件
if (selectedKey.isAcceptable()) {
// 10、若選擇鍵的IO事件是“連接就緒”事件,就獲取客戶端連接
SocketChannel socketChannel = serverSocketChannel.accept();
// 11、切換為非阻塞模式
socketChannel.configureBlocking(false);
// 12、將該通道注冊到selector選擇器上
socketChannel.register(selector, SelectionKey.OP_READ);
} else if (selectedKey.isReadable()) {
// 13、若選擇鍵的IO事件是“可讀”事件,讀取數(shù)據(jù)
SocketChannel socketChannel = (SocketChannel) selectedKey.channel();
// 14、讀取數(shù)據(jù)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int length = 0;
while ((length = socketChannel.read(byteBuffer)) >0) {
byteBuffer.flip();
Logger.info(new String(byteBuffer.array(), 0, length));
byteBuffer.clear();
}
socketChannel.close();
}
// 15、移除選擇鍵
selectedKeys.remove();
}
}
// 7、關(guān)閉連接
serverSocketChannel.close();
}
public static void main(String[] args) throws IOException {
startServer();
}
}
實現(xiàn)DiscardServer 共分為16步,其中第7到第15步是循環(huán)執(zhí)行的。不斷選擇感興趣的IO事件到選擇器的選擇鍵集合中,然后通過selector selectedKeys()獲取該選擇鍵集合,并且進(jìn)行迭代處理。對于新建立的socketChannel客戶端傳輸通道,也要注冊到同一個選擇器上,使用同一個選擇線程,不斷地對所有的注冊通道進(jìn)行選擇鍵的選擇。
在DiscardServer程序中,涉及到兩次選擇器注冊:一次是注冊serverChannel服務(wù)器通道;另一次, 注冊接收到的socketChannel客戶端傳輸通道。serverChannel 服務(wù)器通道注冊的,是新連接的IO事件SelectionKey.OP_ACCEPT;客戶端socketChannel傳輸通道注冊的,是可讀I0事件SelectionKey.OP_READ。.
DiscardServer在對選擇鍵進(jìn)行處理時,通過對類型進(jìn)行判斷,然后進(jìn)行相應(yīng)的處理
(1)如果是SelectionKey.OP_ACCEPT新連接事件類型,代表serverChannel服務(wù)器通道發(fā)生了新連接事件,則通過服務(wù)器通道的accept方法,獲取新的socketChannel 傳輸通道,并且將新通道注冊到選擇器。
(2)如果是SelectionKey.OP_READ可讀事件類型,代表某個客戶端通道有數(shù)據(jù)可讀,則讀取選擇鍵中socketChannel傳輸通道的數(shù)據(jù),然后丟棄。
客戶端的DiscardClient 代碼,則更為簡單??蛻舳耸紫冉⒌椒?wù)器的連接,發(fā)送一些簡單的數(shù)據(jù),然后直接關(guān)閉連接。代碼如下:
import cc.gongchang.cc.gongchang.util.Logger;
import cc.gongchang.config.NioDemoConfig;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NioDiscardClient {
/**
* 客戶端
*/
public static void startClient() throws IOException {
InetSocketAddress address =
new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_IP,
NioDemoConfig.SOCKET_SERVER_PORT);
// 1、獲取通道(channel)
SocketChannel socketChannel = SocketChannel.open(address);
// 2、切換成非阻塞模式
socketChannel.configureBlocking(false);
//不斷的自旋、等待連接完成,或者做一些其他的事情
while (!socketChannel.finishConnect()) {
}
Logger.info("客戶端連接成功");
// 3、分配指定大小的緩沖區(qū)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("hello world".getBytes());
byteBuffer.flip();
socketChannel.write(byteBuffer);
socketChannel.shutdownOutput();
socketChannel.close();
}
public static void main(String[] args) throws IOException {
startClient();
}
}
如果需要執(zhí)行整個程序,首先要執(zhí)行前面的服務(wù)器端程序,然后執(zhí)行后面的客戶端程序。
通過Discard服務(wù)器的開發(fā)實踐,大家對NIO Selector (選擇)的使用流程,應(yīng)該了解得非常清楚了。
下面來看一個稍微復(fù)雜一 -點的案例:在服務(wù)器端接收文件和內(nèi)容。
3.5.6 使用SocketChannel在服務(wù)器端接收文件的實踐案例
本示例演示文件的接收,是服務(wù)器端的程序。和前面介紹的文件發(fā)送的SocketChannel客戶端程序是相互配合使用的。由于在服務(wù)器端,需要用到選擇器,所以在介紹完選擇器后,才開始介紹NIO文件傳輸?shù)腟ocket服務(wù)器端程序。服務(wù)器端接收文件的示例代碼如下所示:
import cc.gongchang.cc.gongchang.util.IOUtil;
import cc.gongchang.cc.gongchang.util.Print;
import cc.gongchang.config.NioDemoConfig;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
public class NioReceiveServer {
private Charset charset = Charset.forName("UTF-8");
/**
* 服務(wù)器端保存的客戶端對象,對應(yīng)一個客戶端文件
*/
static class Client {
//文件名稱
String fileName;
//長度
long fileLength;
//開始傳輸?shù)臅r間
long startTime;
//客戶端的地址
InetSocketAddress remoteAddress;
//輸出的文件通道
FileChannel outChannel;
}
private ByteBuffer buffer
= ByteBuffer.allocate(NioDemoConfig.SERVER_BUFFER_SIZE);
//使用Map保存每個客戶端傳輸,當(dāng)OP_READ通道可讀時,根據(jù)channel找到對應(yīng)的對象
Map<SelectableChannel, Client> clientMap = new HashMap<SelectableChannel, Client>();
public void startServer() throws IOException {
// 1、獲取Selector選擇器
Selector selector = Selector.open();
// 2、獲取通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverChannel.socket();
// 3.設(shè)置為非阻塞
serverChannel.configureBlocking(false);
// 4、綁定連接
InetSocketAddress address
= new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_PORT);
serverSocket.bind(address);
// 5、將通道注冊到選擇器上,并注冊的IO事件為:“接收新連接”
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
Print.tcfo("serverChannel is linstening...");
// 6、輪詢感興趣的I/O就緒事件(選擇鍵集合)
while (selector.select() > 0) {
// 7、獲取選擇鍵集合
Iterator<SelectionKey> it = selector.selectedKeys().iterator();
while (it.hasNext()) {
// 8、獲取單個的選擇鍵,并處理
SelectionKey key = it.next();
// 9、判斷key是具體的什么事件,是否為新連接事件
if (key.isAcceptable()) {
// 10、若接受的事件是“新連接”事件,就獲取客戶端新連接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = server.accept();
if (socketChannel == null) continue;
// 11、客戶端新連接,切換為非阻塞模式
socketChannel.configureBlocking(false);
// 12、將客戶端新連接通道注冊到selector選擇器上
SelectionKey selectionKey =
socketChannel.register(selector, SelectionKey.OP_READ);
// 余下為業(yè)務(wù)處理
Client client = new Client();
client.remoteAddress
= (InetSocketAddress) socketChannel.getRemoteAddress();
clientMap.put(socketChannel, client);
System.out.println(socketChannel.getRemoteAddress() + "連接成功...");
} else if (key.isReadable()) {
processData(key);
}
// NIO的特點只會累加,已選擇的鍵的集合不會刪除
// 如果不刪除,下一次又會被select函數(shù)選中
it.remove();
}
}
}
/**
* 處理客戶端傳輸過來的數(shù)據(jù)
*/
private void processData(SelectionKey key) throws IOException {
Client client = clientMap.get(key.channel());
SocketChannel socketChannel = (SocketChannel) key.channel();
int num = 0;
try {
//清空緩沖區(qū),進(jìn)入到寫入模式
buffer.clear();
while ((num = socketChannel.read(buffer)) > 0) {
//把緩沖區(qū)翻轉(zhuǎn)到讀取模式
buffer.flip();
//客戶端發(fā)送過來的,首先是文件名
if (null == client.fileName) {
// 文件名
String fileName = charset.decode(buffer).toString();
String destPath = IOUtil.getResourcePath(NioDemoConfig.SOCKET_RECEIVE_PATH);
File directory = new File(destPath);
if (!directory.exists()) {
directory.mkdir();
}
client.fileName = fileName;
String fullName = directory.getAbsolutePath()
+ File.separatorChar + fileName;
System.out.println("NIO 傳輸目標(biāo)文件:" + fullName);
File file = new File(fullName);
FileChannel fileChannel = new FileOutputStream(file).getChannel();
client.outChannel = fileChannel;
}
//客戶端發(fā)送過來的,其次是文件長度
else if (0 == client.fileLength) {
// 文件長度
long fileLength = buffer.getLong();
client.fileLength = fileLength;
client.startTime = System.currentTimeMillis();
System.out.println("NIO 傳輸開始:");
}
//客戶端發(fā)送過來的,最后是文件內(nèi)容
else {
// 寫入文件
client.outChannel.write(buffer);
}
buffer.clear();
}
key.cancel();
} catch (IOException e) {
key.cancel();
e.printStackTrace();
return;
}
// 調(diào)用close為-1 到達(dá)末尾
if (num == -1) {
IOUtil.closeQuietly(client.outChannel);
System.out.println("上傳完畢");
key.cancel();
System.out.println("文件接收成功,File Name:" + client.fileName);
System.out.println(" Size:" + IOUtil.getFormatFileSize(client.fileLength));
long endTime = System.currentTimeMillis();
System.out.println("NIO IO 傳輸毫秒數(shù):" + (endTime - client.startTime));
}
}
/**
* 入口
*
* @param args
*/
public static void main(String[] args) throws Exception {
NioReceiveServer server = new NioReceiveServer();
server.startServer();
}
}
NioSendClient代碼的執(zhí)行結(jié)果:
srcPath=/D:/project/netty/chapter3/nio/target/classes/system.properties
destFile=system.dest.properties
Cliect 成功連接服務(wù)端
開始傳輸文件
| 100% |
======== 文件傳輸成功 ========
NioReceiveServer執(zhí)行結(jié)果:
[main|NioReceiveServer.startServer]:serverChannel is linstening...
/127.0.0.1:60626連接成功...
NIO 傳輸目標(biāo)文件:D:\project\netty\chapter3\nio\target\classes\system.dest.properties
NIO 傳輸開始:
上傳完畢
文件接收成功,File Name:system.dest.properties
Size:491B
NIO IO 傳輸毫秒數(shù):1
由于客戶端每次傳輸文件,都會分為多次傳輸:
(1)首先傳入文件名稱。
(2)其次是文件大小。
(3)然后是文件內(nèi)容。
對應(yīng)于每一個客戶端socketChannel, 創(chuàng)建一個 Client 客戶端對象,用于保存客戶端狀態(tài),分別保存文件名、文件大小和寫入的目標(biāo)文件通道outChannel。
socketChannel和Client對象之間是一對一的對應(yīng)關(guān)系:建立連接的時候,以socketChannel作為鍵(Key) ,Client 對象作為值(Value) ,將Client保存在map中。當(dāng)socketChannel傳輸通道有數(shù)據(jù)可讀時,通過選擇鍵key.channel(方法,取出IO事件所在socketChannel 通道。然后通過socketChannel通道,從map中取到對應(yīng)的Client對象。
接收到數(shù)據(jù)時,如果文件名為空,先處理文件名稱,并把文件名保存到Client 對象,同時創(chuàng)建服務(wù)器上的目標(biāo)文件;接下來再讀到數(shù)據(jù),說明接收到了文件大小,把文件大小保存到Client對象;接下來再接到數(shù)據(jù),說明是文件內(nèi)容了,則寫入Client對象的outChannel文件通道中,直到數(shù)據(jù)讀取完畢。
運行方式:啟動這個NioReceiveServer服務(wù)器程序后,再啟動前面介紹的客戶端程序NioSendClient,即可以完成文件的傳輸。
3.6 本章小結(jié)
在編程難度上,Java NIO編程的難度比同步阻塞Java OIO編程大很多。請注意,前面的實踐案例,是比較簡單的,并不是復(fù)雜的通信程序,沒有看到“粘包"和“拆包”等問題。如果加上這些問題,代碼將會更加復(fù)雜。
與Java OIO相比,Java NIO編程大致的特點如下:
(1)在NIO中,服務(wù)器接收新連接的工作,是異步進(jìn)行的。不像Java的OIO那樣,服務(wù)器監(jiān)聽連接,是同步的、阻塞的。NIO可以通過選擇器(也可以說成:多路復(fù)用器),后續(xù)不斷地輪詢選擇器的選擇鍵集合,選擇新到來的連接。
(2)在NIO中,SocketChannel傳輸通道的讀寫操作都是異步的。如果沒有可讀寫的數(shù)據(jù),負(fù)責(zé)IO通信的線程不會同步等待。這樣,線程就可以處理其他連接的通道;不需要像OIO那樣,線程一直阻塞,等待所負(fù)責(zé)的連接可用為止。
(3)在NIO中,一個選擇器線程可以同時處理成千上萬個客戶端連接,性能不會隨著客戶端的增加而線性下降。
總之,有了Linux底層的epoll支持,有了Java NIO Selector選擇器這樣的應(yīng)用層IO復(fù)用技術(shù),Java程序從而可以實現(xiàn)IO通信的高TPS、高并發(fā),使服務(wù)器具備并發(fā)數(shù)十萬、數(shù)百萬的連接能力。Java的NIO技術(shù)非常適合用于高性能、高負(fù)載的網(wǎng)絡(luò)服務(wù)器。鼎鼎大名的通信服務(wù)器中間件Netty,就是基于Java的NIO技術(shù)實現(xiàn)的。
當(dāng)然,Java NIO技術(shù)僅僅是基礎(chǔ),如果要實現(xiàn)通信的高性能和高并發(fā),還離不開高效率的設(shè)計模式。下--章將開始為大家介紹高性能服務(wù)必備的設(shè)計模式:Reactor反應(yīng)器模式。