继续NIO部分的内容。上一篇主要介绍了NIO的基本概念,以及Buffer这一NIO核心类的使用,这第二篇我们来看下一个核心类——Channel的使用。

相比BIO中的InputStream、OutputStream,Channel有这么几点不同:

  • BIO中明确了InputStream只能读、OutputStream只能写,而大多数情况下Channel既能读又能写
  • java.io.InputStream#read()直接返回从流中读到的数据,java.io.OutputStream#write(int)直接将参数中的值写入流,但Channel的读和写都是基于上一篇的Buffer的,而且可以是异步的

FileChannel

直接看一个例子:用FileChannel简单的读写一个文件

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
34
35
36
37
38
39
/**
* 写
*/
private static void write() throws IOException {
try (RandomAccessFile file = new RandomAccessFile("basic-channel-usage.txt", "rw"); FileChannel channel = file.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(32);
String datetime = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).format(LocalDateTime.now());
buffer.put(datetime.getBytes(Charset.forName("utf-8")));
buffer.flip();
System.out.println(buffer.remaining() + "字节被写入.");
while (buffer.hasRemaining()) {
channel.write(buffer);
}
}
}
/**
* 读
*/
private static void read() throws IOException {
try (RandomAccessFile file = new RandomAccessFile("basic-channel-usage.txt", "r"); FileChannel channel = file.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(32);
while (channel.read(buffer) != -1) {
buffer.flip();
System.out.println(buffer.remaining() + "字节被读出.");
while (buffer.hasRemaining()) {
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
System.out.println(new String(data, Charset.forName("utf-8")));
}
buffer.clear();
}
}
}

在使用FileChannel之前必须打开它(同理,使用之后必须关闭它),同操作文件、数据库连接等十分相像。获得java.nio.channels.FileChannel有三种方式

  • java.io.FileInputStream#getChannel
  • java.io.FileOutputStream#getChannel
  • java.io.RandomAccessFile#getChannel

注意:通过FileInputStream和FileOutputStream获得的Channel只能读/写,强行写/读将抛出java.nio.channels.NonWritableChannelException和java.nio.channels.NonReadableChannelException异常。

而后,便可使用

  • java.nio.channels.FileChannel#read(java.nio.ByteBuffer)
  • java.nio.channels.FileChannel#write(java.nio.ByteBuffer)

两个方法,结合Buffer对Channel进行读和写,要注意每次操作后Buffer中索引的变化,否则很容易发生不可描述的BUG。

Channel的另一个特性是Scattering Read/Gathering Write,字面上的意思是分散读和集中写,实际使用上就是将一个Channel中的内容读到多个Buffer,或者将多个Buffer的内容写入一个Channel。

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
34
35
36
37
38
39
/**
* 集中写
*/
private static void gatheringWrite() throws IOException {
try (FileOutputStream stream = new FileOutputStream("basic-channel-usage.txt"); FileChannel channel = stream.getChannel()) {
ByteBuffer[] buffers = new ByteBuffer[4];
for (int i = 0; i < buffers.length; i++) {
buffers[i] = ByteBuffer.allocate(32);
buffers[i].put(UUID.randomUUID().toString().replaceAll("-", "").getBytes());
buffers[i].flip();
}
channel.write(buffers);
System.out.println(channel.position());
}
}
/**
* 分散读
*/
private static void scatteringRead() throws IOException {
try (FileInputStream stream = new FileInputStream("basic-channel-usage.txt"); FileChannel channel = stream.getChannel()) {
ByteBuffer[] buffers = new ByteBuffer[4];
for (int i = 0; i < buffers.length; i++) {
buffers[i] = ByteBuffer.allocate(32);
}
channel.read(buffers);
for (ByteBuffer buffer : buffers) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
System.out.println();
}
}
}

变化比较明显的地方其实只有2行:

  • java.nio.channels.FileChannel#write(java.nio.ByteBuffer[])
  • java.nio.channels.FileChannel#read(java.nio.ByteBuffer[])

它们的参数由上面的单个Buffer变成了一个Buffer数组。对数组中的每一个Buffer,一个Buffer写满/读空后,才会继续下一个写/读Buffer。

最后,FileChannel还有一组其他Channel所不具备的方法

  • java.nio.channels.FileChannel#transferTo
  • java.nio.channels.FileChannel#transferFrom

这两个方法的存在意味着若一次I/O操作中有一方是文件的时候,可以方便地使用这两个方法把数据从一个Channel传输到另一个Channel。因为还没有了解到其他类型的Channel,先用两个FileChannel做示例

