Java NIO學習筆記---Channel

Java NIO學習筆記---Channel

Java NIO 的核心組成部分:

1.Channels

2.Buffers

3.Selectors

  我們首先來學習Channels(java.nio.channels):

通道

  1)通道基礎

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

  channel的jdk源碼:

1
2
3
4
5
6
package java.nio.channels;
    public interface Channel;
    {
        public boolean isOpen();
        public void close() throws IOException;
    }

  與緩衝區不同,通道API主要由接口指定。不同的操作系統上通道實現(Channel Implementation)會有根本性的差異,所以通道API僅僅描述了可以做什麼。因此很自然地,通道實現經常使用操作系統的本地代碼。通道接口允許您以一種受控且可移植的方式來訪問底層的I/O服務。

  Channel是一個對象,可以通過它讀取和寫入數據。拿 NIO 與原來的 I/O 做個比較,通道就像是流。所有數據都通過 Buffer 對象來處理。您永遠不會將字節直接寫入通道中,相反,您是將數據寫入包含一個或者多個字節的緩衝區。同樣,您不會直接從通道中讀取字節,而是將數據從通道讀入緩衝區,再從緩衝區獲取這個字節。

Java NIO 的通道類似流,但又有些不同:

  • 既可以從通道中讀取數據,又可以寫數據到通道。但流的讀寫通常是單向的。
  • 通道可以異步地讀寫。
  • 通道中的數據總是要先讀到一個 Buffer,或者總是要從一個 Buffer 中寫入。

基本上,所有的 IO 在NIO 中都從一個Channel 開始。Channel 有點象流。 數據可以從Channel讀到Buffer中,也可以從Buffer 寫到Channel中。這裏有個圖示: 
                                                      

下面是JAVA NIO中的一些主要Channel的實現: java.nio.channels包中的Channel接口的已知實現類

  • FileChannel:從文件中讀寫數據。
  • DatagramChannel:能通過UDP讀寫網絡中的數據。
  • SocketChannel:能通過TCP讀寫網絡中的數據。
  • ServerSocketChannel:可以監聽新進來的TCP連接,像Web服務器那樣。對每一個新進來的連接都會創建一個SocketChannel。

正如你所看到的,這些通道涵蓋了UDP 和 TCP 網絡IO,以及文件IO。 

  2)通道API
  1.文件通道API
  FileChannel類可以實現常用的read,write以及scatter/gather操作,同時它也提供了很多專用於文件的新方法。這些方法中的許多都是我們所熟悉的文件操作。
  FileChannel類的JDK源碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package java.nio.channels;
    public abstract class FileChannel extends AbstractChannel implements ByteChannel, GatheringByteChannel, ScatteringByteChannel
    {
        // This is a partial API listing
        // All methods listed here can throw java.io.IOException
        public abstract int read (ByteBuffer dst, long position);
        public abstract int write (ByteBuffer src, long position);
        public abstract long size();
        public abstract long position();
        public abstract void position (long newPosition);
        public abstract void truncate (long size);
        public abstract void force (boolean metaData);
        public final FileLock lock();
        public abstract FileLock lock (long position, long size, boolean shared);
        public final FileLock tryLock();
        public abstract FileLock tryLock (long position, long size, boolean shared);
        public abstract MappedByteBuffer map (MapMode mode, long position, long size);
        public static class MapMode;
        public static final MapMode READ_ONLY;
        public static final MapMode READ_WRITE;
        public static final MapMode PRIVATE;
        public abstract long transferTo (long position, long count, WritableByteChannel target);
        public abstract long transferFrom (ReadableByteChannel src, long position, long count);
    } 

  文件通道總是阻塞式的,因此不能被置於非阻塞模式。現代操作系統都有複雜的緩存和預取機制,使得本地磁盤I/O操作延遲很少。網絡文件系統一般而言延遲會多些,不過卻也因該優化而受益。面向流的I/O的非阻塞範例對於面向文件的操作並無多大意義,這是由文件I/O本質上的不同性質造成的。對於文件I/O,最強大之處在於異步I/O(asynchronous I/O),它允許一個進程可以從操作系統請求一個或多個I/O操作而不必等待這些操作的完成。發起請求的進程之後會收到它請求的I/O操作已完成的通知。

  FileChannel對象是線程安全(thread-safe)的。多個進程可以在同一個實例上併發調用方法而不會引起任何問題,不過並非所有的操作都是多線程的(multithreaded)。影響通道位置或者影響文件大小的操作都是單線程的(single-threaded)。如果有一個線程已經在執行會影響通道位置或文件大小的操作,那麼其他嘗試進行此類操作之一的線程必須等待。併發行爲也會受到底層的操作系統或文件系統影響。

  每個FileChannel對象都同一個文件描述符(file descriptor)有一對一的關係,所以上面列出的API方法與在您最喜歡的POSIX(可移植操作系統接口)兼容的操作系統上的常用文件I/O系統調用緊密對應也就不足爲怪了。本質上講,RandomAccessFile類提供的是同樣的抽象內容。在通道出現之前,底層的文件操作都是通過RandomAccessFile類的方法來實現的。FileChannel模擬同樣的I/O服務,因此它的API自然也是很相似的。

  三者之間的方法對比:

  
