nio之所以為為新,在于它并沒在原來I/O的基礎(chǔ)上進行開發(fā),而是提供了全新的類和接口,除了原來的基本功能之外,它還提供了以下新的特征:
? 多路選擇的非封鎖式I/O設(shè)施
?支持文件鎖和內(nèi)存映射
?支持基于Perl風(fēng)格正則表達式的模式匹配設(shè)施
?字符集編碼器和譯碼器
為了支持這些新的功能,nio使用了兩個新的概念:
1. 信道(channel)
信道是一個連接,可用于接收或發(fā)送數(shù)據(jù),如文件和套接字。因為信道連接的是底層的物理設(shè)備,他可以直接支持設(shè)備的讀/寫,或提供文件鎖。對于文件、管道、套接字都存在相應(yīng)的信道類??梢园研诺揽闯墒菙?shù)據(jù)流的替代品。信道沒有包裝類,提高了性能。 所有的信道類都位于java.nio.channels包中。
2. 緩沖區(qū)(buffer)
緩沖區(qū)是一個數(shù)據(jù)容器。可以把它看做內(nèi)存中的一個大的數(shù)組,用來存儲來自信道的同一類型的所有數(shù)據(jù),因此,程序員可以使用字節(jié)、字符、整數(shù)等緩沖區(qū)。字節(jié)緩沖區(qū)提供必要的方法,可以提取或存入所有基本類型(boolean型除外)的數(shù)據(jù)。
buffer類的核心是一塊內(nèi)存區(qū),便于核心代碼和java代碼同時訪問,核心代碼可以直接訪問它,java代碼可以通過API訪問它。
緩沖區(qū)基本上是一塊內(nèi)存區(qū)域,因而可以執(zhí)行一些與內(nèi)存有關(guān)的操作,如清除其中的內(nèi)容,支持讀寫或只讀操作等。所有的buffer類都位于java.nio包中。
下面看如何使用它們:
1. 使用信道
在信道的使用中,文件的信道是最具有代表性的,API也是最多的,下面我們以文件信道為例介紹它。
● 獲取文件信道
文件的信道的類為FileChannel,遺憾的是他并沒有向我們提供打開文件的方法,我們可以通過調(diào)用FileInputStream、FileOutputStream和RandomAccessFile類實例的getChannel()方法來獲取其實例。例如:


● 從信道讀取數(shù)據(jù)
讀取的數(shù)據(jù)會默認放到字節(jié)緩沖區(qū)中。
FileChannel提供了四個API讀取數(shù)據(jù):






● 向信道寫入數(shù)據(jù)
數(shù)據(jù)來源默認是字節(jié)緩沖區(qū)。
FileChannel提供了四個API寫入數(shù)據(jù):








● 使用文件鎖
文件鎖機制主要是在多線程同時讀寫某個文件資源時使用。
FileChannel提供了兩種加鎖機制,lock和tryLock,兩者的區(qū)別在于,lock是同步的,
直至成功才返回,tryLock是異步的,無論成不成功都會立即返回。
● 使用內(nèi)存映射
FileChannel提供的的API為:
MappedByteBuffer map(FileChannel.MapMode mode, long position, long size);
映射模式一個有三種:



2. 使用緩沖區(qū)● 層次結(jié)構(gòu)所有緩沖區(qū)的基類都是Buffer,除Boolean類型外,其它數(shù)據(jù)類型都有對應(yīng)的緩沖區(qū)類,另有一個ByteOrder類,用來設(shè)置緩沖區(qū)的大小端順序,即BigEndian或者是LittleEndian,默認情況下是BigEndian。其層次結(jié)構(gòu)圖如下:

● 獲取緩沖區(qū)對象一共有兩種類型的緩沖區(qū),直接緩沖區(qū)和非直接緩沖區(qū),兩者區(qū)別在于直接緩沖區(qū)上的數(shù)據(jù)操作,虛擬機將盡量使用本機I/O,并盡量避免使用中間緩沖區(qū)。判斷一個緩沖區(qū)是否是直接緩沖區(qū),可以調(diào)用isDirect()方法。有三種方式來獲取一個緩沖區(qū)的對象:
a. 調(diào)用allocate()或者allocateDirect()方法直接分配,其中allocateDirect()返回的是直接緩沖區(qū)。
b. 包裝一個數(shù)組,如:byte[] b = new byte[1024];ByteBuffer bb = ByteBuffer.wrap(b);
c. 內(nèi)存映射,即調(diào)用FileChannel的map()方法。



