1. 程式人生 > 程式設計 >詳解java NIO之Channel(通道)

詳解java NIO之Channel(通道)

通道(Channel)是java.nio的第二個主要創新。它們既不是一個擴充套件也不是一項增強,而是全新、極好的Java I/O示例,提供與I/O服務的直接連線。Channel用於在位元組緩衝區和位於通道另一側的實體(通常是一個檔案或套接字)之間有效地傳輸資料。

channel介紹

通道是訪問I/O服務的導管。I/O可以分為廣義的兩大類別:File I/O和Stream I/O。那麼相應地有兩種型別的通道也就不足為怪了,它們是檔案(file)通道和套接字(socket)通道。我們看到在api裡有一個FileChannel類和三個socket通道類:SocketChannel、ServerSocketChannel和DatagramChannel。

通道可以以多種方式建立。Socket通道有可以直接建立新socket通道的工廠方法。但是一個FileChannel物件卻只能通過在一個開啟的RandomAccessFile、FileInputStream或FileOutputStream物件上呼叫getChannel( )方法來獲取。你不能直接建立一個FileChannel物件。

我們先來看一下FileChannel的用法:

 // 建立檔案輸出位元組流
 FileOutputStream fos = new FileOutputStream("data.txt");
 //得到檔案通道
 FileChannel fc = fos.getChannel();
 //往通道寫入ByteBuffer
 fc.write(ByteBuffer.wrap("Some text ".getBytes()));
 //關閉流
 fos.close();

 //隨機訪問檔案
 RandomAccessFile raf = new RandomAccessFile("data.txt","rw");
 //得到檔案通道
 fc = raf.getChannel();
 //設定通道的檔案位置 為末尾
 fc.position(fc.size()); 
 //往通道寫入ByteBuffer
 fc.write(ByteBuffer.wrap("Some more".getBytes()));
 //關閉
 raf.close();

 //建立檔案輸入流
 FileInputStream fs = new FileInputStream("data.txt");
 //得到檔案通道
 fc = fs.getChannel();
 //分配ByteBuffer空間大小
 ByteBuffer buff = ByteBuffer.allocate(BSIZE);
 //從通道中讀取ByteBuffer
 fc.read(buff);
 //呼叫此方法為一系列通道寫入或相對獲取 操作做好準備
 buff.flip();
 //從ByteBuffer從依次讀取位元組並列印
 while (buff.hasRemaining()){
  System.out.print((char) buff.get());
 }
 fs.close();

再來看一下SocketChannel:

 SocketChannel sc = SocketChannel.open( );
 sc.connect (new InetSocketAddress ("somehost",someport)); 
 ServerSocketChannel ssc = ServerSocketChannel.open( ); 
 ssc.socket( ).bind (new InetSocketAddress (somelocalport)); 
 DatagramChannel dc = DatagramChannel.open( );

可以設定 SocketChannel 為非阻塞模式(non-blocking mode).設定之後,就可以在非同步模式下呼叫connect(),read() 和write()了。如果SocketChannel在非阻塞模式下,此時呼叫connect(),該方法可能在連線建立之前就返回了。為了確定連線是否建立,可以呼叫finishConnect()的方法。像這樣:

socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com",80));

while(! socketChannel.finishConnect() ){
 //wait,or do something else...
}

伺服器端的使用經常會考慮到非阻塞socket通道,因為它們使同時管理很多socket通道變得更容易。但是,在客戶端使用一個或幾個非阻塞模式的socket通道也是有益處的,例如,藉助非阻塞socket通道,GUI程式可以專注於使用者請求並且同時維護與一個或多個伺服器的會話。在很多程式上,非阻塞模式都是有用的。

呼叫finishConnect( )方法來完成連線過程,該方法任何時候都可以安全地進行呼叫。假如在一個非阻塞模式的SocketChannel物件上呼叫finishConnect( )方法,將可能出現下列情形之一:

  • connect( )方法尚未被呼叫。那麼將產生NoConnectionPendingException異常。
  • 連線建立過程正在進行,尚未完成。那麼什麼都不會發生,finishConnect( )方法會立即返回false值。
  • 在非阻塞模式下呼叫connect( )方法之後,SocketChannel又被切換回了阻塞模式。那麼如果有必要的話,呼叫執行緒會阻塞直到連線建立完成,finishConnect( )方法接著就會返回true值。在初次呼叫connect( )或最後一次呼叫finishConnect( )之後,連線建立過程已經完成。那麼SocketChannel物件的內部狀態將被更新到已連線狀態,finishConnect( )方法會返回true值,然後SocketChannel物件就可以被用來傳輸資料了。
  • 連線已經建立。那麼什麼都不會發生,finishConnect( )方法會返回true值。

