Java NIO 中Channel与Channel的传输
- transferFrom()
- transferTo()
在 Java NIO 中,如果其中一个Channel为FileChannel,那么您可以将数据直接从一个通道传输到另一个通道,FileChannel类中有一个transferTo()和一个transferFrom()方法,可以为我们执行这样的操作。
transferFrom()
FileChannel.tranferFrom()方法将数据从源通道传输到FileChannel,例如下面这样:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt","rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt","rs");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
toChannel.transferFrom(fromChannel,position,count);
参数含义:
- fromChannel:源文件Channel
- position: 从源文件的那个索引位置开始写入
- count:传输多少字节
transferTo()
transferTo()方法时从FileChannel传输数据到其他的通道。例如:
RandomAccessFile fromFile = new RandomAccessFile("fromFile.txt","rw");
FileChannel fromChannel = fromFile.getChannel();
RandomAccessFile toFile = new RandomAccessFile("toFile.txt","rw");
FileChannel toChannel = toFile.getChannel();
long position = 0;
long count = fromChannel.size();
fromChannel.transferTo(position,count,toChannel);
注意:一些SoocketChannel实现可能只传输SocketChannel在其内部缓冲区准备好的数据,及时SocketChannel稍后可能有更多的可用数据。
Java NIO Selector
Java NIO Selector 是一个组件,它可以检查一个或多个Java NIO Channel实例,并确定那些Channel准备好用于例如读取或写入的操作。通过这种方式,单个线程可以管理多个通道,从而管理多个网络连接。
为什么选择使用Selector?
它的优点是我们可以使用更少的线程来处理多个通道。事实上我们可以只使用一个线程来处理我们所有的Channel,因为线程也占用操作系统中的一些资源(内存),所以线程之间的切换对于操作系统来说是昂贵的。因此,我们使用的线程越少越好。(当然了,如今的CPU性能越来越好,如果是多核CPU,多线程更能是CPU发挥作用)
下面是一个单线程使用Selector来处理三个Channel的示意图:
创建一个Selector
我们可以通过调用Selector.open()方法来创建一个selector,像下面这样:
Selector selector = Selector.open();
将Channel注册到Selector
为了使用Selector来处理通道,我们必须向Selector注入要处理的Channel,我们可以使用SelectableChannel.register()方法注入,像下面这样:
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
注意,Channel必须处于非阻塞模式下才能与Selector一起使用,这意味着我们不能将FileChannel与Selector一起使用,因为FileChannel无法切换到非阻塞模式。不过,SocketChannel可以正常工作。
注意register方法的第二个参数,它是一个“关注集”,意思是您关注Channel收到那些事件。通过Selector,我们可以监听四种不同的事件:
- Connect
- Accept
- Read
- Write
“触发事件”的通道也被称为“准备好”该事件。因此,已成功连接到另一台服务器的通道是处于“connect ready”也就是连接就绪状态。接受传入连接的服务器套接字通道为“accept”也就是已接受就绪状态。已准备好读取数据的通道为“read”,也就是已读取就绪状态。同理,已准备好写入数据为“write”,也就是已准备好写入就绪状态。这四个事件由四个SelectionKey表示:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
如果想关注多个事件,我们可以将多个常量进行与操作,如下:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey
正如上面所看到的,当我们为Selector中注册Channel时,register方法返回一个SelectionKey对象。这个对象包含有如下的属性:
- Interest Set
- ready set
- Channel
- attached object(可选)
Interest Set
Interest Set 是我们关注“选择”的事件集,我们可以通过SelectionKey读取和写入该集,如下:
int interests = selectionKey.interestOps();
boolean isInterestedInAccept = SelectionKey.OP_ACCEPT == (interests & SelectionKey.OP_ACCEPT);
boolean isInterestedInConnect = SelectionKey.OP_CONNECT == (interests & SelectionKey.OP_CONNECT);
boolean isInterestedInRead = SelectionKey.OP_READ == (interests & SelectionKey.OP_READ);
boolean isInterestedInWrite = SelectionKey.OP_write == (insterests & SelectionKey.OP_WRITE);
向上面这样,我们可以通过与操作来确定某个事件是否在我们的interest set中
Ready Set
ready set 也可以理解为就绪集,是通道准备好的操作集。选择(Selection)后,我们将主要访问ready set。Selection会在稍后介绍。
int readySet = selectionKey.readyOps();
这里我们可以使用与interest set相同测试方法来检测通道准备好那些事件或操作。但是,我们也可以改用下面这四种方式来检测,它们同样返回的是boolean类型的值。
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
channel+Selector
从SelectionKey中访问channel+selector如下:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
Attaching Objects
我们可以将对象附加到SelectionKey,这是识别给定Channel或将更多信息附加到Channel的便捷方式。例如:我们可以将正在使用的Buffer附加到通道中,或者附加一个包含更多聚合数据的对象。如下:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
我们还可以在注册channel时附加对象,如下:
SelectionKey key = channel.register(selector, SelectionKey_OP_READ, theObject);
通过选择器选择Channel
一旦我们注册了一个和多个channel到Selector中,我们可以调用其中之一select()方法.,这些方法返回我们关注的事件中已经准备好的通道(connect、accept、read 或 write),也就是如果我们对准备好读取的Channel感兴趣,我们可以从select()方法中收到准备好读取的Channel。
- int select();
- int select(long timeout);
- int selectNow();
select():会阻塞,直到至少有一个通道为注册的事件做好准备。
select(long timeout):它的作用于select()相同,只是它设置了最长阻塞时间(单位:ms)。
selectNow():它不会阻塞,会立即返回任何准备好的通道。
select方法返回的int值表示有多少通道准备就绪。也就是,自上次调用select以来,准备就绪的通道数量。例如,如果你调用select()并且它返回1,因为一个通道准备就绪,你再次调用select()并且还还有一个通道已经准备好,那么它会再次返回1.
selectedKeys()
一旦调用了select方法后,并且返回值表明有一个或多个通道准备好,那么就可以调用选择器selectedKeys()方法,通过“selected key set”访问就绪通道,如下:
Set<SelectionKey> selectKeys = selector.selectedKyes();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()){
SelectionKey key = keyIterator.next();
if(key.isAcceptable()){
//一个连接被一个ServerSocketChannel接受
}else if(key.isConnectable()){
//已与远程服务器建立连接
}else if(key.isReadable()){
//Channel已经准备好读取
}else if(key.isWritable()){
//Channel已经准备好写入
}
keyIterator.remove();
}
上面的程序中会循环所选键集中的键,对于每个键,它会测试该键以确定改建所引用的通道准备好什么事件。注意调用keyIterator.remove()方法,Selector不会从所选键集本身中删除SelectionKey实例。当我们处理完Channel后必须这样做。下次通道变为就绪时,选择器将再次将其添加到选定键集中。
SelectionKey.channel()方法返回的通道应该转换为对应需要使用的Channel,例如,ServerSocketChannel或SocketChannel等。
wakeUp()
调用了被阻塞的select()方法的线程可以离开select()方法,及时没有通道准备好。这是通过让不同线程在第一个线程调用select()的selector上调用Selector.wakeUp()方法来完成的。然后在select()方法中等待的线程将立即返回。
如果另外一个线程调用了wakeup()方法,而select()中当前没有线程被阻塞,那么下一个调用select()的线程将立即唤醒。
close()
当完成Selector后,将调用其close()方法,这将关闭Selector并是所有注册到此Selector的SelectionKey实例无效。通道本身并未关闭。
示例
这个例子它打开了一个选择器,用它注册了一个通道(通道实例化部分省略),并持续监视选择器以了解四个事件的准备情况。
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
int readyChannels = selector.selectNow();
if(readyChannels == 0) continue;
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> 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();
}
}