继续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篇见。