NIO Channel 在 java.nio.channels
包中,Channel 本身不直接访问数据,而是与 Buffer 交互。也就是说,数据可以从 Channel 读取到 Buffer 中,也可以从 Buffer 写到 Channel 中。如下图:
在 Java NIO 中,Channel 有几种重要实现:
因此,本篇就讨论上面几种 Channel 的使用。
FileChannel 是一种用于读取、写入、映射和操作文件的通道。
在使用之前需要获得 FileChannel ,但是其无法直接获取,我们需要通过使用 InputStream、OutputStream 或 RandomAccessFile 来获取 FileChannel 实例。
通过 RandomAccessFile 来获取 Channel
RandomAccessFile accessFile = new RandomAccessFile("/123.txt", "rw");
FileChannel channel = accessFile.getChannel();
通过 Stream 来获取 Channel
FileInputStream inputStream = new FileInputStream("/123.txt");
FileChannel channel = inputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int readBytes = channel.read(byteBuffer);
在开篇我们就说了 Channel 本身不直接访问数据,需要与 Buffer 做交互。所以首先通过 ByteBuffer.allocate(1024)
为 ByteBuffer 分配空间,然后调用 channel.read(byteBuffer)
方法读取数据到 ByteBuffer 中。read( )
方法返回读取的字节数,如果返回 -1 则表示读到了文件末尾。
String data = "2121213123123123123wwdddd";
ByteBuffer buf = ByteBuffer.allocate(1024);
buf.clear();
buf.put(data.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
Channel 写入数据同样与 Buffer 交互,通过调用 write( )
方法向 Channel 中写入数据。注意写入方法在循环中调用,因为无法保证一次是否写入完成,所以需要重复调用 write()
方法,直到 Buffer
中已经没有尚未写入通道的字节。
如传统 IO 中的输入输出流一样,在 FileChannel 使用后需要调用 close( )
方法关闭。当然在我们使用 RandomAccessFile 或者流获取 Channel 后会及时关闭 RandomAccessFile 或流,在它们的 close( )
方法中已经帮我们关闭了 FileChannel ,因此,一般这种情况不需要显式调用 FileChannle#close()
方法。
public abstract long position() throws IOException;
public abstract FileChannel position(long newPosition) throws IOException;
position()
方法用于返回当前 Channel 的文件位置,即从文件的开头开始计数到当前位置的字节数。
同时 FileChannel 也提供了带有参数的 postion(long newPosition)
方法用于设置当前 Channel 的文件位置。
设置位置可以比该文件的当前大小值大,但并不会改变文件的大小。 这样尝试读取数据将立即返回结束文件的标志 -1 。如果尝试写入数据将导致文件变大以容纳新的字节,先前结束的文件和新写入字节之间的字节的值不确定,即形成所谓的 文件空洞 。
public abstract long size() throws IOException;
用于返回当前 Channel 的文件大小。
public abstract FileChannel truncate(long size) throws IOException;
使用该方法可以截取当前 Channel 的一个文件,截取文件时,文件当中指定长度后面的部分将被删除。
public abstract void force(boolean metaData) throws IOException;
将 Channel 中尚未写入磁盘的数据强制写入磁盘。在向 FileChannel 中写入数据时,操作系统会将数据缓存在内存中,所以无法保证写入到 FileChannel 里的数据一定会即时写到磁盘上。要保证这一点,可以调用 force()
方法。
force()
方法有一个 boolean 类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。
/**
* 将字节从该通道的文件传输到给定的可写字节通道。
*/
public abstract long transferTo(long position, long count,
WritableByteChannel target)
throws IOException;
/**
* 从给定的可读字节通道将字节传输到此通道的文件中。
*/
public abstract long transferFrom(ReadableByteChannel src,
long position, long count)
throws IOException;
DatagramChannel 是一种能够首发 UDP 包的通道。UDP 是无连接的网络协议,所以不能像其它通道一样读取和写入,它发送和接收的是数据包。
DatagramChannel channel = DatagramChannel.open();
channel.socket().bind(new InetSocketAddress(9999));
通过 DatagramChannel.open()
方法打开 DatagramChannel , 然后调用 socket()
方法获取 DatagramSocket 用于接收数据包,并绑定 9999 端口。
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.clear();
channel.receive(byteBuffer);
通过调用 receive()
方法从 DatagramChannel 中接收数据,将接收到的数据放入指定的 ByteBuffer 中,如果数据超过 ByteBuffer 的容量,则将剩余的数据丢弃。
String data = "21231wqweqweqewaaaa";
byteBuffer.clear();
byteBuffer.put(data.getBytes());
byteBuffer.flip();
channel.send(byteBuffer, new InetSocketAddress("aysaml.com", 80));
使用 channel.send()
方法向 host 为 aysaml.com 的 80 端口发送数据。
channel.connect(new InetSocketAddress("aysaml.com", 80));
DatagramChannel 也提供了 read()
和 write()
方法用来接收和发送数据,使用这两个方法前需要通过 connect()
方法连接到指定地址。
SocketChannel 是一个连接到 TCP 网络套接字的通道。可以通过以下2种方式创建 SocketChannel :
SocketChannel channel = SocketChannel.open();
channel.bind(new InetSocketAddress("aysaml.com", 80));
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int readBytes = channel.read(byteBuffer);
String data = "212121qdwdqwdas";
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.clear();
byteBuffer.put(data.getBytes());
byteBuffer.flip();
while (byteBuffer.hasRemaining()){
channel.write(byteBuffer);
}
可以将 SocketChannel 设置为非阻塞模式,对于 connect()
, read()
和 write()
方法在调用之后会立即返回,采用异步的执行模式。
connect()
channel.configureBlocking(false);
channel.connect(new InetSocketAddress("aysaml",80));
while (channel.finishConnect()) {
// 连接后
}
write()
write()
方法可能在没有写出任何内容的时候就返回了,所以要在循环中一直调用直到 Buffer 中没有要写的了。read()
write()
方法类似,可能在没有读到任何内容的时候就返回了,所以需要根据返回的读取字节数来判断是否读到数据。对于服务端来说,ServerSocketChannel 是一个可以监听新进来的 TCP 连接的通道。
ServerSocketChannel channel = ServerSocketChannel.open();
channel.bind(new InetSocketAddress(9999));
for(;;) {
SocketChannel socketChannel = channel.accept();
}
上面例子展示了基本用法,与 SocketChannel 基本相同:
channel.accept()
方法接收新进来的连接,并创建 SocketChannel 。同时,ServerSocketChannel 也可以设置成非阻塞模式:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
serverSocketChannel.configureBlocking(false);
for(;;){
SocketChannel socketChannel =
serverSocketChannel.accept();
if(null != socketChannel) {
// do sth..
}
}
注意在使用 SocketChannel 时注意判空。
关于 Channel 方面我们列举了常用的几种 Channel 的使用方式,其中多次涉及了 Buffer 相关内容,会在下篇继续探讨。