Socket通道是執行緒安全的。併發訪問時無需特別措施來保護髮起訪問的多個執行緒,不過任何時候都只有一個讀操作和一個寫操作在進行中。請記住,sockets是面向流的而非包導向的。它們可以保證傳送的位元組會按照順序到達但無法承諾維持位元組分組。某個傳送器可能給一個socket寫入了20個位元組而接收器呼叫read( )方法時卻只收到了其中的3個位元組。剩下的17個位元組還是傳輸中。由於這個原因,讓多個不配合的執行緒共享某個流socket的同一側絕非一個好的設計選擇。

最後再看一下DatagramChannel:

最後一個socket通道是DatagramChannel。正如SocketChannel對應Socket,ServerSocketChannel對應ServerSocket,每一個DatagramChannel物件也有一個關聯的DatagramSocket物件。不過原命名模式在此並未適用:“DatagramSocketChannel”顯得有點笨拙,因此採用了簡潔的“DatagramChannel”名稱。

正如SocketChannel模擬連線導向的流協議(如TCP/IP),DatagramChannel則模擬包導向的無連線協議(如UDP/IP):

建立DatagramChannel的模式和建立其他socket通道是一樣的:呼叫靜態的open( )方法來建立一個新例項。新DatagramChannel會有一個可以通過呼叫socket( )方法獲取的對等DatagramSocket物件。DatagramChannel物件既可以充當伺服器(監聽者)也可以充當客戶端(傳送者)。如果你希望新建立的通道負責監聽,那麼通道必須首先被繫結到一個埠或地址/埠組合上。繫結DatagramChannel同繫結一個常規的DatagramSocket沒什麼區別,都是委託對等socket物件上的API實現的:

 DatagramChannel channel = DatagramChannel.open( );
 DatagramSocket socket = channel.socket( ); 
 socket.bind (new InetSocketAddress (portNumber));

DatagramChannel是無連線的。每個資料報(datagram)都是一個自包含的實體,擁有它自己的目的地址及不依賴其他資料報的資料淨荷。與面向流的的socket不同,DatagramChannel可以傳送單獨的資料報給不同的目的地址。同樣,DatagramChannel物件也可以接收來自任意地址的資料包。每個到達的資料報都含有關於它來自何處的資訊(源地址)。

一個未繫結的DatagramChannel仍能接收資料包。當一個底層socket被建立時,一個動態生成的埠號就會分配給它。繫結行為要求通道關聯的埠被設定為一個特定的值(此過程可能涉及安全檢查或其他驗證)。不論通道是否繫結,所有傳送的包都含有DatagramChannel的源地址(帶埠號)。未繫結的DatagramChannel可以接收發送給它的埠的包,通常是來回應該通道之前發出的一個包。已繫結的通道接收發送給它們所繫結的熟知埠(wellknown port)的包。資料的實際傳送或接收是通過send( )和receive( )方法來實現的。

注意:假如您提供的ByteBuffer沒有足夠的剩餘空間來存放您正在接收的資料包,沒有被填充的位元組都會被悄悄地丟棄。

Scatter/Gather

通道提供了一種被稱為Scatter/Gather的重要新功能(有時也被稱為向量I/O)。它是指在多個緩衝區上實現一個簡單的I/O操作。對於一個write操作而言,資料是從幾個緩衝區按順序抽取(稱為gather)並沿著通道傳送的。緩衝區本身並不需要具備這種gather的能力(通常它們也沒有此能力)。該gather過程的效果就好比全部緩衝區的內容被連結起來,並在傳送資料前存放到一個大的緩衝區中。對於read操作而言,從通道讀取的資料會按順序被散佈(稱為scatter)到多個緩衝區,將每個緩衝區填滿直至通道中的資料或者緩衝區的最大空間被消耗完。

scatter / gather經常用於需要將傳輸的資料分開處理的場合,例如傳輸一個由訊息頭和訊息體組成的訊息,你可能會將訊息體和訊息頭分散到不同的buffer中,這樣你可以方便的處理訊息頭和訊息體。

Scattering Reads是指資料從一個channel讀取到多個buffer中。如下圖描述:

詳解java NIO之Channel(通道)

程式碼示例如下:

ByteBuffer header = ByteBuffer.allocateDirect (10); 
ByteBuffer body = ByteBuffer.allocateDirect (80); 
ByteBuffer [] buffers = { header,body }; 
int bytesRead = channel.read (buffers);

Gathering Writes是指資料從多個buffer寫入到同一個channel。如下圖描述:

詳解java NIO之Channel(通道)

程式碼示例如下:

 ByteBuffer header = ByteBuffer.allocateDirect (10); 
 ByteBuffer body = ByteBuffer.allocateDirect (80); 
 ByteBuffer [] buffers = { header,body }; 
 channel.write(bufferArray);

