Java Nio 七、Java NIO Selector

最后更新时间:2014-06-23

一个Selector是一个Java NIO的组件,他可以检测一个或者多个NIO的通道,并且决定哪个通道准备好某些事件,例如读或者写。这种方式一个单独的线程就可以管理多个通道,以及多个网络连接。

为什么使用Selector

使用只是单线程去管理多个通道的优点就是你需要更少的线程去处理这些通道。实际上,你可以只是使用一个线程去处理所有的通道。线程切换对于操作系统是昂贵的,并且每一个线程在操作系统中也会消耗一些资源(内存)。因此,使用越少的线程越好。

但是请记住,现在的操作系统和CPU在多任务处理中变得越来越好,以至于多线程的负荷随着时间的推移变得很小了。实际上,如果一个CPU是多核,你可能会浪费CPU的能力在不是多任务的情况下。不管怎样,那个设计的主题属于不同的主题。它足够在这里说了,使用Selector,你可以用单线程处理多个channels。

这里有一个在单线程的情况下去处理三个通道的图解:


创建一个Selector

通过调用Selector.open()方法可以创建一个Selector,像下面这样:

Selector selector = Selector.open();

用Selector注册通道

为了跟随Selector使用一个Channel,你必须用Selector注册一个通道。这个可以使用SelectableChannel.register()方法去实现,像下面这样:

channel.configureBlocking(false);

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

这个Channel必须是在非阻塞模式下使用Selector。这就意味着你不能在FileChannel中使用Selector,因为FileChannel不能切换到非阻塞模式。但是SocketChannel会工作的很好。

注意register()方法的第二个参数。这是一个“兴趣集合”,意味着你对Channel中的什么事件监听感兴趣,通过这个Selector。这里有你可以监听的四种不同的事件类型:

  1. Connect(连接)
  2. Accept(接收)
  3. Read(读)
  4. Write(写)
一个通道“发送一个事件”也可以被说成为那个事件准备好了。以至于,一个通道已经成功的连接到另外一台服务器是叫做“连接“准备。一个服务器的套接字通道接收进来的连接称之为“接收”准备。一个通道有数据准备去读取了称之为”读“准备。一个通道准备为你去写数据了称之为”写“准备。
这四种事件通过四种SelectionKey常量被呈现:
  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE
如果你对不止一个事件感兴趣,可以一起使用这个常量,像下面这样:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; 

SelectionKey's
正如你在前面部分中看到的,当你用一个Selector的register()方法注册一个Channel的时候返回一个SelectionKey对象。这个SelectionKey对象包含了一些感兴趣的属性:
  • 这些兴趣集合
  • 这些准备的集合
  • 通道
  • 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; 

正如你看到的,你可以通过给予的SelectionKey变量进行逻辑与操作去发现是否一个特定的事件在你的感兴趣的集合中。
准备集合
这个准备集合是通道已经准备好的操作集合。一个selection之后,你将会主要访问这个准备的集合。Selection在后面的部分将会被解释。你可以像这样访问这个准备好的集合:
int readySet = selectionKey.readyOps();

你可以用这个感兴趣的集合用相同的方式测试,这个通道准备好了哪些事件哪些操作。但是你也可以使用这四中方法代替,他们将会返回一个Boolean值:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

通道+选择器
从SelectionKey中访问这个通道和选择器是不重要的。这里有怎么做的方式:
Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector();  

附加的对象
你可以附加一个对象到一个SelectionKey中。这是识别一个给予的通道的一种方便的方式,或者附加深一层的信息到这个通道中。例如,你可能附加这个Buffer到你正在使用的通道中,或者一个包含更多聚集数据的对象。这里有一个怎么附加对象的例子:
selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();

当用Selector注册到Channel中的时候,你也可以使用register方法附加一个先前的对象,这里有一个例子:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
通过一个Selector选择通道
一旦你已经使用一个Selector注册一个或者更多的channels,你可以调用select()方法中的一个。这些方法返回你注册在channels中感兴趣的事件(connect,accept,read或者write)。换句话说,如果你感兴趣准备读的channels,你将会接收来自于select()方法中准备读的channels的数据。
这里有select()方法:
  • int select()
  • int select(long timeout)
  • int selectNow()
select()会堵塞,直到至少一个channel准备好了你注册的这些事件。
select(long timeout)跟select()一样,除了它会堵塞这个timeout的时间,时间是毫秒为单位(这个参数)。
selectNow()一点都不会堵塞。它将会立刻返回,不管channels是否准备好。
通过select()方法返回的这个int值,会告诉有多少channels准备好了。这个就是说,自从你调用select()方法的最后时间,变成准备好的多少个channels。如果你调用select方法,并且它返回1,因为一个channel已经准备好了,你可以再一次调用select方法,并且又一个channel已经变成准备好了,它将会再次返回1。如果你对于第一个准备好的channel什么也没有做,你现在有两个准备好的channels,但是只有一个channel在每一个selector调用之间已经准备好了。
selectedKeys()
一旦你已经调用了select()方法中的一个,他的返回值已经说明了一个或者更多的channels准备好了,你可以通过这个“selected key set”访问这个准备的channels,通过调用这个selectors的selectedKeys方法返回的”selected key set“。就像下面这样:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
当你用Selector注册一个通道的时候,这个Channel.register()方法返回一个SelectionKey对象。这一项显示了通道注册在那个selector上。你可以通过selectedKeySet()方法访问这些项。来自于SelectionKey。
你可以迭代这些选中的集合去访问准备好的通道,就像下面这样:
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();
}

在选中的key上面进行循环迭代。对于每一个key,它去测试这个key去决定被这个key准备好的这个通道引用的什么事件。
注意在每次循环的最后这个keyIterator.remove()方法的调用。这个Selector自己不会从选中的集合中去移除SelectionKey实例。你不得不做这个事情,当你去处理这个通道的时候。下一次这个通道将会变成“ready”状态,这个selector将会再一次把他加到选中的集合中。
被SelectionKey.channel()方法返回的通道应该投到你需要起作用的那个通道上去,例如ServerSocketChannel或者SocketChannel等等。
wakeUp()
已经调用select()方法的一个线程被堵塞了,它可以使得离开这个select()方法,甚至如果没有通道已经准备好了。这个可以在已经调用select()方法的第一个线程上的Selector上通过有一个不同的线程调用Selector.wakeup()方法去做这事情。这个在select()方法之内的等待的线程然后会立即返回。
如果一个不同的线程调用了wakeup()方法,以及在select()内部没有线程被堵塞了,调用select()方法的下一个线程将会立即被唤醒。
close()
当你随着这个selector已经完成了,你调用它的close方法。这个将会关闭selector,并且使得所有注册到selector上的SelectionKey实例无效。这个通道他自己不会被关闭。
完整的Selector实例
这里有一个打开selector的一个完整例子,用它注册一个通道(这个通道的实例化不考虑了),对于四个事件(接受,连接,读,写)的准备就绪保持监听这个Selector:
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<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();
  }
}

翻译地址:http://tutorials.jenkov.com/java-nio/selectors.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值