本文開始講解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 }