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