● 緩沖區(qū)基本屬性這幾個屬性是每個緩沖區(qū)都有的并且是常用的操作。
a. 容量(capacity),緩沖區(qū)大小
b. 限制(limit),第一個不應(yīng)被讀取或?qū)懭氲淖止?jié)的索引,總是小于容量。
c. 位置(position),下一個被讀取或?qū)懭氲淖止?jié)的索引,總是小于限制。
d. clear()方法:設(shè)置limit為capacity,position為0。
e. filp()方法:設(shè)置limit為當(dāng)前position,然后設(shè)置position為0。
f. rewind()方法:保持limit不變,設(shè)置position為0。
● 緩沖區(qū)數(shù)據(jù)操作操作包括了讀取和寫入數(shù)據(jù)兩種。讀取數(shù)據(jù)使用get()及其系列方法,除boolean外,每一種類型包括了對應(yīng)的get()方法,如getInt(),getChar()等,get()方法用來讀取字節(jié),支持相對和絕對索引兩種方式。寫入數(shù)據(jù)使用put()及其系列方法,和get()方法是對應(yīng)的。 下面這個例子演示了如何使用緩沖區(qū)和信道:










































3.視圖緩沖區(qū)
上面我們的緩沖區(qū)都是基于字節(jié)的,像IntBuffer、LongBuffer等這些都可以調(diào)用ByteBuffer的 as***Buffer(***表示某個數(shù)據(jù)類型)得到,所以這種類型的緩沖區(qū)又被稱為視圖緩沖區(qū)(View Buffer), 視圖緩沖區(qū)有以下特點:
a. 視圖緩沖區(qū)有自己獨立的position和limit,但它不是一個新的創(chuàng)建,只是原來字節(jié)緩沖區(qū)的一個邏輯緩沖區(qū),字節(jié)緩沖區(qū)的任何修改都會影響視圖緩沖區(qū),反之亦然。
b. 視圖緩沖區(qū)按照數(shù)據(jù)類型的大小進行索引,而不是字節(jié)順序。
c. 也提供了put()和get()及其系列方法,用于數(shù)據(jù)的整塊傳輸。
下面這個例子演示了視圖緩沖區(qū):





























4.映射內(nèi)存緩沖區(qū)
調(diào)用信道的map()方法后,即可將文件的某一部分或全部映射到內(nèi)存中,映射內(nèi)存緩沖區(qū)是一 個直接緩沖區(qū),繼承自ByteBuffer,但相對于ByteBuffer,它有更多的優(yōu)點:
a. 內(nèi)存映射I/O是對信道/緩沖區(qū)技術(shù)的改進。 當(dāng)傳輸大量的數(shù)據(jù)時,內(nèi)存映射I/O速度相對較快,這是因為它使用虛擬內(nèi)存把文件傳輸?shù)竭M程的地址空間中。
b. 映射內(nèi)存也成為共享內(nèi)存,因此可以用于相關(guān)進程(均映射同一文件)之間的整塊數(shù)據(jù)傳輸,這些進程甚至可以不必位于同一系統(tǒng)上,只要每個都可以訪問同一文件即可。
c. 當(dāng)對FileChannel執(zhí)行映射操作,把文件映射到內(nèi)存中時,得到的是一個連接到文件的映射的字節(jié)緩沖區(qū),這種映射的結(jié)果是,當(dāng)輸出緩沖區(qū)的內(nèi)容時,數(shù)據(jù)將出現(xiàn)在文件中,當(dāng)讀入緩沖區(qū)時,相當(dāng)于得到文件中的數(shù)據(jù)。 下面這個例子演示了映射內(nèi)存:


















