Selector
是一个组件(选择器/多路复用器),它可以检查一个或多个Channel通道实例,并确定哪些通道准备好进行读取或写入等操作。通过这种方式,单个线程可以管理多个通道,从而管理多个网络连接。
Selector详解
Selector是SelectableChannel对象的多路复用器。
1、可以通过调用该类的open方法来创建选择器(该方法将使用系统默认的selector provider来创建新的选择器。 也可以通过调用自定义选择器提供者的openSelector方法来创建选择器。 选择器保持打开状态,直到通过其close方法关闭为止。)
Selector selector = Selector.open();
2、也可以通过调用自定义选择器提供者的openSelector方法来创建选择器。
try {
Selector selector = new SelectorProvider() {
@Override
public DatagramChannel openDatagramChannel() throws IOException {
return null;
}
@Override
public DatagramChannel openDatagramChannel(ProtocolFamily family) throws IOException {
return null;
}
@Override
public Pipe openPipe() throws IOException {
return null;
}
@Override
public AbstractSelector openSelector() throws IOException {
return null;
}
@Override
public ServerSocketChannel openServerSocketChannel() throws IOException {
return null;
}
@Override
public SocketChannel openSocketChannel() throws IOException {
return null;
}
}.openSelector();
} catch (IOException e) {
e.printStackTrace();
}
3、Selector可以同时监控多个SelectableChannel的IO状况,是非阻塞IO的核心。一个Selector实例有三个SelectionKey集合。
❍ 所有的SelectionKey集合(键集 key set ):代表了注册在该Selector上的Channel,这个集合可以通过keys()方法返回。
❍ 被选择的SelectionKey集合(所选键集:始终是键集的子集):代表了所有可通过select()方法获取的、需要进行IO处理的Channel,这个集合可以通过selectedKeys()返回。
❍ 被取消的SelectionKey集合(被取消的密钥集 cancelled-key set:始终是密钥集的子集):代表了所有被取消注册关系的Channel,在下一次执行select()方法时,这些Channel对应的SelectionKey会被彻底删除,程序通常无须直接访问(不能直接访问)该集合。
在新创建的选择器中,所有三个集合都是空的。
4、SelectableChannel:它代表可以支持非阻塞IO操作的Channel对象,它可被注册到Selector上,这种注册关系由SelectionKey实例表示。Selector对象提供了一个select()方法,该方法允许应用程序同时监控多个IO Channel。
应用程序可调用SelectableChannel的register()方法将其注册到指定Selector上,当该Selector上的某些SelectableChannel上有需要处理的IO操作时,程序可以调用Selector实例的select()方法获取它们的数量,并可以通过selectedKeys()方法返回它们对应的SelectionKey集合—通过该集合就可以获取所有需要进行IO处理的SelectableChannel集。
SelectableChannel对象支持阻塞和非阻塞两种模式(所有的Channel默认都是阻塞模式),必须使用非阻塞模式才可以利用非阻塞IO操作。SelectableChannel提供了如下两个方法来设置和返回该Channel的模式状态。
❍ SelectableChannel configureBlocking(boolean block):设置是否采用阻塞模式。
❍ boolean isBlocking():返回该Channel是否是阻塞模式。
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
注意:
Channel
必须在非阻塞模式下才能与Selector
一起使用。这意味着之前说过的FileChannel
不能与Selector
一起使用,因为FileChannel
不能切换到非阻塞模式。不过,SocketChannel
可以正常工作。
register()方法的第二个参数。这是一个“interest set”(兴趣集),意思是您想通过Selector在Channel中监听哪些事件。你可以收听四种不同的事件:
- Connect
- Accept
- Read
- Write
"ready":“触发事件”的通道,则该事件“就绪”。
"connect ready":已成功连接到另一个服务器的通道。
"accept":接受传入连接的服务器套接字通道。
"read":具有可读数据的通道即为“可读”通道。
"write":准备好向其写入数据的通道称为“写”就绪。
这四个事件由四个SelectionKey常量表示:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
不同的SelectableChannel所支持的操作不一样,SelectableChannel提供了如下方法来返回它支持的所有操作。
int validOps():返回一个整数值,表示这个Channel所支持的IO操作。
提示:
在SelectionKey中,用静态常量定义了4种IO操作:OP_READ(1)、OP_WRITE(4)、OP_CONNECT(8)、OP_ACCEPT(16),这个值任意2个、3个、4个进行按位或的结果和相加的结果相等,而且它们任意2个、3个、4个相加的结果总是互不相同,所以系统可以根据validOps()方法的返回值确定该SelectableChannel支持的操作。例如返回5,即可知道它支持读(1)和写(4)。int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectableChannel还提供了如下几个方法来获取它的注册状态。
❍ boolean isRegistered():返回该Channel是否已注册在一个或多个Selector上。
❍ SelectionKey keyFor(Selector sel):返回该Channel和sel Selector之间的注册关系,如果不存在注册关系,则返回null。
➢ SelectionKey:该对象代表SelectableChannel和Selector之间的注册关系。
- The interest set
int InterestSet = selectionKey.interestOps(); boolean isInterestedInAccept = SelectionKey.OP_ACCEPT == (interests & SelectionKey.OP_ACCEPT); boolean isInterestedInConnect = SelectionKey.OP_CONNECT == (interests & SelectionKey.OP_CONNECT); boolean isInterestedInRead = SelectionKey.OP_READ == (interests & SelectionKey.OP_READ); boolean isInterestedInWrite = SelectionKey.OP_WRITE == (interests & SelectionKey.OP_WRITE);
将interest set与给定的
SelectionKey
常量进行AND运算,以确定某个事件是否在兴趣集中。- The ready set
ready set是通道准备好的操作集。访问ready set:也可以测试通道准备好哪些事件/操作。int readySet = selectionKey.readyOps();
selectionKey.isAcceptable(); selectionKey.isConnectable(); selectionKey.isReadable(); selectionKey.isWritable();
这四种方法,它们都返回一个布尔值。
- The Channel
- The Selector
从SelectionKey访问通道+选择器是很简单的:Channel channel = selectionKey.channel(); Selector selector = selectionKey.selector();
- An attached object (optional)
也可以将一个对象附加到SelectionKey,这是识别给定通道或将进一步信息附加到通道的一种方便的方法。例如,您可以将正在使用的Buffer附加到通道中,或者附加一个包含更多聚合数据的对象。下面是如何附加对象:
selectionKey.attach(theObject); Object attachedObj = selectionKey.attachment();
在register()方法中,也可以在向Selector注册Channel时附加一个对象:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
➢ ServerSocketChannel:支持非阻塞操作,对应于java.net.ServerSocket这个类,只支持OP_ACCEPT操作。该类也提供了accept()方法,功能相当于ServerSocket提供的accept()方法。
➢ SocketChannel:支持非阻塞操作,对应于java.net.Socket这个类,支持OP_CONNECT、OP_READ和OP_WRITE操作。这个类还实现了ByteChannel接口、ScatteringByteChannel接口和GatheringByteChannel接口,所以可以直接通过SocketChannel来读写ByteBuffer对象。
5、通过选择器选择道
Selector还提供了一系列和select()相关的方法来选择通道:
❍ int select():监控所有注册的Channel,当它们中间有需要处理的IO操作时,该方法返回,并将对应的SelectionKey加入被选择的SelectionKey集合中,该方法返回这些Channel的数量。(这是一个阻塞方法,如果一直没有监听到指定的可用通道,该方法会阻塞当前的线程。)
❍ int select(long timeout):可以设置超时时长的select()操作。(超过延时时长,该方法会返回)
❍ int selectNow():执行一个立即返回的select()操作,相对于无参数的select()方法而言,该方法不会阻塞线程。(无论当前有没有可用通道都立即返回)
❍ Selector wakeup():使一个还未返回的select()方法立刻返回。(中断正在阻塞的select()系列的方法)
关于select()系列的方法的详细说明:
Selector的选择操作由select() 、 select(long)和selectNow()方法执行,包括三个步骤:
1、取消密钥集中的每个密钥都从它所属的每个密钥集中删除,并注销其通道。 此步骤将清空取消的密钥集。(当一个键被取消时,一个键被添加到它的选择器的取消键集中,无论是通过关闭它的通道还是通过调用它的cancel方法。 取消一个键将导致其通道在下一次选择操作期间被注销,此时该键将从所有选择器的键集中删除。)
2、在选择操作开始的那一刻,底层操作系统被查询关于每个剩余通道是否准备好执行由其键的“interest set”标识的任何操作的更新。 对于准备好进行至少一项此类操作的通道,将执行以下两个操作之一:
(1)如果通道的密钥不在所选密钥集中,则将其添加到该集中,并修改其就绪操作集以准确标识通道现在报告已准备就绪的那些操作。 任何先前记录在就绪集中的就绪信息都将被丢弃。
(2)否则,通道的密钥已经在选择的密钥集中,因此它的就绪操作集被修改以标识通道报告为准备就绪的任何新操作。 任何先前记录在就绪集中的就绪信息都将保留; 换句话说,底层系统返回的就绪集被逐位分解为键的当前就绪集。
如果在此步骤开始时键集中的所有键都具有空的“interest set”,则所选键集和任何键的就绪操作集都不会更新。
如果在执行步骤 (2) 时将任何键添加到取消键集中,则它们将按照步骤 (1) 进行处理。
选择操作是否阻塞等待一个或多个通道准备就绪和阻塞等待多长时间,是三种选择方法之间唯一的本质区别。注意:
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(); }
通过选择操作将键添加到所选键集中。 通过调用集合的remove方法或调用从集合中获得的iterator的remove方法,可以直接从选定键集中删除一个键。 键永远不会以任何其他方式从选定键集中删除(这也是为什么当有可选的通道时,我们在操作此通道时还要记得从可选集中删除该键); 特别地,它们不会作为选择操作的其他作用而被删除。 密钥也不能直接添加到选定的密钥集中。
6、关闭通道
void close():关闭此选择器。
如果一个线程当前正因此调用了此选择器的选择方法之一被阻塞,那么它将被中断,就像调用选择器的wakeup方法一样。
哪些与此选择器相关联的任何未取消的键都将失效,它们的通道被取消注册,并且与此选择器相关联的任何其他资源都将被释放。
如果此选择器已关闭,则调用此方法无效。
但通道本身并未关闭。
完整的选择器的例子
下面是一个完整的示例,它打开一个Selector,用它注册一个通道(通道实例化被省略了),并持续监视Selector以确保四个事件(accept、connect、read、write)的“就绪状态”。
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()) {
// 一个连接被一个 ServerSocketChannel 接受。
} else if (key.isConnectable()) {
// 与远程服务器建立了连接。
} else if (key.isReadable()) {
// 一个通道准备好读取。
} else if (key.isWritable()) {
// 一个通道准备好写入。
}
keyIterator.remove();
}
}
NIO的非阻塞式服务器示意图
关于Selector的并发安全性
选择器本身对于多个并发线程使用是安全的; 然而,他们的关键集不是。
选择操作按顺序在选择器本身、键集和选定键集上同步。 它们还在上述步骤 1 和 3 期间在取消的密钥集上同步。
在选择操作正在进行时,对选择器键的兴趣集所做的更改对该操作没有影响; 它们将在下一次选择操作中看到。
钥匙可能会被取消,频道可能会随时关闭。 因此,在一个或多个选择器的键集中存在一个键并不意味着该键有效或它的通道是打开的。 如果另一个线程有可能取消一个键或关闭一个通道,应用程序代码应该注意同步和必要时检查这些条件。
在select()或select(long)方法之一中阻塞的线程可能会被其他线程以三种方式之一中断:
- 通过调用选择器的wakeup方法,
- 通过调用选择器的close方法,或
- 通过调用被阻塞线程的interrupt方法,在这种情况下将设置其中断状态并调用选择器的wakeup方法。
close方法以与选择操作中相同的顺序在选择器和所有三个键集上同步。
通常,选择器的键和选定键集对于多个并发线程使用是不安全的。 如果这样的线程可能直接修改这些集合中的一个,那么应该通过在集合本身上进行同步来控制访问。 这些集合的iterator方法返回的iterator是快速失败的:如果在创建迭代器后以任何方式修改了集合,除了调用迭代器自己的remove方法,则将抛出java.util.ConcurrentModificationException 。