nio之所以為為新,在于它并沒在原來I/O的基礎上進行開發,而是提供了全新的類和接口,除了原來的基本功能之外,它還提供了以下新的特征:
? 多路選擇的非封鎖式I/O設施
?支持文件鎖和內存映射
?支持基于Perl風格正則表達式的模式匹配設施
?字符集編碼器和譯碼器
為了支持這些新的功能,nio使用了兩個新的概念:
1. 信道(channel)
信道是一個連接,可用于接收或發送數據,如文件和套接字。因為信道連接的是底層的物理設備,他可以直接支持設備的讀/寫,或提供文件鎖。對于文件、管道、套接字都存在相應的信道類。可以把信道看成是數據流的替代品。信道沒有包裝類,提高了性能。 所有的信道類都位于java.nio.channels包中。
2. 緩沖區(buffer)
緩沖區是一個數據容器。可以把它看做內存中的一個大的數組,用來存儲來自信道的同一類型的所有數據,因此,程序員可以使用字節、字符、整數等緩沖區。字節緩沖區提供必要的方法,可以提取或存入所有基本類型(boolean型除外)的數據。
buffer類的核心是一塊內存區,便于核心代碼和java代碼同時訪問,核心代碼可以直接訪問它,java代碼可以通過API訪問它。
緩沖區基本上是一塊內存區域,因而可以執行一些與內存有關的操作,如清除其中的內容,支持讀寫或只讀操作等。所有的buffer類都位于java.nio包中。
下面看如何使用它們:
1. 使用信道
在信道的使用中,文件的信道是最具有代表性的,API也是最多的,下面我們以文件信道為例介紹它。
● 獲取文件信道
文件的信道的類為FileChannel,遺憾的是他并沒有向我們提供打開文件的方法,我們可以通過調用FileInputStream、FileOutputStream和RandomAccessFile類實例的getChannel()方法來獲取其實例。例如:
RandomAccessFile raf = new RandomAccessFile(“data.txt”, “rw”);
FileChannel fc = raf.getChannel();
● 從信道讀取數據
讀取的數據會默認放到字節緩沖區中。
FileChannel提供了四個API讀取數據:
a. read(ByteBuffer dst) 將字節序列從此通道讀入給定的緩沖區
b. read(ByteBuffer[] dsts) 將字節序列從此通道讀入給定的緩沖區
c. read(ByteBuffer[] dsts, int offset, int length)
將字節序列從此通道讀入給定緩沖區的子序列中
d. read(ByteBuffer dst, long position)
從給定的文件位置開始,從此通道讀取字節序列,并寫入給定的緩沖區
● 向信道寫入數據
數據來源默認是字節緩沖區。
FileChannel提供了四個API寫入數據:
a. write(ByteBuffer src)
將字節序列從給定的緩沖區寫入此通道
b. write(ByteBuffer[] srcs)
將字節序列從給定的緩沖區寫入此通道
c. write(ByteBuffer[] srcs, int offset, int length)
將字節序列從給定緩沖區的子序列寫入此通道
d. write(ByteBuffer src, long position)
從給定的文件位置開始,將字節序列從給定緩沖區寫入此通道
● 使用文件鎖
文件鎖機制主要是在多線程同時讀寫某個文件資源時使用。
FileChannel提供了兩種加鎖機制,lock和tryLock,兩者的區別在于,lock是同步的,
直至成功才返回,tryLock是異步的,無論成不成功都會立即返回。
● 使用內存映射
FileChannel提供的的API為:
MappedByteBuffer map(FileChannel.MapMode mode, long position, long size);
映射模式一個有三種:
a.只讀: 試圖修改得到的緩沖區將導致拋出 ReadOnlyBufferException.(MapMode.READ_ONLY)
b.讀/寫: 對得到的緩沖區的更改最終將傳播到文件;該更改對映射到同一文件的其他程序不一定是可見的。 (MapMode.READ_WRITE)
c.專用: 對得到的緩沖區的更改不會傳播到文件,并且該更改對映射到同一文件的其他程序也不是可見的;相反,會創建緩沖區已修改部分的專用副本。 (MapMode.PRIVATE)
2. 使用緩沖區● 層次結構所有緩沖區的基類都是Buffer,除Boolean類型外,其它數據類型都有對應的緩沖區類,另有一個ByteOrder類,用來設置緩沖區的大小端順序,即BigEndian或者是LittleEndian,默認情況下是BigEndian。其層次結構圖如下:
● 獲取緩沖區對象一共有兩種類型的緩沖區,直接緩沖區和非直接緩沖區,兩者區別在于直接緩沖區上的數據操作,虛擬機將盡量使用本機I/O,并盡量避免使用中間緩沖區。判斷一個緩沖區是否是直接緩沖區,可以調用isDirect()方法。有三種方式來獲取一個緩沖區的對象:
a. 調用allocate()或者allocateDirect()方法直接分配,其中allocateDirect()返回的是直接緩沖區。
b. 包裝一個數組,如:byte[] b = new byte[1024];ByteBuffer bb = ByteBuffer.wrap(b);
c. 內存映射,即調用FileChannel的map()方法。
ByteBuffer buffer1 = ByteBuffer.allocate(1024);
ByteBuffer buffer2 = ByteBuffer.allocateDirect(1024);
ByteBuffer buffer3 = ByteBuffer.wrap(new String("hello").getBytes());
● 緩沖區基本屬性這幾個屬性是每個緩沖區都有的并且是常用的操作。
a. 容量(capacity),緩沖區大小
b. 限制(limit),第一個不應被讀取或寫入的字節的索引,總是小于容量。
c. 位置(position),下一個被讀取或寫入的字節的索引,總是小于限制。
d. clear()方法:設置limit為capacity,position為0。
e. filp()方法:設置limit為當前position,然后設置position為0。
f. rewind()方法:保持limit不變,設置position為0。
● 緩沖區數據操作操作包括了讀取和寫入數據兩種。讀取數據使用get()及其系列方法,除boolean外,每一種類型包括了對應的get()方法,如getInt(),getChar()等,get()方法用來讀取字節,支持相對和絕對索引兩種方式。寫入數據使用put()及其系列方法,和get()方法是對應的。 下面這個例子演示了如何使用緩沖區和信道:
package nio;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;


