NIO系列三:selector|8月更文挑战

553 阅读7分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

选择器简介

Selector一般称为选择器,它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。可以实现单线程管理多个channels,即管理多个网络链接。选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。Selector的好处在于:使用更少的线程来处理通道,相比使用多个线程,避免了线程上下文切换带来的开销。

可选择通道(SelectableChannel)

并不是所有的Channel,都是可以被Selector 复用的。比方说,FileChannel就不能被选择器复用。判断一个Channel能不能被Selector复用,有一个前提:判断他是否继承了一个抽象类SelectableChannel。如果继承了SelectableChannel,则可以被复用,否则不能。

SelectableChannel类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。所有socket通道,都继承了SelectableChannel类都是可选择的,包括从管道(Pipe)对象的中获得的通道。而FileChannel类,没有继承SelectableChannel,因此是不是可选通道。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。通道和选择器之间的关系,使用注册的方式完成。SelectableChannel可以被注册到Selector对象上,在注册的时候,需要指定通道的哪些操作,是Selector感兴趣的。

注册Channel到Selector

使用Channel.register(Selector sel,int ops)方法,将一个通道注册到一个选择器时。第一个参数,指定通道要注册的选择器是谁。第二个参数指定选择器需要查询的通道操作。 Channel必须是非阻塞的。 所以FileChannel不适用Selector,因为FileChannel不能切换为非阻塞模式,更准确的来说是因为FileChannel没有继承SelectableChannel。Socket channel可以正常使用。 SelectableChannel抽象类 有一个configureBlocking()方法用于使通道处于阻塞模式或非阻塞模式。

abstract SelectableChannel configureBlocking(boolean block)

SelectableChannel抽象类的configureBlocking() 方法是由 AbstractSelectableChannel抽象类实现的,SocketChannel、ServerSocketChannel、DatagramChannel都是直接继承了 AbstractSelectableChannel抽象类 。 register() 方法的第二个参数。这是一个“ interest集合 ”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:Connect,Accept,Read,Write; 通道触发了一个事件意思是该事件已经就绪。比如某个Channel成功连接到另一个服务器称为"连接就绪"。一个Server Socket Channel准备好接收新进入的连接称为"接收就绪"。一个有数据可读的通道可以说是"读就绪"。等待写数据的通道可以说是"写就绪"。 这四种事件用SelectionKey的四个常量来表示: SelectionKey.OP_CONNECT SelectionKey.OP_ACCEPT SelectionKey.OP_READ SelectionKey.OP_WRITE

如果Selector对通道的多操作类型感兴趣,可以用“位或”操作符来实现:int key = SelectionKey.OP_READ | SelectionKey.OP_WRITE ;

选择键(SelectionKey)

选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被SelectableChannel.register()返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。

Channel和Selector的关系确定好后,并且一旦通道处于某种就绪的状态,就可以被选择器查询到。这个工作,使用选择器Selector的select()方法完成。select方法的作用,对感兴趣的通道操作,进行就绪状态的查询。Selector可以不断的查询Channel中发生的操作的就绪状态。并且挑选感兴趣的操作就绪状态。一旦通道有操作的就绪状态达成,并且是Selector感兴趣的操作,就会被Selector选中,放入选择键集合中。

一个选择键,首先是包含了注册在Selector的通道操作的类型,比方说SelectionKey.OP_READ。也包含了特定的通道与特定的选择器之间的注册关系。开发过程中选择键是编程的关键。NIO的编程,就是根据对应的选择键,进行不同的业务逻辑处理。 一个SelectionKey键表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。

key.attachment(); //返回SelectionKey的attachment,attachment可以在注册channel的时候指定。

key.channel(); // 返回该SelectionKey对应的channel。

key.selector(); // 返回该SelectionKey对应的Selector。

key.interestOps(); //返回代表需要Selector监控的IO操作的bit mask

key.readyOps(); // 返回一个bit mask,代表在相应channel上可以进行的IO操作。

key.interestOps():

通过以下方法可以判断Selector是否对Channel的某种事件感兴趣

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; key.readyOps() ready 集合是通道已经准备就绪的操作的集合。JAVA中定义以下几个方法用来检查这些操作是否就绪.

//创建ready集合的方法 int readySet = selectionKey.readyOps(); //检查这些操作是否就绪的方法 key.isAcceptable();//是否可读,是返回 true boolean isWritable()://是否可写,是返回 true boolean isConnectable()://是否可连接,是返回 true boolean isAcceptable()://是否可接收,是返回 true

Selector的使用流程

创建Selector

Selector对象是通过调用静态工厂方法open()来实例化的,如下:

//获取Selector选择器
Selector selector = Selector.open();
Selector的类方法open()内部是向SPI发出请求,通过默认的SelectorProvider对象获取一个新的实例。

//将Channel注册到Selector,要实现Selector管理Channel,需要将channel注册到相应的Selector上,如下:


 //获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

//设置为非阻塞
serverSocketChannel.configureBlocking(false);

//绑定连接
serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));

//将通道注册到选择器上,并制定监听事件为:“接收”事件
serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);

上面通过调用通道的register()方法会将它注册到一个选择器上。

需要注意的是: 与Selector一起使用时,Channel必须处于非阻塞模式下,否则将抛出异常IllegalBlockingModeException。也就是FileChannel不能与Selector一起使用,因为FileChannel不能切换到非阻塞模式,而套接字相关的所有的通道却可以。

另外,一个通道并没有一定要支持所有的四种操作。比如服务器通道ServerSocketChannel支持Accept 接受操作,而SocketChannel客户端通道则不支持。可以通过通道上的validOps()方法,来获取特定通道下所有支持的操作集合。

轮询查询就绪操作

下一步是查询就绪的操作。通过Selector的select()方法,可以查询出已经就绪的通道操作,这些就绪的状态集合,包存在一个元素是SelectionKey对象的Set集合中。

下面是Selector几个查询select()方法:

select():阻塞到至少有一个通道在你注册的事件上就绪了。

select(long timeout):和select()一样,但最长阻塞事件为timeout毫秒。

selectNow():非阻塞,只要有通道就绪就立刻返回。

select()方法返回的int值,表示有多少通道已经就绪,更准确的说,是自前一次select方法以来到这一次select方法之间的时间段上,有多少通道变成就绪状态。

下一步通过调用Selector的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();

}

处理完成后,直接将选择键,从这个集合中移除,防止下一次循环的时候,被重复的处理。键可以但不能添加。试图向已选择的键的集合中添加元素将抛出java.lang.UnsupportedOperationException。