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