public class BufferDemo
{

public static void main(String[] args) throws Exception
{
//分配一個非直接緩沖區
ByteBuffer bb = ByteBuffer.allocate(100);
//向緩沖區寫入0到100的字節制

for(int i = 0; i <100; i++)
{
byte b = (byte) (Math.random() * 100);
bb.put(b); }
System.out.println("寫入文件前的緩沖區數據");
bb.flip();
while(bb.hasRemaining())
System.out.print(bb.get() + " ");
System.out.println();
//獲取一個關聯到文件buffer.txt的信道
FileChannel fc = new FileOutputStream("buffer.txt").getChannel();
//將緩沖區數據寫到文件中
bb.flip();
fc.write(bb);
//防止緩存
fc.force(true);
//關閉信道
fc.close();
bb = null;
fc = null;
//下面從文件中讀取數據
fc = new FileInputStream("buffer.txt").getChannel();
ByteBuffer bb2 = ByteBuffer.allocate((int) fc.size());
fc.read(bb2);
System.out.println("從文件讀取的緩沖區數據");
bb2.flip();
while(bb2.hasRemaining())
System.out.print(bb2.get() + " ");
System.out.println();
fc.close();
bb2 = null;
fc = null; }}
3.視圖緩沖區
上面我們的緩沖區都是基于字節的,像IntBuffer、LongBuffer等這些都可以調用ByteBuffer的 as***Buffer(***表示某個數據類型)得到,所以這種類型的緩沖區又被稱為視圖緩沖區(View Buffer), 視圖緩沖區有以下特點:
a. 視圖緩沖區有自己獨立的position和limit,但它不是一個新的創建,只是原來字節緩沖區的一個邏輯緩沖區,字節緩沖區的任何修改都會影響視圖緩沖區,反之亦然。
b. 視圖緩沖區按照數據類型的大小進行索引,而不是字節順序。
c. 也提供了put()和get()及其系列方法,用于數據的整塊傳輸。
下面這個例子演示了視圖緩沖區:
package nio;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import java.nio.channels.FileChannel;


