Java NIO提供了與標準IO不同的IO工作方式:
Channels and Buffers(通道和緩沖區(qū)):標準的IO基于字節(jié)流和字符流進行操作的,而NIO是基于通道(Channel)和緩沖區(qū)(Buffer)進行操作倔韭,數(shù)據(jù)總是從通道讀取到緩沖區(qū)中航棱,或者從緩沖區(qū)寫入到通道中。
Asynchronous IO(異步IO):Java NIO可以讓你異步的使用IO靠胜,例如:當線程從通道讀取數(shù)據(jù)到緩沖區(qū)時掉瞳,線程還是可以進行其他事情。當數(shù)據(jù)被寫入到緩沖區(qū)時浪漠,線程可以繼續(xù)處理它陕习。從緩沖區(qū)寫入通道也類似。
Selectors(選擇器):Java NIO引入了選擇器的概念郑藏,選擇器用于監(jiān)聽多個通道的事件(比如:連接打開衡查,數(shù)據(jù)到達)。因此必盖,單個的線程可以監(jiān)聽多個數(shù)據(jù)通道拌牲。
下面就來詳細介紹Java NIO的相關(guān)知識。
(本部分原文鏈接歌粥,作者:Jakob Jenkov塌忽, 譯者:airu,校對:丁一)
Java NIO 由以下幾個核心部分組成:
Channels
Buffers
Selectors
雖然Java NIO 中除此之外還有很多類和組件失驶,但在我看來土居,Channel,Buffer 和 Selector 構(gòu)成了核心的API嬉探。其它組件擦耀,如Pipe和FileLock,只不過是與三個核心組件共同使用的工具類涩堤。因此眷蜓,在概述中我將集中在這三個組件上。其它組件會在單獨的章節(jié)中講到胎围。
Channel 和 Buffer
基本上吁系,所有的 IO 在NIO 中都從一個Channel 開始。Channel 有點象流白魂。 數(shù)據(jù)可以從Channel讀到Buffer中汽纤,也可以從Buffer 寫到Channel中。這里有個圖示:
Channel和Buffer有好幾種類型福荸。下面是JAVA NIO中的一些主要Channel的實現(xiàn):
FileChannel
DatagramChannel
SocketChannel
ServerSocketChannel
正如你所看到的蕴坪,這些通道涵蓋了UDP 和 TCP 網(wǎng)絡IO,以及文件IO敬锐。
與這些類一起的有一些有趣的接口背传,但為簡單起見捆等,我盡量在概述中不提到它們。本教程其它章節(jié)與它們相關(guān)的地方我會進行解釋续室。
以下是Java NIO里關(guān)鍵的Buffer實現(xiàn):
ByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
這些Buffer覆蓋了你能通過IO發(fā)送的基本數(shù)據(jù)類型:byte, short, int, long, float, double 和 char栋烤。
Java NIO 還有個 Mappedyteuffer,用于表示內(nèi)存映射文件挺狰, 我也不打算在概述中說明明郭。
Selector
Selector允許單線程處理多個 Channel。如果你的應用打開了多個連接(通道)丰泊,但每個連接的流量都很低薯定,使用Selector就會很方便。例如瞳购,在一個聊天服務器中话侄。
這是在一個單線程中使用一個Selector處理3個Channel的圖示:
要使用Selector,得向Selector注冊Channel学赛,然后調(diào)用它的select()方法年堆。這個方法會一直阻塞到某個注冊的通道有事件就緒。一旦這個方法返回盏浇,線程就可以處理這些事件变丧,事件的例子有如新連接進來,數(shù)據(jù)接收等绢掰。
(本部分原文地址痒蓬,作者:Jakob Jenkov,譯者:郭蕾滴劲,校對:方騰飛)
當學習了Java NIO和IO的API后攻晒,一個問題馬上涌入腦海:
引用
我應該何時使用IO,何時使用NIO呢班挖?在本文中鲁捏,我會盡量清晰地解析Java NIO和IO的差異、它們的使用場景聪姿,以及它們?nèi)绾斡绊懩拇a設(shè)計碴萧。
Java NIO和IO的主要區(qū)別
下表總結(jié)了Java NIO和IO之間的主要差別乙嘀,我會更詳細地描述表中每部分的差異末购。
IONIO
Stream orientedBuffer oriented
Blocking IONon blocking IO
?Selectors
面向流與面向緩沖
Java NIO和IO之間第一個最大的區(qū)別是,IO是面向流的虎谢,NIO是面向緩沖區(qū)的盟榴。 Java IO面向流意味著每次從流中讀一個或多個字節(jié),直至讀取所有字節(jié)婴噩,它們沒有被緩存在任何地方擎场。此外羽德,它不能前后移動流中的數(shù)據(jù)。如果需要前后移動從流中讀取的數(shù)據(jù)迅办,需要先將它緩存到一個緩沖區(qū)宅静。 Java NIO的緩沖導向方法略有不同。數(shù)據(jù)讀取到一個它稍后處理的緩沖區(qū)站欺,需要時可在緩沖區(qū)中前后移動姨夹。這就增加了處理過程中的靈活性。但是矾策,還需要檢查是否該緩沖區(qū)中包含所有您需要處理的數(shù)據(jù)磷账。而且,需確保當更多的數(shù)據(jù)讀入緩沖區(qū)時贾虽,不要覆蓋緩沖區(qū)里尚未處理的數(shù)據(jù)逃糟。
阻塞與非阻塞IO
Java IO的各種流是阻塞的。這意味著蓬豁,當一個線程調(diào)用read() 或 write()時绰咽,該線程被阻塞,直到有一些數(shù)據(jù)被讀取地粪,或數(shù)據(jù)完全寫入剃诅。該線程在此期間不能再干任何事情了。 Java NIO的非阻塞模式驶忌,使一個線程從某通道發(fā)送請求讀取數(shù)據(jù)矛辕,但是它僅能得到目前可用的數(shù)據(jù),如果目前沒有數(shù)據(jù)可用時付魔,就什么都不會獲取聊品。而不是保持線程阻塞,所以直至數(shù)據(jù)變的可以讀取之前几苍,該線程可以繼續(xù)做其他的事情翻屈。 非阻塞寫也是如此。一個線程請求寫入一些數(shù)據(jù)到某通道妻坝,但不需要等待它完全寫入伸眶,這個線程同時可以去做別的事情。 線程通常將非阻塞IO的空閑時間用于在其它通道上執(zhí)行IO操作刽宪,所以一個單獨的線程現(xiàn)在可以管理多個輸入和輸出通道(channel)厘贼。
選擇器(Selectors)
Java NIO的選擇器允許一個單獨的線程來監(jiān)視多個輸入通道,你可以注冊多個通道使用一個選擇器圣拄,然后使用一個單獨的線程來“選擇”通道:這些通道里已經(jīng)有可以處理的輸入嘴秸,或者選擇已準備寫入的通道。這種選擇機制,使得一個單獨的線程很容易來管理多個通道岳掐。
NIO和IO如何影響應用程序的設(shè)計
無論您選擇IO或NIO工具箱凭疮,可能會影響您應用程序設(shè)計的以下幾個方面:
對NIO或IO類的API調(diào)用。
數(shù)據(jù)處理串述。
用來處理數(shù)據(jù)的線程數(shù)执解。
API調(diào)用
當然,使用NIO的API調(diào)用時看起來與使用IO時有所不同纲酗,但這并不意外材鹦,因為并不是僅從一個InputStream逐字節(jié)讀取,而是數(shù)據(jù)必須先讀入緩沖區(qū)再處理耕姊。
數(shù)據(jù)處理
使用純粹的NIO設(shè)計相較IO設(shè)計桶唐,數(shù)據(jù)處理也受到影響。
在IO設(shè)計中茉兰,我們從InputStream或 Reader逐字節(jié)讀取數(shù)據(jù)尤泽。假設(shè)你正在處理一基于行的文本數(shù)據(jù)流,例如:
代碼?
Name:?Anna??
Age:25
Email:?anna@mailserver.com??
Phone:1234567890
該文本行的流可以這樣處理:
Java代碼?
InputStream?input?=?…?;//?get?the?InputStream?from?the?client?socket
BufferedReader?reader?=newBufferedReader(newInputStreamReader(input));
String?nameLine???=?reader.readLine();??
String?ageLine????=?reader.readLine();??
String?emailLine??=?reader.readLine();??
String?phoneLine??=?reader.readLine();??
請注意處理狀態(tài)由程序執(zhí)行多久決定规脸。換句話說坯约,一旦reader.readLine()方法返回,你就知道肯定文本行就已讀完莫鸭, readline()阻塞直到整行讀完闹丐,這就是原因。你也知道此行包含名稱被因;同樣卿拴,第二個readline()調(diào)用返回的時候,你知道這行包含年齡等梨与。 正如你可以看到堕花,該處理程序僅在有新數(shù)據(jù)讀入時運行,并知道每步的數(shù)據(jù)是什么粥鞋。一旦正在運行的線程已處理過讀入的某些數(shù)據(jù)缘挽,該線程不會再回退數(shù)據(jù)(大多如此)。下圖也說明了這條原則:
從一個阻塞的流中讀數(shù)據(jù)
而一個NIO的實現(xiàn)會有所不同呻粹,下面是一個簡單的例子:
Java代碼?
ByteBuffer?buffer?=?ByteBuffer.allocate(48);
intbytesRead?=?inChannel.read(buffer);
注意第二行壕曼,從通道讀取字節(jié)到ByteBuffer。當這個方法調(diào)用返回時等浊,你不知道你所需的所有數(shù)據(jù)是否在緩沖區(qū)內(nèi)腮郊。你所知道的是,該緩沖區(qū)包含一些字節(jié)凿掂,這使得處理有點困難伴榔。
假設(shè)第一次 read(buffer)調(diào)用后,讀入緩沖區(qū)的數(shù)據(jù)只有半行庄萎,例如踪少,“Name:An”,你能處理數(shù)據(jù)嗎糠涛?顯然不能援奢,需要等待,直到整行數(shù)據(jù)讀入緩存忍捡,在此之前集漾,對數(shù)據(jù)的任何處理毫無意義。
所以砸脊,你怎么知道是否該緩沖區(qū)包含足夠的數(shù)據(jù)可以處理呢具篇?好了,你不知道凌埂。發(fā)現(xiàn)的方法只能查看緩沖區(qū)中的數(shù)據(jù)驱显。其結(jié)果是,在你知道所有數(shù)據(jù)都在緩沖區(qū)里之前瞳抓,你必須檢查幾次緩沖區(qū)的數(shù)據(jù)埃疫。這不僅效率低下,而且可以使程序設(shè)計方案雜亂不堪孩哑。例如:
Java代碼?
ByteBuffer?buffer?=?ByteBuffer.allocate(48);
intbytesRead?=?inChannel.read(buffer);
while(!?bufferFull(bytesRead)?)?{
bytesRead?=?inChannel.read(buffer);??
}??
bufferFull()方法必須跟蹤有多少數(shù)據(jù)讀入緩沖區(qū)栓霜,并返回真或假,這取決于緩沖區(qū)是否已滿横蜒。換句話說胳蛮,如果緩沖區(qū)準備好被處理,那么表示緩沖區(qū)滿了丛晌。
bufferFull()方法掃描緩沖區(qū)鹰霍,但必須保持在bufferFull()方法被調(diào)用之前狀態(tài)相同。如果沒有茵乱,下一個讀入緩沖區(qū)的數(shù)據(jù)可能無法讀到正確的位置茂洒。這是不可能的,但卻是需要注意的又一問題瓶竭。
如果緩沖區(qū)已滿督勺,它可以被處理。如果它不滿斤贰,并且在你的實際案例中有意義智哀,你或許能處理其中的部分數(shù)據(jù)。但是許多情況下并非如此荧恍。下圖展示了“緩沖區(qū)數(shù)據(jù)循環(huán)就緒”:
從一個通道里讀數(shù)據(jù)瓷叫,直到所有的數(shù)據(jù)都讀到緩沖區(qū)里
總結(jié)
NIO可讓您只使用一個(或幾個)單線程管理多個通道(網(wǎng)絡連接或文件)屯吊,但付出的代價是解析數(shù)據(jù)可能會比從一個阻塞流中讀取數(shù)據(jù)更復雜票从。
如果需要管理同時打開的成千上萬個連接湿痢,這些連接每次只是發(fā)送少量的數(shù)據(jù)循衰,例如聊天服務器舰攒,實現(xiàn)NIO的服務器可能是一個優(yōu)勢兜看。同樣哮伟,如果你需要維持許多打開的連接到其他計算機上亮蒋,如P2P網(wǎng)絡中献幔,使用一個單獨的線程來管理你所有出站連接煮寡,可能是一個優(yōu)勢虹蓄。一個線程多個連接的設(shè)計方案如下圖所示:
單線程管理多個連接
如果你有少量的連接使用非常高的帶寬,一次發(fā)送大量的數(shù)據(jù)幸撕,也許典型的IO服務器實現(xiàn)可能非常契合薇组。下圖說明了一個典型的IO服務器設(shè)計:
一個典型的IO服務器設(shè)計:一個連接通過一個線程處理
(本部分原文鏈接,作者:Jakob Jenkov坐儿,譯者:airu体箕,校對:丁一)
Java NIO的通道類似流,但又有些不同:
既可以從通道中讀取數(shù)據(jù)挑童,又可以寫數(shù)據(jù)到通道累铅。但流的讀寫通常是單向的。
通道可以異步地讀寫站叼。
通道中的數(shù)據(jù)總是要先讀到一個Buffer娃兽,或者總是要從一個Buffer中寫入。
正如上面所說尽楔,從通道讀取數(shù)據(jù)到緩沖區(qū)投储,從緩沖區(qū)寫入數(shù)據(jù)到通道。如下圖所示:
Channel的實現(xiàn)
這些是Java NIO中最重要的通道的實現(xiàn):
FileChannel:從文件中讀寫數(shù)據(jù)阔馋。
DatagramChannel:能通過UDP讀寫網(wǎng)絡中的數(shù)據(jù)玛荞。
SocketChannel:能通過TCP讀寫網(wǎng)絡中的數(shù)據(jù)。
ServerSocketChannel:可以監(jiān)聽新進來的TCP連接呕寝,像Web服務器那樣勋眯。對每一個新進來的連接都會創(chuàng)建一個SocketChannel。
基本的 Channel 示例
下面是一個使用FileChannel讀取數(shù)據(jù)到Buffer中的示例:
Java代碼?
RandomAccessFile?aFile?=newRandomAccessFile("data/nio-data.txt","rw");
FileChannel?inChannel?=?aFile.getChannel();??
ByteBuffer?buf?=?ByteBuffer.allocate(48);
intbytesRead?=?inChannel.read(buf);
while(bytesRead?!=?-1)?{
System.out.println("Read?"+?bytesRead);
buf.flip();??
while(buf.hasRemaining()){
System.out.print((char)?buf.get());
}??
buf.clear();??
bytesRead?=?inChannel.read(buf);??
}??
aFile.close();??
注意 buf.flip() 的調(diào)用下梢,首先讀取數(shù)據(jù)到Buffer客蹋,然后反轉(zhuǎn)Buffer,接著再從Buffer中讀取數(shù)據(jù)。下一節(jié)會深入講解Buffer的更多細節(jié)孽江。
Java NIO中的Buffer用于和NIO通道進行交互讶坯。如你所知,數(shù)據(jù)是從通道讀入緩沖區(qū)岗屏,從緩沖區(qū)寫入到通道中的辆琅。
緩沖區(qū)本質(zhì)上是一塊可以寫入數(shù)據(jù)漱办,然后可以從中讀取數(shù)據(jù)的內(nèi)存。這塊內(nèi)存被包裝成NIO Buffer對象婉烟,并提供了一組方法娩井,用來方便的訪問該塊內(nèi)存。
Buffer的基本用法
使用Buffer讀寫數(shù)據(jù)一般遵循以下四個步驟:
寫入數(shù)據(jù)到Buffer
調(diào)用flip()方法
從Buffer中讀取數(shù)據(jù)
調(diào)用clear()方法或者compact()方法
當向buffer寫入數(shù)據(jù)時隅很,buffer會記錄下寫了多少數(shù)據(jù)撞牢。一旦要讀取數(shù)據(jù)率碾,需要通過flip()方法將Buffer從寫模式切換到讀模式叔营。在讀模式下,可以讀取之前寫入到buffer的所有數(shù)據(jù)所宰。
一旦讀完了所有的數(shù)據(jù)绒尊,就需要清空緩沖區(qū),讓它可以再次被寫入仔粥。有兩種方式能清空緩沖區(qū):調(diào)用clear()或compact()方法婴谱。clear()方法會清空整個緩沖區(qū)。compact()方法只會清除已經(jīng)讀過的數(shù)據(jù)躯泰。任何未讀的數(shù)據(jù)都被移到緩沖區(qū)的起始處谭羔,新寫入的數(shù)據(jù)將放到緩沖區(qū)未讀數(shù)據(jù)的后面。
下面是一個使用Buffer的例子:
Java代碼?
RandomAccessFile?aFile?=newRandomAccessFile("data/nio-data.txt","rw");
FileChannel?inChannel?=?aFile.getChannel();??
//create?buffer?with?capacity?of?48?bytes
ByteBuffer?buf?=?ByteBuffer.allocate(48);
intbytesRead?=?inChannel.read(buf);//read?into?buffer.
while(bytesRead?!=?-1)?{
buf.flip();//make?buffer?ready?for?read
while(buf.hasRemaining()){
System.out.print((char)?buf.get());//?read?1?byte?at?a?time
??}??
buf.clear();//make?buffer?ready?for?writing
??bytesRead?=?inChannel.read(buf);??
}??
aFile.close();??
Buffer的capacity,position和limit
緩沖區(qū)本質(zhì)上是一塊可以寫入數(shù)據(jù)麦向,然后可以從中讀取數(shù)據(jù)的內(nèi)存瘟裸。這塊內(nèi)存被包裝成NIO Buffer對象,并提供了一組方法诵竭,用來方便的訪問該塊內(nèi)存话告。
為了理解Buffer的工作原理,需要熟悉它的三個屬性:
capacity
position
limit
position和limit的含義取決于Buffer處在讀模式還是寫模式卵慰。不管Buffer處在什么模式沙郭,capacity的含義總是一樣的。
這里有一個關(guān)于capacity裳朋,position和limit在讀寫模式中的說明病线,詳細的解釋在插圖后面。
capacity
作為一個內(nèi)存塊鲤嫡,Buffer有一個固定的大小值氧苍,也叫“capacity”.你只能往里寫capacity個byte、long泛范,char等類型让虐。一旦Buffer滿了,需要將其清空(通過讀數(shù)據(jù)或者清除數(shù)據(jù))才能繼續(xù)寫數(shù)據(jù)往里寫數(shù)據(jù)罢荡。
position
當你寫數(shù)據(jù)到Buffer中時赡突,position表示當前的位置对扶。初始的position值為0.當一個byte、long等數(shù)據(jù)寫到Buffer后惭缰, position會向前移動到下一個可插入數(shù)據(jù)的Buffer單元浪南。position最大可為capacity – 1。
當讀取數(shù)據(jù)時漱受,也是從某個特定位置讀络凿。當將Buffer從寫模式切換到讀模式,position會被重置為0昂羡。當從Buffer的position處讀取數(shù)據(jù)時絮记,position向前移動到下一個可讀的位置。
limit
在寫模式下虐先,Buffer的limit表示你最多能往Buffer里寫多少數(shù)據(jù)怨愤。 寫模式下,limit等于Buffer的capacity蛹批。
當切換Buffer到讀模式時撰洗, limit表示你最多能讀到多少數(shù)據(jù)。因此腐芍,當切換Buffer到讀模式時差导,limit會被設(shè)置成寫模式下的position值。換句話說猪勇,你能讀到之前寫入的所有數(shù)據(jù)(limit被設(shè)置成已寫數(shù)據(jù)的數(shù)量设褐,這個值在寫模式下就是position)
Buffer的類型
Java NIO 有以下Buffer類型:
ByteBuffer
MappedByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
如你所見,這些Buffer類型代表了不同的數(shù)據(jù)類型埠对。換句話說络断,就是可以通過char,short项玛,int貌笨,long,float 或 double類型來操作緩沖區(qū)中的字節(jié)襟沮。
MappedByteBuffer 有些特別锥惋,在涉及它的專門章節(jié)中再講。
Buffer的分配
要想獲得一個Buffer對象首先要進行分配开伏。 每一個Buffer類都有一個allocate方法膀跌。下面是一個分配48字節(jié)capacity的ByteBuffer的例子。
Java代碼?
ByteBuffer?buf?=?ByteBuffer.allocate(48);
這是分配一個可存儲1024個字符的CharBuffer:
Java代碼?
CharBuffer?buf?=?CharBuffer.allocate(1024);
向Buffer中寫數(shù)據(jù)
寫數(shù)據(jù)到Buffer有兩種方式:
從Channel寫到Buffer固灵。
通過Buffer的put()方法寫到Buffer里捅伤。
從Channel寫到Buffer的例子
Java代碼?
intbytesRead?=?inChannel.read(buf);//read?into?buffer.
通過put方法寫B(tài)uffer的例子:
Java代碼?
buf.put(127);
put方法有很多版本,允許你以不同的方式把數(shù)據(jù)寫入到Buffer中巫玻。例如丛忆, 寫到一個指定的位置祠汇,或者把一個字節(jié)數(shù)組寫入到Buffer。 更多Buffer實現(xiàn)的細節(jié)參考JavaDoc熄诡。
flip()方法
flip方法將Buffer從寫模式切換到讀模式可很。調(diào)用flip()方法會將position設(shè)回0,并將limit設(shè)置成之前position的值凰浮。
換句話說我抠,position現(xiàn)在用于標記讀的位置,limit表示之前寫進了多少個byte袜茧、char等 —— 現(xiàn)在能讀取多少個byte菜拓、char等。
從Buffer中讀取數(shù)據(jù)
從Buffer中讀取數(shù)據(jù)有兩種方式:
從Buffer讀取數(shù)據(jù)到Channel惫周。
使用get()方法從Buffer中讀取數(shù)據(jù)尘惧。
從Buffer讀取數(shù)據(jù)到Channel的例子:
Java代碼?
//read?from?buffer?into?channel.
intbytesWritten?=?inChannel.write(buf);
使用get()方法從Buffer中讀取數(shù)據(jù)的例子
Java代碼?
byteaByte?=?buf.get();
get方法有很多版本康栈,允許你以不同的方式從Buffer中讀取數(shù)據(jù)递递。例如,從指定position讀取啥么,或者從Buffer中讀取數(shù)據(jù)到字節(jié)數(shù)組登舞。更多Buffer實現(xiàn)的細節(jié)參考JavaDoc。
rewind()方法
Buffer.rewind()將position設(shè)回0悬荣,所以你可以重讀Buffer中的所有數(shù)據(jù)菠秒。limit保持不變,仍然表示能從Buffer中讀取多少個元素(byte氯迂、char等)践叠。
clear()與compact()方法
一旦讀完Buffer中的數(shù)據(jù),需要讓Buffer準備好再次被寫入嚼蚀〗疲可以通過clear()或compact()方法來完成。
如果調(diào)用的是clear()方法轿曙,position將被設(shè)回0弄捕,limit被設(shè)置成 capacity的值。換句話說导帝,Buffer 被清空了守谓。Buffer中的數(shù)據(jù)并未清除,只是這些標記告訴我們可以從哪里開始往Buffer里寫數(shù)據(jù)您单。
如果Buffer中有一些未讀的數(shù)據(jù)斋荞,調(diào)用clear()方法,數(shù)據(jù)將“被遺忘”虐秦,意味著不再有任何標記會告訴你哪些數(shù)據(jù)被讀過平酿,哪些還沒有讯檐。
如果Buffer中仍有未讀的數(shù)據(jù),且后續(xù)還需要這些數(shù)據(jù)染服,但是此時想要先先寫些數(shù)據(jù)别洪,那么使用compact()方法。
compact()方法將所有未讀的數(shù)據(jù)拷貝到Buffer起始處柳刮。然后將position設(shè)到最后一個未讀元素正后面挖垛。limit屬性依然像clear()方法一樣,設(shè)置成capacity”牛現(xiàn)在Buffer準備好寫數(shù)據(jù)了痢毒,但是不會覆蓋未讀的數(shù)據(jù)。
mark()與reset()方法
通過調(diào)用Buffer.mark()方法蚕甥,可以標記Buffer中的一個特定position哪替。之后可以通過調(diào)用Buffer.reset()方法恢復到這個position。例如:
Java代碼?
buffer.mark();??
//call?buffer.get()?a?couple?of?times,?e.g.?during?parsing.
buffer.reset();//set?position?back?to?mark.
equals()與compareTo()方法
可以使用equals()和compareTo()方法兩個Buffer菇怀。
equals()
當滿足下列條件時凭舶,表示兩個Buffer相等:
有相同的類型(byte、char爱沟、int等)帅霜。
Buffer中剩余的byte、char等的個數(shù)相等呼伸。
Buffer中所有剩余的byte身冀、char等都相同。
如你所見括享,equals只是比較Buffer的一部分搂根,不是每一個在它里面的元素都比較。實際上铃辖,它只比較Buffer中的剩余元素剩愧。
compareTo()方法
compareTo()方法比較兩個Buffer的剩余元素(byte、char等)澳叉, 如果滿足下列條件隙咸,則認為一個Buffer“小于”另一個Buffer:
第一個不相等的元素小于另一個Buffer中對應的元素。
所有元素都相等成洗,但第一個Buffer比另一個先耗盡(第一個Buffer的元素個數(shù)比另一個少)五督。
(譯注:剩余元素是從 position到limit之間的元素)
(本部分原文地址,作者:Jakob Jenkov?? 譯者:郭蕾)
Java NIO開始支持scatter/gather瓶殃,scatter/gather用于描述從Channel(譯者注:Channel在中文經(jīng)常翻譯為通道)中讀取或者寫入到Channel的操作充包。
分散(scatter)從Channel中讀取是指在讀操作時將讀取的數(shù)據(jù)寫入多個buffer中。因此,Channel將從Channel中讀取的數(shù)據(jù)“分散(scatter)”到多個Buffer中基矮。
聚集(gather)寫入Channel是指在寫操作時將多個buffer的數(shù)據(jù)寫入同一個Channel淆储,因此,Channel 將多個Buffer中的數(shù)據(jù)“聚集(gather)”后發(fā)送到Channel家浇。
scatter / gather經(jīng)常用于需要將傳輸?shù)臄?shù)據(jù)分開處理的場合本砰,例如傳輸一個由消息頭和消息體組成的消息,你可能會將消息體和消息頭分散到不同的buffer中钢悲,這樣你可以方便的處理消息頭和消息體点额。
Scattering Reads
Scattering Reads是指數(shù)據(jù)從一個channel讀取到多個buffer中。如下圖描述:
代碼示例如下:
Java代碼?
ByteBuffer?header?=?ByteBuffer.allocate(128);
ByteBuffer?body???=?ByteBuffer.allocate(1024);
ByteBuffer[]?bufferArray?=?{?header,?body?};??
channel.read(bufferArray);??
注意buffer首先被插入到數(shù)組莺琳,然后再將數(shù)組作為channel.read() 的輸入?yún)?shù)还棱。read()方法按照buffer在數(shù)組中的順序?qū)腸hannel中讀取的數(shù)據(jù)寫入到buffer,當一個buffer被寫滿后惭等,channel緊接著向另一個buffer中寫珍手。
Scattering Reads在移動下一個buffer前,必須填滿當前的buffer辞做,這也意味著它不適用于動態(tài)消息(譯者注:消息大小不固定)琳要。換句話說,如果存在消息頭和消息體凭豪,消息頭必須完成填充(例如 128byte)焙蹭,Scattering Reads才能正常工作晒杈。
Gathering Writes
Gathering Writes是指數(shù)據(jù)從多個buffer寫入到同一個channel嫂伞。如下圖描述:
代碼示例如下:
Java代碼?
ByteBuffer?header?=?ByteBuffer.allocate(128);
ByteBuffer?body???=?ByteBuffer.allocate(1024);
//write?data?into?buffers
ByteBuffer[]?bufferArray?=?{?header,?body?};??
channel.write(bufferArray);??
buffers數(shù)組是write()方法的入?yún)ⅲ瑆rite()方法會按照buffer在數(shù)組中的順序拯钻,將數(shù)據(jù)寫入到channel帖努,注意只有position和limit之間的數(shù)據(jù)才會被寫入。因此粪般,如果一個buffer的容量為128byte拼余,但是僅僅包含58byte的數(shù)據(jù),那么這58byte的數(shù)據(jù)將被寫入到channel中亩歹。因此與Scattering Reads相反匙监,Gathering Writes能較好的處理動態(tài)消息。
(本部分原文地址小作,作者:Jakob Jenkov亭姥,譯者:郭蕾,校對:周泰)
在Java NIO中顾稀,如果兩個通道中有一個是FileChannel达罗,那你可以直接將數(shù)據(jù)從一個channel(譯者注:channel中文常譯作通道)傳輸?shù)搅硗庖粋€channel。
transferFrom()
FileChannel的transferFrom()方法可以將數(shù)據(jù)從源通道傳輸?shù)紽ileChannel中(譯者注:這個方法在JDK文檔中的解釋為將字節(jié)從給定的可讀取字節(jié)通道傳輸?shù)酱送ǖ赖奈募校┚哺选O旅媸且粋€簡單的例子:
Java代碼?
RandomAccessFile?fromFile?=newRandomAccessFile("fromFile.txt","rw");
FileChannel??????fromChannel?=?fromFile.getChannel();??
RandomAccessFile?toFile?=newRandomAccessFile("toFile.txt","rw");
FileChannel??????toChannel?=?toFile.getChannel();??
longposition?=0;
longcount?=?fromChannel.size();
toChannel.transferFrom(position,?count,?fromChannel);??
方法的輸入?yún)?shù)position表示從position處開始向目標文件寫入數(shù)據(jù)粮揉,count表示最多傳輸?shù)淖止?jié)數(shù)巡李。如果源通道的剩余空間小于 count 個字節(jié),則所傳輸?shù)淖止?jié)數(shù)要小于請求的字節(jié)數(shù)扶认。
此外要注意侨拦,在SoketChannel的實現(xiàn)中,SocketChannel只會傳輸此刻準備好的數(shù)據(jù)(可能不足count字節(jié))辐宾。因此阳谍,SocketChannel可能不會將請求的所有數(shù)據(jù)(count個字節(jié))全部傳輸?shù)紽ileChannel中。
transferTo()
transferTo()方法將數(shù)據(jù)從FileChannel傳輸?shù)狡渌腸hannel中螃概。下面是一個簡單的例子:
Java代碼?
RandomAccessFile?fromFile?=newRandomAccessFile("fromFile.txt","rw");
FileChannel??????fromChannel?=?fromFile.getChannel();??
RandomAccessFile?toFile?=newRandomAccessFile("toFile.txt","rw");
FileChannel??????toChannel?=?toFile.getChannel();??
longposition?=0;
longcount?=?fromChannel.size();
fromChannel.transferTo(position,?count,?toChannel);??
是不是發(fā)現(xiàn)這個例子和前面那個例子特別相似矫夯?除了調(diào)用方法的FileChannel對象不一樣外,其他的都一樣吊洼。
上面所說的關(guān)于SocketChannel的問題在transferTo()方法中同樣存在训貌。SocketChannel會一直傳輸數(shù)據(jù)直到目標buffer被填滿。
(本部分原文鏈接冒窍,作者:Jakob Jenkov递沪,譯者:浪跡v,校對:丁一)
Selector(選擇器)是Java NIO中能夠檢測一到多個NIO通道综液,并能夠知曉通道是否為諸如讀寫事件做好準備的組件款慨。這樣,一個單獨的線程可以管理多個channel谬莹,從而管理多個網(wǎng)絡連接檩奠。
(1)? 為什么使用Selector?
僅用單個線程來處理多個Channels的好處是,只需要更少的線程來處理通道附帽。事實上埠戳,可以只用一個線程處理所有的通道。對于操作系統(tǒng)來說蕉扮,線程之間上下文切換的開銷很大整胃,而且每個線程都要占用系統(tǒng)的一些資源(如內(nèi)存)。因此喳钟,使用的線程越少越好屁使。
但是,需要記住奔则,現(xiàn)代的操作系統(tǒng)和CPU在多任務方面表現(xiàn)的越來越好蛮寂,所以多線程的開銷隨著時間的推移,變得越來越小了应狱。實際上共郭,如果一個CPU有多個內(nèi)核,不使用多任務可能是在浪費CPU能力。不管怎么說除嘹,關(guān)于那種設(shè)計的討論應該放在另一篇不同的文章中写半。在這里,只要知道使用Selector能夠處理多個通道就足夠了尉咕。
下面是單線程使用一個Selector處理3個channel的示例圖:
(2)? Selector的創(chuàng)建
通過調(diào)用Selector.open()方法創(chuàng)建一個Selector叠蝇,如下:
Java代碼?
Selector?selector?=?Selector.open();??
(3) 向Selector注冊通道
為了將Channel和Selector配合使用,必須將channel注冊到selector上年缎。通過SelectableChannel.register()方法來實現(xiàn)悔捶,如下:
Java代碼?
channel.configureBlocking(false);
SelectionKey?key?=?channel.register(selector,??
????Selectionkey.OP_READ);??
與Selector一起使用時,Channel必須處于非阻塞模式下单芜。這意味著不能將FileChannel與Selector一起使用蜕该,因為FileChannel不能切換到非阻塞模式。而套接字通道都可以洲鸠。
注意register()方法的第二個參數(shù)堂淡。這是一個“interest集合”,意思是在通過Selector監(jiān)聽Channel時對什么事件感興趣扒腕【畹恚可以監(jiān)聽四種不同類型的事件:
Connect
Accept
Read
Write
通道觸發(fā)了一個事件意思是該事件已經(jīng)就緒。所以瘾腰,某個channel成功連接到另一個服務器稱為“連接就緒”皆的。一個server socket channel準備好接收新進入的連接稱為“接收就緒”。一個有數(shù)據(jù)可讀的通道可以說是“讀就緒”蹋盆。等待寫數(shù)據(jù)的通道可以說是“寫就緒”费薄。
這四種事件用SelectionKey的四個常量來表示:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
如果你對不止一種事件感興趣,那么可以用“位或”操作符將常量連接起來怪嫌,如下:
Java代碼?
intinterestSet?=?SelectionKey.OP_READ?|?SelectionKey.OP_WRITE;
在下面還會繼續(xù)提到interest集合义锥。
(4)? SelectionKey
在上一小節(jié)中,當向Selector注冊Channel時岩灭,register()方法會返回一個SelectionKey對象。這個對象包含了一些你感興趣的屬性:
interest集合
ready集合
Channel
Selector
附加的對象(可選)
下面我會描述這些屬性赂鲤。
interest集合
就像向Selector注冊通道一節(jié)中所描述的噪径,interest集合是你所選擇的感興趣的事件集合∈酰可以通過SelectionKey讀寫interest集合找爱,像這樣:
Java代碼?
intinterestSet?=?selectionKey.interestOps();
booleanisInterestedInAccept??=?(interestSet?&?SelectionKey.OP_ACCEPT)?==?SelectionKey.OP_ACCEPT;
booleanisInterestedInConnect?=?interestSet?&?SelectionKey.OP_CONNECT;
booleanisInterestedInRead????=?interestSet?&?SelectionKey.OP_READ;
booleanisInterestedInWrite???=?interestSet?&?SelectionKey.OP_WRITE;
可以看到泡孩,用“位與”操作interest 集合和給定的SelectionKey常量车摄,可以確定某個確定的事件是否在interest 集合中。
ready集合
ready 集合是通道已經(jīng)準備就緒的操作的集合。在一次選擇(Selection)之后吮播,你會首先訪問這個ready set变屁。Selection將在下一小節(jié)進行解釋∫夂荩可以這樣訪問ready集合:
int readySet = selectionKey.readyOps();
可以用像檢測interest集合那樣的方法粟关,來檢測channel中什么事件或操作已經(jīng)就緒。但是环戈,也可以使用以下四個方法闷板,它們都會返回一個布爾類型:
Java代碼?
selectionKey.isAcceptable();??
selectionKey.isConnectable();??
selectionKey.isReadable();??
selectionKey.isWritable();??
Channel + Selector
從SelectionKey訪問Channel和Selector很簡單。如下:
Java代碼?
Channel??channel??=?selectionKey.channel();??
Selector?selector?=?selectionKey.selector();??
附加的對象
可以將一個對象或者更多信息附著到SelectionKey上院塞,這樣就能方便的識別某個給定的通道遮晚。例如,可以附加 與通道一起使用的Buffer拦止,或是包含聚集數(shù)據(jù)的某個對象鹏漆。使用方法如下:
Java代碼?
selectionKey.attach(theObject);??
Object?attachedObj?=?selectionKey.attachment();??
還可以在用register()方法向Selector注冊Channel的時候附加對象。如:
Java代碼?
SelectionKey?key?=?channel.register(selector,?SelectionKey.OP_READ,?theObject);??
(5)? 通過Selector選擇通道
一旦向Selector注冊了一或多個通道创泄,就可以調(diào)用幾個重載的select()方法艺玲。這些方法返回你所感興趣的事件(如連接、接受鞠抑、讀或?qū)懀┮呀?jīng)準備就緒的那些通道饭聚。換句話說,如果你對“讀就緒”的通道感興趣搁拙,select()方法會返回讀事件已經(jīng)就緒的那些通道秒梳。
下面是select()方法:
int select()
int select(long timeout)
int selectNow()
select()阻塞到至少有一個通道在你注冊的事件上就緒了。
select(long timeout)和select()一樣箕速,除了最長會阻塞timeout毫秒(參數(shù))青责。
selectNow()不會阻塞,不管什么通道就緒都立刻返回(譯者注:此方法執(zhí)行非阻塞的選擇操作棍苹。如果自從前一次選擇操作后肤视,沒有通道變成可選擇的,則此方法直接返回零字柠。)探越。
select()方法返回的int值表示有多少通道已經(jīng)就緒。亦即窑业,自上次調(diào)用select()方法后有多少通道變成就緒狀態(tài)钦幔。如果調(diào)用select()方法,因為有一個通道變成就緒狀態(tài)常柄,返回了1鲤氢,若再次調(diào)用select()方法搀擂,如果另一個通道就緒了,它會再次返回1卷玉。如果對第一個就緒的channel沒有做任何操作哨颂,現(xiàn)在就有兩個就緒的通道,但在每次select()方法調(diào)用之間揍庄,只有一個通道就緒了咆蒿。
selectedKeys()
一旦調(diào)用了select()方法,并且返回值表明有一個或更多個通道就緒了蚂子,然后可以通過調(diào)用selector的selectedKeys()方法沃测,訪問“已選擇鍵集(selected key set)”中的就緒通道。如下所示:
Java代碼?
Set?selectedKeys?=?selector.selectedKeys();??
當像Selector注冊Channel時食茎,Channel.register()方法會返回一個SelectionKey 對象蒂破。這個對象代表了注冊到該Selector的通道”鹩妫可以通過SelectionKey的selectedKeySet()方法訪問這些對象附迷。
可以遍歷這個已選擇的鍵集合來訪問就緒的通道。如下:
Java代碼?
Set?selectedKeys?=?selector.selectedKeys();??
Iterator?keyIterator?=?selectedKeys.iterator();??
while(keyIterator.hasNext())?{
????SelectionKey?key?=?keyIterator.next();??
if(key.isAcceptable())?{
//?a?connection?was?accepted?by?a?ServerSocketChannel.
}elseif(key.isConnectable())?{
//?a?connection?was?established?with?a?remote?server.
}elseif(key.isReadable())?{
//?a?channel?is?ready?for?reading
}elseif(key.isWritable())?{
//?a?channel?is?ready?for?writing
????}??
keyIterator.remove();
}??
這個循環(huán)遍歷已選擇鍵集中的每個鍵哎媚,并檢測各個鍵所對應的通道的就緒事件喇伯。
注意每次迭代末尾的keyIterator.remove()調(diào)用。Selector不會自己從已選擇鍵集中移除SelectionKey實例拨与。必須在處理完通道時自己移除稻据。下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中买喧。
SelectionKey.channel()方法返回的通道需要轉(zhuǎn)型成你要處理的類型捻悯,如ServerSocketChannel或SocketChannel等。
(6)? wakeUp()
某個線程調(diào)用select()方法后阻塞了淤毛,即使沒有通道已經(jīng)就緒今缚,也有辦法讓其從select()方法返回。只要讓其它線程在第一個線程調(diào)用select()方法的那個對象上調(diào)用Selector.wakeup()方法即可低淡。阻塞在select()方法上的線程會立馬返回姓言。
如果有其它線程調(diào)用了wakeup()方法,但當前沒有線程阻塞在select()方法上查牌,下個調(diào)用select()方法的線程會立即“醒來(wake up)”事期。
(7)? close()
用完Selector后調(diào)用其close()方法會關(guān)閉該Selector,且使注冊到該Selector上的所有SelectionKey實例無效纸颜。通道本身并不會關(guān)閉。
(8)? 完整的示例
這里有一個完整的示例绎橘,打開一個Selector胁孙,注冊一個通道注冊到這個Selector上(通道的初始化過程略去),然后持續(xù)監(jiān)控這個Selector的四種事件(接受唠倦,連接,讀涮较,寫)是否就緒稠鼻。
Java代碼?
Selector?selector?=?Selector.open();??
channel.configureBlocking(false);
SelectionKey?key?=?channel.register(selector,?SelectionKey.OP_READ);??
while(true)?{
intreadyChannels?=?selector.select();
if(readyChannels?==0)continue;
??Set?selectedKeys?=?selector.selectedKeys();??
??Iterator?keyIterator?=?selectedKeys.iterator();??
while(keyIterator.hasNext())?{
????SelectionKey?key?=?keyIterator.next();??
if(key.isAcceptable())?{
//?a?connection?was?accepted?by?a?ServerSocketChannel.
}elseif(key.isConnectable())?{
//?a?connection?was?established?with?a?remote?server.
}elseif(key.isReadable())?{
//?a?channel?is?ready?for?reading
}elseif(key.isWritable())?{
//?a?channel?is?ready?for?writing
????}??
keyIterator.remove();
??}??
}??
(本部分原文鏈接,作者:Jakob Jenkov狂票,譯者:周泰候齿,校對:丁一)
Java NIO中的FileChannel是一個連接到文件的通道」胧簦可以通過文件通道讀寫文件慌盯。
FileChannel無法設(shè)置為非阻塞模式,它總是運行在阻塞模式下掂器。
打開FileChannel
在使用FileChannel之前亚皂,必須先打開它。但是国瓮,我們無法直接打開一個FileChannel灭必,需要通過使用一個InputStream、OutputStream或RandomAccessFile來獲取一個FileChannel實例乃摹。下面是通過RandomAccessFile打開FileChannel的示例:
Java代碼?
RandomAccessFile?aFile?=newRandomAccessFile("data/nio-data.txt","rw");
FileChannel?inChannel?=?aFile.getChannel();??
從FileChannel讀取數(shù)據(jù)
調(diào)用多個read()方法之一從FileChannel中讀取數(shù)據(jù)禁漓。如:
Java代碼?
ByteBuffer?buf?=?ByteBuffer.allocate(48);
intbytesRead?=?inChannel.read(buf);
首先,分配一個Buffer孵睬。從FileChannel中讀取的數(shù)據(jù)將被讀到Buffer中播歼。
然后,調(diào)用FileChannel.read()方法肪康。該方法將數(shù)據(jù)從FileChannel讀取到Buffer中荚恶。read()方法返回的int值表示了有多少字節(jié)被讀到了Buffer中。如果返回-1磷支,表示到了文件末尾谒撼。
向FileChannel寫數(shù)據(jù)
使用FileChannel.write()方法向FileChannel寫數(shù)據(jù),該方法的參數(shù)是一個Buffer雾狈。如:
Java代碼?
String?newData?="New?String?to?write?to?file..."+?System.currentTimeMillis();
ByteBuffer?buf?=?ByteBuffer.allocate(48);
buf.clear();??
buf.put(newData.getBytes());??
buf.flip();??
while(buf.hasRemaining())?{
????channel.write(buf);??
}??
注意FileChannel.write()是在while循環(huán)中調(diào)用的廓潜。因為無法保證write()方法一次能向FileChannel寫入多少字節(jié),因此需要重復調(diào)用write()方法善榛,直到Buffer中已經(jīng)沒有尚未寫入通道的字節(jié)辩蛋。
關(guān)閉FileChannel
用完FileChannel后必須將其關(guān)閉。如:
Java代碼?
channel.close();??
FileChannel的position方法
有時可能需要在FileChannel的某個特定位置進行數(shù)據(jù)的讀/寫操作移盆〉吭海可以通過調(diào)用position()方法獲取FileChannel的當前位置。
也可以通過調(diào)用position(long pos)方法設(shè)置FileChannel的當前位置咒循。
這里有兩個例子:
Java代碼?
longpos?=?channel.position();
channel.position(pos?+123);
如果將位置設(shè)置在文件結(jié)束符之后据途,然后試圖從文件通道中讀取數(shù)據(jù)绞愚,讀方法將返回-1 —— 文件結(jié)束標志。
如果將位置設(shè)置在文件結(jié)束符之后颖医,然后向通道中寫數(shù)據(jù)位衩,文件將撐大到當前位置并寫入數(shù)據(jù)。這可能導致“文件空洞”熔萧,磁盤上物理文件中寫入的數(shù)據(jù)間有空隙糖驴。
FileChannel的size方法
FileChannel實例的size()方法將返回該實例所關(guān)聯(lián)文件的大小。如:
Java代碼?
longfileSize?=?channel.size();
FileChannel的truncate方法
可以使用FileChannel.truncate()方法截取一個文件佛致。截取文件時贮缕,文件將中指定長度后面的部分將被刪除。如:
Java代碼?
channel.truncate(1024);
這個例子截取文件的前1024個字節(jié)晌杰。
FileChannel的force方法
FileChannel.force()方法將通道里尚未寫入磁盤的數(shù)據(jù)強制寫到磁盤上跷睦。出于性能方面的考慮,操作系統(tǒng)會將數(shù)據(jù)緩存在內(nèi)存中肋演,所以無法保證寫入到FileChannel里的數(shù)據(jù)一定會即時寫到磁盤上抑诸。要保證這一點,需要調(diào)用force()方法爹殊。
force()方法有一個boolean類型的參數(shù)蜕乡,指明是否同時將文件元數(shù)據(jù)(權(quán)限信息等)寫到磁盤上。
下面的例子同時將文件數(shù)據(jù)和元數(shù)據(jù)強制寫到磁盤上:
Java代碼?
channel.force(true);
(本部分原文鏈接梗夸,作者:Jakob Jenkov层玲,譯者:鄭玉婷,校對:丁一)
Java NIO中的SocketChannel是一個連接到TCP網(wǎng)絡套接字的通道反症⌒量椋可以通過以下2種方式創(chuàng)建SocketChannel:
打開一個SocketChannel并連接到互聯(lián)網(wǎng)上的某臺服務器。
一個新連接到達ServerSocketChannel時铅碍,會創(chuàng)建一個SocketChannel润绵。
打開 SocketChannel
下面是SocketChannel的打開方式:
Java代碼?
SocketChannel?socketChannel?=?SocketChannel.open();??
socketChannel.connect(newInetSocketAddress("http://jenkov.com",80));
關(guān)閉 SocketChannel
當用完SocketChannel之后調(diào)用SocketChannel.close()關(guān)閉SocketChannel:
Java代碼?
socketChannel.close();??
從 SocketChannel 讀取數(shù)據(jù)
要從SocketChannel中讀取數(shù)據(jù),調(diào)用一個read()的方法之一胞谈。以下是例子:
Java代碼?
ByteBuffer?buf?=?ByteBuffer.allocate(48);
intbytesRead?=?socketChannel.read(buf);
首先尘盼,分配一個Buffer。從SocketChannel讀取到的數(shù)據(jù)將會放到這個Buffer中烦绳。
然后卿捎,調(diào)用SocketChannel.read()。該方法將數(shù)據(jù)從SocketChannel 讀到Buffer中径密。read()方法返回的int值表示讀了多少字節(jié)進Buffer里午阵。如果返回的是-1,表示已經(jīng)讀到了流的末尾(連接關(guān)閉了)享扔。
寫入 SocketChannel
寫數(shù)據(jù)到SocketChannel用的是SocketChannel.write()方法趟庄,該方法以一個Buffer作為參數(shù)括细。示例如下:
Java代碼?
String?newData?="New?String?to?write?to?file..."+?System.currentTimeMillis();
ByteBuffer?buf?=?ByteBuffer.allocate(48);
buf.clear();??
buf.put(newData.getBytes());??
buf.flip();??
while(buf.hasRemaining())?{
????channel.write(buf);??
}??
注意SocketChannel.write()方法的調(diào)用是在一個while循環(huán)中的伪很。Write()方法無法保證能寫多少字節(jié)到SocketChannel戚啥。所以,我們重復調(diào)用write()直到Buffer沒有要寫的字節(jié)為止锉试。
非阻塞模式
可以設(shè)置 SocketChannel 為非阻塞模式(non-blocking mode).設(shè)置之后猫十,就可以在異步模式下調(diào)用connect(), read() 和write()了。
connect()
如果SocketChannel在非阻塞模式下呆盖,此時調(diào)用connect()拖云,該方法可能在連接建立之前就返回了。為了確定連接是否建立应又,可以調(diào)用finishConnect()的方法宙项。像這樣:
Java代碼?
socketChannel.configureBlocking(false);
socketChannel.connect(newInetSocketAddress("http://jenkov.com",80));
while(!?socketChannel.finishConnect()?){
//wait,?or?do?something?else...
}??
write()
非阻塞模式下,write()方法在尚未寫出任何內(nèi)容時可能就返回了株扛。所以需要在循環(huán)中調(diào)用write()尤筐。前面已經(jīng)有例子了,這里就不贅述了洞就。
read()
非阻塞模式下,read()方法在尚未讀取到任何數(shù)據(jù)時可能就返回了盆繁。所以需要關(guān)注它的int返回值,它會告訴你讀取了多少字節(jié)旬蟋。
非阻塞模式與選擇器
非阻塞模式與選擇器搭配會工作的更好油昂,通過將一或多個SocketChannel注冊到Selector,可以詢問選擇器哪個通道已經(jīng)準備好了讀取倾贰,寫入等冕碟。Selector與SocketChannel的搭配使用會在后面詳講。
(本部分原文鏈接匆浙,作者:Jakob Jenkov安寺,譯者:鄭玉婷,校對:丁一)
Java NIO中的 ServerSocketChannel 是一個可以監(jiān)聽新進來的TCP連接的通道吞彤,就像標準IO中的ServerSocket一樣我衬。ServerSocketChannel類在 java.nio.channels包中。
這里有個例子:
Java代碼?
ServerSocketChannel?serverSocketChannel?=?ServerSocketChannel.open();??
serverSocketChannel.socket().bind(newInetSocketAddress(9999));
while(true){
????SocketChannel?socketChannel?=??
????????????serverSocketChannel.accept();??
//do?something?with?socketChannel...
}??
打開 ServerSocketChannel
通過調(diào)用 ServerSocketChannel.open() 方法來打開ServerSocketChannel.如:
Java代碼?
ServerSocketChannel?serverSocketChannel?=?ServerSocketChannel.open();??
關(guān)閉 ServerSocketChannel
通過調(diào)用ServerSocketChannel.close() 方法來關(guān)閉ServerSocketChannel. 如:
Java代碼?
serverSocketChannel.close();??
監(jiān)聽新進來的連接
通過 ServerSocketChannel.accept() 方法監(jiān)聽新進來的連接饰恕。當 accept()方法返回的時候挠羔,它返回一個包含新進來的連接的 SocketChannel。因此埋嵌,accept()方法會一直阻塞到有新連接到達破加。
通常不會僅僅只監(jiān)聽一個連接,在while循環(huán)中調(diào)用 accept()方法. 如下面的例子:
Java代碼?
while(true){
????SocketChannel?socketChannel?=??
????????????serverSocketChannel.accept();??
//do?something?with?socketChannel...
}??
當然雹嗦,也可以在while循環(huán)中使用除了true以外的其它退出準則范舀。
非阻塞模式
ServerSocketChannel可以設(shè)置成非阻塞模式合是。在非阻塞模式下,accept() 方法會立刻返回锭环,如果還沒有新進來的連接聪全,返回的將是null。 因此辅辩,需要檢查返回的SocketChannel是否是null难礼。如:
Java代碼?
ServerSocketChannel?serverSocketChannel?=?ServerSocketChannel.open();??
serverSocketChannel.socket().bind(newInetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
while(true){
????SocketChannel?socketChannel?=??
????????????serverSocketChannel.accept();??
if(socketChannel?!=null){
//do?something?with?socketChannel...
????}??
}??
(本部分原文鏈接,作者:Jakob Jenkov玫锋,譯者:鄭玉婷蛾茉,校對:丁一)
Java NIO中的DatagramChannel是一個能收發(fā)UDP包的通道。因為UDP是無連接的網(wǎng)絡協(xié)議撩鹿,所以不能像其它通道那樣讀取和寫入谦炬。它發(fā)送和接收的是數(shù)據(jù)包。
打開 DatagramChannel
下面是 DatagramChannel 的打開方式:
Java代碼?
DatagramChannel?channel?=?DatagramChannel.open();??
channel.socket().bind(newInetSocketAddress(9999));
這個例子打開的 DatagramChannel可以在UDP端口9999上接收數(shù)據(jù)包节沦。
接收數(shù)據(jù)
通過receive()方法從DatagramChannel接收數(shù)據(jù)键思,如:
Java代碼?
ByteBuffer?buf?=?ByteBuffer.allocate(48);
buf.clear();??
channel.receive(buf);??
receive()方法會將接收到的數(shù)據(jù)包內(nèi)容復制到指定的Buffer. 如果Buffer容不下收到的數(shù)據(jù),多出的數(shù)據(jù)將被丟棄散劫。
發(fā)送數(shù)據(jù)
通過send()方法從DatagramChannel發(fā)送數(shù)據(jù)稚机,如:
Java代碼?
String?newData?="New?String?to?write?to?file..."+?System.currentTimeMillis();
ByteBuffer?buf?=?ByteBuffer.allocate(48);
buf.clear();??
buf.put(newData.getBytes());??
buf.flip();??
intbytesSent?=?channel.send(buf,newInetSocketAddress("jenkov.com",80));
這個例子發(fā)送一串字符到”jenkov.com”服務器的UDP端口80。 因為服務端并沒有監(jiān)控這個端口获搏,所以什么也不會發(fā)生赖条。也不會通知你發(fā)出的數(shù)據(jù)包是否已收到,因為UDP在數(shù)據(jù)傳送方面沒有任何保證常熙。
連接到特定的地址
可以將DatagramChannel“連接”到網(wǎng)絡中的特定地址的纬乍。由于UDP是無連接的,連接到特定地址并不會像TCP通道那樣創(chuàng)建一個真正的連接裸卫。而是鎖住DatagramChannel 仿贬,讓其只能從特定地址收發(fā)數(shù)據(jù)。
這里有個例子:
Java代碼?
channel.connect(newInetSocketAddress("jenkov.com",80));
當連接后墓贿,也可以使用read()和write()方法茧泪,就像在用傳統(tǒng)的通道一樣。只是在數(shù)據(jù)傳送方面沒有任何保證聋袋。這里有幾個例子:
Java代碼?
intbytesRead?=?channel.read(buf);
intbytesWritten?=?channel.write(but);
(本部分原文鏈接队伟,作者:Jakob Jenkov,譯者:黃忠幽勒,校對:丁一)
Java NIO 管道是2個線程之間的單向數(shù)據(jù)連接嗜侮。Pipe有一個source通道和一個sink通道。數(shù)據(jù)會被寫到sink通道,從source通道讀取锈颗。
這里是Pipe原理的圖示:
創(chuàng)建管道
通過Pipe.open()方法打開管道顷霹。例如:
Java代碼?
Pipe?pipe?=?Pipe.open();??
向管道寫數(shù)據(jù)
要向管道寫數(shù)據(jù),需要訪問sink通道击吱。像這樣:
Java代碼?
Pipe.SinkChannel?sinkChannel?=?pipe.sink();??
通過調(diào)用SinkChannel的write()方法淋淀,將數(shù)據(jù)寫入SinkChannel,像這樣:
Java代碼?
String?newData?="New?String?to?write?to?file..."+?System.currentTimeMillis();
ByteBuffer?buf?=?ByteBuffer.allocate(48);
buf.clear();??
buf.put(newData.getBytes());??
buf.flip();??
while(buf.hasRemaining())?{
????<b>sinkChannel.write(buf);</b>??
}??
從管道讀取數(shù)據(jù)
從讀取管道的數(shù)據(jù),需要訪問source通道姨拥,像這樣:
Java代碼?
Pipe.SourceChannel?sourceChannel?=?pipe.source();??
調(diào)用source通道的read()方法來讀取數(shù)據(jù)绅喉,像這樣:
Java代碼?
ByteBuffer?buf?=?ByteBuffer.allocate(48);
intbytesRead?=?inChannel.read(buf);
read()方法返回的int值會告訴我們多少字節(jié)被讀進了緩沖區(qū)。