FILECHANNEL RANDOMACCESSFILE POSIX SYSTEM CALL
read( ) read( ) read( )
write( ) write( ) write( )
size( ) length( ) fstat( )
position( ) getFilePointer( ) lseek( )
position (long newPosition) seek( ) lseek( )
truncate( ) setLength( ) ftruncate( )
force( ) getFD().sync( ) fsync( )

 

  2.Socket通道
  新的socket通道類可以運行非阻塞模式並且是可選擇的。這兩個性能可以激活大程序(如網絡服務器和中間件組件)巨大的可伸縮性和靈活性。本節中我們會看到,再也沒有爲每個socket連接使用一個線程的必要了,也避免了管理大量線程所需的上下文交換總開銷。藉助新的NIO類,一個或幾個線程就可以管理成百上千的活動socket連接了並且只有很少甚至可能沒有性能損失。所有的socket通道類(DatagramChannel、SocketChannel和ServerSocketChannel)都繼承了位於java.nio.channels.spi包中的AbstractSelectableChannel。這意味着我們可以用一個Selector對象來執行socket通道的就緒選擇(readiness selection)。

  請注意DatagramChannel和SocketChannel實現定義讀和寫功能的接口而ServerSocketChannel不實現。ServerSocketChannel負責監聽傳入的連接和創建新的SocketChannel對象,它本身從不傳輸數據。

  在我們具體討論每一種socket通道前,您應該瞭解socket和socket通道之間的關係。之前的章節中有寫道,通道是一個連接I/O服務導管並提供與該服務交互的方法。就某個socket而言,它不會再次實現與之對應的socket通道類中的socket協議API,而java.net中已經存在的socket通道都可以被大多數協議操作重複使用。

  全部socket通道類(DatagramChannel、SocketChannel和ServerSocketChannel)在被實例化時都會創建一個對等socket對象。這些是我們所熟悉的來自java.net的類(Socket、ServerSocket和DatagramSocket),它們已經被更新以識別通道。對等socket可以通過調用socket( )方法從一個通道上獲取。此外,這三個java.net類現在都有getChannel( )方法。

  Socket通道將與通信協議相關的操作委託給相應的socket對象。socket的方法看起來好像在通道類中重複了一遍,但實際上通道類上的方法會有一些新的或者不同的行爲。

  要把一個socket通道置於非阻塞模式,我們要依靠所有socket通道類的公有超級類:SelectableChannel。就緒選擇(readiness selection)是一種可以用來查詢通道的機制,該查詢可以判斷通道是否準備好執行一個目標操作,如讀或寫。非阻塞I/O和可選擇性是緊密相連的,那也正是管理阻塞模式的API代碼要在SelectableChannel超級類中定義的原因。

  設置或重新設置一個通道的阻塞模式是很簡單的,只要調用configureBlocking( )方法即可,傳遞參數值爲true則設爲阻塞模式,參數值爲false值設爲非阻塞模式。真的,就這麼簡單!您可以通過調用isBlocking( )方法來判斷某個socket通道當前處於哪種模式。

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

  偶爾地,我們也會需要防止socket通道的阻塞模式被更改。API中有一個blockingLock( )方法,該方法會返回一個非透明的對象引用。返回的對象是通道實現修改阻塞模式時內部使用的。只有擁有此對象的鎖的線程才能更改通道的阻塞模式。

