Java NIO学习教程(三)

原文地址: link.

6.Java NIO通道到通道传输

Java NIO中,如果其中一个Channel是FileChannel,您可以将数据直接从一个Channel传输到另一个Channel。 FileChannel类有一个transferTo( )和transferFrom( )方法,它可以完成此操作。

transferFrom( )
FileChannel.transferFrom( )方法将数据从源Channel传输到FileChannel。这是一个简单的例子:

public class Exam3 {
    public static void main(String[] args) throws IOException {
        RandomAccessFile fromFile = new RandomAccessFile("data.txt", "rw");
        FileChannel fromChannel = fromFile.getChannel();

        RandomAccessFile toFile = new RandomAccessFile("data2.txt", "rw");
        FileChannel toChannel = toFile.getChannel();

        long position = 0;
        long count = fromChannel.size();

        toChannel.transferFrom(fromChannel, position, count);
    }
}

参数positioncount,告诉目标文件中开始写入的位置(position),以及最大传输的字节数(count)。如果源通道的字节数少于count,则传输的字节数减少。

另外,一些SocketChannel实现可能只传输SocketChannel在其内部缓冲区中已准备好的数据——即使SocketChannel稍后可能有更多可用数据。因此,它可能不会将请求的整个数据(count)从SocketChannel传输到FileChannel。

transferTo( )
transferTo( )方法从FileChannel传输到其他某个通道。这是一个简单的例子:

public class Exam4 {
    public static void main(String[] args) throws IOException {
        RandomAccessFile fromFile = new RandomAccessFile("data.txt", "rw");
        FileChannel fromChannel = fromFile.getChannel();

        RandomAccessFile toFile = new RandomAccessFile("data2.txt", "rw");
        FileChannel toChannel = toFile.getChannel();

        long position = 0;
        long count = fromChannel.size();

        fromChannel.transferTo(position, count, toChannel);
    }
}

请注意该示例与之前的示例有多相似。唯一真正的区别是哪个FileChannel发起方法的调用。其余的都是一样的。

SocketChannel的问题也与transferTo( )方法一起出现。SocketChannel实现可能只从FileChannel传输字节,直到发送缓冲区已满,然后停止。

7.Java NIO Selector

Java NIO Selector是一个可以检查一个或多个Java NIO Channel实例的组件,并确定哪些Channel可以用于读取或写入。这样,单个线程可以管理多个Channel,从而管理多个网络连接。

为什么使用Selector?
仅使用单个线程来处理多个通道的优点是需要更少的线程来处理通道。实际上,只需使用一个线程来处理所有通道。对于操作系统而言,在线程之间切换是昂贵的,并且每个线程也占用操作系统中的一些资源(存储器)。因此,使用的线程越少越好。

但请记住,现代操作系统和CPU在多任务处理中变得越来越好,因此随着时间的推移,多线程的开销会变得越来越小。事实上,如果CPU具有多个内核,没有进行多任务处理(并发)可能会浪费CPU运算能力。无论如何,该设计讨论属于不同的内容。这里可以说,你可以使用Selector使用单个线程处理多个通道。

以下是使用Selector处理3个Channel的线程图示:

图6

创建Selector
可以通过调用Selector.open( )方法创建一个Selector,如下所示:

Selector selector = Selector.open();

使用Selector注册Channel
要使用带选择器的通道,必须使用选择器注册通道。这是使用SelectableChannel.register( )方法完成的,如下所示:

channel.configureBlocking(false);

SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

通道必须处于非阻塞模式才能与选择器一起使用。这意味着无法将FileChannel与Selector一起使用,因为FileChannel无法切换到非阻塞模式。套接字通道可以正常工作。

注意register( )方法的第二个参数。这是一个“兴趣集”,意味着你有兴趣在频道中通过选择器收听哪些事件。你可以收听四种不同的活动:

  • Connect(连接)
  • 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对象。该SelectionKey对象包含一些有趣的属性:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object (optional)

我将在下面描述这些属性。

Interest Set
兴趣集是你感兴趣的“选择”事件集,如“使用Selector注册Channel”部分所述。你可以通过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;    

正如你所看到的,你可以使用给定SelectionKey常量与兴趣集合进行比较,以确定某个事件是否在兴趣集中。

Ready Set
Ready Set是通道准备好的一组操作。将主要在选择后访问Ready Set
Selection将在后面的部分中解释。可以像这样访问Ready Set