NIO網(wǎng)絡(luò)支持
●服務(wù)器端:接收請求的應(yīng)用程序。
●客戶端:向服務(wù)器端發(fā)出請求的應(yīng)用程序。
●套接字通道:客戶端與服務(wù)器端之間的通信通道。它能識別服務(wù)器端的IP地址和端口號。數(shù)據(jù)以Buffer中元素的形式通過套接字通道傳送。
●選擇器:所有非阻塞技術(shù)的主要對象。它監(jiān)視著已注冊的套接字通道,并序列化服務(wù)器需要應(yīng)答的請求。
●關(guān)鍵字:選擇器用來對對象的請求進行排序。每個關(guān)鍵字代表一個單獨的客戶端子請求并包含識別客戶端和請求類型的信息。
圖一:使用非阻塞套接字體系的結(jié)構(gòu)圖。

你可能注意到,客戶端應(yīng)用程序同時執(zhí)行對服務(wù)器端的請求,接著選擇器將其集中起來,創(chuàng)建關(guān)鍵字,然后將其發(fā)送至服務(wù)器端。這看起來像是阻塞(Blocking)體系,因為在一定時間內(nèi)只處理一個請求,但事實并非如此。實際上,每個關(guān)鍵字不代表從客戶端發(fā)至服務(wù)器端的整個信息流,僅僅只是一部分。我們不要忘了選擇器能分割那些被關(guān)鍵字標(biāo)識的子請求里的數(shù)據(jù)。因此,如果有更多連續(xù)地數(shù)據(jù)發(fā)送至服務(wù)器端,那么選擇器就會創(chuàng)建更多的根據(jù)時間共享策略(Time-sharing policy)來進行處理的關(guān)鍵字。強調(diào)一下,在圖一中關(guān)鍵字的顏色與客戶端的顏色相對應(yīng)。
服務(wù)器端非阻塞(Server Nonblocking)
我以前的部分介紹過的實體都有與其相當(dāng)?shù)腏ava實體。客戶端和服務(wù)器端是兩個Java應(yīng)用程序。套接字通道是SocketChannel類的實例,這個類允許通過網(wǎng)絡(luò)傳送數(shù)據(jù)。它們能被Java程序員看作是一個新的套接字。SocketChannel類被定義在java.nio.channel包中。
選擇器是一個Selector類的對象。該類的每個實例均能監(jiān)視更多的套接字通道,進而建立更多的連接。當(dāng)一些有意義的事發(fā)生在通道上(如客戶端試圖連接服務(wù)器端或進行讀/寫操作),選擇器便會通知應(yīng)用程序處理請求。選擇器會創(chuàng)建一個關(guān)鍵字,這個關(guān)鍵字是SelectionKey類的一個實例。每個關(guān)鍵字都保存著應(yīng)用程序的標(biāo)識及請求的類型。其中,請求的類型可以是如下之一:
●嘗試連接(客戶端)
●嘗試連接(服務(wù)器端)
●讀取操作
●寫入操作
一個通用的實現(xiàn)非阻塞服務(wù)器的算法如下:
基本上,服務(wù)器端的實現(xiàn)是由選擇器等待事件和創(chuàng)建關(guān)鍵字的無限循環(huán)組成的。根據(jù)關(guān)鍵字的類型,及時的執(zhí)行操作。關(guān)鍵字存在以下4種可能的類型。
Acceptable: 相應(yīng)的客戶端要求連接。
Connectable:服務(wù)器端接受連接。
Readable:服務(wù)器端可讀。
Writeable:服務(wù)器端可寫。
通常一個表示接受的關(guān)鍵字創(chuàng)建在服務(wù)器端。事實上,這種關(guān)鍵字僅僅通知一下服務(wù)器端客戶端請求連接。在這種環(huán)境下,正如你通過算法得到的結(jié)論一樣,服務(wù)器端個性化套接字通道和連接這個通道到選擇器以便進行讀/寫操作。從這一刻起,當(dāng)接受客戶端讀或?qū)懖僮鲿r,選擇器將為客戶端創(chuàng)建Readable或Writeable關(guān)鍵字。從而,服務(wù)器端將截取這些關(guān)鍵字并執(zhí)行正確的動作。
現(xiàn)在,你可以用下面這個推薦算法和Java語言寫服務(wù)器端了。用這種方法能成功的創(chuàng)建套接字通道,選擇器,和套接字-選擇器注冊(socket-selector registration)。