2.2.1 ServerSocketChannel
讓我們從最簡單的ServerSocketChannel來開始對socket通道類的討論。以下是ServerSocketChannel的完整API:

1
2
3
4
5
6
7
public abstract class ServerSocketChannel extends AbstractSelectableChannel
  {
      public static ServerSocketChannel open() throws IOException;
      public abstract ServerSocket socket();
      public abstract ServerSocket accept()throws IOException;
      public final int validOps();
  }  

ServerSocketChannel是一個基於通道的socket監聽器。它同我們所熟悉的java.net.ServerSocket執行相同的基本任務,不過它增加了通道語義,因此能夠在非阻塞模式下運行。

由於ServerSocketChannel沒有bind( )方法,因此有必要取出對等的socket並使用它來綁定到一個端口以開始監聽連接。我們也是使用對等ServerSocket的API來根據需要設置其他的socket選項。

同它的對等體java.net.ServerSocket一樣,ServerSocketChannel也有accept( )方法。一旦您創建了一個ServerSocketChannel並用對等socket綁定了它,然後您就可以在其中一個上調用accept( )。如果您選擇在ServerSocket上調用accept( )方法,那麼它會同任何其他的ServerSocket表現一樣的行爲:總是阻塞並返回一個java.net.Socket對象。如果您選擇在ServerSocketChannel上調用accept( )方法則會返回SocketChannel類型的對象,返回的對象能夠在非阻塞模式下運行。

如果以非阻塞模式被調用,當沒有傳入連接在等待時,ServerSocketChannel.accept( )會立即返回null。正是這種檢查連接而不阻塞的能力實現了可伸縮性並降低了複雜性。可選擇性也因此得到實現。我們可以使用一個選擇器實例來註冊一個ServerSocketChannel對象以實現新連接到達時自動通知的功能。以下代碼演示瞭如何使用一個非阻塞的accept( )方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.ronsoft.books.nio.channels;
   import java.nio.ByteBuffer;
   import java.nio.channels.ServerSocketChannel;
   import java.nio.channels.SocketChannel;
   import java.net.InetSocketAddress;
 
   public class ChannelAccept
   {
       public static final String GREETING = "Hello I must be going.\r\n";
       public static void main (String [] argv) throws Exception
       {
           int port = 1234// default
           if (argv.length > 0) {
               port = Integer.parseInt (argv [0]);
           }
           ByteBuffer buffer = ByteBuffer.wrap (GREETING.getBytes());
           ServerSocketChannel ssc = ServerSocketChannel.open();
           ssc.socket().bind (new InetSocketAddress (port));
           ssc.configureBlocking (false);
           while (true) {
               System.out.println ("Waiting for connections");
               SocketChannel sc = ssc.accept();
               if (sc == null) {
                   Thread.sleep (2000);
               else {
                   System.out.println ("Incoming connection from: " + sc.socket().getRemoteSocketAddress());
                   buffer.rewind();
                   sc.write (buffer);
                   sc.close();
               }
           }
       }
   }

2.2.2 SocketChannel

下面開始學習SocketChannel,它是使用最多的socket通道類:

Java NIO中的SocketChannel是一個連接到TCP網絡套接字的通道。可以通過以下2種方式創建SocketChannel:

  1. 打開一個SocketChannel並連接到互聯網上的某臺服務器。
  2. 一個新連接到達ServerSocketChannel時,會創建一個SocketChannel。

打開 SocketChannel

下面是SocketChannel的打開方式:

1
2
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://jenkov.com"80));

關閉 SocketChannel

當用完SocketChannel之後調用SocketChannel.close()關閉SocketChannel:

1
socketChannel.close();  

從 SocketChannel 讀取數據

要從SocketChannel中讀取數據,調用一個read()的方法之一。以下是例子:

1
2
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);

  首先,分配一個Buffer。從SocketChannel讀取到的數據將會放到這個Buffer中。然後,調用SocketChannel.read()。該方法將數據從SocketChannel 讀到Buffer中。read()方法返回的int值表示讀了多少字節進Buffer裏。如果返回的是-1,表示已經讀到了流的末尾(連接關閉了)。