int readySet = selectionKey.readyOps();

可以使用与"interest set"相同的方式进行测试,以及channel准备好的事件/操作。但是,也可以使用这四种方法,它们都会重设一个布尔值:

  • selectionKey.isAcceptable();
  • selectionKey.isConnectable();
  • selectionKey.isReadable();
  • selectionKey.isWritable();

Channel + Selector
从SelectionKey中访问channel + selector是方便简单。以下是它的完成方式:

Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector();    

Attaching Objects
你可以将对象附加到SelectionKey这是一种识别给定通道或将更多信息附加到通道的便捷方式。例如,你可以将正在使用的缓冲区与通道或包含更多聚合数据的对象相关联。以下是如何附加对象:

selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

你还可以在register( )方法中使用选择器注册Channel时附加对象。这是看起来像这样:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);

通过Selector选择Channel
使用Selector注册一个或多个通道后,可以调用其中一个select( )方法。这些方法返回你感兴趣的事件(连接,接收,读取或写入)“准备好”的通道。换句话说,如果你对准备好阅读的通道感兴趣,你将收到准备好从这些select( )方法中阅读的通道。

以下是select( )方法:

  • int select( )
  • int select(long timeout)
  • int selectNow( )

select( ) 阻塞,直到至少有一个频道为你注册的事件做好准备。

select(long timeout) 与select( )相同,只是它会阻塞最大超时时间(毫秒为单位)。

selectNow( ) 完全没有阻塞。它会立即返回任何已准备好的通道。

select( )方法返回的int值表示准备好了多少个通道。也就是说,自上次调用select( )以来已经准备好了多少个通道。如果你调用select( )并且它返回1,因为一个通道已准备好,并且你再次调用select( ),并且另一个通道已准备好,它将再次返回1。如果你没有对第一个准备好的通道做任何事情,你现在有2个就绪通道,但每次**select()**调用之间只有一个通道准备就绪。

selectedKeys( )
一旦调用了其中一个**select( )**方法并且其返回值指示一个或多个通道已就绪,你可以通过调用选择器selectedKeys( )方法,通过“选定的键集”访问就绪通道。看起来就像这样:

Set<SelectionKey> selectedKeys = selector.selectedKeys();    

使用Selector注册通道时,Channel.register( )方法返回SelectionKey对象。
此键表示通道与该选择器的注册。你可以通过selectedKeySet( )方法访问这些键。来自SelectionKey。

你可以迭代此选定的key集以访问就绪通道。看起来就像这样:

Set<SelectionKey> selectedKeys = selector.selectedKeys();

Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

while(keyIterator.hasNext()) {
    
    SelectionKey key = keyIterator.next();

    if(key.isAcceptable()) {
        // ServerSocketChannel接收了连接.

    } else if (key.isConnectable()) {
        // 与远程服务建立连接.

    } else if (key.isReadable()) {
        // 通道准备好读取

    } else if (key.isWritable()) {
        // 通道准备好写入
    }

    keyIterator.remove();
}

此循环迭代所选key集中的key。对于每个key,它测试key以确定key引用的Channel准备好了什么。

注意每次迭代结束时的keyIterator.remove( )调用。Selector不会从所选键集本身中删除SelectionKey实例。处理完通道后,你必须这样做。下一次通道变为“就绪”时,选择器将再次将其添加到所选键组。

SelectionKey.channel( )方法返回的通道应该转换为你需要使用的通道,例如ServerSocketChannel或SocketChannel等。

wakeUp( )
已调用被阻塞的select( )方法的线程可以离开select( )方法,即使尚未准备好任何通道。这是通过让一个不同的线程调用Selector上的Selector.wakeup( )方法来完成的,在第一个线程调用了select( )方法。然后在select( )内等待的线程将立即返回。

如果另一个线程调用wakeup( )并且当前在select( )中没有阻塞线程,则调用select( )的下一个线程将立即“唤醒”。

close( )
完成选择器后,调用其close( )方法。这将关闭Selector并使使用此Selector注册的所有SelectionKey实例无效。Channel本身并未关闭。

完整Selector示例
这是一个完整的例子,它打开一个Selector,用它注册一个通道(通道实例化被省略),并继续监视Selector以获得四个事件(接收,连接,读,写)的“准备就绪”。

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();
  }
}

【上篇】Java NIO学习教程(二)==>点击

【下篇】Java NIO学习教程(四)==>点击

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值