1
2
3
4
5
6
7
8
9
/**
* 转移
*/
private static void transfer() throws IOException {
try (FileChannel channel1 = new RandomAccessFile("transfer-fr.txt", "rw").getChannel();
FileChannel channel2 = new RandomAccessFile("transfer-to.txt", "rw").getChannel()) {
channel1.transferTo(0, channel1.size(), channel2);
}
}

ServerSocketChannel和SocketChannel

这两个Channel用于TCP通信,基本的使用方法上与基于BIO的ServerSocket和Socket没有太大的差异。

对ServerSocketChannel而言,首先要打开(open)它,然后将它绑定(bind)到某个Socket地址上,而后它才能接受(accept)传入的连接。

1
2
3
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress("localhost", 8888));
SocketChannel clientChannel = serverChannel.accept();

对与SocketChannel,更是只需要一步:打开(open)连接。

1
SocketChannel channel = SocketChannel.open(new InetSocketAddress("localhost", 8888))

无论是服务端还是客户端,取得了SocketChannel的后续操作,同之前的FileChannel就十分相像了,调用下面的2个方法实现读/写。

  • java.nio.channels.SocketChannel#read(java.nio.ByteBuffer)
  • java.nio.channels.SocketChannel#write(java.nio.ByteBuffer)

DatagramChannel

有TCP自然就有UDP,作为一种面向无连接、不可靠的协议,它的读/写是通过发送和接收数据报来实现的。不像TCP有复杂的建立连接的过程,使用DatagramChannel之前只需要指明“我想像哪个Socket地址发送数据报”,或者“我想从哪个Socket地址接收数据报”即可。

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
/**
* 接收
*/
private static void receive() throws IOException {
try (DatagramChannel channel = DatagramChannel.open()) {
ByteBuffer buffer = ByteBuffer.allocate(32);
channel.socket().bind(new InetSocketAddress(9999));
channel.receive(buffer);
buffer.flip();
while (buffer.hasRemaining()) {
System.out.println(buffer.getLong());
}
buffer.clear();
}
}
/**
* 发送
*/
private static void send() throws IOException {
try (DatagramChannel channel = DatagramChannel.open()) {
ByteBuffer buffer = ByteBuffer.allocate(32);
buffer.putLong(System.currentTimeMillis());
buffer.flip();
while (buffer.hasRemaining()) {
channel.send(buffer, new InetSocketAddress(9999));
}
buffer.clear();
}
}

关键的方法就2个

  • java.nio.channels.DatagramChannel#send
  • java.nio.channels.DatagramChannel#receive

但是注意到DatagramChannel中其实也是有read和write方法的。

  • java.nio.channels.DatagramChannel#read(java.nio.ByteBuffer)
  • java.nio.channels.DatagramChannel#write(java.nio.ByteBuffer)

它们的作用是什么呢?上面的Demo是先将Channel的Socket绑定到某个地址再接收数据,或者在发送时才指定发往哪个Socket地址。DatagramChannel亦可以先“连接”到某个地址。

  • java.nio.channels.DatagramChannel#connect

随后收发数据都只能面向这个地址,但切记UDP是无连接的,这个connect并不会向TCP那样建立真正的连接,最多只是设置一种绑定关系。

关于非阻塞

打住!不觉得哪里不对吗?既然是NIO,怎么到目前为止还没有看出哪里体现出非阻塞了……事实上,Channel们既可以仍是阻塞的,也能够通过这个方法并传入参数false,设置成非阻塞的。

  • java.nio.channels.spi.AbstractSelectableChannel#configureBlocking

注意:FileChannel并没有继承至java.nio.channels.spi.AbstractSelectableChannel,也就意味着FileChannel暂时没办法设置成非阻塞的。

设置成非阻塞以后,再调用曾经的阻塞方法,比如accept,read,write,都将立即返回。我们应当通过判断返回值,来确定刚刚调用的非阻塞方法究竟结果几何。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try (ServerSocketChannel serverChannel = ServerSocketChannel.open()) {
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress("localhost", 8888));
System.out.println("服务端在" + serverChannel.getLocalAddress() + "上监听……");
while (true) {
try (SocketChannel clientChannel = serverChannel.accept()) {
if (clientChannel == null) {
System.out.println("没有连接");
continue;
}
//略
}
}
}

上面这样的代码看着都汗……实际上,更多的时候,非阻塞要配合还没有提到的最后一个核心类——Selector一起使用,我们第3篇见。