全面解讀Java NIO工作原理
本文簡(jiǎn)介: JDK 1.4 中引入的新輸入輸出 (NIO) 庫(kù)在標(biāo)準(zhǔn) Java 代碼中提供了高速的、面向塊的 I/O。本實(shí)用教程從高級(jí)概念到底層的編程細(xì)節(jié),非常詳細(xì)地介紹了 NIO 庫(kù)。您將學(xué)到諸如緩沖區(qū)和通道這樣的關(guān)鍵 I/O 元素的知識(shí),并考察更新后的庫(kù)中的標(biāo)準(zhǔn) I/O 是如何工作的。您還將了解只能通過(guò) NIO 來(lái)完成的工作,如異步 I/O 和直接緩沖區(qū)。
◆ 輸入/輸出:概念性描述
I/O 簡(jiǎn)介
I/O ? 或者輸入/輸出 ? 指的是計(jì)算機(jī)與外部世界或者一個(gè)程序與計(jì)算機(jī)的其余部分的之間的接口。它對(duì)于任何計(jì)算機(jī)系統(tǒng)都非常關(guān)鍵,因而所有 I/O 的主體實(shí)際上是內(nèi)置在操作系統(tǒng)中的。單獨(dú)的程序一般是讓系統(tǒng)為它們完成大部分的工作。
在 Java 編程中,直到最近一直使用 流 的方式完成 I/O。所有 I/O 都被視為單個(gè)的字節(jié)的移動(dòng),通過(guò)一個(gè)稱為 Stream 的對(duì)象一次移動(dòng)一個(gè)字節(jié)。流 I/O 用于與外部世界接觸。它也在內(nèi)部使用,用于將對(duì)象轉(zhuǎn)換為字節(jié),然后再轉(zhuǎn)換回對(duì)象。
NIO 與原來(lái)的 I/O 有同樣的作用和目的,但是它使用不同的方式? 塊 I/O。正如您將在本教程中學(xué)到的,塊 I/O 的效率可以比流 I/O 高許多。
為什么要使用 NIO?
NIO 的創(chuàng)建目的是為了讓 Java 程序員可以實(shí)現(xiàn)高速 I/O 而無(wú)需編寫自定義的本機(jī)代碼。NIO 將最耗時(shí)的 I/O 操作(即填充和提取緩沖區(qū))轉(zhuǎn)移回操作系統(tǒng),因而可以極大地提高速度。
流與塊的比較
原來(lái)的 I/O 庫(kù)(在 java.io.*中) 與 NIO 最重要的區(qū)別是數(shù)據(jù)打包和傳輸?shù)姆绞健U缜懊嫣岬降?,原?lái)的 I/O 以流的方式處理數(shù)據(jù),而 NIO 以塊的方式處理數(shù)據(jù)。
面向流 的 I/O 系統(tǒng)一次一個(gè)字節(jié)地處理數(shù)據(jù)。一個(gè)輸入流產(chǎn)生一個(gè)字節(jié)的數(shù)據(jù),一個(gè)輸出流消費(fèi)一個(gè)字節(jié)的數(shù)據(jù)。為流式數(shù)據(jù)創(chuàng)建過(guò)濾器非常容易。鏈接幾個(gè)過(guò)濾器,以便每個(gè)過(guò)濾器只負(fù)責(zé)單個(gè)復(fù)雜處理機(jī)制的一部分,這樣也是相對(duì)簡(jiǎn)單的。不利的一面是,面向流的 I/O 通常相當(dāng)慢。
一個(gè) 面向塊 的 I/O 系統(tǒng)以塊的形式處理數(shù)據(jù)。每一個(gè)操作都在一步中產(chǎn)生或者消費(fèi)一個(gè)數(shù)據(jù)塊。按塊處理數(shù)據(jù)比按(流式的)字節(jié)處理數(shù)據(jù)要快得多。但是面向塊的 I/O 缺少一些面向流的 I/O 所具有的優(yōu)雅性和簡(jiǎn)單性。
集成的 I/O
在 JDK 1.4 中原來(lái)的 I/O 包和 NIO 已經(jīng)很好地集成了。 java.io.* 已經(jīng)以 NIO 為基礎(chǔ)重新實(shí)現(xiàn)了,所以現(xiàn)在它可以利用 NIO 的一些特性。例如, java.io.* 包中的一些類包含以塊的形式讀寫數(shù)據(jù)的方法,這使得即使在更面向流的系統(tǒng)中,處理速度也會(huì)更快。
也可以用 NIO 庫(kù)實(shí)現(xiàn)標(biāo)準(zhǔn) I/O 功能。例如,可以容易地使用塊 I/O 一次一個(gè)字節(jié)地移動(dòng)數(shù)據(jù)。但是正如您會(huì)看到的,NIO 還提供了原 I/O 包中所沒(méi)有的許多好處。
◆ 通道和緩沖區(qū)
概 述
通道 和 緩沖區(qū) 是 NIO 中的核心對(duì)象,幾乎在每一個(gè) I/O 操作中都要使用它們。
通道是對(duì)原 I/O 包中的流的模擬。到任何目的地(或來(lái)自任何地方)的所有數(shù)據(jù)都必須通過(guò)一個(gè) Channel 對(duì)象。一個(gè) Buffer 實(shí)質(zhì)上是一個(gè)容器對(duì)象。發(fā)送給一個(gè)通道的所有對(duì)象都必須首先放到緩沖區(qū)中;同樣地,從通道中讀取的任何數(shù)據(jù)都要讀到緩沖區(qū)中。
在本節(jié)中,您會(huì)了解到 NIO 中通道和緩沖區(qū)是如何工作的。
什么是緩沖區(qū)?
Buffer 是一個(gè)對(duì)象, 它包含一些要寫入或者剛讀出的數(shù)據(jù)。 在 NIO 中加入 Buffer 對(duì)象,體現(xiàn)了新庫(kù)與原 I/O 的一個(gè)重要區(qū)別。在面向流的 I/O 中,您將數(shù)據(jù)直接寫入或者將數(shù)據(jù)直接讀到 Stream 對(duì)象中。
在 NIO 庫(kù)中,所有數(shù)據(jù)都是用緩沖區(qū)處理的。在讀取數(shù)據(jù)時(shí),它是直接讀到緩沖區(qū)中的。在寫入數(shù)據(jù)時(shí),它是寫入到緩沖區(qū)中的。任何時(shí)候訪問(wèn) NIO 中的數(shù)據(jù),您都是將它放到緩沖區(qū)中。
緩沖區(qū)實(shí)質(zhì)上是一個(gè)數(shù)組。通常它是一個(gè)字節(jié)數(shù)組,但是也可以使用其他種類的數(shù)組。但是一個(gè)緩沖區(qū)不 僅僅 是一個(gè)數(shù)組。緩沖區(qū)提供了對(duì)數(shù)據(jù)的結(jié)構(gòu)化訪問(wèn),而且還可以跟蹤系統(tǒng)的讀/寫進(jìn)程。
緩沖區(qū)類型
最常用的緩沖區(qū)類型是 ByteBuffer。一個(gè) ByteBuffer 可以在其底層字節(jié)數(shù)組上進(jìn)行 get/set 操作(即字節(jié)的獲取和設(shè)置)。
ByteBuffer 不是 NIO 中唯一的緩沖區(qū)類型。事實(shí)上,對(duì)于每一種基本 Java 類型都有一種緩沖區(qū)類型:
• ByteBuffer
• CharBuffer
• ShortBuffer
• IntBuffer
• LongBuffer
• FloatBuffer
• DoubleBuffer
每一個(gè) Buffer 類都是 Buffer 接口的一個(gè)實(shí)例。 除了 ByteBuffer,每一個(gè) Buffer 類都有完全一樣的操作,只是它們所處理的數(shù)據(jù)類型不一樣。因?yàn)榇蠖鄶?shù)標(biāo)準(zhǔn) I/O 操作都使用 ByteBuffer,所以它具有所有共享的緩沖區(qū)操作以及一些特有的操作。
現(xiàn)在您可以花一點(diǎn)時(shí)間運(yùn)行 UseFloatBuffer.java,它包含了類型化的緩沖區(qū)的一個(gè)應(yīng)用例子。
什么是通道?
Channel是一個(gè)對(duì)象,可以通過(guò)它讀取和寫入數(shù)據(jù)。拿 NIO 與原來(lái)的 I/O 做個(gè)比較,通道就像是流。
正如前面提到的,所有數(shù)據(jù)都通過(guò) Buffer 對(duì)象來(lái)處理。您永遠(yuǎn)不會(huì)將字節(jié)直接寫入通道中,相反,您是將數(shù)據(jù)寫入包含一個(gè)或者多個(gè)字節(jié)的緩沖區(qū)。同樣,您不會(huì)直接從通道中讀取字節(jié),而是將數(shù)據(jù)從通道讀入緩沖區(qū),再?gòu)木彌_區(qū)獲取這個(gè)字節(jié)。
通道類型
通道與流的不同之處在于通道是雙向的。而流只是在一個(gè)方向上移動(dòng)(一個(gè)流必須是 InputStream 或者 OutputStream 的子類), 而 通道 可以用于讀、寫或者同時(shí)用于讀寫。
因?yàn)樗鼈兪请p向的,所以通道可以比流更好地反映底層操作系統(tǒng)的真實(shí)情況。特別是在 UNIX 模型中,底層操作系統(tǒng)通道是雙向的。
◆ 從理論到實(shí)踐:NIO 中的讀和寫
概 述
讀和寫是 I/O 的基本過(guò)程。從一個(gè)通道中讀取很簡(jiǎn)單:只需創(chuàng)建一個(gè)緩沖區(qū),然后讓通道將數(shù)據(jù)讀到這個(gè)緩沖區(qū)中。寫入也相當(dāng)簡(jiǎn)單:創(chuàng)建一個(gè)緩沖區(qū),用數(shù)據(jù)填充它,然后讓通道用這些數(shù)據(jù)來(lái)執(zhí)行寫入操作。
在本節(jié)中,我們將學(xué)習(xí)有關(guān)在 Java 程序中讀取和寫入數(shù)據(jù)的一些知識(shí)。我們將回顧 NIO 的主要組件(緩沖區(qū)、通道和一些相關(guān)的方法),看看它們是如何交互以進(jìn)行讀寫的。在接下來(lái)的幾節(jié)中,我們將更詳細(xì)地分析這其中的每個(gè)組件以及其交互。
從文件中讀取
在我們第一個(gè)練習(xí)中,我們將從一個(gè)文件中讀取一些數(shù)據(jù)。如果使用原來(lái)的 I/O,那么我們只需創(chuàng)建一個(gè) FileInputStream 并從它那里讀取。而在 NIO 中,情況稍有不同:我們首先從 FileInputStream 獲取一個(gè) FileInputStream 對(duì)象,然后使用這個(gè)通道來(lái)讀取數(shù)據(jù)。
在 NIO 系統(tǒng)中,任何時(shí)候執(zhí)行一個(gè)讀操作,您都是從通道中讀取,但是您不是 直接 從通道讀取。因?yàn)樗袛?shù)據(jù)最終都駐留在緩沖區(qū)中,所以您是從通道讀到緩沖區(qū)中。
因此讀取文件涉及三個(gè)步驟:(1) 從 FileInputStream 獲取 Channel,(2) 創(chuàng)建 Buffer,(3) 將數(shù)據(jù)從 Channel 讀到 Buffer 中。
現(xiàn)在,讓我們看一下這個(gè)過(guò)程。
三個(gè)容易的步驟
第一步是獲取通道。我們從 FileInputStream 獲取通道:
- FileInputStream fin = new FileInputStream( "readandshow.txt" );
- FileChannel fc = fin.getChannel();
下一步是創(chuàng)建緩沖區(qū):
- ByteBuffer buffer = ByteBuffer.allocate( 1024 );
最后,需要將數(shù)據(jù)從通道讀到緩沖區(qū)中,如下所示:
- fc.read( buffer );
您會(huì)注意到,我們不需要告訴通道要讀 多少數(shù)據(jù) 到緩沖區(qū)中。每一個(gè)緩沖區(qū)都有復(fù)雜的內(nèi)部統(tǒng)計(jì)機(jī)制,它會(huì)跟蹤已經(jīng)讀了多少數(shù)據(jù)以及還有多少空間可以容納更多的數(shù)據(jù)
寫入文件
在 NIO 中寫入文件類似于從文件中讀取。首先從 FileOutputStream 獲取一個(gè)通道:
- FileOutputStream fout = new FileOutputStream( "writesomebytes.txt" );
- FileChannel fc = fout.getChannel();
下一步是創(chuàng)建一個(gè)緩沖區(qū)并在其中放入一些數(shù)據(jù) - 在這里,數(shù)據(jù)將從一個(gè)名為 message 的數(shù)組中取出,這個(gè)數(shù)組包含字符串 "Some bytes" 的 ASCII 字節(jié)(本教程后面將會(huì)解釋 buffer.flip() 和 buffer.put() 調(diào)用)。
- ByteBuffer buffer = ByteBuffer.allocate( 1024 );
- for (int i=0; i<message.length; ++i) {
- buffer.put( message[i] );
- }
- buffer.flip();
最后一步是寫入緩沖區(qū)中
- fc.write( buffer );
注意在這里同樣不需要告訴通道要寫入多數(shù)據(jù)。緩沖區(qū)的內(nèi)部統(tǒng)計(jì)機(jī)制會(huì)跟蹤它包含多少數(shù)據(jù)以及還有多少數(shù)據(jù)要寫入。
讀寫結(jié)合
下面我們將看一下在結(jié)合讀和寫時(shí)會(huì)有什么情況。我們以一個(gè)名為 CopyFile.java 的簡(jiǎn)單程序作為這個(gè)練習(xí)的基礎(chǔ),它將一個(gè)文件的所有內(nèi)容拷貝到另一個(gè)文件中。CopyFile.java 執(zhí)行三個(gè)基本操作:首先創(chuàng)建一個(gè) Buffer,然后從源文件中將數(shù)據(jù)讀到這個(gè)緩沖區(qū)中,然后將緩沖區(qū)寫入目標(biāo)文件。這個(gè)程序不斷重復(fù) ― 讀、寫、讀、寫 ― 直到源文件結(jié)束。
CopyFile 程序讓您看到我們?nèi)绾螜z查操作的狀態(tài),以及如何使用 clear() 和 flip() 方法重設(shè)緩沖區(qū),并準(zhǔn)備緩沖區(qū)以便將新讀取的數(shù)據(jù)寫到另一個(gè)通道中。
運(yùn)行 CopyFile 例子
因?yàn)榫彌_區(qū)會(huì)跟蹤它自己的數(shù)據(jù),所以 CopyFile 程序的內(nèi)部循環(huán) (inner loop) 非常簡(jiǎn)單,如下所示:
- fcin.read( buffer );
- fcout.write( buffer );
第一行將數(shù)據(jù)從輸入通道 fcin 中讀入緩沖區(qū),第二行將這些數(shù)據(jù)寫到輸出通道 fcout 。
檢查狀態(tài)
下一步是檢查拷貝何時(shí)完成。當(dāng)沒(méi)有更多的數(shù)據(jù)時(shí),拷貝就算完成,并且可以在 read() 方法返回 -1 是判斷這一點(diǎn),如下所示:
- int r = fcin.read( buffer );
- if (r==-1) {
- break;
- }
重設(shè)緩沖區(qū)
最后,在從輸入通道讀入緩沖區(qū)之前,我們調(diào)用 clear() 方法。同樣,在將緩沖區(qū)寫入輸出通道之前,我們調(diào)用 flip() 方法,如下所示
- buffer.clear();int r = fcin.read( buffer );
- if (r==-1) {
- break;
- }
- buffer.flip();
- fcout.write( buffer );
clear() 方法重設(shè)緩沖區(qū),使它可以接受讀入的數(shù)據(jù)。 flip() 方法讓緩沖區(qū)可以將新讀入的數(shù)據(jù)寫入另一個(gè)通道。
#p#
◆ 緩沖區(qū)內(nèi)部細(xì)節(jié)
概 述
本節(jié)將介紹 NIO 中兩個(gè)重要的緩沖區(qū)組件:狀態(tài)變量和訪問(wèn)方法 (accessor)。
狀態(tài)變量是前一節(jié)中提到的"內(nèi)部統(tǒng)計(jì)機(jī)制"的關(guān)鍵。每一個(gè)讀/寫操作都會(huì)改變緩沖區(qū)的狀態(tài)。通過(guò)記錄和跟蹤這些變化,緩沖區(qū)就可能夠內(nèi)部地管理自己的資源。
在從通道讀取數(shù)據(jù)時(shí),數(shù)據(jù)被放入到緩沖區(qū)。在有些情況下,可以將這個(gè)緩沖區(qū)直接寫入另一個(gè)通道,但是在一般情況下,您還需要查看數(shù)據(jù)。這是使用 訪問(wèn)方法 get() 來(lái)完成的。同樣,如果要將原始數(shù)據(jù)放入緩沖區(qū)中,就要使用訪問(wèn)方法 put()。
在本節(jié)中,您將學(xué)習(xí)關(guān)于 NIO 中的狀態(tài)變量和訪問(wèn)方法的內(nèi)容。我們將描述每一個(gè)組件,并讓您有機(jī)會(huì)看到它的實(shí)際應(yīng)用。雖然 NIO 的內(nèi)部統(tǒng)計(jì)機(jī)制初看起來(lái)可能很復(fù)雜,但是您很快就會(huì)看到大部分的實(shí)際工作都已經(jīng)替您完成了。您可能習(xí)慣于通過(guò)手工編碼進(jìn)行簿記 ― 即使用字節(jié)數(shù)組和索引變量,現(xiàn)在它已在 NIO 中內(nèi)部地處理了。
狀態(tài)變量
可以用三個(gè)值指定緩沖區(qū)在任意時(shí)刻的狀態(tài):
• position
• limit
• capacity
這三個(gè)變量一起可以跟蹤緩沖區(qū)的狀態(tài)和它所包含的數(shù)據(jù)。我們將在下面的小節(jié)中詳細(xì)分析每一個(gè)變量,還要介紹它們?nèi)绾芜m應(yīng)典型的讀/寫(輸入/輸出)進(jìn)程。在這個(gè)例子中,我們假定要將數(shù)據(jù)從一個(gè)輸入通道拷貝到一個(gè)輸出通道。
Position
您可以回想一下,緩沖區(qū)實(shí)際上就是美化了的數(shù)組。在從通道讀取時(shí),您將所讀取的數(shù)據(jù)放到底層的數(shù)組中。 position 變量跟蹤已經(jīng)寫了多少數(shù)據(jù)。更準(zhǔn)確地說(shuō),它指定了下一個(gè)字節(jié)將放到數(shù)組的哪一個(gè)元素中。因此,如果您從通道中讀三個(gè)字節(jié)到緩沖區(qū)中,那么緩沖區(qū)的 position 將會(huì)設(shè)置為3,指向數(shù)組中第四個(gè)元素。
同樣,在寫入通道時(shí),您是從緩沖區(qū)中獲取數(shù)據(jù)。 position 值跟蹤從緩沖區(qū)中獲取了多少數(shù)據(jù)。更準(zhǔn)確地說(shuō),它指定下一個(gè)字節(jié)來(lái)自數(shù)組的哪一個(gè)元素。因此如果從緩沖區(qū)寫了5個(gè)字節(jié)到通道中,那么緩沖區(qū)的 position 將被設(shè)置為5,指向數(shù)組的第六個(gè)元素。
Limit
limit 變量表明還有多少數(shù)據(jù)需要取出(在從緩沖區(qū)寫入通道時(shí)),或者還有多少空間可以放入數(shù)據(jù)(在從通道讀入緩沖區(qū)時(shí))。
position 總是小于或者等于 limit。
Capacity
緩沖區(qū)的 capacity 表明可以儲(chǔ)存在緩沖區(qū)中的最大數(shù)據(jù)容量。實(shí)際上,它指定了底層數(shù)組的大小 ― 或者至少是指定了準(zhǔn)許我們使用的底層數(shù)組的容量。
limit 決不能大于 capacity。
觀察變量
我們首先觀察一個(gè)新創(chuàng)建的緩沖區(qū)。出于本例子的需要,我們假設(shè)這個(gè)緩沖區(qū)的 總?cè)萘?為8個(gè)字節(jié)。 Buffer 的狀態(tài)如下所示:
回想一下 ,limit 決不能大于 capacity,此例中這兩個(gè)值都被設(shè)置為 8。我們通過(guò)將它們指向數(shù)組的尾部之后(如果有第8個(gè)槽,則是第8個(gè)槽所在的位置)來(lái)說(shuō)明這點(diǎn)。
position 設(shè)置為0。如果我們讀一些數(shù)據(jù)到緩沖區(qū)中,那么下一個(gè)讀取的數(shù)據(jù)就進(jìn)入 slot 0 。如果我們從緩沖區(qū)寫一些數(shù)據(jù),從緩沖區(qū)讀取的下一個(gè)字節(jié)就來(lái)自 slot 0 。 position 設(shè)置如下所示:
由于 capacity 不會(huì)改變,所以我們?cè)谙旅娴挠懻撝锌梢院雎运?/p>
第一次讀取
現(xiàn)在我們可以開始在新創(chuàng)建的緩沖區(qū)上進(jìn)行讀/寫操作。首先從輸入通道中讀一些數(shù)據(jù)到緩沖區(qū)中。第一次讀取得到三個(gè)字節(jié)。它們被放到數(shù)組中從 position 開始的位置,這時(shí) position 被設(shè)置為 0。讀完之后,position 就增加到 3,如下所示:
limit 沒(méi)有改變。
第二次讀取
在第二次讀取時(shí),我們從輸入通道讀取另外兩個(gè)字節(jié)到緩沖區(qū)中。這兩個(gè)字節(jié)儲(chǔ)存在由 position 所指定的位置上, position 因而增加 2:
limit 沒(méi)有改變。
flip
現(xiàn)在我們要將數(shù)據(jù)寫到輸出通道中。在這之前,我們必須調(diào)用 flip() 方法。這個(gè)方法做兩件非常重要的事:
1.它將 limit 設(shè)置為當(dāng)前 position。
2.它將 position 設(shè)置為 0。
前一小節(jié)中的圖顯示了在 flip 之前緩沖區(qū)的情況。下面是在 flip 之后的緩沖區(qū):
我們現(xiàn)在可以將數(shù)據(jù)從緩沖區(qū)寫入通道了。 position 被設(shè)置為 0,這意味著我們得到的下一個(gè)字節(jié)是第一個(gè)字節(jié)。 limit 已被設(shè)置為原來(lái)的 position,這意味著它包括以前讀到的所有字節(jié),并且一個(gè)字節(jié)也不多。
第一次寫入
在第一次寫入時(shí),我們從緩沖區(qū)中取四個(gè)字節(jié)并將它們寫入輸出通道。這使得 position 增加到 4,而 limit 不變,如下所示:
第二次寫入
我們只剩下一個(gè)字節(jié)可寫了。 limit在我們調(diào)用 flip() 時(shí)被設(shè)置為 5,并且 position 不能超過(guò) limit。所以最后一次寫入操作從緩沖區(qū)取出一個(gè)字節(jié)并將它寫入輸出通道。這使得 position 增加到 5,并保持 limit 不變,如下所示:
clear
最后一步是調(diào)用緩沖區(qū)的 clear() 方法。這個(gè)方法重設(shè)緩沖區(qū)以便接收更多的字節(jié)。 Clear 做兩種非常重要的事情:
1.它將 limit 設(shè)置為與 capacity 相同。
2.它設(shè)置 position 為 0。
下圖顯示了在調(diào)用 clear() 后緩沖區(qū)的狀態(tài):
緩沖區(qū)現(xiàn)在可以接收新的數(shù)據(jù)了。
訪問(wèn)方法
到目前為止,我們只是使用緩沖區(qū)將數(shù)據(jù)從一個(gè)通道轉(zhuǎn)移到另一個(gè)通道。然而,程序經(jīng)常需要直接處理數(shù)據(jù)。例如,您可能需要將用戶數(shù)據(jù)保存到磁盤。在這種情況下,您必須將這些數(shù)據(jù)直接放入緩沖區(qū),然后用通道將緩沖區(qū)寫入磁盤。
或者,您可能想要從磁盤讀取用戶數(shù)據(jù)。在這種情況下,您要將數(shù)據(jù)從通道讀到緩沖區(qū)中,然后檢查緩沖區(qū)中的數(shù)據(jù)。
在本節(jié)的最后,我們將詳細(xì)分析如何使用 ByteBuffer 類的 get() 和 put() 方法直接訪問(wèn)緩沖區(qū)中的數(shù)據(jù)。
get() 方法
ByteBuffer 類中有四個(gè) get() 方法:
1.byte get();
2.ByteBuffer get( byte dst[] );
3.ByteBuffer get( byte dst[], int offset, int length );
4.byte get( int index );
第一個(gè)方法獲取單個(gè)字節(jié)。第二和第三個(gè)方法將一組字節(jié)讀到一個(gè)數(shù)組中。第四個(gè)方法從緩沖區(qū)中的特定位置獲取字節(jié)。那些返回ByteBuffer 的方法只是返回調(diào)用它們的緩沖區(qū)的 this 值。
此外,我們認(rèn)為前三個(gè) get() 方法是相對(duì)的,而最后一個(gè)方法是絕對(duì)的。 相對(duì) 意味著 get() 操作服從 limit 和 position 值 ― 更明確地說(shuō),字節(jié)是從當(dāng)前 position 讀取的,而 position 在 get 之后會(huì)增加。另一方面,一個(gè) 絕對(duì) 方法會(huì)忽略 limit 和 position 值,也不會(huì)影響它們。事實(shí)上,它完全繞過(guò)了緩沖區(qū)的統(tǒng)計(jì)方法。
上面列出的方法對(duì)應(yīng)于 ByteBuffer 類。其他類有等價(jià)的 get() 方法,這些方法除了不是處理字節(jié)外,其它方面是是完全一樣的,它們處理的是與該緩沖區(qū)類相適應(yīng)的類型。
put()方法
ByteBuffer 類中有五個(gè) put() 方法:
1.ByteBuffer put( byte b );
2.ByteBuffer put( byte src[] );
3.ByteBuffer put( byte src[], int offset, int length );
4.ByteBuffer put( ByteBuffer src );
5.ByteBuffer put( int index, byte b );
第一個(gè)方法 寫入(put) 單個(gè)字節(jié)。第二和第三個(gè)方法寫入來(lái)自一個(gè)數(shù)組的一組字節(jié)。第四個(gè)方法將數(shù)據(jù)從一個(gè)給定的源ByteBuffer 寫入這個(gè) ByteBuffer。第五個(gè)方法將字節(jié)寫入緩沖區(qū)中特定的 位置 。那些返回 ByteBuffer 的方法只是返回調(diào)用它們的緩沖區(qū)的 this 值。
與 get() 方法一樣,我們將把 put() 方法劃分為 相對(duì) 或者 絕對(duì) 的。前四個(gè)方法是相對(duì)的,而第五個(gè)方法是絕對(duì)的。
上面顯示的方法對(duì)應(yīng)于 ByteBuffer 類。其他類有等價(jià)的 put() 方法,這些方法除了不是處理字節(jié)之外,其它方面是完全一樣的。它們處理的是與該緩沖區(qū)類相適應(yīng)的類型。
類型化的 get() 和 put() 方法
除了前些小節(jié)中描述的 get() 和 put() 方法, ByteBuffer 還有用于讀寫不同類型的值的其他方法,如下所示:
• getByte()
• getChar()
• getShort()
• getInt()
• getLong()
• getFloat()
• getDouble()
• putByte()
• putChar()
• putShort()
• putInt()
• putLong()
• putFloat()
• putDouble()
事實(shí)上,這其中的每個(gè)方法都有兩種類型 ― 一種是相對(duì)的,另一種是絕對(duì)的。它們對(duì)于讀取格式化的二進(jìn)制數(shù)據(jù)(如圖像文件的頭部)很有用。
您可以在例子程序 TypesInByteBuffer.java 中看到這些方法的實(shí)際應(yīng)用。
緩沖區(qū)的使用:一個(gè)內(nèi)部循環(huán)
下面的內(nèi)部循環(huán)概括了使用緩沖區(qū)將數(shù)據(jù)從輸入通道拷貝到輸出通道的過(guò)程。
- while (true) {
- buffer.clear();
- int r = fcin.read( buffer );
- if (r==-1) {
- break;
- }
- buffer.flip();
- fcout.write( buffer );}
read() 和 write() 調(diào)用得到了極大的簡(jiǎn)化,因?yàn)樵S多工作細(xì)節(jié)都由緩沖區(qū)完成了。 clear() 和 flip() 方法用于讓緩沖區(qū)在讀和寫之間切換。
#p#
◆ 關(guān)于緩沖區(qū)的更多內(nèi)容
概 述
到目前為止,您已經(jīng)學(xué)習(xí)了使用緩沖區(qū)進(jìn)行日常工作所需要掌握的大部分內(nèi)容。我們的例子沒(méi)怎么超出標(biāo)準(zhǔn)的讀/寫過(guò)程種類,在原來(lái)的 I/O 中可以像在 NIO 中一樣容易地實(shí)現(xiàn)這樣的標(biāo)準(zhǔn)讀寫過(guò)程。
本節(jié)將討論使用緩沖區(qū)的一些更復(fù)雜的方面,比如緩沖區(qū)分配、包裝和分片。我們還會(huì)討論 NIO 帶給 Java 平臺(tái)的一些新功能。您將學(xué)到如何創(chuàng)建不同類型的緩沖區(qū)以達(dá)到不同的目的,如可保護(hù)數(shù)據(jù)不被修改的 只讀 緩沖區(qū),和直接映射到底層操作系統(tǒng)緩沖區(qū)的 直接 緩沖區(qū)。我們將在本節(jié)的最后介紹如何在 NIO 中創(chuàng)建內(nèi)存映射文件。
緩沖區(qū)分配和包裝
在能夠讀和寫之前,必須有一個(gè)緩沖區(qū)。要?jiǎng)?chuàng)建緩沖區(qū),您必須 分配 它。我們使用靜態(tài)方法 allocate() 來(lái)分配緩沖區(qū):
- ByteBuffer buffer = ByteBuffer.allocate( 1024 );
allocate() 方法分配一個(gè)具有指定大小的底層數(shù)組,并將它包裝到一個(gè)緩沖區(qū)對(duì)象中 ― 在本例中是一個(gè) ByteBuffer。
您還可以將一個(gè)現(xiàn)有的數(shù)組轉(zhuǎn)換為緩沖區(qū),如下所示:
- byte array[] = new byte[1024];
- ByteBuffer buffer = ByteBuffer.wrap( array );
本例使用了 wrap() 方法將一個(gè)數(shù)組包裝為緩沖區(qū)。必須非常小心地進(jìn)行這類操作。一旦完成包裝,底層數(shù)據(jù)就可以通過(guò)緩沖區(qū)或者直接訪問(wèn)。
緩沖區(qū)分片
slice() 方法根據(jù)現(xiàn)有的緩沖區(qū)創(chuàng)建一種 子緩沖區(qū) 。也就是說(shuō),它創(chuàng)建一個(gè)新的緩沖區(qū),新緩沖區(qū)與原來(lái)的緩沖區(qū)的一部分共享數(shù)據(jù)。
使用例子可以最好地說(shuō)明這點(diǎn)。讓我們首先創(chuàng)建一個(gè)長(zhǎng)度為 10 的 ByteBuffer:
- ByteBuffer buffer = ByteBuffer.allocate( 10 )
然后使用數(shù)據(jù)來(lái)填充這個(gè)緩沖區(qū),在第 n 個(gè)槽中放入數(shù)字 n:
- for (int i=0; i<buffer.capacity(); ++i) {
- buffer.put( (byte)i );
- }
現(xiàn)在我們對(duì)這個(gè)緩沖區(qū) 分片 ,以創(chuàng)建一個(gè)包含槽 3 到槽 6 的子緩沖區(qū)。在某種意義上,子緩沖區(qū)就像原來(lái)的緩沖區(qū)中的一個(gè) 窗口 。
窗口的起始和結(jié)束位置通過(guò)設(shè)置 position 和 limit 值來(lái)指定,然后調(diào)用 Buffer 的 slice() 方法:
- buffer.position( 3 );
- buffer.limit( 7 );
- ByteBuffer slice = buffer.slice();
片 是緩沖區(qū)的 子緩沖區(qū) 。不過(guò), 片段 和 緩沖區(qū) 共享同一個(gè)底層數(shù)據(jù)數(shù)組,我們?cè)谙乱还?jié)將會(huì)看到這一點(diǎn)。
緩沖區(qū)份片和數(shù)據(jù)共享
我們已經(jīng)創(chuàng)建了原緩沖區(qū)的子緩沖區(qū),并且我們知道緩沖區(qū)和子緩沖區(qū)共享同一個(gè)底層數(shù)據(jù)數(shù)組。讓我們看看這意味著什么。
我們遍歷子緩沖區(qū),將每一個(gè)元素乘以 11 來(lái)改變它。例如,5 會(huì)變成 55。
- for (int i=0; i<slice.capacity(); ++i) {
- byte b = slice.get( i );
- b *= 11;
- slice.put( i, b );
- }
最后,再看一下原緩沖區(qū)中的內(nèi)容:
- buffer.position( 0 );
- buffer.limit( buffer.capacity() );
- while (buffer.remaining()>0) {
- System.out.println( buffer.get() );
- }
結(jié)果表明只有在子緩沖區(qū)窗口中的元素被改變了:
$ java SliceBuffer 0 1 2 33 44 55 66 7 8 9
緩沖區(qū)片對(duì)于促進(jìn)抽象非常有幫助??梢跃帉懽约旱暮瘮?shù)處理整個(gè)緩沖區(qū),而且如果想要將這個(gè)過(guò)程應(yīng)用于子緩沖區(qū)上,您只需取主緩沖區(qū)的一個(gè)片,并將它傳遞給您的函數(shù)。這比編寫自己的函數(shù)來(lái)取額外的參數(shù)以指定要對(duì)緩沖區(qū)的哪一部分進(jìn)行操作更容易。
只讀緩沖區(qū)
只讀緩沖區(qū)非常簡(jiǎn)單 ― 您可以讀取它們,但是不能向它們寫入??梢酝ㄟ^(guò)調(diào)用緩沖區(qū)的 asReadOnlyBuffer() 方法,將任何常規(guī)緩沖區(qū)轉(zhuǎn)換為只讀緩沖區(qū),這個(gè)方法返回一個(gè)與原緩沖區(qū)完全相同的緩沖區(qū)(并與其共享數(shù)據(jù)),只不過(guò)它是只讀的。
只讀緩沖區(qū)對(duì)于保護(hù)數(shù)據(jù)很有用。在將緩沖區(qū)傳遞給某個(gè)對(duì)象的方法時(shí),您無(wú)法知道這個(gè)方法是否會(huì)修改緩沖區(qū)中的數(shù)據(jù)。創(chuàng)建一個(gè)只讀的緩沖區(qū)可以 保證 該緩沖區(qū)不會(huì)被修改。
不能將只讀的緩沖區(qū)轉(zhuǎn)換為可寫的緩沖區(qū)。
直接和間接緩沖區(qū)
另一種有用的 ByteBuffer 是直接緩沖區(qū)。 直接緩沖區(qū) 是為加快 I/O 速度,而以一種特殊的方式分配其內(nèi)存的緩沖區(qū)。
實(shí)際上,直接緩沖區(qū)的準(zhǔn)確定義是與實(shí)現(xiàn)相關(guān)的。Sun 的文檔是這樣描述直接緩沖區(qū)的:
給定一個(gè)直接字節(jié)緩沖區(qū),Java 虛擬機(jī)將盡最大努力直接對(duì)它執(zhí)行本機(jī) I/O 操作。也就是說(shuō),它會(huì)在每一次調(diào)用底層操作系統(tǒng)的本機(jī) I/O 操作之前(或之后),嘗試避免將緩沖區(qū)的內(nèi)容拷貝到一個(gè)中間緩沖區(qū)中(或者從一個(gè)中間緩沖區(qū)中拷貝數(shù)據(jù))。
您可以在例子程序 FastCopyFile.java 中看到直接緩沖區(qū)的實(shí)際應(yīng)用,這個(gè)程序是 CopyFile.java 的另一個(gè)版本,它使用了直接緩沖區(qū)以提高速度。
還可以用內(nèi)存映射文件創(chuàng)建直接緩沖區(qū)。
內(nèi)存映射文件 I/O
內(nèi)存映射文件 I/O 是一種讀和寫文件數(shù)據(jù)的方法,它可以比常規(guī)的基于流或者基于通道的 I/O 快得多。
內(nèi)存映射文件 I/O 是通過(guò)使文件中的數(shù)據(jù)神奇般地出現(xiàn)為內(nèi)存數(shù)組的內(nèi)容來(lái)完成的。這其初聽起來(lái)似乎不過(guò)就是將整個(gè)文件讀到內(nèi)存中,但是事實(shí)上并不是這樣。一般來(lái)說(shuō),只有文件中實(shí)際讀取或者寫入的部分才會(huì)送入(或者 映射 )到內(nèi)存中。
內(nèi)存映射并不真的神奇或者多么不尋?!,F(xiàn)代操作系統(tǒng)一般根據(jù)需要將文件的部分映射為內(nèi)存的部分,從而實(shí)現(xiàn)文件系統(tǒng)。Java 內(nèi)存映射機(jī)制不過(guò)是在底層操作系統(tǒng)中可以采用這種機(jī)制時(shí),提供了對(duì)該機(jī)制的訪問(wèn)。
盡管創(chuàng)建內(nèi)存映射文件相當(dāng)簡(jiǎn)單,但是向它寫入可能是危險(xiǎn)的。僅只是改變數(shù)組的單個(gè)元素這樣的簡(jiǎn)單操作,就可能會(huì)直接修改磁盤上的文件。修改數(shù)據(jù)與將數(shù)據(jù)保存到磁盤是沒(méi)有分開的。
將文件映射到內(nèi)存
了解內(nèi)存映射的最好方法是使用例子。在下面的例子中,我們要將一個(gè) FileChannel (它的全部或者部分)映射到內(nèi)存中。為此我們將使用 FileChannel.map() 方法。下面代碼行將文件的前 1024 個(gè)字節(jié)映射到內(nèi)存中:
- MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE, 0, 1024 );
map() 方法返回一個(gè) MappedByteBuffer,它是 ByteBuffer 的子類。因此,您可以像使用其他任何 ByteBuffer 一樣使用新映射的緩沖區(qū),操作系統(tǒng)會(huì)在需要時(shí)負(fù)責(zé)執(zhí)行行映射。
◆ 分散和聚集
概 述
分散/聚集 I/O 是使用多個(gè)而不是單個(gè)緩沖區(qū)來(lái)保存數(shù)據(jù)的讀寫方法。
一個(gè)分散的讀取就像一個(gè)常規(guī)通道讀取,只不過(guò)它是將數(shù)據(jù)讀到一個(gè)緩沖區(qū)數(shù)組中而不是讀到單個(gè)緩沖區(qū)中。同樣地,一個(gè)聚集寫入是向緩沖區(qū)數(shù)組而不是向單個(gè)緩沖區(qū)寫入數(shù)據(jù)。
分散/聚集 I/O 對(duì)于將數(shù)據(jù)流劃分為單獨(dú)的部分很有用,這有助于實(shí)現(xiàn)復(fù)雜的數(shù)據(jù)格式。
分散/聚集 I/O
通道可以有選擇地實(shí)現(xiàn)兩個(gè)新的接口: ScatteringByteChannel 和 GatheringByteChannel。一個(gè) ScatteringByteChannel 是一個(gè)具有兩個(gè)附加讀方法的通道:
• long read( ByteBuffer[] dsts );
• long read( ByteBuffer[] dsts, int offset, int length );
這些 long read() 方法很像標(biāo)準(zhǔn)的 read 方法,只不過(guò)它們不是取單個(gè)緩沖區(qū)而是取一個(gè)緩沖區(qū)數(shù)組。
在 分散讀取 中,通道依次填充每個(gè)緩沖區(qū)。填滿一個(gè)緩沖區(qū)后,它就開始填充下一個(gè)。在某種意義上,緩沖區(qū)數(shù)組就像一個(gè)大緩沖區(qū)。
分散/聚集的應(yīng)用
分散/聚集 I/O 對(duì)于將數(shù)據(jù)劃分為幾個(gè)部分很有用。例如,您可能在編寫一個(gè)使用消息對(duì)象的網(wǎng)絡(luò)應(yīng)用程序,每一個(gè)消息被劃分為固定長(zhǎng)度的頭部和固定長(zhǎng)度的正文。您可以創(chuàng)建一個(gè)剛好可以容納頭部的緩沖區(qū)和另一個(gè)剛好可以容難正文的緩沖區(qū)。當(dāng)您將它們放入一個(gè)數(shù)組中并使用分散讀取來(lái)向它們讀入消息時(shí),頭部和正文將整齊地劃分到這兩個(gè)緩沖區(qū)中。
我們從緩沖區(qū)所得到的方便性對(duì)于緩沖區(qū)數(shù)組同樣有效。因?yàn)槊恳粋€(gè)緩沖區(qū)都跟蹤自己還可以接受多少數(shù)據(jù),所以分散讀取會(huì)自動(dòng)找到有空間接受數(shù)據(jù)的第一個(gè)緩沖區(qū)。在這個(gè)緩沖區(qū)填滿后,它就會(huì)移動(dòng)到下一個(gè)緩沖區(qū)。
聚集寫入
聚集寫入 類似于分散讀取,只不過(guò)是用來(lái)寫入。它也有接受緩沖區(qū)數(shù)組的方法:
• long write( ByteBuffer[] srcs );
• long write( ByteBuffer[] srcs, int offset, int length );
聚集寫對(duì)于把一組單獨(dú)的緩沖區(qū)中組成單個(gè)數(shù)據(jù)流很有用。為了與上面的消息例子保持一致,您可以使用聚集寫入來(lái)自動(dòng)將網(wǎng)絡(luò)消息的各個(gè)部分組裝為單個(gè)數(shù)據(jù)流,以便跨越網(wǎng)絡(luò)傳輸消息。
從例子程序 UseScatterGather.java 中可以看到分散讀取和聚集寫入的實(shí)際應(yīng)用。
◆ 文件鎖定
概 述
文件鎖定初看起來(lái)可能讓人迷惑。它 似乎 指的是防止程序或者用戶訪問(wèn)特定文件。事實(shí)上,文件鎖就像常規(guī)的 Java 對(duì)象鎖 ― 它們是 勸告式的(advisory) 鎖。它們不阻止任何形式的數(shù)據(jù)訪問(wèn),相反,它們通過(guò)鎖的共享和獲取賴允許系統(tǒng)的不同部分相互協(xié)調(diào)。
您可以鎖定整個(gè)文件或者文件的一部分。如果您獲取一個(gè)排它鎖,那么其他人就不能獲得同一個(gè)文件或者文件的一部分上的鎖。如果您獲得一個(gè)共享鎖,那么其他人可以獲得同一個(gè)文件或者文件一部分上的共享鎖,但是不能獲得排它鎖。文件鎖定并不總是出于保護(hù)數(shù)據(jù)的目的。例如,您可能臨時(shí)鎖定一個(gè)文件以保證特定的寫操作成為原子的,而不會(huì)有其他程序的干擾。
大多數(shù)操作系統(tǒng)提供了文件系統(tǒng)鎖,但是它們并不都是采用同樣的方式。有些實(shí)現(xiàn)提供了共享鎖,而另一些僅提供了排它鎖。事實(shí)上,有些實(shí)現(xiàn)使得文件的鎖定部分不可訪問(wèn),盡管大多數(shù)實(shí)現(xiàn)不是這樣的。
在本節(jié)中,您將學(xué)習(xí)如何在 NIO 中執(zhí)行簡(jiǎn)單的文件鎖過(guò)程,我們還將探討一些保證被鎖定的文件盡可能可移植的方法。
鎖定文件
要獲取文件的一部分上的鎖,您要調(diào)用一個(gè)打開的 FileChannel 上的 lock() 方法。注意,如果要獲取一個(gè)排它鎖,您必須以寫方式打開文件。
- RandomAccessFile raf = new RandomAccessFile( "usefilelocks.txt", "rw" );
- FileChannel fc = raf.getChannel();
- FileLock lock = fc.lock( start, end, false );
在擁有鎖之后,您可以執(zhí)行需要的任何敏感操作,然后再釋放鎖:
- lock.release();
在釋放鎖后,嘗試獲得鎖的其他任何程序都有機(jī)會(huì)獲得它。
本小節(jié)的例子程序 UseFileLocks.java 必須與它自己并行運(yùn)行。這個(gè)程序獲取一個(gè)文件上的鎖,持有三秒鐘,然后釋放它。如果同時(shí)運(yùn)行這個(gè)程序的多個(gè)實(shí)例,您會(huì)看到每個(gè)實(shí)例依次獲得鎖。
文件鎖定可能是一個(gè)復(fù)雜的操作,特別是考慮到不同的操作系統(tǒng)是以不同的方式實(shí)現(xiàn)鎖這一事實(shí)。下面的指導(dǎo)原則將幫助您盡可能保持代碼的可移植性:
• 只使用排它鎖。
• 將所有的鎖視為勸告式的(advisory)。
#p#
◆ 連網(wǎng)和異步 I/O
概 述
連網(wǎng)是學(xué)習(xí)異步 I/O 的很好基礎(chǔ),而異步 I/O 對(duì)于在 Java 語(yǔ)言中執(zhí)行任何輸入/輸出過(guò)程的人來(lái)說(shuō),無(wú)疑都是必須具備的知識(shí)。NIO 中的連網(wǎng)與 NIO 中的其他任何操作沒(méi)有什么不同 ― 它依賴通道和緩沖區(qū),而您通常使用 InputStream 和 OutputStream 來(lái)獲得通道。
本節(jié)首先介紹異步 I/O 的基礎(chǔ) ― 它是什么以及它不是什么,然后轉(zhuǎn)向更實(shí)用的、程序性的例子。
異步 I/O
異步 I/O 是一種 沒(méi)有阻塞地 讀寫數(shù)據(jù)的方法。通常,在代碼進(jìn)行 read() 調(diào)用時(shí),代碼會(huì)阻塞直至有可供讀取的數(shù)據(jù)。同樣,write() 調(diào)用將會(huì)阻塞直至數(shù)據(jù)能夠?qū)懭搿?/p>
另一方面,異步 I/O 調(diào)用不會(huì)阻塞。相反,您將注冊(cè)對(duì)特定 I/O 事件的興趣 ― 可讀的數(shù)據(jù)的到達(dá)、新的套接字連接,等等,而在發(fā)生這樣的事件時(shí),系統(tǒng)將會(huì)告訴您。
異步 I/O 的一個(gè)優(yōu)勢(shì)在于,它允許您同時(shí)根據(jù)大量的輸入和輸出執(zhí)行 I/O。同步程序常常要求助于輪詢,或者創(chuàng)建許許多多的線程以處理大量的連接。使用異步 I/O,您可以監(jiān)聽任何數(shù)量的通道上的事件,不用輪詢,也不用額外的線程。
我們將通過(guò)研究一個(gè)名為 MultiPortEcho.java 的例子程序來(lái)查看異步 I/O 的實(shí)際應(yīng)用。這個(gè)程序就像傳統(tǒng)的 echo server,它接受網(wǎng)絡(luò)連接并向它們回響它們可能發(fā)送的數(shù)據(jù)。不過(guò)它有一個(gè)附加的特性,就是它能同時(shí)監(jiān)聽多個(gè)端口,并處理來(lái)自所有這些端口的連接。并且它只在單個(gè)線程中完成所有這些工作。
Selectors
本節(jié)的闡述對(duì)應(yīng)于 MultiPortEcho 的源代碼中的 go() 方法的實(shí)現(xiàn),因此應(yīng)該看一下源代碼,以便對(duì)所發(fā)生的事情有個(gè)更全面的了解。
異步 I/O 中的核心對(duì)象名為 Selector。Selector 就是您注冊(cè)對(duì)各種 I/O 事件的興趣的地方,而且當(dāng)那些事件發(fā)生時(shí),就是這個(gè)對(duì)象告訴您所發(fā)生的事件。
所以,我們需要做的第一件事就是創(chuàng)建一個(gè) Selector:
- Selector selector = Selector.open();
然后,我們將對(duì)不同的通道對(duì)象調(diào)用 register() 方法,以便注冊(cè)我們對(duì)這些對(duì)象中發(fā)生的 I/O 事件的興趣。register() 的第一個(gè)參數(shù)總是這個(gè) Selector。
打開一個(gè) ServerSocketChannel
為了接收連接,我們需要一個(gè) ServerSocketChannel。事實(shí)上,我們要監(jiān)聽的每一個(gè)端口都需要有一個(gè) ServerSocketChannel 。對(duì)于每一個(gè)端口,我們打開一個(gè) ServerSocketChannel,如下所示:
- ServerSocketChannel ssc = ServerSocketChannel.open();
- ssc.configureBlocking( false );
- ServerSocket ss = ssc.socket();
- InetSocketAddress address = new InetSocketAddress( ports[i] );
- ss.bind( address );
第一行創(chuàng)建一個(gè)新的 ServerSocketChannel ,最后三行將它綁定到給定的端口。第二行將 ServerSocketChannel 設(shè)置為 非阻塞的 。我們必須對(duì)每一個(gè)要使用的套接字通道調(diào)用這個(gè)方法,否則異步 I/O 就不能工作。
選擇鍵
下一步是將新打開的 ServerSocketChannels 注冊(cè)到 Selector上。為此我們使用 ServerSocketChannel.register() 方法,如下所示
- SelectionKey key = ssc.register( selector, SelectionKey.OP_ACCEPT );
register() 的第一個(gè)參數(shù)總是這個(gè) Selector。第二個(gè)參數(shù)是 OP_ACCEPT,這里它指定我們想要監(jiān)聽 accept 事件,也就是在新的連接建立時(shí)所發(fā)生的事件。這是適用于 ServerSocketChannel 的唯一事件類型。
請(qǐng)注意對(duì) register() 的調(diào)用的返回值。 SelectionKey 代表這個(gè)通道在此 Selector 上的這個(gè)注冊(cè)。當(dāng)某個(gè) Selector 通知您某個(gè)傳入事件時(shí),它是通過(guò)提供對(duì)應(yīng)于該事件的 SelectionKey 來(lái)進(jìn)行的。SelectionKey 還可以用于取消通道的注冊(cè)。
內(nèi)部循環(huán)
現(xiàn)在已經(jīng)注冊(cè)了我們對(duì)一些 I/O 事件的興趣,下面將進(jìn)入主循環(huán)。使用 Selectors 的幾乎每個(gè)程序都像下面這樣使用內(nèi)部循環(huán):
- int num = selector.select();
- Set selectedKeys = selector.selectedKeys();
- Iterator it = selectedKeys.iterator();
- while (it.hasNext()) {
- SelectionKey key = (SelectionKey)it.next();
- // ... deal with I/O event ...}
首先,我們調(diào)用 Selector 的 select() 方法。這個(gè)方法會(huì)阻塞,直到至少有一個(gè)已注冊(cè)的事件發(fā)生。當(dāng)一個(gè)或者更多的事件發(fā)生時(shí),select() 方法將返回所發(fā)生的事件的數(shù)量。
接下來(lái),我們調(diào)用 Selector 的 selectedKeys() 方法,它返回發(fā)生了事件的 SelectionKey 對(duì)象的一個(gè) 集合。
我們通過(guò)迭代 SelectionKeys 并依次處理每個(gè) SelectionKey 來(lái)處理事件。對(duì)于每一個(gè) SelectionKey,您必須確定發(fā)生的是什么 I/O 事件,以及這個(gè)事件影響哪些 I/O 對(duì)象。
監(jiān)聽新連接
程序執(zhí)行到這里,我們僅注冊(cè)了 ServerSocketChannel,并且僅注冊(cè)它們“接收”事件。為確認(rèn)這一點(diǎn),我們對(duì) SelectionKey 調(diào)用readyOps() 方法,并檢查發(fā)生了什么類型的事件:
- if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
- // Accept the new connection
- // ...
- }
可以肯定地說(shuō), readOps() 方法告訴我們?cè)撌录切碌倪B接。
接受新的連接
因?yàn)槲覀冎肋@個(gè)服務(wù)器套接字上有一個(gè)傳入連接在等待,所以可以安全地接受它;也就是說(shuō),不用擔(dān)心 accept() 操作會(huì)阻塞:
- ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
- SocketChannel sc = ssc.accept();
下一步是將新連接的 SocketChannel 配置為非阻塞的。而且由于接受這個(gè)連接的目的是為了讀取來(lái)自套接字的數(shù)據(jù),所以我們還必須將 SocketChannel 注冊(cè)到 Selector上,如下所示:
- sc.configureBlocking( false );
- SelectionKey newKey = sc.register( selector, SelectionKey.OP_READ );
注意我們使用 register() 的 OP_READ 參數(shù),將 SocketChannel 注冊(cè)用于 讀取 而不是 接受 新連接。
刪除處理過(guò)的 SelectionKey
在處理 SelectionKey 之后,我們幾乎可以返回主循環(huán)了。但是我們必須首先將處理過(guò)的 SelectionKey 從選定的鍵集合中刪除。如果我們沒(méi)有刪除處理過(guò)的鍵,那么它仍然會(huì)在主集合中以一個(gè)激活的鍵出現(xiàn),這會(huì)導(dǎo)致我們嘗試再次處理它。我們調(diào)用迭代器的remove() 方法來(lái)刪除處理過(guò)的 SelectionKey:
- it.remove();
現(xiàn)在我們可以返回主循環(huán)并接受從一個(gè)套接字中傳入的數(shù)據(jù)(或者一個(gè)傳入的 I/O 事件)了。
傳入的 I/O
當(dāng)來(lái)自一個(gè)套接字的數(shù)據(jù)到達(dá)時(shí),它會(huì)觸發(fā)一個(gè) I/O 事件。這會(huì)導(dǎo)致在主循環(huán)中調(diào)用 Selector.select(),并返回一個(gè)或者多個(gè) I/O 事件。這一次, SelectionKey 將被標(biāo)記為 OP_READ 事件,如下所示:
- } else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
- // Read the data
- SocketChannel sc = (SocketChannel)key.channel();
- // ...}
與以前一樣,我們?nèi)〉冒l(fā)生 I/O 事件的通道并處理它。在本例中,由于這是一個(gè) echo server,我們只希望從套接字中讀取數(shù)據(jù)并馬上將它發(fā)送回去。
每次返回主循環(huán),我們都要調(diào)用 select 的 Selector()方法,并取得一組 SelectionKey。每個(gè)鍵代表一個(gè) I/O 事件。我們處理事件,從選定的鍵集中刪除 SelectionKey,然后返回主循環(huán)的頂部。
這個(gè)程序有點(diǎn)過(guò)于簡(jiǎn)單,因?yàn)樗哪康闹皇钦故井惒?I/O 所涉及的技術(shù)。在現(xiàn)實(shí)的應(yīng)用程序中,您需要通過(guò)將通道從 Selector 中刪除來(lái)處理關(guān)閉的通道。而且您可能要使用多個(gè)線程。這個(gè)程序可以僅使用一個(gè)線程,因?yàn)樗皇且粋€(gè)演示,但是在現(xiàn)實(shí)場(chǎng)景中,創(chuàng)建一個(gè)線程池來(lái)負(fù)責(zé) I/O 事件處理中的耗時(shí)部分會(huì)更有意義。
字符集
根據(jù) Sun 的文檔,一個(gè) Charset 是“十六位 Unicode 字符序列與字節(jié)序列之間的一個(gè)命名的映射”。實(shí)際上,一個(gè) Charset 允許您以盡可能最具可移植性的方式讀寫字符序列。
Java 語(yǔ)言被定義為基于 Unicode。然而在實(shí)際上,許多人編寫代碼時(shí)都假設(shè)一個(gè)字符在磁盤上或者在網(wǎng)絡(luò)流中用一個(gè)字節(jié)表示。這種假設(shè)在許多情況下成立,但是并不是在所有情況下都成立,而且隨著計(jì)算機(jī)變得對(duì) Unicode 越來(lái)越友好,這個(gè)假設(shè)就日益變得不能成立了。
在本節(jié)中,我們將看一下如何使用 Charsets 以適合現(xiàn)代文本格式的方式處理文本數(shù)據(jù)。這里將使用的示例程序相當(dāng)簡(jiǎn)單,不過(guò),它觸及了使用 Charset 的所有關(guān)鍵方面:為給定的字符編碼創(chuàng)建 Charset,以及使用該 Charset 解碼和編碼文本數(shù)據(jù)。
編碼/解碼
要讀和寫文本,我們要分別使用 CharsetDecoder 和 CharsetEncoder。將它們稱為 編碼器 和 解碼器 是有道理的。一個(gè) 字符 不再表示一個(gè)特定的位模式,而是表示字符系統(tǒng)中的一個(gè)實(shí)體。因此,由某個(gè)實(shí)際的位模式表示的字符必須以某種特定的 編碼 來(lái)表示。
CharsetDecoder 用于將逐位表示的一串字符轉(zhuǎn)換為具體的 char 值。同樣,一個(gè) CharsetEncoder 用于將字符轉(zhuǎn)換回位。
在下一個(gè)小節(jié)中,我們將考察一個(gè)使用這些對(duì)象來(lái)讀寫數(shù)據(jù)的程序。
處理文本的正確方式
現(xiàn)在我們將分析這個(gè)例子程序 UseCharsets.java。這個(gè)程序非常簡(jiǎn)單 ― 它從一個(gè)文件中讀取一些文本,并將該文本寫入另一個(gè)文件。但是它把該數(shù)據(jù)當(dāng)作文本數(shù)據(jù),并使用 CharBuffer 來(lái)將該數(shù)句讀入一個(gè) CharsetDecoder 中。同樣,它使用 CharsetEncoder 來(lái)寫回該數(shù)據(jù)。
我們將假設(shè)字符以 ISO-8859-1(Latin1) 字符集(這是 ASCII 的標(biāo)準(zhǔn)擴(kuò)展)的形式儲(chǔ)存在磁盤上。盡管我們必須為使用 Unicode 做好準(zhǔn)備,但是也必須認(rèn)識(shí)到不同的文件是以不同的格式儲(chǔ)存的,而 ASCII 無(wú)疑是非常普遍的一種格式。事實(shí)上,每種 Java 實(shí)現(xiàn)都要求對(duì)以下字符編碼提供完全的支持:
• US-ASCII
• ISO-8859-1
• UTF-8
• UTF-16BE
• UTF-16LE
• UTF-16
示例程序
在打開相應(yīng)的文件、將輸入數(shù)據(jù)讀入名為 inputData 的 ByteBuffer 之后,我們的程序必須創(chuàng)建 ISO-8859-1 (Latin1) 字符集的一個(gè)實(shí)例:
- Charset latin1 = Charset.forName( "ISO-8859-1" );
然后,創(chuàng)建一個(gè)解碼器(用于讀取)和一個(gè)編碼器 (用于寫入):
- CharsetDecoder decoder = latin1.newDecoder();
- CharsetEncoder encoder = latin1.newEncoder();
為了將字節(jié)數(shù)據(jù)解碼為一組字符,我們把 ByteBuffer 傳遞給 CharsetDecoder,結(jié)果得到一個(gè) CharBuffer
- CharBuffer cb = decoder.decode( inputData );
如果想要處理字符,我們可以在程序的此處進(jìn)行。但是我們只想無(wú)改變地將它寫回,所以沒(méi)有什么要做的。
要寫回?cái)?shù)據(jù),我們必須使用 CharsetEncoder 將它轉(zhuǎn)換回字節(jié):
- ByteBuffer outputData = encoder.encode( cb );
在轉(zhuǎn)換完成之后,我們就可以將數(shù)據(jù)寫到文件中了。
結(jié)束語(yǔ)和參考資料
結(jié)束語(yǔ)
正如您所看到的, NIO 庫(kù)有大量的特性。在一些新特性(例如文件鎖定和字符集)提供新功能的同時(shí),許多特性在優(yōu)化方面也非常優(yōu)秀。
在基礎(chǔ)層次上,通道和緩沖區(qū)可以做的事情幾乎都可以用原來(lái)的面向流的類來(lái)完成。但是通道和緩沖區(qū)允許以 快得多 的方式完成這些相同的舊操作 ― 事實(shí)上接近系統(tǒng)所允許的最大速度。
不過(guò) NIO 最強(qiáng)大的長(zhǎng)度之一在于,它提供了一種在 Java 語(yǔ)言中執(zhí)行進(jìn)行輸入/輸出的新的(也是迫切需要的)結(jié)構(gòu)化方式。隨諸如緩沖區(qū)、通道和異步 I/O 這些概念性(且可實(shí)現(xiàn)的)實(shí)體而來(lái)的,是我們重新思考 Java 程序中的 I/O過(guò)程的機(jī)會(huì)。這樣,NIO 甚至為我們最熟悉的 I/O 過(guò)程也帶來(lái)了新的活力,同時(shí)賦予我們通過(guò)和以前不同并且更好的方式執(zhí)行它們的機(jī)會(huì)。
原文鏈接:http://www.cnblogs.com/rollenholt/archive/2011/09/29/2195730.html
【編輯推薦】
- Java NIO 異步讀取網(wǎng)絡(luò)數(shù)據(jù)
- Java NIO(異步IO)Socket通信例子
- 影響Java NIO框架性能的因數(shù)
- Java的NIO以及線程并發(fā)
- 基于事件的NIO多線程服務(wù)器