使用得當的話,Scatter/Gather會是一個極其強大的工具。它允許你委託作業系統來完成辛苦活:將讀取到的資料分開存放到多個儲存桶(bucket)或者將不同的資料區塊合併成一個整體。這是一個巨大的成就,因為作業系統已經被高度優化來完成此類工作了。它節省了您來回移動資料的工作,也就避免了緩衝區拷貝和減少了您需要編寫、除錯的程式碼數量。既然您基本上通過提供資料容器引用來組合資料,那麼按照不同的組合構建多個緩衝區陣列引用,各種資料區塊就可以以不同的方式來組合了。下面的例子好地詮釋了這一點:

public class GatheringTest {
 private static final String DEMOGRAPHIC = "output.txt";
 public static void main (String [] argv) throws Exception {
  int reps = 10;
  if (argv.length > 0) {
   reps = Integer.parseInt(argv[0]);
  }
  FileOutputStream fos = new FileOutputStream(DEMOGRAPHIC);
  GatheringByteChannel gatherChannel = fos.getChannel();

  ByteBuffer[] bs = utterBS(reps);

  while (gatherChannel.write(bs) > 0) {
   // 不做操作,讓通道把資料輸出到檔案寫完
  }
  System.out.println("Mindshare paradigms synergized to " + DEMOGRAPHIC);
  fos.close();
 }
 private static String [] col1 = { "Aggregate","Enable","Leverage","Facilitate","Synergize","Repurpose","Strategize","Reinvent","Harness"
         };

 private static String [] col2 = { "cross-platform","best-of-breed","frictionless","ubiquitous","extensible","compelling","mission-critical","collaborative","integrated"
         };

 private static String [] col3 = { "methodologies","infomediaries","platforms","schemas","mindshare","paradigms","functionalities","web services","infrastructures" };

 private static String newline = System.getProperty ("line.separator");


 private static ByteBuffer [] utterBS (int howMany) throws Exception {
  List list = new LinkedList();
  for (int i = 0; i < howMany; i++) {
   list.add(pickRandom(col1," "));
   list.add(pickRandom(col2," "));
   list.add(pickRandom(col3,newline));
  }
  ByteBuffer[] bufs = new ByteBuffer[list.size()];
  list.toArray(bufs);
  return (bufs);
 }
 private static Random rand = new Random( );


 /**
  * 隨機生成字元
  * @param strings
  * @param suffix
  * @return
  * @throws Exception
  */
 private static ByteBuffer pickRandom (String [] strings,String suffix) throws Exception {
  String string = strings [rand.nextInt (strings.length)];
  int total = string.length() + suffix.length( );
  ByteBuffer buf = ByteBuffer.allocate (total);
  buf.put (string.getBytes ("US-ASCII"));
  buf.put (suffix.getBytes ("US-ASCII"));
  buf.flip( );
  return (buf);
 }
}

輸出為:

Reinvent integrated web services
Aggregate best-of-breed platforms
Harness frictionless platforms
Repurpose extensible paradigms
Facilitate ubiquitous methodologies
Repurpose integrated methodologies
Facilitate mission-critical paradigms
Synergize compelling methodologies
Reinvent compelling functionalities
Facilitate extensible platforms

雖然這種輸出沒有什麼意義,但是gather確是很容易的讓我們把它輸出出來。

Pipe

java.nio.channels包中含有一個名為Pipe(管道)的類。廣義上講,管道就是一個用來在兩個實體之間單向傳輸資料的導管。
Java NIO 管道是2個執行緒之間的單向資料連線。Pipe有一個source通道和一個sink通道。資料會被寫到sink通道,從source通道讀取。Pipe類建立一對提供環回機制的Channel物件。這兩個通道的遠端是連線起來的,以便任何寫在SinkChannel物件上的資料都能出現在SourceChannel物件上。

下面我們來建立一條Pipe,並向Pipe中寫資料:

//通過Pipe.open()方法開啟管道
Pipe pipe = Pipe.open();

//要向管道寫資料,需要訪問sink通道
Pipe.SinkChannel sinkChannel = pipe.sink();

//通過呼叫SinkChannel的write()方法,將資料寫入SinkChannel
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()) {
 sinkChannel.write(buf);
}

再看如何從管道中讀取資料:

讀取管道的資料,需要訪問source通道:

Pipe.SourceChannel sourceChannel = pipe.source();

呼叫source通道的read()方法來讀取資料:

ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = sourceChannel.read(buf);

read()方法返回的int值會告訴我們多少位元組被讀進了緩衝區。

到此我們就把通道的簡單用法講完了,要想會用還是得多去練習,多模擬使用,這樣才知道什麼時候用以及怎麼用,下節我們來講選擇器-Selectors。

以上就是詳解java NIO之Channel(通道)的詳細內容,更多關於JAVA NIO channel(通道)的資料請關注我們其它相關文章!