Java NIO之Channel

本文開始講解Java NIO 的三個核心組件,Channel,Buffer,Selector。先從Channel開始,Channel指的是通道。Channel用於在字節緩衝區和位於通道另一側的實體(通常是一個文件或套接字)之間有效地傳輸數據。通道是一種途徑,藉助該途徑,可以用最小的總開銷來訪問操作系統本身的I/O服務。

通道基礎

首先,看一下基本的Channel接口,下面是Channel接口的完整源碼:

public interface Channel extends Closeable {

    /**
     * Tells whether or not this channel is open.  </p>
     *
     * @return <tt>true</tt> if, and only if, this channel is open
     */
    public boolean isOpen();

    /**
     * Closes this channel.
     *
     * <p> After a channel is closed, any further attempt to invoke I/O
     * operations upon it will cause a {@link ClosedChannelException} to be
     * thrown.
     *
     * <p> If this channel is already closed then invoking this method has no
     * effect.
     *
     * <p> This method may be invoked at any time.  If some other thread has
     * already invoked it, however, then another invocation will block until
     * the first invocation is complete, after which it will return without
     * effect. </p>
     *
     * @throws  IOException  If an I/O error occurs
     */
    public void close() throws IOException;

}

可以從底層的Channel接口看到,對所有通道來說只有兩種共同的操作:檢查一個通道是否打開isOpen()和關閉一個打開的通道close(),其餘所有的東西都是那些實現Channel接口以及它的子接口的類。

從Channel接口引申出的其他接口都是面向字節的子接口:


通道可以是單向的也可以是雙向的。一個Channel類可能實現定義read()方法的ReadableByteChannel接口,而另一個Channel類也許實現WritableByteChannel接口以提供write()方法。實現這兩種接口其中之一的類都是單向的,只能在一個方向上傳輸數據。如果一個類同時實現這兩個接口,那麼它是雙向的,可以雙向傳輸數據,就像下面的ByteChannel。

public interface ReadableByteChannel extends Channel {

    public int read(ByteBuffer dst) throws IOException;

}
****************************************************************
public interface WritableByteChannel extends Channel{

    public int write(ByteBuffer src) throws IOException;

}
****************************************************************
public interface ByteChannel
    extends ReadableByteChannel, WritableByteChannel
{

}

通道可以以阻塞(blocking)或非阻塞(nonblocking)模式運行,非阻塞模式的通道永遠不會讓調用的線程休眠,請求的操作要麼立即完成,要麼返回一個結果表明未進行任何操作。只有面向流的(stream-oriented)的通道,如sockets和pipes才能使用非阻塞模式。

比方說非阻塞的通道SocketChannel。可以看出,socket通道類從SelectableChannel類引申而來,從SelectableChannel引申而來的類可以和支持有條件的選擇的選擇器(Selectors)一起使用。將非阻塞I/O和選擇器組合起來可以使開發者的程序利用多路複用I/O

public abstract class SocketChannel
    extends AbstractSelectableChannel
    implements ByteChannel, ScatteringByteChannel, GatheringByteChannel
{
    ...
}

public abstract class AbstractSelectableChannel
    extends SelectableChannel
{
   ...
}

文件通道

通道是訪問I/O服務的導管,I/O可以分爲廣義的兩大類:File I/O和Stream I/O。那麼相應的,通道也有兩種類型,它們是文件(File)通道和套接字(Socket)通道。文件通道指的是FileChannel,套接字通道則有三個,分別是SocketChannel、ServerSocketChannel和DatagramChannel。

通道可以以多種方式創建。Socket通道可以有直接創建Socket通道的工廠方法,但是一個FileChannel對象卻只能通過在一個打開的RandomAccessFile、FileInputStream或FileOutputStream對象上調用getChannel()方法來獲取,開發者不能直接創建一個FileChannel。並且文件通道總是阻塞式的,因此不能被置於非阻塞模式下。

