JavaNIO selector (7)

  • Why Use a Selector?
  • Creating a Selector
  • Registering Channels with the Selector
  • SelectionKey’s
    • Interest Set
    • Ready Set
    • Channel + Selector
    • Attaching Objects
  • Selecting Channels via a Selector
  • selectedKeys()
    • wakeUp()
  • close()
  • Full Selector Example

Selector(多路复用器)可以监测一个或多个NIO Channel,并察觉哪些channel 已经读或者写就绪了。使用Selector,只要单线程就可以监测多个NIOchannel,也即多个连接。

为啥要用Selector?

单线程处理多channel的好处是处理channel的线程数减少了。事实上,一个线程就可以处理所有的channel了。线程上下文的切换对操作系统来说是很昂贵的,而且每个线程都需要占据操作系统的资源(内存)。因此,线程数当然越少越好。

不过,现代的操作系统和CPU对多任务的支持越来越强,多线程的平均开销也越来越少。其实,如果CPU是多核的,单任务执行可能是在浪费CPU的算力。当然这是题外话了,在这里,要知道“使用一个Selector,单线程处理多个channel”就行了。
这个图片是一个Selector处理多个Channel的示意图:
这里写图片描述

创建一个Selector

Selector selector = Selector.open();

向Selector注册Channel

要用一个selector处理channel,首先必须要将channel注册到selector。可以用SelectableChannel.register()方法实现:

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

使用Selector必须开启non-blocking (非阻塞)模式,这就是说:不能使用Selector处理FileChannel,因为FileChannel不能切换为阻塞模式,但是SocketChannel可以。 (译者注: 在Java7以后,**似乎**FileChannel也是可以的?)

注意一下 register()的第二个参数。这个是“interest set”(兴趣集合),指的是希望通过Selector监听到的事件类型。这样的事件有四种类型:

  • Connect (连接)
  • Accept (接收)
  • Read(读)
  • Write(写)

通道“触发”一个事件也可以说事件“准备就绪”了。channel成功连接到另一台服务器叫“ connect 就绪”。ServerSocketChannel准备好接收新进入的连接叫“accept”就绪。channel中待读数据就绪时叫“read”就绪。channel 中可以开始写数据的状态叫“write”就绪。

这四种事件都有SelectionKey常量来表示:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

若你需要监听多个事件,可以这样写:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;    

关于这个“interest set”我会在此文中多说点。

SelectionKey

如上所述,注册一个Channel到Selector会返回一个SelectionKey对象,这个对象有些很有意思的特性:

  • The interest set
  • The ready set
  • The Channel
  • The Selector
  • An attached object (optional) 附加的对象(可选)
    下面细述这些特性:

interest set

“interest set” 是一组你有兴趣“selecting”的事件集合,如前“向Selector注册Channel”一目所说。你可以通过SelectionKey这样读写“interest set”:

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;    

可见,你可以用 And “且”运算来处理interest set和给定的SelectionKey ,这样可以找出某个事件是不是在“interest set”中。

Ready Set (“就绪集合”)

“ready set”就是一组channel准备就绪的操作。你选中一个channel后(selection),首先要访问“ready set”。Selection操作下文再讨论。你可以这样访问:

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(附加的对象)

You can attach an object to a SelectionKey this is a handy way of recognizing a given channel, or attaching further information to the channel. For instance, you may attach the Buffer you are using with the channel, or an object containing more aggregate data. Here is how you attach objects:

你可以把一个对象“附加”(attach)到SelectionKey,使用这种方式用来识别给定的channel,或者将附加更多的信息到channel是很方便的。比如,你可以将使用中的Buffer附加到channel,后者附加到一个包含更多聚合数据的对象。举例如:

electionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();

你可以在使用register()函数向Selector注册Channel时就附加一个对象:

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

通过Selector选择Channel

当你向一个Selector注册一个或多个channel时,你可以调用重载的多个Select()方法。这些方法返回你正在监听而且事件(connect, accept, read or write)已经“就绪”的channel。换言之:若你对进入“读就绪”状态的channel感兴趣,你就能通过select()方法获取“读就绪”状态的通道。
下列是select()方法:

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

select():阻塞到至少一个channel在你注册的事件上就绪。

select(long timeout) :跟select()类似,只不过最长阻塞 timeout 毫秒
selectNow(): 不会阻塞。只要有一个channel 是就绪状态就立即返回。(非阻塞选择,如果上次选择操作后,没有通道变成就绪状态,方法返回零)

select()方法返回的int 说明了多少channel是就绪(ready)状态的,亦即,从上次调用select()开始有多个channel变“就绪”状态。如果调用select()返回1,说明有1个channel变“就绪”状态了,再次调用select(),又有一个channel就绪,又会返回1 。如果对前一个channel不做任何处理的话,现在就有2个就绪的channel了,不过,在每次调用select()间隔中,只有一个channel准备就绪。

selectedKeys()

一旦你调用select()方法,并且返回值表名有channel是就绪状态的,你就可以通过selectedKeys()访问“Selected key set ”(已选键集合)中就绪的channel。如下所示:

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

当你注册一个channel到selector,channel.register()返回SelectionKey对象。这个key对象是对“向selector注册channel”这种行为的抽象(译者注:根据JDK的描述,SelectionKey是“token”,即注册的“标识”)。你通过selectedKeySet()访问到的正是key的集合。
你可以迭代“selected key set ”(已选键集合)来访问就绪的channel,示例:

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

这个循环会迭代 “selected key set (已选键集合)中的key。每次迭代都会确认key引用的channel是那种“就绪”类型。

注意每次迭代最后的 keyIterator.remove(); 。Selector自身不会从“已选键集合”中删除SelectionKey实例,你必须在完成处理channel后自己处理。下次,这个channel变成“就绪”状态时,selector 会再次将其将入“已选键集合”。

SelectionKey.channel()返回的channel应该被强转为你需要处理的channel类型,比如 ServerSocketChannel,或者SocketChannel等。

wakeUp() 唤醒

调用select()的线程会被阻塞,但是即使在没有channel就绪时,也可以强制退出select()方法,方法是让另一个线程在被阻塞的线程调用select()的相同selector上调用Selector.wakeup(),被阻塞线程内正在执行的select()将会立即返回键。

如果一个不同的线程调wakeUp(),而select()并没有阻塞到任何线程,那么下一个调用select()的线程将会立即 “wake up ”(唤醒)。

close()

用Selector完成操作后,需要调用close()方法。该方法将关闭Selector,并将其上注册的SelectionKey实例失效,但是channel 本身不会被关闭。

完整例子

下面是一个完整的例子,包括了打开Selector,注册channel到Selector,监测Selector上注册的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<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();
  }
}

猜你喜欢

转载自blog.csdn.net/qq_30118563/article/details/80417790