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