Selector 是 Java NIO 中的一个组件,用于检查一个或多个通道 Channel 的状态是否处于可读、可写状态。如此可以实现单线程管理多个通道,也就是可以管理多个网络连接。
用单线程处理多个 Channel 的好处是我需要更少的线程来处理 Channel 。实际上,你甚至可以用一个线程来处理所有的Channel。从操作系统的角度来看,切换线程的开销是比较昂贵的,并且每个线程都需要占用系统资源,因此暂用线程越少越好。
简而言之,通过 Selector 我们可以实现单线程操作多个 Channel。下面是单线程使用一个 Selector 处理 3 个 Channel 的示例图:
Java NIO Selector中有三个重要的组成:Selector、SelectableChannel 和 SelectionKey。
Selector选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。选择器所在线程不停地更新通道的就绪状态,对通道注册的连接、数据读写事件等事件进行响应。
SelectableChannel 是一个抽象类,提供了通道的可选择性所需要的公共方法的实现,它是所有支持就绪检查的通道类的父类。
因为 FileChannel 类没有继承 SelectableChannel,因此不是可选通道。而所有 Socket 通道都是可选择的,包括从管道 (Pipe) 对象的中获得的通道。SelectableChannel 可以被注册到 Selector 对象上,并且注册时可以指定感兴趣的事件操作,比如:数据读取、数据写入操作。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。
选择键封装了特定的通道与特定的选择器的注册关系。选择键对象由被 SelectableChannel.register() 返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。
Selector 对象是通过调用静态工厂方法 open() 来实例化的,如下:
1
|
Selector Selector = Selector.open();
|
为了将 Channel 和 Selector 配合使用,必须将 Channel 注册到 Selector 上。通过 SelectableChannel.register() 方法来实现,如下:
1
|
channel.configureBlocking(false);
|
与 Selector 一起使用时,Channel 必须处于非阻塞模式下。这意味着不能将 FileChannel 与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式,而套接字通道都可以。
注意 register() 方法的第二个参数。这是一个兴趣 (interest) 集合,意思是在通过 Selector 监听 Channel 时对什么事件感兴趣。可以监听四种不同类型的事件:
SocketChannel 到来的连接事件。SelectionKey.OP_ACCEPT,专注于监听 ServerSocketChannel 接受 SocketChannel 的事件。SelectionKey.OP_READ,监听数据完全到达,通道可读的事件。SelectionKey.OP_READ,监听数据准备完成,通道可写的事件。注意:并非所有的操作在所有的可选择通道上都能被支持。比如
ServerSocketChannel支持Accept操作,而SocketChannel中不支持。我们可以通过通道上的validOps()方法来获取特定通道下所有支持的操作集合。
以上四种事件用 SelectionKey 的四个常量来表示:
1
|
public static final int OP_READ = 1 << 0; // 1
|
如果一个通道同时对多种操作感兴趣,可以用 “位或” 操作符将常量连接起来,如下:
1
|
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
|
可以将一个对象或者更多信息附着到 SelectionKey 上,这样就能方便的识别某个给定的通道。例如,可以附加与通道一起使用的 Buffer,或是包含聚集数据的某个对象。使用方法如下:
1
|
selectionKey.attach(theObject);
|
还可以在用 register() 方法向 Selector 注册 Channel 的时候附加对象,例如:
1
|
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
|
如果要取消该对象,则可以通过该种方式:
1
|
selectionKey.attach(null);
|
一旦向 Selector 注册了一或多个通道,就可以调用几个重载的 select() 方法。这些方法返回你所感兴趣的事件 (如连接、接受、读或写) 已经准备就绪的那些通道。换句话说,如果你对“读就绪”的通道感兴趣,select() 方法会返回读事件已经就绪的那些通道的 SelectionKey。
下面是 select() 方法的几个重载:
select(long timeout) 和 select() 一样,除了最长会阻塞timeout毫秒(参数)。0。也可以通过遍历 SelectionKey 上的已选择键集合来访问就绪的通道,如下:
1
|
Set<SelectionKey> selectedKeys = selector.selectedKeys();
|
注意:每次迭代完成时
Selector自己不会将已经处理完成的SelectionKey实例移除,在迭代的末尾需要调用keyIterator.remove()方法手动移除。
SelectionKey.channel() 方法返回的通道需要强转为你要处理的类型,如:ServerSocketChannel 或 SocketChannel 等。
1
|
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
|
ServerSocketChannel 实例,设置为非阻塞模式,并绑定指定的服务端口;Selector 实例;serverSocketChannel 注册到 selector 上面,并指定事件 OP_ACCEPT,最底层的 socket 通过 channel 和 selector 建立关联;Accept) 的socket,select方法会被阻塞一段时间并返回 0;socket 已经准备好,selector 的 select() 方法会返回 socket 的个数,而且 selectedKeys 方法会返回 socket 对应的事件(connect、accept、read 和 write);这里简单的介绍了 Java NIO 中选择器的用法,有关 Selector 底层的实现原理需要进一步查看源码。
欢迎关注技术公众号: 零壹技术栈
本帐号将持续分享后端技术干货,包括虚拟机基础,多线程编程,高性能框架,异步、缓存和消息中间件,分布式和微服务,架构学习和进阶等学习资料和文章。
原文:https://www.cnblogs.com/ostenant/p/9695187.html