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 = 1 << 0;
/**
* 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 = 1 << 2;
/**
* 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 = 1 << 3;
/**
* 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 = 1 << 4;
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集合是通道已经准备好的就绪事件集合。
int readySet = selectionKey.readyOps();
也可以使用如下方法获取一个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时附加对象。
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
4.通过Selector选择通道
(1)select()方法
一旦在Selector上注册了感兴趣事件的通道channel,调用select方法就可以返回这些感兴趣事件就绪的通道。select方法有以下三种重载版本。
a.select() 阻塞到至少有一个通道在你注册的事件上就绪
b.select(long timeOut) 阻塞到至少有一个通道在你注册的事件上就绪或者超时timeOut
c.selectNow() 立即返回。如果没有就绪的通道则返回0
select方法的返回值表示就绪通道的个数。
(2)selectedKeys()
调用select方法并且返回了一个或多个就绪的通道之后,就可以调用selectedKeys方法来返回已就绪通道的集合。
Set selectedKeys = selector.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 == 0) 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();
}
}