天天看点

NIO边看边记 之 selector选择器(六)

Selector(选择器)可同时监听多个通道上感兴趣的事件(如accept事件、read就绪事件、write就绪事件)。使用Selector可以实现一个线程管理多个通道,进而可以管理多个连接。

1.为什么要使用Selector

如果不使用Selector要监听多个channel上感兴趣的事件,则需要多线程操作,一个线程监听一个通道的事件。这样导致线程上下文切换的开销(内存)、增加了编程的复杂度。

2.Selector的创建、注册

(1)创建

Selector selector = Selector.open();
           

(2)注册

创建了Selector之后,需要将需要监听的通道以及该通道上感兴趣的事件注册在selector上。

其中感兴趣的事件包括一下4种:

SelectionKey中定义的常量:

/**
     * Operation-set bit for read operations.
     *
     * <p> Suppose that a selection key's interest set contains
     * <tt>OP_READ</tt> at the start of a <a
     * href="Selector.html#selop">selection operation</a>.  If the selector
     * detects that the corresponding channel is ready for reading, has reached
     * end-of-stream, has been remotely shut down for further reading, or has
     * an error pending, then it will add <tt>OP_READ</tt> to the key's
     * ready-operation set and add the key to its selected-key set.  </p>
     */
    public static final int OP_READ =  << ;
    /**
     * Operation-set bit for write operations.  </p>
     *
     * <p> Suppose that a selection key's interest set contains
     * <tt>OP_WRITE</tt> at the start of a <a
     * href="Selector.html#selop">selection operation</a>.  If the selector
     * detects that the corresponding channel is ready for writing, has been
     * remotely shut down for further writing, or has an error pending, then it
     * will add <tt>OP_WRITE</tt> to the key's ready set and add the key to its
     * selected-key set.  </p>
     */
    public static final int OP_WRITE =  << ;
    /**
     * Operation-set bit for socket-connect operations.  </p>
     *
     * <p> Suppose that a selection key's interest set contains
     * <tt>OP_CONNECT</tt> at the start of a <a
     * href="Selector.html#selop">selection operation</a>.  If the selector
     * detects that the corresponding socket channel is ready to complete its
     * connection sequence, or has an error pending, then it will add
     * <tt>OP_CONNECT</tt> to the key's ready set and add the key to its
     * selected-key set.  </p>
     */
    public static final int OP_CONNECT =  << ;
    /**
     * Operation-set bit for socket-accept operations.  </p>
     *
     * <p> Suppose that a selection key's interest set contains
     * <tt>OP_ACCEPT</tt> at the start of a <a
     * href="Selector.html#selop">selection operation</a>.  If the selector
     * detects that the corresponding server-socket channel is ready to accept
     * another connection, or has an error pending, then it will add
     * <tt>OP_ACCEPT</tt> to the key's ready set and add the key to its
     * selected-key set.  </p>
     */
    public static final int OP_ACCEPT =  << ;
           

SelectionKey.OP_READ表示关注读数据就绪事件

SelectionKey.OP_WRITE表示关注写数据就绪事件

SelectionKey.OP_CONNECT表示关注socket channel的连接完成事件

SelectionKey.OP_ACCEPT表示关注server-socket channel的accept事件

从上面的常量定义可以看到,在注册一个通道时可以关注该通道上的多个感兴趣事件,感兴趣事件之间使用“或”运算。

int intrestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE
           

注册通道使用下面的方法:

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

从上面代码可以看到,需要将channel设置成非阻塞的,因此,FileChannel不可注册到selector上。因为,FileChannel不可工作在非阻塞模式下。而所有套接字通道都可以。

3.SelectionKey

从上面的通道注册方法可以看到,register方法会返回一个SelectionKey对象。通过该对象可以得到以下几个属性:

interest集合

ready集合

channel

selector

附加对象

(1)interest集合

可以通过SelectionKey#interestOps()方法获取 注册的感兴趣的事件。

int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;
           

(2)ready集合

ready集合是通道已经准备好的就绪事件集合。

也可以使用如下方法获取一个boolean值:

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

(3)channel和selector

Channel  channel  = selectionKey.channel();
Selector selector = selectionKey.selector();
           

(4)附加对象

将一个对象或者更多信息附着在SelectionKey上。

**selectionKey.attach(theObject);

Object attachedObj = selectionKey.attachment();**

也可以在使用register向selector注册channel时附加对象。

4.通过Selector选择通道

(1)select()方法

一旦在Selector上注册了感兴趣事件的通道channel,调用select方法就可以返回这些感兴趣事件就绪的通道。select方法有以下三种重载版本。

a.select() 阻塞到至少有一个通道在你注册的事件上就绪

b.select(long timeOut) 阻塞到至少有一个通道在你注册的事件上就绪或者超时timeOut

c.selectNow() 立即返回。如果没有就绪的通道则返回0

select方法的返回值表示就绪通道的个数。

(2)selectedKeys()

调用select方法并且返回了一个或多个就绪的通道之后,就可以调用selectedKeys方法来返回已就绪通道的集合。

就像调用register方法时会返回感兴趣事件的通道的一个SelectionKey一样。这个selectedKeys返回所有已就绪(已选择)通道的集合。

示例代码如下:

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

上面代码遍历这个已就绪(已选择)通道的集合,并分别处理就绪事件。

注意每次循环调用都要记得remove。因为selector不会自己从已选择集合中移除selectionKey实例。必须在处理完通道时自己移除。这样,在下次select时,会将这个就绪通道添加到已选择通道集合中。

SelectionKey#channel()需要将channel类型转换成需要处理的channel类型,如ServerSocketChannel或者SocketChannel。

5.wakeUp()

当一个Selector调用select方法时,会阻塞直到有感兴趣的事件通道就绪。但是也可在其他线程中在那个selector上调用wakeUp方法,使阻塞在select上的线程立即返回。

如果调用wakeUp时并没有select线程阻塞,则下次调用select时会立即返回。

6.close()

用完selector之后可以通过调用close方法来关闭它,此时会将所有注册在该selector上的selectionKey实例失效。但是相应的channel并没有关闭。

示例代码:

Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
  int readyChannels = selector.select();
  if(readyChannels == ) 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();
  }
}