我們使用OP_ACCEPT,意思是選擇器僅能報告客戶端嘗試連接服務(wù)器端。其他可能的選項是:OP_CONNECT,在客戶端下使用;OP_READ; 和OP_WRITE。










































循環(huán)的第一行是調(diào)用select方法,它會阻塞進程執(zhí)行并等待選擇器上記錄的事件。在這段代碼中,套接字通道由服務(wù)器變量指代。實際上,服務(wù)器端不是一個SocketChannel對象,而是一個ServerSocketChannel對象。它象SocketChannel一樣是SelectableChannel類的一般化,通常用于服務(wù)器端的應(yīng)用程序。
選擇器等待的事件是客戶端嘗試連接。當(dāng)這樣的操作出現(xiàn)時,服務(wù)器端的應(yīng)用程序便獲得一個由選擇器創(chuàng)建的關(guān)鍵字和檢查每個關(guān)鍵字的類型。你也許注意到,當(dāng)一個關(guān)鍵字被處理時,它不得不調(diào)用remove方法從這組關(guān)鍵字中被移出。如果這個關(guān)鍵字的類型是可接受的(isAcceptable()=true),那么服務(wù)器端便通過調(diào)用accept方法來查找客戶端套接字通道,設(shè)置它為非阻塞,并將OP_READ選項把它登記進選擇器中。我們也可以使用OP_WRITE 或者是OP_READ|OP_WRITE選項,但為了簡單,我實現(xiàn)的服務(wù)器端僅僅能從通道中讀取,不能進行寫入操作。
客戶端套接字通道現(xiàn)在已經(jīng)登記入選擇器并可進行讀取操作。從而,當(dāng)客戶端在套接字通道上寫數(shù)據(jù)時,選擇器將通知服務(wù)器端應(yīng)用程序這里有一些數(shù)據(jù)讀。隨著可讀關(guān)鍵字的創(chuàng)建,從而isReadable()=true。在這個例子中,應(yīng)用程序從套接字通道上讀取數(shù)據(jù)使用的是32個字節(jié)的ByteBuffer,字節(jié)譯碼使用的是ISO-8859-1編碼規(guī)則,同時讀取的數(shù)據(jù)也會顯示在服務(wù)器端的控制臺上。
客戶端非阻塞(Client Nonblocking)
為了檢驗編制的服務(wù)器端能否以非阻塞的方法工作正常,我將實現(xiàn)一個客戶端以期在套接字通道上連續(xù)地寫字符串“Client XXX”,這里的“XXX”是命令行所傳遞的參數(shù)。例如,當(dāng)客戶端運行的命令行的參數(shù)是89時,服務(wù)器端的控制臺上就會顯示“Client 89 Client 89 Client 89 Client 89 ...”。如果其它的客戶端開始的參數(shù)是92時會發(fā)生些什么呢?如果服務(wù)器端已阻塞,任何事情都不會發(fā)生,服務(wù)器端還是顯示連續(xù)的字符串“Client 89”。自從我們的服務(wù)器使用了非阻塞套接字,那么控制臺就會顯示下面這樣的字符串:"Client 89 Client 89 Client 92 Client 89 Client 92 Client 92 Client 89 Client 89 ...",這意味著在套接字通道上的讀/寫操作并不阻塞服務(wù)器應(yīng)用程序的執(zhí)行。
這里有一段客戶端應(yīng)用程序的代碼:







































while (selector.select(500)> 0)
意思是客戶端嘗試連接,最大時長是500毫秒;如果服務(wù)器端沒有應(yīng)答,selete方法將返回0,因為在通道上的服務(wù)器沒有激活。在循環(huán)里,服務(wù)器端檢測關(guān)鍵字是否可連接。在這個例子中,如果有一些不確定的連接,客戶端就關(guān)閉那些不確定的連接,然后寫入字符串“Client”后面接著從命令行參數(shù)中帶來的變量ID。