Java NIO - Channel

前言

上文講到Java NIO一些基本概念。在標準的IO中,都是基於字節流/字符流進行數據操做的,而在NIO中則是是基於ChannelBuffer進行操做,其中的Channel的雖然模擬了的概念,實則大不相同。服務器

本文將詳細闡述NIO中的通道Channel的概念和具體的用法。網絡

Channel和Stream的區別

區別 Stream Channel
是否支持異步 不支持 支持
是否支持雙向數據傳輸 不支持,只能單向 支持,既能夠從通道讀取數據,也能夠向通道寫入數據
是否結合Buffer使用 必須結合Buffer使用
性能 較低 較高

Channel用於在字節緩衝區和位於通道另外一側的服務(一般是文件或者套接字)之間以便有效的進行數據傳輸。藉助通道,能夠用最小的總開銷來訪問操做系統自己的I/O服務。dom

須要注意的是Channel必須結合Buffer使用,應用程序不能直接向通道中讀/寫數據,也就是緩衝區充當着應用程序和通道數據流動的轉換的角色。異步

正文

Channel的源碼

查看Channel的源碼。全部的接口都實現於Channel接口,從接口上來看,全部的通道都有這兩種操做:檢查通道的開啓狀態關閉通道socket

1
2
3
4
5
public interface Channel extends Closeable {
public boolean isOpen();

public void close() throws IOException;
}

Channel的分類

廣義上來講通道能夠被分爲兩類:文件I/O和網絡I/O,也就是文件通道套接字通道。若是分的更細緻一點則是:工具

  • FileChannel:從文件讀寫數據;
  • SocketChannel:經過TCP讀寫網絡數據;
  • ServerSocketChannel:能夠監聽新進來的TCP鏈接,並對每一個連接建立對應的SocketChannel
  • DatagramChannel:經過UDP讀寫網絡中的數據。

Channel的特性

單向or雙向

通道既能夠是單向的也能夠是雙向的。只實現ReadableByteChannel接口中的read()方法或者只實現WriteableByteChannel接口中的write()方法的通道皆爲單向通道,同時實現ReadableByteChannelWriteableByteChannel雙向通道,好比ByteChannel性能

1
2
public interface ByteChannel extends ReadableByteChannel, WritableByteChannel {
}

對於Socket通道來講,它們一直是雙向的,而對於FileChannel來講,它一樣實現了ByteChannel,可是經過FileInputStreamgetChannel()獲取的FileChannel只具備文件的只讀權限測試

注意:調用FileChannel的write()方法會拋出了NonWriteChannelException異常。this

阻塞or非阻塞

通道的工做模式有兩種:阻塞或非阻塞。在非阻塞模式下,調用的線程不會休眠,請求的操做會馬上返回結果;在阻塞模式下,調用的線程會產生休眠。編碼

FileChannel不能運行在非阻塞模式下,其他的通道均可阻塞運行也能夠以非阻塞的方式運行。

另外從SelectableChannel引伸出的類能夠和支持有條件選擇的Selector結合使用,進而充分利用多路複用I/O(Multiplexed I/O)來提升性能

SelectableChannel的源碼中有如下幾個抽象方法,能夠看出支持配置兩種工做模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class SelectableChannel extends AbstractInterruptibleChannel implements Channel {
/**
* 配置是否爲Channel阻塞模式
*/
public abstract SelectableChannel configureBlocking(boolean block) throws IOException;
/**
* 判斷是否爲Channel阻塞模式
*/
public abstract boolean isBlocking();
/**
* 獲取阻塞的鎖對象
*/
public abstract Object blockingLock();
}

 

對於Socket通道類來講,一般與Selector共同使用以提升性能。須要注意的是通道不能被同時使用,一個打開的通道表明着與一個特定I/O服務進行鏈接並封裝了該鏈接的狀態,通道一旦關閉,該鏈接便會斷開

通道的close()比較特殊,不管在通道時在阻塞模式下仍是非阻塞模式下,因爲close()方法的調用而致使底層I/O關閉均可能會形成線程的暫時阻塞。在一個已關閉的通道上調用close()並無任何意義,只會當即返回。

Channel的實戰

對於Socket通道來講存在直接建立新Socket通道的方法,而對於文件通道來講,升級以後的FileInputStream、FileOutputStream和RandomAccessFile提供了getChannel()方法來獲取通道。

FileChannel

Java NIO中的FileChannel是一個鏈接到文件的通道,能夠經過文件通道讀寫文件。文件通道老是阻塞式的,所以FileChannel沒法設置爲非阻塞模式

文件讀寫