寫入 SocketChannel

寫數據到SocketChannel用的是SocketChannel.write()方法,該方法以一個Buffer作爲參數。示例如下:

1
2
3
4
5
6
7
8
9
10
11
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()) {
    channel.write(buf);
}

  注意SocketChannel.write()方法的調用是在一個while循環中的。Write()方法無法保證能寫多少字節到SocketChannel。所以,我們重複調用write()直到Buffer沒有要寫的字節爲止。

非阻塞模式

可以設置 SocketChannel 爲非阻塞模式(non-blocking mode).設置之後,就可以在異步模式下調用connect(), read() 和write()了。

connect()

如果SocketChannel在非阻塞模式下,此時調用connect(),該方法可能在連接建立之前就返回了。爲了確定連接是否建立,可以調用finishConnect()的方法。像這樣:

1
2
3
4
5
6
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://jenkov.com"80));
 
while(! socketChannel.finishConnect() ){
    //wait, or do something else...
}

write()

非阻塞模式下,write()方法在尚未寫出任何內容時可能就返回了。所以需要在循環中調用write()。前面已經有例子了,這裏就不贅述了。

read()

非阻塞模式下,read()方法在尚未讀取到任何數據時可能就返回了。所以需要關注它的int返回值,它會告訴你讀取了多少字節。

非阻塞模式與選擇器

非阻塞模式與選擇器搭配會工作的更好,通過將一或多個SocketChannel註冊到Selector,可以詢問選擇器哪個通道已經準備好了讀取,寫入等。Selector與SocketChannel的搭配使用會在後面詳講。

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

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

DatagramChannel是無連接的。每個數據報(datagram)都是一個自包含的實體,擁有它自己的目的地址及不依賴其他數據報的數據負載。與面向流的的socket不同,DatagramChannel可以發送單獨的數據報給不同的目的地址。同樣,DatagramChannel對象也可以接收來自任意地址的數據包。每個到達的數據報都含有關於它來自何處的信息(源地址)。

最後給出一個基本的channel實例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt""rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
//讀取的字節數,可能爲零,如果該通道已到達流的末尾,則返回 -1
while (bytesRead != -1) { System.out.println("Read " + bytesRead);
buf.flip();
//反轉緩衝區
while(buf.hasRemaining()){
System.out.print((char) buf.get());
//讀取此緩衝區當前位置的字節,然後該位置遞增。
} buf.clear();
bytesRead = inChannel.read(buf);
//從緩衝區讀取數據
} aFile.close();

引用鏈接:感謝文章內容的原創作者;

1.http://www.iteye.com/magazines/132-Java-NIO

2.Java NIO中文版

3.http://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html

4.http://www.yangyong.me/java-nio%E5%85%A5%E9%97%A8%E4%B8%8E%E8%AF%A6%E8%A7%A3/

5.http://ifeve.com/socket-channel/

分類: Java NIO
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章