public class ViewBufferDemo
{

public static void main(String[] args) throws Exception
{
//將文件內容讀到緩沖區中
FileChannel fc = new FileInputStream("buffer.txt").getChannel();
ByteBuffer bb = ByteBuffer.allocate((int) fc.size());
fc.read(bb);
fc.close();
fc = null;
System.out.println("從文件讀取的字節緩沖區數據");
bb.flip();
while(bb.hasRemaining())
System.out.print(bb.get() + " ");
System.out.println();
//獲取視圖緩沖區
bb.flip();
IntBuffer ib = bb.asIntBuffer();
System.out.println("將字節緩沖區作為整形緩沖區的數據");
while(ib.hasRemaining())
System.out.print(ib.get() + " ");
System.out.println();
bb = null;
ib = null;
}}
4.映射內存緩沖區
調用信道的map()方法后,即可將文件的某一部分或全部映射到內存中,映射內存緩沖區是一 個直接緩沖區,繼承自ByteBuffer,但相對于ByteBuffer,它有更多的優點:
a. 內存映射I/O是對信道/緩沖區技術的改進。 當傳輸大量的數據時,內存映射I/O速度相對較快,這是因為它使用虛擬內存把文件傳輸到進程的地址空間中。
b. 映射內存也成為共享內存,因此可以用于相關進程(均映射同一文件)之間的整塊數據傳輸,這些進程甚至可以不必位于同一系統上,只要每個都可以訪問同一文件即可。
c. 當對FileChannel執行映射操作,把文件映射到內存中時,得到的是一個連接到文件的映射的字節緩沖區,這種映射的結果是,當輸出緩沖區的內容時,數據將出現在文件中,當讀入緩沖區時,相當于得到文件中的數據。 下面這個例子演示了映射內存:
package nio;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class CopyFile
{

public static void main(String[] args) throws Exception
{
FileChannel fIChan, fOChan;
MappedByteBuffer mBuf;
fIChan = new FileInputStream("buffer.txt").getChannel();
fOChan = new FileOutputStream("bufferTemp.txt").getChannel();
mBuf = fIChan.map(FileChannel.MapMode.READ_ONLY, 0, fIChan.size()); fOChan.write(mBuf);
fIChan.close();
fOChan.close();
fIChan = null;
fOChan = null;
mBuf = null;
}}
NIO網絡支持
●服務器端:接收請求的應用程序。
●客戶端:向服務器端發出請求的應用程序。
●套接字通道:客戶端與服務器端之間的通信通道。它能識別服務器端的IP地址和端口號。數據以Buffer中元素的形式通過套接字通道傳送。
●選擇器:所有非阻塞技術的主要對象。它監視著已注冊的套接字通道,并序列化服務器需要應答的請求。
●關鍵字:選擇器用來對對象的請求進行排序。每個關鍵字代表一個單獨的客戶端子請求并包含識別客戶端和請求類型的信息。
圖一:使用非阻塞套接字體系的結構圖。
你可能注意到,客戶端應用程序同時執行對服務器端的請求,接著選擇器將其集中起來,創建關鍵字,然后將其發送至服務器端。這看起來像是阻塞(Blocking)體系,因為在一定時間內只處理一個請求,但事實并非如此。實際上,每個關鍵字不代表從客戶端發至服務器端的整個信息流,僅僅只是一部分。我們不要忘了選擇器能分割那些被關鍵字標識的子請求里的數據。因此,如果有更多連續地數據發送至服務器端,那么選擇器就會創建更多的根據時間共享策略(Time-sharing policy)來進行處理的關鍵字。強調一下,在圖一中關鍵字的顏色與客戶端的顏色相對應。
服務器端非阻塞(Server Nonblocking)
我以前的部分介紹過的實體都有與其相當的Java實體。客戶端和服務器端是兩個Java應用程序。套接字通道是SocketChannel類的實例,這個類允許通過網絡傳送數據。它們能被Java程序員看作是一個新的套接字。SocketChannel類被定義在java.nio.channel包中。
選擇器是一個Selector類的對象。該類的每個實例均能監視更多的套接字通道,進而建立更多的連接。當一些有意義的事發生在通道上(如客戶端試圖連接服務器端或進行讀/寫操作),選擇器便會通知應用程序處理請求。選擇器會創建一個關鍵字,這個關鍵字是SelectionKey類的一個實例。每個關鍵字都保存著應用程序的標識及請求的類型。其中,請求的類型可以是如下之一:
●嘗試連接(客戶端)
●嘗試連接(服務器端)
●讀取操作
●寫入操作
一個通用的實現非阻塞服務器的算法如下:
基本上,服務器端的實現是由選擇器等待事件和創建關鍵字的無限循環組成的。根據關鍵字的類型,及時的執行操作。關鍵字存在以下4種可能的類型。
Acceptable: 相應的客戶端要求連接。
Connectable:服務器端接受連接。
Readable:服務器端可讀。
Writeable:服務器端可寫。
通常一個表示接受的關鍵字創建在服務器端。事實上,這種關鍵字僅僅通知一下服務器端客戶端請求連接。在這種環境下,正如你通過算法得到的結論一樣,服務器端個性化套接字通道和連接這個通道到選擇器以便進行讀/寫操作。從這一刻起,當接受客戶端讀或寫操作時,選擇器將為客戶端創建Readable或Writeable關鍵字。從而,服務器端將截取這些關鍵字并執行正確的動作。
現在,你可以用下面這個推薦算法和Java語言寫服務器端了。用這種方法能成功的創建套接字通道,選擇器,和套接字-選擇器注冊(socket-selector registration)。
// Create the server socket channel
ServerSocketChannel server = ServerSocketChannel.open();
// nonblocking I/O
server.configureBlocking(false);
// host-port 8000
server.socket().bind(new java.net.InetSocketAddress(host,8000));
System.out.println("Server attivo porta 8000");
// Create the selector
Selector selector = Selector.open();
// Recording server to selector (type OP_ACCEPT)
server.register(selector,SelectionKey.OP_ACCEPT);
我們使用OP_ACCEPT,意思是選擇器僅能報告客戶端嘗試連接服務器端。其他可能的選項是:OP_CONNECT,在客戶端下使用;OP_READ; 和OP_WRITE。
// Infinite server loop