(一). 文件寫操做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void testWriteOnFileChannel() {
try {
RandomAccessFile randomAccess = new RandomAccessFile("D://test.txt", "rw");
FileChannel fileChannel = randomAccess.getChannel();

byte[] bytes = new String("Java Non-blocking IO").getBytes();
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);

// 將緩衝區中的字節寫入文件通道中
fileChannel.write(byteBuffer);
// 強制將通道中未寫入磁盤的數據馬上寫入到磁盤
fileChannel.force(true);
// 清空緩衝區,釋放內存
byteBuffer.clear();
fileChannel.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

(二). 文件讀操做:

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
public static void testReadOnFileChannel() {
try {
FileInputStream inputStream = new FileInputStream(new File("D://test.txt"));
FileChannel fileChannel = inputStream.getChannel();

ByteBuffer byteBuffer = ByteBuffer.allocate(10);
// 不斷地寫入緩衝區,寫一次讀一次
while (fileChannel.read(byteBuffer) != -1) {
// 緩衝區從寫模式切換爲讀模式
byteBuffer.flip();
// 開始讀取
while (byteBuffer.hasRemaining()) {
// 一個字節一個字節地讀取,並向後移動position地位置
System.out.print((char) byteBuffer.get());
}
// 緩衝區不會被自動覆蓋,須要主動調用該方法(實際上仍是覆蓋)
byteBuffer.clear();
}
fileChannel.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

文件讀寫測試:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
System.out.println("Start to write");
// 經過FileChannel寫入數據
testWriteOnFileChannel();

System.out.println("Start to read");
// 經過FileChannel讀取數據
testReadOnFileChannel();
}

 

測試結果:

transferFrom和transferTo

(一). transferFrom()的使用

FileChanneltransferFrom()方法能夠將數據從源通道傳輸到FileChannel中。下面是一個簡單的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void testTransferFrom(){
try {
RandomAccessFile fromFile = new RandomAccessFile("D://file1.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("D://file2.txt", "rw");
FileChannel toChannel = toFile.getChannel();

long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel, position, count);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}

(二). transferTo()的使用

transferTo()方法將數據從FileChannel傳輸到目標channel中。下面是一個簡單的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void testTransferTo() {
try {
RandomAccessFile fromFile = new RandomAccessFile("D://file1.txt", "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("D://file3.txt", "rw");
FileChannel toChannel = toFile.getChannel();

long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

ServerSocketChannel

Java NIO中的ServerSocketChannel是一個能夠監聽新進來的TCP鏈接的通道。它相似ServerSocket,要注意的是和DatagramChannelSocketChannel不一樣,ServerSocketChannel自己不具有傳輸數據的能力,而只是負責監聽傳入的鏈接和建立新的SocketChannel

ServerSocketChannel的用法

(一). 建立ServerSocketChannel

經過ServerSocketChannel.open()方法來建立一個新的ServerSocketChannel對象,該對象關聯了一個未綁定ServerSocket通道。經過調用該對象上的socket()方法能夠獲取與之關聯的ServerSocket

1
ServerSocketChannel socketChannel = ServerSocketChannel.open();

(二). 爲ServerSocketChannel綁定監聽端口號

JDK 1.7以前,ServerSocketChannel沒有bind()方法,所以須要經過他關聯的的socket對象的socket()來綁定。

1
2
// JDK1.7以前
serverSocketChannel.socket().bind(new InetSocketAddress(25000));

JDK1.7及之後,能夠直接經過ServerSocketChannelbind()方法來綁定端口號

1
2
// JDK1.7以後
serverSocketChannel.bind(new InetSocketAddress(25000));

(三). 設置ServerSocketChannel的工做模式

ServerSocketChannel底層默認採用阻塞的工做模式,它提供了一個configureBlocking()方法,容許配置ServerSocketChannel非阻塞方式運行。

1
2
// 設置爲非阻塞模式
serverSocketChannel.configureBlocking(false);

進一步查看configureBlocking源碼以下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public final SelectableChannel configureBlocking(boolean block) throws IOException {
synchronized (regLock) {
if (!isOpen())
throw new ClosedChannelException();
if (blocking == block)
return this;
if (block && haveValidKeys())
throw new IllegalBlockingModeException();
implConfigureBlocking(block);
blocking = block;
}
return this;
}

Javadoc解釋configureBlocking()方法用於調整底層通道的工做模式,即阻塞和非阻塞,默認是阻塞工做模式。

若是block設置爲true,直接返回當前的阻塞式的通道;若是block設置爲false,configureBlocking()方法會調用implConfigureBlocking()方法。這裏implConfigureBlocking()是由ServerSocketChannelImpl實現,最終調用了IOUtil中的native方法configureBlocking()。

(四). 監聽新進來的鏈接

經過ServerSocketChannel.accept()方法監聽新進來的鏈接,這裏須要根據configureBlocking()的配置區分兩種工做模式的使用:

  • 阻塞模式下,當accept()方法返回的時候,它返回一個包含新鏈接SocketChannel,不然accept()方法會一直阻塞到有新鏈接到達。
  • 非阻塞模式下,在沒有新鏈接的狀況下,accept()會當即返回null,該模式下一般不會僅僅監聽一個鏈接,所以需在while循環中調用accept()方法.

阻塞模式:

1
2
3
4
5
6
while(true) {
SocketChannel socketChannel = serverSocketChannel.accept();
// 新鏈接沒到達以前,後面的程序沒法繼續執行
InetSocketAddress remoteAddress = (InetSocketAddress) socketChannel.getRemoteAddress();
// 其餘操做
}

非阻塞模式:

1
2
3
4
5
6
7
8
while(true) {
SocketChannel socketChannel = serverSocketChannel.accept();
// 新鏈接沒到達以前,後面程序一直循環,直到檢測到socketChannel不爲null時進入真正的執行邏輯
if(socketChannel != null) {
InetSocketAddress remoteAddress = (InetSocketAddress) socketChannel.getRemoteAddress();
// 其餘操做
}
}

(五). 關閉ServerSocketChannel

經過調用ServerSocketChannel.close()方法來關閉ServerSocketChannel

1
serverSocketChannel.close();

ServerSocketChannel的完整示例

(一). 阻塞模式

代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void blockingTest() throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(25000));

System.out.println("ServerSocketChannel listening on 25000...");

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

while(true) {
SocketChannel socketChannel = serverSocketChannel.accept();
InetSocketAddress remoteAddress = (InetSocketAddress) socketChannel.getRemoteAddress();
System.out.println("Remote address: " + remoteAddress.getHostString());

while (socketChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
System.out.print((char) byteBuffer.get());
}
byteBuffer.clear();
}
}
}

運行結果:

(二). 非阻塞模式

代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static void nonBlockingTest() throws IOException {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(25001));
System.out.println("ServerSocketChannel listening on 25001...");

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
while (true) {
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("SocketChannel: " + socketChannel);
if (socketChannel != null) {
InetSocketAddress remoteAddress = (InetSocketAddress) socketChannel.getRemoteAddress();
System.out.println("Remote address: " + remoteAddress.getHostString());

while (socketChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
System.out.print((char) byteBuffer.get());
}
byteBuffer.clear();
}
}
}
}

運行結果:


SocketChannel

Java NIO中的SocketChannel是一個鏈接到TCP網絡套接字通道,它是Socket類的對等類。

一般SocketChannel客戶端服務器發起鏈接請求,每一個SocketChannel對象建立時都關聯一個對等的Socket對象。一樣SocketChannel也能夠運行在非阻塞模式下。

SocketChannel的用法

SocketChannel建立的方式有兩種:

  • 客戶端主動建立:客戶端打開一個SocketChannel並鏈接到某臺服務器上;
  • 服務端被動建立:一個新鏈接到達ServerSocketChannel時,服務端會建立一個SocketChannel

(一). 建立SocketChannel

經過SocketChannel的靜態方法open()建立SocketChannel對象。此時通道雖然打開,但並未創建鏈接。此時若是進行I/O操做會拋出NotYetConnectedException異常。

1
SocketChannel socketChannel = SocketChannel.open();

(二). 鏈接指定服務器

經過SocketChannel對象的connect()鏈接指定地址。該通道一旦鏈接,將保持鏈接狀態直到被關閉。可經過isConnected()來肯定某個SocketChannel當前是否已鏈接。

  • 阻塞模式

若是在客戶端SocketChannel阻塞模式下,即服務器端ServerSocketChannel也爲阻塞模式

1
2
3
socketChannel.connect(new InetSocketAddress("127.0.0.1", 25000));
// connect()方法調用之後,socketChannel底層的鏈接建立完成後,纔會執行後面的打印語句
System.out.println("鏈接建立完成...");
  • 非阻塞模式

兩點須要注意:其一,SocketChannel須要經過configureBlocking()設置爲非阻塞模式;其二,非阻塞模式下,connect()方法調用後會異步返回,爲了肯定鏈接是否創建,須要調用finishConnect()的方法。

1
2
3
4
5
6
7
8
9
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 25001));
// connect()方法調用之後,異步返回,須要手動調用finishConnect確保鏈接建立

