JavaNIO Channels和流有一些相似,但是又有些不同:
下面分别介绍一下Channel最重要的一些实现类:
下面是一个基本的使用FileChannel读取数据到buffer的例子:
public class FileChannelExam {
public static void main(String[] args){
try {
String path = FileChannelExam.class.getResource("/data/nio-data.txt").getPath();
// 创建一个文件通道
RandomAccessFile file = new RandomAccessFile(path, "rw");
FileChannel channel = file.getChannel();
// 创建一个字节buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据到buffer
int len = channel.read(buffer);
while (len != -1){
System.out.println("Read " + len);
// 将写模式转变为读模式,
// 将写模式下的buffer内容最后位置设为读模式下的limit位置,作为读越界位,同时将读位置设为0
// 表示转换后重头开始读,同时消除写模式的mark标记
buffer.flip();
// 判断当前读取位置是否到达越界位(position < limit)
while (buffer.hasRemaining()){
// 读取当前position的字节(position++)
System.out.println(buffer.get());
}
// 清空当前buffer内容
buffer.clear();
len = channel.read(buffer);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
需要注意buffer.flip()方法,首先我们从Channel读取数据写入到Buffer,然后调用flip将切换到读模式,才能从buffer中读取数据。
在Java NIO中我们可以直接将数据从一个Channel传输到另一个Channel中,比如FileChannel中有transferTo()和transferFrom()方法。
transferFrom()方法可以将一个源channel中的数据传输到一个FileChannel中
String fromPath = FileChannelExam2.class.getResource("/data/nio-data.txt").getPath();
String toPath = FileChannelExam2.class.getResource("/data/nio-data-to.txt").getPath();
RandomAccessFile fromFile = new RandomAccessFile(fromPath, "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile(toPath, "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel, position, count);
transferFrom()有三个参数,源channel,position,count;position定义目标channel写入的起始位置,count定义写入数据的容量,如果源channel中的数据量小于count,只会写入源channel数据的量。
另外,在SocketChannel的实现中,当前SocketChannel已经读取一部分数据,稍后仍会读取更多数据情况下,并不一定能将完整的数据读取到FileChannel中。
transferTo()方法可以将FileChannel中的数据传输到其他channel中
String fromPath = FileChannelExam2.class.getResource("/data/nio-data.txt").getPath();
String toPath = FileChannelExam2.class.getResource("/data/nio-data-to.txt").getPath();
RandomAccessFile fromFile = new RandomAccessFile(fromPath, "rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile(toPath, "rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position, count, toChannel);
上边两个例子有些相似,唯一的区别就是调用的方法和调用方法的对象。这个方法和SocketChannel也会存在和transferFrom同样的问题。
Selector是Java NIO中用于管理一个或多个Channel的组件,控制决定对哪些Channel进行读写;通过使用Selector让一个单线程可以管理多个Channel甚至多个网络连接。
使用Selector最大的优势就是可以在较少的线程中控制更多的Channel。事实上我们可以使用一个线程控制需要使用的所有Channel。操作系统线程的运行和切换需要一定的开销,使用的线程越小,系统开销也就越少;因此使用Selector可以节省很多系统开销。下图展示了一个线程使用Selector控制三个Channel的情形。
Selector选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。当这么做的时候,可以选择将被激发的线程挂起,直到有就绪的的通道。
SelectableChannel这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。因为FileChannel类没有继承SelectableChannel,因此不是可选通道,而所有socket通道都是可选择的,包括从管道(Pipe)对象的中获得的通道。SelectableChannel可以被注册到Selector对象上,同时可以指定对那个选择器而言,那种操作是感兴趣的。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。
选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被SelectableChannel.register()返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。
JAVA NIO共定义了四种操作类型:OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT(定义在SelectionKey中),分别对应读、写、请求连接、接受连接等网络Socket操作。ServerSocketChannel和SocketChannel可以注册自己感兴趣的操作类型,当对应操作类型的就绪条件满足时OS会通知channel,下表描述各种Channel允许注册的操作类型,Y表示允许注册,N表示不允许注册,其中服务器SocketChannel指由服务器ServerSocketChannel.accept()返回的对象。
|
OP_READ |
OP_WRITE |
OP_CONNECT |
OP_ACCEPT |
服务器ServerSocketChannel |
N |
N |
N |
Y |
服务器SocketChannel |
Y |
Y |
N |
N |
客户端SocketChannel |
Y |
Y |
Y |
N |
Selector selector = Selector.open();
想要通过Selector中控制Channel,必须将Channel注册到Selector中,通过SelectableChannel.register()方法实现。
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
需要注意的是注册到Selector的Channel必须是非阻塞模式的(non-blocking),FileChannel是无法使用的因为FileChannel无法切换到非阻塞模式,SocketChannel非常适合配合Selector使用。
如果在注册Channel的时候希望监听多个事件可以使用“|”连接静态变量
SelectionKey key = channel.register(selector,
SelectionKey.OP_READ|SelectionKey.OP_WRITE);
Channel注册到Selector后会返回一个SelectionKey对象,这个对象包含了下面一些重要属性:
监听集合(interest set)是channel在selector监听的事件类型的集合,可以同SelectionKey读写这个配置。
int interestSet = selectionKey.interestOps();
// 通过 & 操作判断监听配置是否包含某类事件
boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
就绪集合(ready set)是channel已经就绪的操作的集合,我们主要在一个selection操作后访问就绪集合。
int readySet = selectionKey.readyOps();
// 可以使用和interest set 同样的方法测试集合中是否包含某类事件,
// 也可以通过调用下边的一些方法进行判断:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
可以给SelectionKey添加一个附加对象,通常用来标记Channel或者Channel的特征信息。例如,我们可以将和Channel配合使用的Buffer附加到SelectionKey上。
// 附加对象
selectionKey.attach(theObject);
// 获取附加对象
Object attachedObj = selectionKey.attachment();
// 还可以再注册channel的时候直接添加附加对象
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
将多个Channel注册到Selector后,我们就可以通过调用select()方法选择监听了特定事件(connect,accept,read,write)并且已经就绪的Channel。换种说法就是,如果你已经注册了一个监听read事件的channel,它就会通过select()方法接收到read事件。
select方法有几种不同的重载:
三个方法的返回值是最后一次调用select()后就绪的channel的数量,如果你调用select()返回1,表示调用select()后有一个channel准备就绪了;当你再次调用sleect()时再返回1,表示这次又有一个channel就绪了,如果对第一次调用就绪的channel没有做任何操作,这时总共有两个已经准备就绪的channel,在两次调用中都只有一个channel变为就绪状态。
调用select()方法返回就绪channel个数后,可以调用selectedKeys()方法获取就绪channel的SelectionKey集合
Set selectedKeys = selector.selectedKeys();
我们可以通过这个集合访问已经就绪的channel
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
一个线程调用select()后可以通过再次调用select()离开阻塞状态;也可以通过其他线程调用wakeUp()方法是阻塞在select()的Selecor立即返回。
使用完Selector后可以使用close()方法关闭它,这会关闭Selector和清除注册到Selector的SelecionKey对象,但Channel本身并不会关闭。
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
int readyChannels = selector.select();
if(readyChannels == 0) continue;
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
}