for(;;)
{
// Waiting for events
selector.select();
// Get keys
Set keys = selector.selectedKeys();
Iterator i = keys.iterator();
// For each keys

while(i.hasNext())
{
SelectionKey key = (SelectionKey) i.next();
// Remove the current key
i.remove();
// if isAccetable = true// then a client required a connection

if (key.isAcceptable())
{
// get client socket channel
SocketChannel client = server.accept();
// Non Blocking I/O
client.configureBlocking(false);
// recording to the selector (reading)
client.register(selector, SelectionKey.OP_READ);
continue; }
// if isReadable = true // then the server is ready to read

if (key.isReadable())
{
SocketChannel client = (SocketChannel) key.channel();
// Read byte coming from the client
int BUFFER_SIZE = 32;
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);

try
{
client.read(buffer);

} catch (Exception e)
{
// client is no longer active
e.printStackTrace();
continue;
} // Show bytes on the console
buffer.flip();
Charset charset=Charset.forName("ISO-8859-1");
CharsetDecoder decoder = charset.newDecoder();
CharBuffer charBuffer = decoder.decode(buffer);
System.out.print(charBuffer.toString());
continue;
} }}
循環的第一行是調用select方法,它會阻塞進程執行并等待選擇器上記錄的事件。在這段代碼中,套接字通道由服務器變量指代。實際上,服務器端不是一個SocketChannel對象,而是一個ServerSocketChannel對象。它象SocketChannel一樣是SelectableChannel類的一般化,通常用于服務器端的應用程序。
選擇器等待的事件是客戶端嘗試連接。當這樣的操作出現時,服務器端的應用程序便獲得一個由選擇器創建的關鍵字和檢查每個關鍵字的類型。你也許注意到,當一個關鍵字被處理時,它不得不調用remove方法從這組關鍵字中被移出。如果這個關鍵字的類型是可接受的(isAcceptable()=true),那么服務器端便通過調用accept方法來查找客戶端套接字通道,設置它為非阻塞,并將OP_READ選項把它登記進選擇器中。我們也可以使用OP_WRITE 或者是OP_READ|OP_WRITE選項,但為了簡單,我實現的服務器端僅僅能從通道中讀取,不能進行寫入操作。
客戶端套接字通道現在已經登記入選擇器并可進行讀取操作。從而,當客戶端在套接字通道上寫數據時,選擇器將通知服務器端應用程序這里有一些數據讀。隨著可讀關鍵字的創建,從而isReadable()=true。在這個例子中,應用程序從套接字通道上讀取數據使用的是32個字節的ByteBuffer,字節譯碼使用的是ISO-8859-1編碼規則,同時讀取的數據也會顯示在服務器端的控制臺上。
客戶端非阻塞(Client Nonblocking)
為了檢驗編制的服務器端能否以非阻塞的方法工作正常,我將實現一個客戶端以期在套接字通道上連續地寫字符串“Client XXX”,這里的“XXX”是命令行所傳遞的參數。例如,當客戶端運行的命令行的參數是89時,服務器端的控制臺上就會顯示“Client 89 Client 89 Client 89 Client 89 ...”。如果其它的客戶端開始的參數是92時會發生些什么呢?如果服務器端已阻塞,任何事情都不會發生,服務器端還是顯示連續的字符串“Client 89”。自從我們的服務器使用了非阻塞套接字,那么控制臺就會顯示下面這樣的字符串:"Client 89 Client 89 Client 92 Client 89 Client 92 Client 92 Client 89 Client 89 ...",這意味著在套接字通道上的讀/寫操作并不阻塞服務器應用程序的執行。
這里有一段客戶端應用程序的代碼:
// Create client SocketChannel
SocketChannel client = SocketChannel.open();
// nonblocking I/O
client.configureBlocking(false);
// Connection to host port 8000
client.connect(new java.net.InetSocketAddress(host,8000));
// Create selector
Selector selector = Selector.open();
// Record to selector (OP_CONNECT type)
SelectionKey clientKey = client.register(selector, SelectionKey.OP_CONNECT);
// Waiting for the connection