while(!socketChannel.finishConnect()){
// 檢測到還未建立成功則睡眠10ms
TimeUnit.MILLISECONDS.sleep(10);
}
System.out.println("鏈接建立完成...");

(三). 從SocketChannel讀數據

利用SocketChannel對象的read()方法將數據從SocketChannel讀取Buffer

1
2
3
4
5
6
7
8
9
10
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

// 非阻塞模式下,read()方法在還沒有讀取到任何數據時可能就返回了,因此須要關注它的int返回值。
while (socketChannel.read(byteBuffer) != -1) {
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
System.out.println((char) byteBuffer.get());
}
byteBuffer.clear();
}

(四). 向SocketChannel寫數據

利用SocketChannel對象的write()Buffer的數據寫入SocketChannel

1
2
3
4
5
6
7
8
9
10
11
12
13
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("Client Blocking SocketChannel".getBytes());
// byteBuffer.put("Client Non-Blocking SocketChannel".getBytes());
byteBuffer.flip();

// 非阻塞模式下,write()方法在還沒有寫出任何內容時可能就返回了。因此須要在循環中調用write()
while (byteBuffer.hasRemaining()) {
socketChannel.write(byteBuffer);
}

// 保持睡眠,觀察控制檯輸出
TimeUnit.SECONDS.sleep(20000);
socketChannel.close();

