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在使用上并非太容易。

相关文章
相关标签/搜索