Selector 的创建
通过调用 Selector.open()
方法可以创建一个 Selector 对象:
Selector selector = Selector.open();
注册 Channel 到 Selector
要实现 Selector 管理 Channel,需要将 Channel 注册到相应的 Selector 上。注册时还需要指定 Channel 所关心的事件类型,例如“接收”事件:
代码语言:java复制// 1、获取 Selector 选择器
Selector selector = Selector.open();
// 2、获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3. 设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4、绑定连接
serverSocketChannel.bind(new InetSocketAddress(9999));
// 5、将通道注册到选择器上,并指定监听事件为:“接收”事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
注意事项
(1)与 Selector 一起使用时,Channel 必须处于非阻塞模式下,否则将抛出异常 IllegalBlockingModeException
。
(2)一个通道,并没有一定要支持所有的四种操作(ACCEPT
, CONNECT
, READ
, WRITE
)。比如 ServerSocketChannel
支持 Accept 接受操作,而 SocketChannel
客户端通道则不支持 Accept。可以通过通道上的 validOps()
方法,来获取特定通道下所有支持的操作集合。
轮询查询就绪操作
通过 Selector 的 select()
方法,可以查询出已经就绪的通道操作,这些就绪的状态集合包存在一个元素是 SelectionKey
对象的 Set
集合中。
select() 方法重载
select()
: 阻塞到至少有一个通道在你注册的事件上就绪了。select(long timeout)
: 和select()
一样,但最长阻塞事件为timeout
毫秒。selectNow()
: 非阻塞,只要有通道就绪就立刻返回。
select()
方法返回的 int 值,表示有多少通道已经就绪。一旦调用 select()
方法,并且返回值不为 0 时,在 Selector 中有一个 selectedKeys()
方法,用来访问已选择键集合。可以迭代集合的每一个选择键元素,根据就绪操作的类型,完成对应的操作:
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();
}
注意事项
- 在处理完一个 SelectionKey 后,一定要从
selectedKeys
集合中移除它,否则下次 select() 时,这个 SelectionKey 仍然处于选中状态,会重复处理。 - 在处理 SelectionKey 时,要小心不要执行阻塞操作,因为这会阻塞整个 Selector 线程。如果需要进行耗时操作,请使用其他线程。
停止选择的方法
当使用 Selector
时,我们可能需要在某个时候停止选择操作或关闭 Selector
。以下是您提到的两种方法的详细说明:
wakeup() 方法
Selector
的 wakeup()
方法用于唤醒在 select()
调用中阻塞的线程。无论 select()
是正在阻塞等待就绪的通道,还是已经被 select(long timeout)
设置了一个超时时间,调用 wakeup()
都会使得 select()
调用立即返回。
如果 select()
调用没有阻塞(即已经返回或者还没有开始调用),那么下一次对 select()
的调用将立即返回,无需等待。
Selector selector = Selector.open();
// ... 注册通道到 selector ...
// 某个线程中调用 select()
int readyChannels = selector.select();
// 另一个线程或同一个线程的其他部分调用 wakeup()
selector.wakeup();
// 这将使得上面的 select() 调用返回,即使没有任何通道就绪
close() 方法
Selector
的 close()
方法会关闭选择器,并唤醒所有在 select()
调用中阻塞的线程。与 wakeup()
不同的是,close()
会导致所有注册到该选择器的通道被注销,并且所有的 SelectionKey
实例都将被取消。但请注意,close()
方法并不会关闭通道本身,只是取消它们与选择器的关联。
一旦选择器被关闭,任何后续对选择器的访问(除了 isOpen()
)都将抛出 ClosedSelectorException
。
Selector selector = Selector.open();
// ... 注册通道到 selector ...
// 某个线程中调用 select()
int readyChannels = selector.select();
// 另一个线程或同一个线程的其他部分调用 close()
selector.close();
// 这将使得上面的 select() 调用返回(如果它还在阻塞的话),
// 并且所有注册的通道都被注销,所有的 SelectionKey 都被取消
// 尝试再次使用 selector 将抛出 ClosedSelectorException
// 例如:selector.select(); // 这将抛出 ClosedSelectorException
在实际应用中,应该根据具体需求选择使用wakeup()
还是close()
。如果你只是想唤醒阻塞的线程,并且希望继续使用该选择器,那么应该使用wakeup()
。如果你打算完全关闭选择器并清理所有资源,那么应该使用close()
。