while (selector.select(500)> 0)
{
// Get keys
Set keys = selector.selectedKeys();
Iterator i = keys.iterator();
// For each key

while (i.hasNext())
{
SelectionKey key = (SelectionKey)i.next();
// Remove the current key
i.remove();
// Get the socket channel held by the key
SocketChannel channel = (SocketChannel)key.channel();
// Attempt a connection

if (key.isConnectable())
{
// Connection OK
System.out.println("Server Found");
// Close pendent connections
if (channel.isConnectionPending())
channel.finishConnect();
// Write continuously on the buffer
ByteBuffer buffer = null;

for (;;)
{
buffer = ByteBuffer.wrap( new String(" Client " + id + " ").getBytes()); channel.write(buffer);
buffer.clear();
}
}
}}
也許,客戶端應用程序的結構讓你回憶起服務器端應用程序的結構。然而,這里也有許多不同的地方。套接字通道使用OP_CONNECT選項連接到選擇器上,意思是當服務器接受連接時選擇器將不得不通知客戶端,這個循環不是無窮的。While循環的條件是:
while (selector.select(500)> 0)
意思是客戶端嘗試連接,最大時長是500毫秒;如果服務器端沒有應答,selete方法將返回0,因為在通道上的服務器沒有激活。在循環里,服務器端檢測關鍵字是否可連接。在這個例子中,如果有一些不確定的連接,客戶端就關閉那些不確定的連接,然后寫入字符串“Client”后面接著從命令行參數中帶來的變量ID。