高性能的Java通信冗尤。絕對離不開Java NIO技術(shù)口锭,現(xiàn)在主流的技術(shù)框架或中間件服務(wù)器,都使用了JavaNIO技術(shù)厘唾,譬如Tomcat褥符、Jetty. Netty.。學(xué)習(xí)和享握NIO技術(shù)抚垃,已經(jīng)不是一項(xiàng)加分能喷楣,而是一項(xiàng)必備技能。不管是面試鹤树,還是實(shí)際開發(fā)铣焊,作為Java的“攻城師”,都必須掌握NIO的原理和開發(fā)實(shí)踐技能罕伯。
3.1 Java NIO簡介
在1.4版本之前曲伊,JavaIO類庫是阻塞1O;從1.4版本開始,引進(jìn)了新的異步IO庫坟募,被稱為JavaNew IO類庫岛蚤,簡稱為JAVA NIO。New IO類庫的目標(biāo)懈糯,就是要讓Java支持非阻塞IO涤妒。基于這個(gè)原因赚哗,更多的人喜歡稱Java NIO為非阻塞IO (Non-Block IO)她紫,稱“老的"阻塞式Java IO為OIO(Old 10)∮齑ⅲ總體上說贿讹,NIO彌補(bǔ)了原來面向流的OIO同步阻塞的不足,它為標(biāo)準(zhǔn)Java代碼提供了高速的扩所、面向緩沖區(qū)的IO.
Java NIO由以下三個(gè)核心組件組成:
- Channel (通道)
- Buffer (緩沖區(qū))
- Selector (選擇器)
如果理解了第1章的四種IO模型围详,大家一眼就能識別出來朴乖,Java NIO屬于第三種模型-- IO多路復(fù)用模型祖屏。當(dāng)然, Java NIO組件买羞,提供了統(tǒng)一的API袁勺,為大家屏蔽了底層的不同操作系統(tǒng)的差異。
后面的章節(jié)畜普,我們會對以上的三個(gè)Java NIO的核心組件期丰,展開詳細(xì)介紹。 先來看看Java的NIO和OIO的簡單對比吃挑。
3.1.1 NIO 和OIO的對比
在Java中钝荡,NIO和OIO的區(qū)別,主要體現(xiàn)在三個(gè)方面:
- (1) OIO是面向流(Stream Oriented)的舶衬,NIO 是面向緩沖區(qū)( Buffer Oriented)的埠通。
何謂面向流,何謂面向緩沖區(qū)呢?
OIO是面向字節(jié)流或字符流的逛犹,在一般的OIO操作中端辱,我們以流式的方式順序地從一個(gè)流(Stream)中讀取一個(gè)或多個(gè)字節(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)用一個(gè)read 方法讀取一個(gè)文件的內(nèi)容, 那么調(diào)用read的線程會被阻塞住悯嗓,直到read 操作完成件舵。
而在NIO的非阻塞模式中,當(dāng)我們調(diào)用read方法時(shí)脯厨,如果此時(shí)有數(shù)據(jù)铅祸,則read讀取數(shù)據(jù)并返回;如果此時(shí)沒有數(shù)據(jù)合武,則read直接返回临梗,而不會阻塞當(dāng)前線程。NIO的非阻塞稼跳,是如何做到的呢?其實(shí)在上一章盟庞,答案已經(jīng)揭曉了,NIO 使用了通道和通道的多路復(fù)用技術(shù)汤善。
- (3) OIO沒有選擇器(Selector) 概念什猖,而NIO有選擇器的概念。
NIO的實(shí)現(xiàn)红淡,是基于底層的選擇器的系統(tǒng)調(diào)用不狮。NIO的選擇器,需要底層操作系統(tǒng)提供支持在旱。而OIO不需要用到選擇器摇零。
3.1.2 通道(Channel)
在OIO中,同一個(gè)網(wǎng)絡(luò)連接會關(guān)聯(lián)到兩個(gè)流颈渊,一個(gè)是輸入流( Input Stream)遂黍,另一個(gè)輸出流(Output Stream)。通過這兩個(gè)流俊嗽,不斷地進(jìn)行輸入和輸出雾家。
在NIO中,同一個(gè)網(wǎng)絡(luò)連接使用一個(gè)通道表示绍豁,所有的NIO的IO操作都是從通道開始的芯咧。一個(gè)通道類似于OIO中的兩個(gè)流的結(jié)合體,既可以從通道讀取,也可以向通道寫入敬飒。
3.1.3 Selector 選擇器
首先邪铲,回顧一個(gè)基礎(chǔ)的問題,什么是IO多路復(fù)用无拗?指的是一個(gè)進(jìn)程/線程可以同時(shí)監(jiān)視多個(gè)文件描述符(一個(gè)網(wǎng)絡(luò)連接带到,操作系統(tǒng)底層使用個(gè) 文件描述符來表示), 一且其中的一個(gè)或者多個(gè)文件描述符可讀或者可寫英染,系統(tǒng)內(nèi)核就通知該進(jìn)程/線程揽惹。在Java應(yīng)用層面,如何實(shí)現(xiàn)對多個(gè)文件描述符的監(jiān)視呢四康?需要用到一個(gè)非常重要的Java NIO組件一 Selector選擇器搪搏。
選擇器的神奇功能是什么呢?它一個(gè)IO事件的查詢器 。通過選擇器闪金,一 個(gè)線程可以查詢多個(gè)通道的I0事件的就緒狀態(tài)疯溺。
實(shí)現(xiàn)IO多路復(fù)用,從具體的開發(fā)層面來說哎垦,首先把通道注冊到選擇器中囱嫩,然后通過選擇器內(nèi)部的機(jī)制,可以查詢(select) 這些注冊的通道是否有已經(jīng)就緒的IO事件(例如可讀撼泛、可寫挠说、網(wǎng)絡(luò)連接完成等)澡谭。
一個(gè)選擇器只需要一個(gè)線程進(jìn)行監(jiān)控愿题,換句話說,我們可以很簡單地使用一個(gè)線程蛙奖,通過選擇器去管理多個(gè)通道潘酗。這是非常高效的,這種高效來自于Java的選擇器組件Selector雁仲,以及其背后的操作系統(tǒng)底層的IO多路復(fù)用的支持仔夺。
與OIO相比,使用選擇器的最大優(yōu)勢:系統(tǒng)開銷小攒砖,系統(tǒng)不必為每一 個(gè)網(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)備了第三個(gè)重要的組件一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ì)上是一個(gè)內(nèi)存塊鱼辙,既可以寫入數(shù)據(jù),也可以從中讀取數(shù)據(jù)玫镐。NIO的Buffer類倒戏,是一個(gè)抽象類,位于java.nio包中恐似,其內(nèi)部是一個(gè)內(nèi)存塊(數(shù)組)杜跷。
NIO的Buf與普通的內(nèi)存塊(Java數(shù)組)不同的是:NIO Buffer對象,提供了一組更加有效的方法矫夷,用來進(jìn)行寫入和讀取的交替訪問葛闷。
需要強(qiáng)調(diào)的是: Bufier 類是個(gè)非線程安全類。
3.2.1 Buffer 類
Buffer類是一一個(gè)抽象類双藕,對應(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類型沥匈。
實(shí)際上,使用最多的還是ByteBuffer二進(jìn)制字節(jié)緩沖區(qū)類型忘渔,后面會看到高帖。
3.2.2 Buffer類的重要屬性
Buffer類在其內(nèi)部,有一個(gè)byte[]數(shù)組內(nèi)存塊辨萍,作為內(nèi)存緩沖區(qū)棋恼。為了記錄讀寫的狀態(tài)和位置返弹,Buffer類提供了一些重要的屬性。 其中爪飘,有三個(gè)重要的成員屬性:capacity (容量)义起、position (讀寫位置)、limit (讀寫的限制)师崎。
除此之外默终,還有一個(gè)標(biāo)記屬性: mark (標(biāo)記),可以將當(dāng)前的position臨時(shí)存入mark中:需要的時(shí)候犁罩,可以再從mark標(biāo)記恢復(fù)到position位置齐蔽。
- capacity 屬性
Buffer類的capacity屬性,表示內(nèi)部容量的大小床估。一且寫入的對象數(shù)量超過 了capacity 容量含滴,緩沖區(qū)就滿了,不能再寫入了丐巫。
Buffer 類的capacity屬性一旦初始化谈况,就不能再改變。原因是什么呢? Buffer類的對象在初始化時(shí)递胧,會按照capacity 分配內(nèi)部的內(nèi)存碑韵。在內(nèi)存分配好之后,它的大小當(dāng)然就不能改變了缎脾。
再強(qiáng)調(diào)一下祝闻, capacity 容量不是指內(nèi)存塊byte[]數(shù)組的字節(jié)的數(shù)量。capacity 容量指的是寫入的數(shù)據(jù)對象的數(shù)量遗菠。
前面講到联喘,Buffer類是一個(gè)抽象類, Java不能直接用來新建對象舷蒲。使用的時(shí)候耸袜,必須使用Buffer的某個(gè)子類,例如使用DoubleBuffer牲平,則寫入的數(shù)據(jù)是double類型,如果其capacity是100域滥,那么我們最多可以寫入100個(gè)double數(shù)據(jù)纵柿。
- position屬性
Buffer類的position屬性,表示當(dāng)前的位置启绰。position 屬性與緩沖區(qū)的讀寫模式有關(guān)昂儒。在不同的模式下,position 屬性的值是不同的委可。當(dāng)緩沖區(qū)進(jìn)行讀寫的模式改變時(shí)渊跋,position 會進(jìn)行調(diào)整腊嗡。
在寫入模式下,position 的值變化規(guī)則如下: (1) 在剛進(jìn)入到寫模式時(shí)拾酝,position 值為0燕少,表示當(dāng)前的寫入位置為從頭開始。(2) 每當(dāng)一個(gè)數(shù)據(jù)寫到緩沖區(qū)之后蒿囤,position 會向后移動到下一個(gè)可寫的位置客们。(3) 初始的position值為0,最大可寫值position 為limit - 1材诽。 當(dāng)position值達(dá)到limitt底挫, 緩沖區(qū)就已經(jīng)無空間可寫了。
在讀模式下脸侥,position 的值變化規(guī)則如下: (1) 當(dāng)緩沖區(qū)剛開始進(jìn)入到讀模式時(shí)建邓,position 會重置為0。(2) 當(dāng)從緩沖區(qū)讀取時(shí)睁枕,也是從poitionin位置開始讀涝缝。讀取數(shù)據(jù)后, position向前移到下一個(gè)可讀的位置譬重。(3) position 最大的值為最大可讀上限limit拒逮,當(dāng)position達(dá)到limit時(shí),表明緩沖區(qū)已經(jīng)無數(shù)據(jù)可讀臀规。
起點(diǎn)在哪里呢滩援?當(dāng)新建一個(gè)緩沖區(qū)時(shí),緩沖區(qū)處于寫入模式塔嬉,這時(shí)是可以寫數(shù)據(jù)的玩徊。數(shù)據(jù)寫入后,如果要從緩沖區(qū)讀取數(shù)據(jù)谨究,這就要進(jìn)行模式的切換恩袱,可以使用(即調(diào)用) flip 翻轉(zhuǎn)方法,將緩沖區(qū)變成讀取模式胶哲。
這個(gè)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)入到寫模式時(shí)锨推,limit 的值會被設(shè)置成緩沖區(qū)的capacity容量值,表示可以一直將緩沖區(qū)的容量寫滿公壤。
在讀模式下换可,limit 的值含義為最多能從緩沖區(qū)中讀取到多少數(shù)據(jù)。
一般來說厦幅, 是先寫入再讀取沾鳄。當(dāng)緩沖區(qū)寫入完成后,就可以開始從Buffer 讀取數(shù)據(jù)确憨,可以使用flip翻轉(zhuǎn)方法捡偏,這時(shí)壁涎,limit的值也會進(jìn)行非常大的調(diào)整呐芥。
具體如何調(diào)整呢银受?將寫模式下的position值,設(shè)置成讀模式下的limit值塔猾,也就是說篙骡,將之前寫入的最大數(shù)量,作為可以讀取的上限值丈甸。
在flip翻轉(zhuǎn)時(shí)糯俗,屬性的調(diào)整,將沙及position睦擂、limit 兩個(gè)屬性得湘,這種調(diào)整比較微妙,不是太好理解顿仇,舉一個(gè)簡單例子:
首先淘正,創(chuàng)建緩沖區(qū)。剛開始夺欲,緩沖區(qū)處于寫模式跪帝。position 為0,limit 為最大容量些阅。
然后,向緩沖區(qū)寫數(shù)據(jù)斑唬。每寫入一個(gè)數(shù)據(jù)市埋,position 向后面移動一個(gè)位置黎泣, 也就是position的值加1。假定寫入了5個(gè)數(shù)缤谎,當(dāng)寫入完成后抒倚,position 的值為5。
這時(shí)坷澡,使用(即調(diào)用) fip方法托呕,將緩沖區(qū)切換到讀模式。limit 的值频敛,先會被設(shè)置成寫模式時(shí)的position值项郊。這里新的limit是5,表示可以讀取的最大上限是5個(gè)數(shù)斟赚。同時(shí)着降,新的position會被重置為0,表示可以從0開始讀拗军。
3.2.3 4 個(gè)屬性的小結(jié)
除了前面的3個(gè)屬性任洞,第4個(gè)屬性mark (標(biāo)記)比較簡單。就是相當(dāng)一個(gè)暫存屬性发侵,暫時(shí)保存position的值交掏,方便后面的重復(fù)使用position值。
下面用一個(gè)表格總結(jié)一下Buffer 類的4個(gè)重要屬性刃鳄,參見表3-1盅弛。
表3-1 Buffer 四個(gè)重要屬性的取值說明
屬性 | 說明 |
---|---|
capacity | 容量,即可以容納的最大數(shù)據(jù)量:在緩沖區(qū)創(chuàng)建時(shí)設(shè)置并且不能改變 |
limit | 上限铲汪,緩沖區(qū)中當(dāng)前的數(shù)據(jù)量 |
position | 位置熊尉,緩沖區(qū)中下一個(gè)要被讀或?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類使用中常用的幾個(gè)方法掌腰,包含Buffer實(shí)例的獲取狰住、對Buffer實(shí)例的寫入、讀取齿梁、重復(fù)讀催植、標(biāo)記和重置等。
3.3.1 allocate()創(chuàng)建緩沖區(qū)
在使用Buffer (緩沖區(qū))之前勺择,我們首先需要獲取Buffer 子類的實(shí)例對象创南,并且分配內(nèi)存空間。
為了獲取一個(gè)Buffer實(shí)例對象省核,這里并不是使用子類的構(gòu)造器new來創(chuàng)建一個(gè)實(shí)例對象稿辙,而是調(diào)用子類的allocate0方法。
下面的程序片段就是用來獲取一個(gè)整型Buffer 類的緩沖區(qū)實(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 main(String[] args) {
allocatTest();
}
}
例子中邻储,IntBufe 是具體的Bufter子類赋咽,通過調(diào)用IntBuffer.allocate(20),創(chuàng)建另一個(gè)IntBuffer實(shí)例對象吨娜,并且分配了20*4個(gè)字節(jié)的內(nèi)存空間脓匿。
通過程序的輸出結(jié)果,我們可以查看一個(gè)新建緩沖[區(qū)實(shí)例對象的主要屬性值宦赠,如下所示:
------------after allocate------------------
position=0
limit=20
capacity=20
從上面的運(yùn)行結(jié)果陪毡,可以看出:
一個(gè)緩沖區(qū)在新建后,處于寫入的模式勾扭,position 寫入位置為0毡琉,最大可寫上限limit的初始化值(這里是20),而緩沖區(qū)的容量capacity也是初始化值尺借。
3.3.2 put()寫入到緩沖區(qū)
在調(diào)用allocate 方法分配內(nèi)存绊起、返回了實(shí)例對象后,緩沖區(qū)實(shí)例對象處于寫模式燎斩,可以寫入對象虱歪。要寫入緩沖區(qū),需要調(diào)用put方法栅表。put方法很簡單笋鄙,只有一個(gè)參數(shù),即為所需要寫入的對象怪瓶。不過萧落,寫入的數(shù)據(jù)類型要求與緩沖區(qū)的類型保持一致。
接著前面的例子洗贰,向剛剛創(chuàng)建的intBuffer緩存實(shí)例對象中找岖,寫入的5個(gè)整數(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個(gè)元素后敛滋,同樣輸出緩沖區(qū)的主要屬性值许布,輸出的結(jié)果如下:
分配內(nèi)存
------------after allocate------------------
position=0
limit=20
capacity=20
寫入
------------after putTest------------------
position=5
limit=20
capacity=20
從結(jié)果可以看到,position 變成了5,指向了第6個(gè)可以寫入的元素位置绎晃。而limit最大寫入元素的上限蜜唾、capacity 最大容量的值,并沒有發(fā)生變化庶艾。
3.3.3 flip()翻轉(zhuǎn)
向緩沖區(qū)寫入數(shù)據(jù)之后袁余,是否可以直接從緩沖區(qū)中讀取數(shù)據(jù)呢?呵呵,不能咱揍。
這時(shí)緩沖區(qū)還處于寫模式颖榜,如果需要讀取數(shù)據(jù),還需要將緩沖區(qū)轉(zhuǎn)換成讀模式。flip()翻轉(zhuǎn)方法是Buffer類提供的一個(gè)模式轉(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)記梯澜,因?yàn)閙ark保存的是寫模式F的臨時(shí)為止寞冯,繼續(xù)使用舊的mark標(biāo)記,會造成位置混亂晚伙。
有關(guān)上面的三步吮龄,其實(shí)可以查看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ū)切換成讀取模式。這時(shí),可以開始從緩沖區(qū)中進(jìn)行數(shù)據(jù)讀取了取具。讀數(shù)據(jù)很簡單脖隶,調(diào)用get方法,每次從position 的位置讀取一一個(gè)數(shù)據(jù)暇检,并且進(jìn)行相應(yīng)的緩沖區(qū)屬性的調(diào)整产阱。
接著前面flip 的使用實(shí)例,演示一下緩沖區(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());
}
}
先讀兩個(gè)构蹬,再讀3個(gè),運(yùn)行后悔据,輸出的結(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 指向了一個(gè)沒有數(shù)據(jù)的元素位置怖亭,已經(jīng)不能再讀了。此時(shí)再讀拙毫,會拋出BufferUnderflowException異常依许。
這里強(qiáng)調(diào)一下,在讀完之后缀蹄, 是否可以立即進(jìn)行寫入模式呢?不能∏吞現(xiàn)在還處于讀取模式,我們必須調(diào)用Buffer clear()或Buffer compact()缺前,即清空或者壓縮緩沖區(qū)蛀醉,才能變成寫入模式,讓其重新可寫衅码。
另外拯刁,還有一個(gè)問題:緩沖區(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ū)中讀取多少個(gè)元素旁振。
(3)mark標(biāo)記被清理, 表示之前的臨時(shí)位置不能再用了烟勋。
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());
}
}
這段代碼,和前面的讀取示例代碼基本相同较解,只是增加了一個(gè) mark調(diào)用畜疾。
3.3.6 mark( )和reset( )
Buffer.mark()方法的作用是將當(dāng)前position的值保存起來,放在mark屬性中印衔,讓mark屬性記住這個(gè)臨時(shí)位置;之后啡捶, 可以調(diào)用Buffer.reset(方法將mark的值恢復(fù)到position中。
也就是說奸焙,Buffer.mark()和 Buffer.reset()方法是配套使用的瞎暑。兩種方法都需要內(nèi)部mark屬性的支持。
在前面重復(fù)讀取緩沖區(qū)的示例代碼中与帆,讀到第3個(gè)元素(i==2時(shí))了赌,調(diào)用mark0方法,把當(dāng)前位置position的值保存到mark屬性中玄糟,這時(shí)mark屬性的值為2勿她。
接下來,就可以調(diào)用reset方法阵翎,將mark屬性的值恢復(fù)到position中逢并。然后可以從位置2 (第三個(gè)元素)開始讀。
繼續(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個(gè)元素開始讀取數(shù)據(jù)。上面的程序代碼的輸出結(jié)果是:
------------after reset------------------
position=2
limit=5
capacity=20
j = 2
j = 3
j = 4
調(diào)用reset方法之后,position 的值為2灶伊。此時(shí)去讀取緩沖區(qū)疆前,輸出后面的三個(gè)元素為2、3聘萨、4竹椒。
3.3.7 clear( )清空緩沖區(qū)
在讀取模式下,調(diào)用clear(方法將緩沖區(qū)切換為寫入模式米辐。此方法會將position 清零胸完,limit :設(shè)置為capacity最大容量值,可以一直寫入翘贮,直到緩沖區(qū)寫滿赊窥。
接著上面的實(shí)例,演示一下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());
}
}
運(yùn)行結(jié)果:
清空
------------after clear------------------
position=0
limit=20
capacity=20
在緩沖區(qū)處于讀取模式時(shí)锨能,調(diào)用clear(),緩沖區(qū)會被切換成寫入模式芍耘。調(diào)用clear()之后址遇,我們可以看到清空了position 的值,即設(shè)置寫入的起始位置為0斋竞,并且寫入的上限為最大容量倔约。
3.3.8 使用Buffer 類的基本步驟
總體來說,使用Java NIO Buffer類的基本步驟如下:
(1)使用創(chuàng)建子類實(shí)例對象的allocate()方法坝初, 創(chuàng)建一個(gè)Buffer類的實(shí)例對象浸剩。
(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中一個(gè)連接就是用一個(gè)Channel (通道)來表示。大家知道围俘,從更廣泛的層面來說砸讳,一個(gè)通道可以表示一個(gè)底層的文件描述符琢融,例如硬件設(shè)備、文件簿寂、網(wǎng)絡(luò)連接等漾抬。然而,遠(yuǎn)遠(yuǎn)不止如此常遂,除了可以對應(yīng)到底層文件描述符纳令,JavaNIO的通道還可以更加細(xì)化。例如克胳,對應(yīng)不同的網(wǎng)絡(luò)傳輸協(xié)議類型平绩,在Java中都有不同的NIO Channel (通道)實(shí)現(xiàn)。
3.4.1 Channel (通道)的主要類型
這里不對紛繁復(fù)雜的Java NIO通道類型進(jìn)行過多的描述漠另,僅僅聚焦于介紹其中最為重要的四種Channel (通道)實(shí)現(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連接請求领猾,為每個(gè)監(jiān)聽到的請求米同,創(chuàng)建一個(gè)SocketChannel套接字通道。
(4) DatagramChannel數(shù)據(jù)報(bào)通道摔竿,用于UDP協(xié)議的數(shù)據(jù)讀寫面粮。
這個(gè)四種通道,涵蓋了文件IO继低、TCP網(wǎng)絡(luò)熬苍、UDP IO基礎(chǔ)IO。下面從Channel (通道)的獲取袁翁、讀取柴底、寫入、關(guān)閉四個(gè)重要的操作粱胜,來對四種通道進(jìn)行簡單的介紹柄驻。
3.4.2 FileChannel 文件通道
FileChannel是專門操作文件的通道。通過FileChannel, 既可以從一個(gè)文件中讀取數(shù)據(jù)焙压,也可以將數(shù)據(jù)寫入到文件中鸿脓。特別申明一下抑钟,F(xiàn)ileChannel 為阻塞模式,不能設(shè)置為非阻塞模式野哭。
下面分別介紹: FileChannel 的獲取在塔、讀取、寫入虐拓、關(guān)閉四個(gè)操作心俗。
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文件隨機(jī)訪問類城榛,獲取FileChannel文件通道:
//創(chuàng)建RandomAccessFile隨機(jī)訪問對象
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() ;
//獲取一個(gè)字節(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ù),這時(shí)绍撞,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);
}
注意:此時(shí)的ByteBuffer 緩沖區(qū)要求是可讀的,處于讀模式下弓坞。
4.關(guān)閉通道
當(dāng)通道使用完成后隧甚,必須將其關(guān)閉。關(guān)閉非常簡單渡冻,調(diào)用close方法即可戚扳。
/ /關(guān)閉通道
channel.close() ;
5.強(qiáng)制刷新到磁盤
在將緩沖區(qū)寫入通道時(shí)度苔,出于性能原因董瞻,操作系統(tǒng)不可能每次都實(shí)時(shí)將數(shù)據(jù)寫入磁盤辽旋。如果需要保證寫入通道的緩沖數(shù)據(jù)谷遂,最終都真正地寫入磁盤,可以調(diào)用FileChannel的force()方法砍艾。
// 制刷新到磁盤
channel. force (true) ;
3.4.3 使用FileChannel完成文件復(fù)制的實(shí)踐案例
下面是一個(gè)簡單的實(shí)戰(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ù)制兩個(gè)資源目錄下的文件
*/
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();
}
//強(qiáng)制刷新磁盤
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();
}
}
}
特別強(qiáng)調(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切換:第一次切換時(shí)涛菠,翻轉(zhuǎn)buf,變成讀取模式;第二次切換時(shí)撇吞,清除buf,變成寫入模式俗冻。
上面的示例代碼,主要的目的在于:演示文件通道以及字節(jié)緩沖區(qū)的使用牍颈。作為文件復(fù)制的程序來說迄薄,實(shí)戰(zhàn)代碼的效率不是最高的。
更高效的文件復(fù)制煮岁,可以調(diào)用文件通道的transferFrom方法讥蔽。具體的代碼涣易,可以參見FileNIOFastCopyDemo類。如下所示:
public class FileNIOFastCopyDemo {
public static void main(String[] args) {
//演示復(fù)制資源文件
nioCopyResouceFile();
}
/**
* 復(fù)制兩個(gè)資源目錄下的文件
*/
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個(gè)字節(jié)新症,沒有就復(fù)制剩余的
count = size - pos > 1024 ? 1024 : size - pos;
//復(fù)制內(nèi)存,偏移量pos + count長度
pos += outChannel.transferFrom(inChannel, pos, count);
}
//強(qiáng)制刷新磁盤
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ò)連接的通道有兩個(gè)响禽,一個(gè)是SocketChannel 負(fù)責(zé)連接傳輸徒爹,另一個(gè)是ServerSocketChannel負(fù)責(zé)連接的監(jiān)聽。
NIO中的SocketChannel傳輸通道芋类,與OIO中的Socket類對應(yīng)隆嗅。
NIO中的ServerSocketChannel監(jiān)聽通道,對應(yīng)于OIO中的ServerSocket 類梗肝。
ServerSocketChannel應(yīng)用于服務(wù)器端榛瓮,而SocketChannel同時(shí)處于服務(wù)器端和客戶端。換句話說巫击,對應(yīng)于一個(gè)連接禀晓,兩端都有-一個(gè)負(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()獲得一個(gè)套接字傳輸通道翼悴;然后,將socket套接字設(shè)置為非阻塞模式;最后说订,通過connect()實(shí)例方法抄瓦,對服務(wù)器的IP和端口發(fā)起連接潮瓶。
/ /獲得一個(gè)套接字傳輸通道
SocketChannelsocketChannel = SocketChannel.open() ;
//設(shè)置為非阻塞模式
socketChannel.configureBlocking (false) ;
/ /對服務(wù)器的IP和端口發(fā)起連接
socketChannel.connect (new InetSocketAddress("127.0.0.1",80)) ;
非阻塞情況下钙姊,與服務(wù)器的連接可能還沒有真正建立毯辅,socketChannel.connect 方法就返回了,因此需要不斷地自旋煞额,檢查當(dāng)前是否是連接到了主機(jī):
while(! socketChannel. finishConnect() ) {
//不斷地自旋思恐、等待,或者做-些其他的事情.....
}
在服務(wù)器端膊毁,如何獲取傳輸套接字呢?
當(dāng)新連接事件到來時(shí)胀莹,在服務(wù)器端的ServerSocketChannel 能成功地查詢出一個(gè)新連接事件,并且通過調(diào)用服務(wù)器端ServerSocketChannel監(jiān)聽套接字的accept()方法婚温,來獲取新連接的套接字通道:
//新連接事件到來描焰,首先通過事件,獲取服務(wù)器監(jiān)聽通道
ServerSocketChannel server = (Server SocketChannel) key. channel() ;
//獲取新連接的套接字通道
SocketChannel socketChannel = server.accept() ;
//設(shè)置為非阻塞模式
socketChannel.configureBlocking (false) ;
強(qiáng)調(diào)一下栅螟,NIO套接字通道荆秦,主要用于非阻塞應(yīng)用場景。所以力图,需要調(diào)用configureBlocking(false)步绸,從阻塞模式設(shè)置為非阻塞模式。
2.讀取SocketChannel傳輸通道
當(dāng)SocketChannel 通道可讀時(shí)吃媒,可以從SocketChannel讀取數(shù)據(jù)瓤介,具體方法與前面的文件通道讀取方法是相同的。調(diào)用read方法赘那,將數(shù)據(jù)讀入緩沖區(qū)ByteBuffer.
ByteBufferbuf = ByteBuffer.allocate(1024) ;
int bytesRead = socketChannel.read(buf) ;
在讀取時(shí)刑桑,因?yàn)槭钱惒降模虼宋覀儽仨殭z查read的返回值募舟,以便判斷當(dāng)前是否讀取到了數(shù)據(jù)漾月。read()方法的返回值,是讀取的字節(jié)數(shù)胃珍。如果返回-1,那么表示讀取到對方的輸出結(jié)束標(biāo)志蜓陌,對方已經(jīng)輸出結(jié)束觅彰,準(zhǔn)備關(guān)閉連接。實(shí)際上钮热,通過read方法讀數(shù)據(jù)填抬,本身是很簡單的,比較困難的是隧期,在非阻塞模式下飒责,如何知道通道何時(shí)是可讀的呢?這就需要用到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ā)送一個(gè)輸出的結(jié)束標(biāo)志(-1)嗅义。然后調(diào)用socketChannel.close(方法屏歹,關(guān)閉套接字連接。
//終止輸出方法之碗,向?qū)Ψ桨l(fā)送一個(gè)輸出的結(jié)束標(biāo)志
socketChannel. shutdownOutput () ;
//關(guān)閉套接字連接
IOutil. closeQuietly(socketChannel) ;
3.4.5 使用SocketChannel發(fā)送文件的實(shí)踐案例
下面的實(shí)踐案例是使用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)容。代碼中的配置項(xiàng)冶忱,如服務(wù)器的IP尾菇、 服務(wù)器端口、待發(fā)送的源文件名稱(帶路至)囚枪、遠(yuǎn)程的目標(biāo)文件名稱等配置信息派诬,都是從system.properties配置文件中讀取的,通過自定義的NioDemoConfig配置類來完成配置链沼。
在運(yùn)行以上客戶端的程序之前默赂,需要先運(yùn)行服務(wù)器端的程序。服務(wù)器端的類與客戶端的源代馬在同一個(gè)包下括勺,類名為NioReceiveServer, 具體參見源代碼工程缆八,我們稍后再詳細(xì)介紹這個(gè)類。
3.4.6 DatagramChannel 數(shù)據(jù)報(bào)通道
和Socket套接字的TCP傳輸協(xié)議不同疾捍,UDP協(xié)議不是面向連接的協(xié)議奈辰。使用UDP協(xié)議時(shí),只要知道服務(wù)器的IP和端口乱豆,就可以直接向?qū)Ψ桨l(fā)送數(shù)據(jù)奖恰。在Java中使用UDP協(xié)議傳輸數(shù)據(jù),比TCP協(xié)議更加簡單。在Java NIO中瑟啃,使用DatagramChannel數(shù)據(jù)報(bào)通道來處理UDP協(xié)議的數(shù)據(jù)傳輸论泛。
1.獲取DatagramChannel數(shù)據(jù)報(bào)通道
獲取數(shù)據(jù)報(bào)通道的方式很簡單,調(diào)用DatagramChannel 類的open 靜態(tài)方法即可蛹屿。然后調(diào)用configureBlocking (false) 方法屁奏,設(shè)置成非阻塞模式。
//獲取Datagr amChannel數(shù)據(jù)報(bào)通道
DatagramChannel channel = DatagramChannel.open() ;
//設(shè)置為非阻塞模式
datagramChannel .configureBlocking(false) ;
如果需要接收數(shù)據(jù)蜡峰,還需要調(diào)用bind方法綁定一個(gè)數(shù)據(jù)報(bào)的監(jiān)聽端口了袁,具體如下:
//調(diào)用bind方法綁定-個(gè)數(shù)據(jù)報(bào)的監(jiān)聽端口
channel.socket().bind(new InetSocketAddress (18080) ) ;
2.讀取DatagramChannel數(shù)據(jù)報(bào)通道數(shù)據(jù)
當(dāng)DatagramChannel通道可讀時(shí),可以從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通道何時(shí)是可讀的呢?和SocketChannel 一樣奠伪,同樣需要用到NIO的新組件-Selector 通道選擇器跌帐,稍后介紹。
3.寫入DatagramChannel數(shù)據(jù)報(bào)通道
向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ù)的時(shí)候,需要指定接收方的地址(IP 和端口)欲芹。
4.關(guān)閉DatagramChannel數(shù)據(jù)報(bào)通道
這個(gè)比較簡單蜜葱,直接調(diào)用close(方法,即可關(guān)閉數(shù)據(jù)報(bào)通道耀石。
//簡單關(guān)閉即可
dChannel. close() ;
3.4.7 使用 DatagramChannel數(shù)據(jù)包通道發(fā)送數(shù)據(jù)的實(shí)踐案例
下面是一個(gè)使用DatagramChannel數(shù)據(jù)包通到發(fā)送數(shù)據(jù)的客戶端示例程序代碼。其功能是:獲取用戶的輸入數(shù)據(jù),通過DatagramChannel數(shù)據(jù)報(bào)通道滞伟,將數(shù)據(jù)發(fā)送到遠(yuǎn)程的服務(wù)器揭鳞。客戶端的完整程序代碼如下:
public class UDPClient {
public void send() throws IOException {
//操作一:獲取DatagramChannel數(shù)據(jù)報(bào)通道
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ù)報(bào)通道發(fā)送數(shù)據(jù)
dChannel.send(buffer,
new InetSocketAddress(NioDemoConfig.SOCKET_SERVER_IP
, NioDemoConfig.SOCKET_SERVER_PORT));
buffer.clear();
}
//操作四:關(guān)閉DatagramChannel數(shù)據(jù)報(bào)通道
dChannel.close();
}
public static void main(String[] args) throws IOException {
new UDPClient().send();
}
}
通過示例程序代碼可以看出野崇,在客戶端使DatagramChannel數(shù)據(jù)報(bào)通道發(fā)送數(shù)據(jù),比起在客戶端使用套接字SocketChannel發(fā)送數(shù)據(jù)亩钟,簡單很多乓梨。
接下來看看在服務(wù)器端應(yīng)該如何使用DatagramChannel數(shù)據(jù)包通道接收數(shù)據(jù)呢?
下面貼出服務(wù)器端通過DatagramChannel數(shù)據(jù)包通道接收數(shù)據(jù)的程序代碼,可能大家目前不一定可以看懂清酥,因?yàn)榇a中用到了Selector 選擇器扶镀,但是不要緊,下一個(gè)小節(jié)就介紹它焰轻。
服務(wù)器端的接收功能是:通過DatagramChannel數(shù)據(jù)報(bào)通道臭觉,綁定一個(gè)服務(wù)器地址(IP+端口),接收客戶端發(fā)送過來的UDP數(shù)據(jù)報(bào)辱志。服務(wù)器端的完整代碼如下:
public class UDPServer {
public void receive() throws IOException {
//操作一:獲取DatagramChannel數(shù)據(jù)報(bào)通道
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ù)報(bào)通道數(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)控?cái)?shù)據(jù)的到來,使用了Selector選擇器和悦。什么是選擇器?如何使用選擇器呢?欲知后事如何退疫,請聽下節(jié)分解。
3.5 詳解NIO Selector選擇器
Java NIO的三大核心組件: Channel (通道)鸽素、Buffer (緩沖區(qū))褒繁、Selector (選擇器)。其中通道和緩沖區(qū)馍忽,二者的聯(lián)系也比較密切:數(shù)據(jù)總是從通道讀到緩沖區(qū)內(nèi)棒坏,或者從緩沖區(qū)寫入到通道中。
至此遭笋,前面兩個(gè)組件已經(jīng)介紹完畢坝冕,下面迎來了最后一個(gè)非常重要的角色一選擇器( Selector)。
3.5.1 選擇器以及注冊
選擇器(Selector) 是什么呢?選擇器和通道的關(guān)系又是什么?
簡單地說:選擇器的使命是完成IO的多路復(fù)用瓦呼。一個(gè)通道代表一條連接通路喂窟, 通過選擇器可以同時(shí)監(jiān)控多個(gè)通道的IO (輸入輸出)狀況。選擇器和通道的關(guān)系,是監(jiān)控和被監(jiān)控的關(guān)系磨澡。
選擇器提供了獨(dú)特的API方法碗啄,能夠選出(select) 所監(jiān)控的通道擁有哪些已經(jīng)準(zhǔn)備好的、就緒的I0操作事件稳摄。
一般來說稚字,一個(gè)單線程處理一個(gè)選擇器,一個(gè)選擇器可以監(jiān)控很多通道厦酬。通過選擇器胆描,一個(gè)單線程可以處理數(shù)百、數(shù)千仗阅、數(shù)萬昌讲、甚至更多的通道。在極端情況下(數(shù)萬個(gè)連接)霹菊,只用一個(gè)線程就可以處理所有的通道剧蚣,這樣會大量地減少線程之間上下文切換的開銷。
通道和選擇器之間的關(guān)系旋廷,通過register (注冊)的方式完成鸠按。調(diào)用通道的Channel.register(Selector sel,int ops)方法饶碘,可以將通道實(shí)例注冊到一個(gè)選擇器中目尖。register 方法有兩個(gè)參數(shù):第一個(gè)參數(shù),指定通道注冊到的選擇器實(shí)例扎运;第二個(gè)參數(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)控通道的多種事件,可以用“按位或”運(yùn)算符來實(shí)現(xiàn)负拟。例如烦衣,同時(shí)監(jiān)控可讀和可寫IO事件:
/ /監(jiān)控通道的多種事件,用“按位或”運(yùn)算符來實(shí)現(xiàn)
int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;
什么是IO事件呢?這個(gè)概念容易混淆掩浙,這里特別說明一下花吟。這里的IO事件不是對通道的IO操作,而是通道的某個(gè)IO操作的一種就緒狀態(tài)厨姚,表示通道具備完成某個(gè)IO操作的條件衅澈。
比方說,某個(gè)SocketChannel通道谬墙,完成了和對端的握手連接今布,則處于“連接就緒”(OP_ CONNECT)狀態(tài)经备。
再比方說,某個(gè)ServerSocketChannel服務(wù)器通道部默,監(jiān)聽到一個(gè)新連接的到來弄喘,則處于“接收就緒”(OP_ ACCEPT)狀態(tài)。
還比方說甩牺,一個(gè)有數(shù)據(jù)可讀的SocketChannel通道,處于“讀就緒”(OP_ READ)狀態(tài)累奈;一個(gè)等待寫入數(shù)據(jù)的贬派,處于“寫就緒”(OP_ WRITE)狀態(tài)。
3.5.2 SelectableChannel 可選擇通道
并不是所有的通道澎媒,都是可以被選擇器監(jiān)控或選擇的搞乏。比方說,F(xiàn)ileChannel 文件通道就不能被選擇器復(fù)用戒努。判斷一個(gè)通道能否被選擇器監(jiān)控或選擇请敦,有一個(gè)前提:判斷它是否繼承了抽象類SelectableChannel (可選擇通道)。如果繼承了SelectableChannel储玫, 則可以被選擇侍筛,否則不能。
簡單地說撒穷,一條通道若能被選擇匣椰,必須繼承SelectableChannel類。
SelectableChannel類端礼,是何方神圣呢?它提供了實(shí)現(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)一個(gè)新的概念:SelectionKey 選擇鍵酸茴。SelectionKey 選擇鍵是什么呢?簡單地說兢交,SelectionKey選擇鍵就是那些被選擇器選中的IO事件薪捍。前面講到,一個(gè)IO事件發(fā)生(就緒狀態(tài)達(dá)成)后配喳,如果之前在選擇器中注冊過酪穿,就會被選擇器選中,并放入SelectionKey選擇鍵集合中;如果之前沒有注冊過晴裹,即使發(fā)生了IO 事件被济,也不會被選擇器選中。SelectionKey 選擇鍵和I0的關(guān)系涧团,可以簡單地理解為:選擇鍵只磷,就是被選中了的IO事件。
在編程時(shí)泌绣,選擇鍵的功能是很強(qiáng)大的钮追。通過SelectionKey選擇鍵,不僅僅可以獲得通道的IO事件類型赞别,比方說SelectionKey.OP_READ畏陕;還可以獲得發(fā)生IO事件所在的通道;另外仿滔,也可以獲得選出選擇鍵的選擇器實(shí)例惠毁。
3.5.4 選擇器使用流程
使用選擇器,主要有以下三步:
(1)獲取選擇器實(shí)例; (2)將通道注冊到選擇器中; (3)輪詢感興趣的IO就緒事件(選擇鍵集合)崎页。
第一步:獲取選擇器實(shí)例
選擇器實(shí)例是通過調(diào)用靜態(tài)工廠方法open()來獲取的鞠绰,具體如下:
//調(diào)用靜態(tài)工廠方法open ()來獲取Selector實(shí)例
Selector selector = Selector.open() ;
Selector選擇器的類方法open()的內(nèi)部,是向選擇器SPI (SelectorProvider)發(fā)出請求飒焦,通過默認(rèn)的SelectorProvider (選擇器提供者)對象蜈膨,獲取一個(gè)新的選擇器實(shí)例。Java中SPI全稱為(Service Provider Interface,服務(wù)提供者接口)牺荠,是JDK的一種可以擴(kuò)展的服務(wù)提供和發(fā)現(xiàn)機(jī)制翁巍。Java 通過SPI的方式,提供選擇器的默認(rèn)實(shí)現(xiàn)版本休雌。也就是說灶壶,其他的服務(wù)提供商可以通過SPI的方式,提供定制化版本的選擇器的動態(tài)替換或者擴(kuò)展杈曲。
第二步:將通道注冊到選擇器實(shí)例
要實(shí)現(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 通道注冊到了一個(gè)選擇器上恰响。當(dāng)然趣钱,在注冊之前,首先要準(zhǔn)備好通道胚宦。
這里需要注意:注冊到選擇器的通道首有,必須處于非阻塞模式下,否則將拋出IllegalBlockingModeException異常枢劝。這意味著绞灼,F(xiàn)ileChannel 文件通道不能與選擇器一起使用, 因?yàn)镕ileChannel文件通道只有阻塞模式呈野,不能切換到非阻塞模式;而Socket套接字相關(guān)的所有通道都可以。
其次印叁,還需要注意: 一個(gè)通道被冒,并不一定要支持所有的四種IO事件。例如服務(wù)器監(jiān)聽通道ServerSocketChannel轮蜕,僅僅支持Accept ( 接收到新連接) IO事件昨悼;而SocketChannel傳輸通道,則不支持Accept (接收到新連接) IO事件跃洛。
如何判斷通道支持哪些事件呢?可以在注冊之前率触,可以通過通道的validOps0方法,來獲取該通道所有支持的IO事件集合汇竭。
第三步:選出感興趣的IO就緒事件(選擇鍵集合)
通過Selector選擇器的select()方法葱蝗,選出已經(jīng)注冊的、已經(jīng)就緒的IO事件,保存到SelectionKey選擇鍵集合中细燎。SelectionKey 集合保存在選擇器實(shí)例內(nèi)部两曼,是一個(gè)元素為SelectionKey類型的集合(Set)。調(diào)用選擇器的selectedKeys(方法玻驻,可以取得選擇鍵集合悼凑。
接下來,需要迭代集合的每一個(gè)選擇鍵,根據(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 () ;
}
}
處理完成后,需要將選擇鍵從這個(gè)SelectionKey集合中移除,防止下一次循環(huán)的時(shí)候膘茎,被重復(fù)的處理桃纯。SelectionKey 集合不能添加元素,如果試圖向SelectionKey 選擇鍵集合中添加元素披坏,則將拋出java.lang.UnsupportedOperationException異常态坦。
用于選擇就緒的IO事件的select()方法,有多個(gè)重載的實(shí)現(xiàn)版本棒拂,具體如下:
(1) select): 阻塞調(diào)用伞梯,一直到至少有一個(gè)通道發(fā)生了注冊的I0事件。
(2) select(long timeout);和select()-樣帚屉,但最長阻塞時(shí)間為timeout指定的毫秒數(shù)谜诫。
(3) selectNow():非阻塞,不管有沒有I0事件攻旦,都會立刻返回喻旷。
select()方法返回的整數(shù)值(int 整數(shù)類型),表示發(fā)生了IO事件的通道數(shù)量牢屋。更準(zhǔn)確地說且预,是從上一次select到這一次select 之間,有多少通道發(fā)生了IO事件烙无。強(qiáng)調(diào)一.下锋谐,select()方法返回的數(shù)量,指的是通道數(shù)截酷,而不是IO事件數(shù)涮拗,準(zhǔn)確地說,是指發(fā)生了選擇器感興趣的IO事件的通道數(shù)迂苛。
3.5.5 使用NIO實(shí)現(xiàn)Discard服務(wù)器的實(shí)踐案例
Discard服務(wù)器的功能很簡單:僅僅讀取客戶端通道的輸入數(shù)據(jù)三热,讀取完成后直接關(guān)閉客戶端通道;并且讀取到的數(shù)據(jù)直接拋棄掉( Discard)三幻。Discard 服務(wù)器足夠簡單明了康铭,作為第一個(gè)學(xué)習(xí)NIO的通信實(shí)例,較有參考價(jià)值赌髓。;
下面的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、獲取單個(gè)的選擇鍵舌仍,并處理
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();
}
}
實(shí)現(xiàn)DiscardServer 共分為16步,其中第7到第15步是循環(huán)執(zhí)行的蚣驼。不斷選擇感興趣的IO事件到選擇器的選擇鍵集合中魄幕,然后通過selector selectedKeys()獲取該選擇鍵集合,并且進(jìn)行迭代處理颖杏。對于新建立的socketChannel客戶端傳輸通道纯陨,也要注冊到同一個(gè)選擇器上,使用同一個(gè)選擇線程留储,不斷地對所有的注冊通道進(jìn)行選擇鍵的選擇队丝。
在DiscardServer程序中,涉及到兩次選擇器注冊:一次是注冊serverChannel服務(wù)器通道;另一次欲鹏, 注冊接收到的socketChannel客戶端傳輸通道。serverChannel 服務(wù)器通道注冊的臭墨,是新連接的IO事件SelectionKey.OP_ACCEPT;客戶端socketChannel傳輸通道注冊的赔嚎,是可讀I0事件SelectionKey.OP_READ。.
DiscardServer在對選擇鍵進(jìn)行處理時(shí)胧弛,通過對類型進(jìn)行判斷尤误,然后進(jìn)行相應(yīng)的處理
(1)如果是SelectionKey.OP_ACCEPT新連接事件類型,代表serverChannel服務(wù)器通道發(fā)生了新連接事件结缚,則通過服務(wù)器通道的accept方法损晤,獲取新的socketChannel 傳輸通道,并且將新通道注冊到選擇器红竭。
(2)如果是SelectionKey.OP_READ可讀事件類型尤勋,代表某個(gè)客戶端通道有數(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í)行整個(gè)程序往枣,首先要執(zhí)行前面的服務(wù)器端程序伐庭,然后執(zhí)行后面的客戶端程序。
通過Discard服務(wù)器的開發(fā)實(shí)踐分冈,大家對NIO Selector (選擇)的使用流程圾另,應(yīng)該了解得非常清楚了。
下面來看一個(gè)稍微復(fù)雜一 -點(diǎn)的案例:在服務(wù)器端接收文件和內(nèi)容雕沉。
3.5.6 使用SocketChannel在服務(wù)器端接收文件的實(shí)踐案例
本示例演示文件的接收集乔,是服務(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)一個(gè)客戶端文件
*/
static class Client {
//文件名稱
String fileName;
//長度
long fileLength;
//開始傳輸?shù)臅r(shí)間
long startTime;
//客戶端的地址
InetSocketAddress remoteAddress;
//輸出的文件通道
FileChannel outChannel;
}
private ByteBuffer buffer
= ByteBuffer.allocate(NioDemoConfig.SERVER_BUFFER_SIZE);
//使用Map保存每個(gè)客戶端傳輸丈攒,當(dāng)OP_READ通道可讀時(shí)哩罪,根據(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、獲取單個(gè)的選擇鍵捕捂,并處理
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的特點(diǎn)只會累加景殷,已選擇的鍵的集合不會刪除
// 如果不刪除,下一次又會被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)于每一個(gè)客戶端socketChannel, 創(chuàng)建一個(gè) Client 客戶端對象伊约,用于保存客戶端狀態(tài),分別保存文件名孕蝉、文件大小和寫入的目標(biāo)文件通道outChannel屡律。
socketChannel和Client對象之間是一對一的對應(yīng)關(guān)系:建立連接的時(shí)候,以socketChannel作為鍵(Key) 降淮,Client 對象作為值(Value) 超埋,將Client保存在map中。當(dāng)socketChannel傳輸通道有數(shù)據(jù)可讀時(shí)佳鳖,通過選擇鍵key.channel(方法霍殴,取出IO事件所在socketChannel 通道。然后通過socketChannel通道系吩,從map中取到對應(yīng)的Client對象来庭。
接收到數(shù)據(jù)時(shí),如果文件名為空穿挨,先處理文件名稱月弛,并把文件名保存到Client 對象,同時(shí)創(chuàng)建服務(wù)器上的目標(biāo)文件;接下來再讀到數(shù)據(jù)絮蒿,說明接收到了文件大小,把文件大小保存到Client對象;接下來再接到數(shù)據(jù)叁鉴,說明是文件內(nèi)容了土涝,則寫入Client對象的outChannel文件通道中,直到數(shù)據(jù)讀取完畢幌墓。
運(yùn)行方式:啟動這個(gè)NioReceiveServer服務(wù)器程序后但壮,再啟動前面介紹的客戶端程序NioSendClient,即可以完成文件的傳輸。
3.6 本章小結(jié)
在編程難度上常侣,Java NIO編程的難度比同步阻塞Java OIO編程大很多蜡饵。請注意,前面的實(shí)踐案例胳施,是比較簡單的溯祸,并不是復(fù)雜的通信程序,沒有看到“粘包"和“拆包”等問題。如果加上這些問題焦辅,代碼將會更加復(fù)雜博杖。
與Java OIO相比,Java NIO編程大致的特點(diǎn)如下:
(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中单匣,一個(gè)選擇器線程可以同時(shí)處理成千上萬個(gè)客戶端連接夕凝,性能不會隨著客戶端的增加而線性下降。
總之,有了Linux底層的epoll支持户秤,有了Java NIO Selector選擇器這樣的應(yīng)用層IO復(fù)用技術(shù)码秉,Java程序從而可以實(shí)現(xiàn)IO通信的高TPS、高并發(fā)鸡号,使服務(wù)器具備并發(fā)數(shù)十萬转砖、數(shù)百萬的連接能力。Java的NIO技術(shù)非常適合用于高性能鲸伴、高負(fù)載的網(wǎng)絡(luò)服務(wù)器府蔗。鼎鼎大名的通信服務(wù)器中間件Netty,就是基于Java的NIO技術(shù)實(shí)現(xiàn)的。
當(dāng)然汞窗,Java NIO技術(shù)僅僅是基礎(chǔ)姓赤,如果要實(shí)現(xiàn)通信的高性能和高并發(fā),還離不開高效率的設(shè)計(jì)模式仲吏。下--章將開始為大家介紹高性能服務(wù)必備的設(shè)計(jì)模式:Reactor反應(yīng)器模式不铆。