(五). 關閉SocketChannel

利用SocketChannel對象的close()方法關閉SocketChannel

1
socketChannel.close();

SocketChannel的完整示例

(一). 阻塞模式

代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void blockingWrite() throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 25000));

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("Client Blocking SocketChannel".getBytes());
byteBuffer.flip();

while (byteBuffer.hasRemaining()) {
socketChannel.write(byteBuffer);
}

TimeUnit.SECONDS.sleep(20000);
socketChannel.close();
}

服務端打印結果:

(一). 非阻塞模式

代碼示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void nonBlockingWrite() throws Exception {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1", 25001));

while(!socketChannel.finishConnect()){
TimeUnit.MILLISECONDS.sleep(10);
}

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("Client Non-Blocking SocketChannel".getBytes());
byteBuffer.flip();
while (byteBuffer.hasRemaining()) {
socketChannel.write(byteBuffer);
}

TimeUnit.SECONDS.sleep(20000);
socketChannel.close();
}

服務端打印結果:


DatagramChannel

Java NIO中的DatagramChannel是一個能收發UDP的通道,其底層實現爲DatagramSocket + SelectorDatagramChannel能夠調用socket()方法獲取對等DatagramSocket對象。
DatagramChannel對象既能夠充當服務端(監聽者),也能夠充當客戶端(發送者)。若是須要新建立的通道負責監聽,那麼該通道必須綁定一個端口(或端口組):

DatagramChannel的完整示例

數據報發送方:

1
2
3
4
5
6
public static void main(String[] args) throws Exception {
DatagramChannel datagramChannel = DatagramChannel.open();
ByteBuffer byteBuffer = ByteBuffer.wrap("DatagramChannel Sender".getBytes());
int byteSent = datagramChannel.send(byteBuffer, new InetSocketAddress("127.0.0.1", 50020));
System.out.println("Byte sent is: " + byteSent);
}

數據報接收方:

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) throws Exception {
DatagramChannel datagramChannel = DatagramChannel.open();
datagramChannel.socket().bind(new InetSocketAddress(50020));

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
datagramChannel.receive(byteBuffer);
byteBuffer.flip();

while (byteBuffer.hasRemaining()) {
System.out.print((char) byteBuffer.get());
}
}

先運行DatagramChannelReceiveTest,再運行DatagramChannelSendTest,觀察控制檯輸出:

數據報發送方:

數據報接收方:


工具類Channels

NIO通道提供了一個便捷的通道類Channels,其中定義了幾種靜態的工廠方法以簡化通道轉換。其中經常使用的方法以下:

方法 返回 描述
newChannel(InputStream in) ReadableByteChannel 返回一個將從給定的輸入流讀取數據的通道。
newChannel(OutputStream out) WritableByteChannel 返回一個將向給定的輸出流寫入數據的通道。
newInputStream(ReadableByteChannel ch) InputStream 返回一個將從給定的通道讀取字節的流。
newOutputStream(WritableByteChannel ch) OutputStream 返回一個將向給定的通道寫入字節的流。
newReader(ReadableByteChannel ch, CharsetDecoder dec, int minBufferCap) Reader 返回一個reader,它將從給定的通道讀取字節並依據提供的字符集名稱對讀取到的字節進行解碼。
newReader(ReadableByteChannel ch, String csName) Reader 返回一個reader,它將從給定的通道讀取字節並依據提供的字符集名稱將讀取到的字節解碼成字符。
newWriter(WritableByteChannel ch, CharsetEncoder dec, int minBufferCap) Writer 返回一個writer,它將使用提供的字符集名稱對字符編碼並寫到給定的通道中。
newWriter(WritableByteChannel ch, String csName) Writer 返回一個writer,它將依據提供的字符集名稱對字符編碼並寫到給定的通道中。

總結

本文針對NIO中的通道的作了詳細的介紹,對於文件通道FileChannel網絡通道SocketChannelServerSocketChannelDatagramChannel進行了實戰演示。

篇幅較長,可見NIO提供的原生的通道API在使用上並非太容易。

相關文章
相關標籤/搜索