public static void main(String[] args) throws Exception
{
//讀文件
    File file = new File("D:/files/readchannel.txt");
    FileInputStream fis = new FileInputStream(file);
    FileChannel fc = fis.getChannel();
    ByteBuffer bb = ByteBuffer.allocate(35);
    fc.read(bb);
    bb.flip();
    while (bb.hasRemaining())
    {
        System.out.print((char)bb.get());
    }
    bb.clear();
    fc.close();
//寫文件
File file = new File("D:/files/writechannel.txt");
    RandomAccessFile raf = new RandomAccessFile(file, "rw");
    FileChannel fc = raf.getChannel();
    ByteBuffer bb = ByteBuffer.allocate(10);
    String str = "abcdefghij";
    bb.put(str.getBytes());
    bb.flip();
    fc.write(bb);
    bb.clear();
    fc.close();
}

Socket通道

Socket通道與文件通道有着不一樣的特徵:
1、NIO的Socket通道類可以運行於非阻塞模式並且是可選擇的,因此,再也沒有爲每個Socket連接使用一個線程的必要了。這一特性避免了管理大量線程所需的上下文交換總開銷,藉助NIO類,一個或幾個線程就可以管理成百上千的活動Socket連接了並且只有很少甚至沒有性能損失

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

3、每個Socket通道(在java.nio.channels包中)都有一個關聯的java.net.socket對象,反之卻不是如此,如果使用傳統方式(直接實例化)創建了一個Socket對象,它就不會有關聯的SocketChannel並且它的getChannel()方法將總是返回null。

現在簡單寫一下Socket服務端代碼和客戶端代碼:

public class NonBlockingSocketServer
 2 {
 3     public static void main(String[] args) throws Exception
 4     {
 5         int port = 1234;
 6         if (args != null && args.length > 0)
 7         {
 8             port = Integer.parseInt(args[0]);
 9         }
10         ServerSocketChannel ssc = ServerSocketChannel.open();
11         ssc.configureBlocking(false);
12         ServerSocket ss = ssc.socket();
13         ss.bind(new InetSocketAddress(port));
14         System.out.println("開始等待客戶端的數據!時間爲" + System.currentTimeMillis());
15         while (true)
16         {
17             SocketChannel sc = ssc.accept();
18             if (sc == null)
19             {
20                 // 如果當前沒有數據,等待1秒鐘再次輪詢是否有數據,在學習了Selector之後此處可以使用Selector
21                 Thread.sleep(1000);
22             }
23             else
24             {
25                 System.out.println("客戶端已有數據到來,客戶端ip爲:" + sc.socket().getRemoteSocketAddress() 
26                         + ", 時間爲" + System.currentTimeMillis()) ;
27                 ByteBuffer bb = ByteBuffer.allocate(100);
28                 sc.read(bb);
29                 bb.flip();
30                 while (bb.hasRemaining())
31                 {
32                     System.out.print((char)bb.get());
33                 }
34                 sc.close();
35                 System.exit(0);
36             }
37         }
38     }
39 }

和BIO的區別可以理解爲,在ServerSocket做了一層wrapper,並且數據的讀寫要通過ByteBuffer。

1 public class NonBlockingSocketClient
 2 {
 3     private static final String STR = "Hello World!";
 4     private static final String REMOTE_IP= "127.0.0.1";
 5     
 6     public static void main(String[] args) throws Exception
 7     {
 8         int port = 1234;
 9         if (args != null && args.length > 0)
10         {
11             port = Integer.parseInt(args[0]);
12         }
13         SocketChannel sc = SocketChannel.open();
14         sc.configureBlocking(false);
15         sc.connect(new InetSocketAddress(REMOTE_IP, port));
16         while (!sc.finishConnect())
17         {
18             System.out.println("同" + REMOTE_IP+ "的連接正在建立,請稍等!");
19             Thread.sleep(10);
20         }
21         System.out.println("連接已建立,待寫入內容至指定ip+端口!時間爲" + System.currentTimeMillis());
22         ByteBuffer bb = ByteBuffer.allocate(STR.length());
23         bb.put(STR.getBytes());
24         bb.flip(); // 寫緩衝區的數據之前一定要先反轉(flip)
25         sc.write(bb);
26         bb.clear();
27         sc.close();
28     }
29 }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章