Java NIO之Selector(选择器)

历史回顾:

Java NIO 概览

Java NIO 之 Buffer(缓冲区)

Java NIO 之 Channel(通道)

其他高赞文章:

面试中关于Redis的问题看这篇就够了

一文轻松搞懂redis集群原理及搭建与使用

超详细的Java面试题总结(三)之Java集合篇常见问题

一 Selector(选择器)介绍
Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。

Selector(选择器)

使用Selector的好处在于: 使用更少的线程来就可以来处理通道了, 相比使用多个线程,避免了线程上下文切换带来的开销。

二 Selector(选择器)的使用方法介绍

  1. Selector的创建
    通过调用Selector.open()方法创建一个Selector对象,如下:

Selector selector = Selector.open();
这里需要说明一下

  1. 注册Channel到Selector
    channel.configureBlocking(false);
    SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
    Channel必须是非阻塞的。
    所以FileChannel不适用Selector,因为FileChannel不能切换为非阻塞模式,更准确的来说是因为FileChannel没有继承SelectableChannel。Socket channel可以正常使用。

SelectableChannel抽象类 有一个 configureBlocking() 方法用于使通道处于阻塞模式或非阻塞模式。

abstract SelectableChannel configureBlocking(boolean block)
注意:
SelectableChannel抽象类的configureBlocking() 方法是由 AbstractSelectableChannel抽象类实现的,SocketChannel、ServerSocketChannel、DatagramChannel都是直接继承了 AbstractSelectableChannel抽象类 。
大家有兴趣可以看看NIO的源码,各种抽象类和抽象类上层的抽象类。我本人暂时不准备研究NIO源码,因为还有很多事情要做,需要研究的同学可以自行看看。

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
如果你对不止一种事件感兴趣,使用或运算符即可,如下:

int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
3. SelectionKey介绍
一个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
从SelectionKey访问Channel和Selector很简单。如下:

Channel channel = key.channel();
Selector selector = key.selector();
key.attachment();
可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。例如,可以附加 与通道一起使用的Buffer,或是包含聚集数据的某个对象。使用方法如下:

key.attach(theObject);
Object attachedObj = key.attachment();
还可以在用register()方法向Selector注册Channel的时候附加对象。如:

SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
4. 从Selector中选择channel(Selecting Channels via a Selector)
选择器维护注册过的通道的集合,并且这种注册关系都被封装在SelectionKey当中.

Selector维护的三种类型SelectionKey集合:

已注册的键的集合(Registered key set)

所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过 keys() 方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引发java.lang.UnsupportedOperationException。

已选择的键的集合(Selected key set)

所有与选择器关联的通道所生成的键的集合称为已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过 keys() 方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将引发java.lang.UnsupportedOperationException。

已取消的键的集合(Cancelled key set)

已注册的键的集合的子集,这个集合包含了 cancel() 方法被调用过的键(这个键已经被无效化),但它们还没有被注销。这个集合是选择器对象的私有成员,因而无法直接访问。

注意:
当键被取消( 可以通过isValid( ) 方法来判断)时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效。当再次调用 select( ) 方法时(或者一个正在进行的select()调用结束时),已取消的键的集合中的被取消的键将被清理掉,并且相应的注销也将完成。通道会被注销,而新的SelectionKey将被返回。当通道关闭时,所有相关的键会自动取消(记住,一个通道可以被注册到多个选择器上)。当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相关的键将立即被无效化(取消)。一旦键被无效化,调用它的与选择相关的方法就将抛出CancelledKeyException。

select()方法介绍:

在刚初始化的Selector对象中,这三个集合都是空的。 通过Selector的select()方法可以选择已经准备就绪的通道 (这些通道包含你感兴趣的的事件)。比如你对读就绪的通道感兴趣,那么select()方法就会返回读事件已经就绪的那些通道。下面是Selector几个重载的select()方法:

int select():阻塞到至少有一个通道在你注册的事件上就绪了。
int select(long timeout):和select()一样,但最长阻塞时间为timeout毫秒。
int selectNow():非阻塞,只要有通道就绪就立刻返回。
select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在select()调用时进入就绪的通道不会在本次调用中被记入,而在前一次select()调用进入就绪但现在已经不在处于就绪的通道也不会被记入。例如:首次调用select()方法,如果有一个通道变成就绪状态,返回了1,若再次调用select()方法,如果另一个通道就绪了,它会再次返回1。如果对第一个就绪的channel没有做任何操作,现在就有两个就绪的通道,但在每次select()方法调用之间,只有一个通道就绪了。

一旦调用select()方法,并且返回值不为0时,则 可以通过调用Selector的selectedKeys()方法来访问已选择键集合 。如下:
Set selectedKeys=selector.selectedKeys();
进而可以放到和某SelectionKey关联的Selector和Channel。如下所示:

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();
}
5. 停止选择的方法
选择器执行选择的过程,系统底层会依次询问每个通道是否已经就绪,这个过程可能会造成调用线程进入阻塞状态,那么我们有以下三种方式可以唤醒在select()方法中阻塞的线程。

wakeup()方法 :通过调用Selector对象的wakeup()方法让处在阻塞状态的select()方法立刻返回
该方法使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有进行中的选择操作,那么下一次对select()方法的一次调用将立即返回。
close()方法 :通过close()方法关闭Selector,
该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),同时使得注册到该Selector的所有Channel被注销,所有的键将被取消,但是Channel本身并不会关闭。
三 模板代码
一个服务端的模板代码:

有了模板代码我们在编写程序时,大多数时间都是在模板代码中添加相应的业务代码

ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(“localhost”, 8080));
ssc.configureBlocking(false);

Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);

while(true) {
int readyNum = selector.select();
if (readyNum == 0) {
continue;
}

Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();

while(it.hasNext()) {
    SelectionKey key = it.next();

    if(key.isAcceptable()) {
        // 接受连接
    } else if (key.isReadable()) {
        // 通道可读
    } else if (key.isWritable()) {
        // 通道可写
    }

    it.remove();
}

}
四 客户端与服务端简单交互实例
服务端:

package selector;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class WebServer {
public static void main(String[] args) {
try {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(“127.0.0.1”, 8000));
ssc.configureBlocking(false);

        Selector selector = Selector.open();
        // 注册 channel,并且指定感兴趣的事件是 Accept
        ssc.register(selector, SelectionKey.OP_ACCEPT);

        ByteBuffer readBuff = ByteBuffer.allocate(1024);
        ByteBuffer writeBuff = ByteBuffer.allocate(128);
        writeBuff.put("received".getBytes());
        writeBuff.flip();

        while (true) {
            int nReady = selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> it = keys.iterator();

            while (it.hasNext()) {
                SelectionKey key = it.next();
                it.remove();

                if (key.isAcceptable()) {
                    // 创建新的连接,并且把连接注册到selector上,而且,
                    // 声明这个channel只对读操作感兴趣。
                    SocketChannel socketChannel = ssc.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                }
                else if (key.isReadable()) {
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    readBuff.clear();
                    socketChannel.read(readBuff);

                    readBuff.flip();
                    System.out.println("received : " + new String(readBuff.array()));
                    key.interestOps(SelectionKey.OP_WRITE);
                }
                else if (key.isWritable()) {
                    writeBuff.rewind();
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    socketChannel.write(writeBuff);
                    key.interestOps(SelectionKey.OP_READ);
                }
            }
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

}
客户端:

package selector;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class WebClient {
public static void main(String[] args) throws IOException {
try {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress(“127.0.0.1”, 8000));

        ByteBuffer writeBuffer = ByteBuffer.allocate(32);
        ByteBuffer readBuffer = ByteBuffer.allocate(32);

        writeBuffer.put("hello".getBytes());
        writeBuffer.flip();

        while (true) {
            writeBuffer.rewind();
            socketChannel.write(writeBuffer);
            readBuffer.clear();
            socketChannel.read(readBuffer);
        }
    } catch (IOException e) {
    }
}

}
运行结果:

先运行服务端,再运行客户端,服务端会不断收到客户端发送过来的消息。
运行结果

首页 新随笔 管理随笔 - 10 文章 - 0 评论 - 10
NIO(四、Selector)
目录
NIO(一、概述)
NIO(二、Buffer)
NIO(三、Channel)
NIO(四、Selector)

Selector
前面两个章节都描述了Buffer和Channel,那这个章节就描述NIO三个最核心部分的最后一块内容 - 选择器(Selector)

如何使用
  在前面的章节中描述过多路复用,一个线程通过选择器处理和管理多个通道。由此可见,选择器是用来处理多个通道并监听其通道事件的组件。

Create
  只需要调用 open() 即可创建一个Selector对象:
Selector selector = Selector.open();
Register
  通过 register() 方法注册通道:
ServerSocketChannel channel = ServerSocketChannel.open();
channel.configureBlocking(false);
SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT);
  在注册通道之前,把通道设置成非阻塞模式,观察源码会发现 register() 会校验当前通道是否为非阻塞模式,当是阻塞模式时,会抛出IllegalBlockingModeException 异常。在前面一个章节也提过,为什么FileChannel没有继承SelectableChannel,因为它不需要多路复用,所以在使用通道的时候,只有FileChannel不能向选择器注册通道,凡是继承SelectableChannel都能够向选择器注册通道。
  注册通道方法的第二个参数是SelectionKey中定义的操作类型,你可以填入任何你感兴趣的操作类型,只要这个通道支持,同样,在执行 register() 方法时也会校验该通道是否能够支持该操作。
  注册方法同样也会返回一个SelectionKey对象。

Attach Object
  注册通道的 register() 方法有一个重载方法,可以向选择器注册通道的时候,选择想要带上的附加对象:
public abstract SelectionKey register(Selector sel, int ops, Object att)
throws ClosedChannelException;
  例如,使用时附加上一个字符串:

String ch_name = “123”;
SelectionKey selectionKey = channel.register(selector, SelectionKey.OP_ACCEPT,ch_name);
  获取这个字符串可通过 attachment() 来获取:

// 接收数据
String ch_name_accept = (String) selectionKey.attachment();
  当然,注册时返回的SelectionKey对象也可以在使用时候附加你想要的附加对象:

selectionKey.attach(ch_name);
Block
  因为是一个线程通过选择器来操作通道,那么选择器在操作通道时,必定在处理一个通道的时候,另一个事件已就绪的通道处于等待状态。在确定一个通道事件就绪之后,才能去操作这个通道。上文中讲到使用注册方法register使用的代码示例,将ServerSocketChannel对象向选择器注册,同时关注了这个通道的OP_ACCEPT操作类型事件,那么我们什么时候能确定该通道的accept事件就绪,可以操作这个通道了。选择器为我们提供了三个重载的 select() 方法,这三个方法的主要功能就是选择是否阻塞直到该选择器中的通道所关注的事件就绪,来让程序继续往下运行。
  首先看 select() 方法,该方法会一直阻塞下去,直到选择器中的通道关注的事件就绪:
selector.select();
  参数5000是5秒,参数以毫秒为单位。这个方法会一直阻塞5秒,5秒之内如果没有通道事件就绪的话程序会往下运行:

selector.select(5000);
  selectNow()其实就是非阻塞,无论有无通道事件就绪,程序都会向下执行:

selector.selectNow();
  这三个方法的本质区别无非是选择器阻塞或者等待一个或多个通道的事件就绪有多长时间。

Keys & SelectionKeys
  我们每为一个通道执行 register() 注册方法,就会返回一个SelectionKey,那么这个选择器所有已就绪的SelectionKey就是通过selectedKeys()来获取:
Set selectionKeys = selector.selectedKeys();
  一般这个方法是在select() 之后执行,因为到这一步就意味着要通过这个轮询每个就绪的通道。

Iterator iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
if (key.isAcceptable()) {
// 执行通道的操作
}
//执行完成移除
iterator.remove();
}
  到这里说的是已就绪的通道,那么所有的 SelectionKey 集可以通过 keys() 方法获取:

Set keys = selector.keys();
wake up
  在使用Selector对象的 select() 或者 select(long) 方法时候,当前线程很可能一直阻塞下去,那么用另一个线程去执行 Selector.wakeUp() 方法会唤醒当前被阻塞的线程,使其 select() 立即返回。
  当然,如果当前线程没有阻塞,那么执行了wakeUp() 方法之后,下一个线程的 select() 方法会被立即返回,不再被阻塞下去。
close
  显然,close() 方法能够关闭当前的选择器。
  当一个线程当前呈阻塞状态,那么中止这种状态需要执行选择器的 wakeUp() 方法,close()方法的实现正是这么做的,先唤醒被阻塞的线程,然后继续接下来的操作。接下来就会会置空所有的通道、所有就绪的SelectionKey,让这个选择器上的轮询组件也闲置下来。
SelectionKey
  SelectionKey的功能类似于通道的一个注册令牌。
  这个类定义了4个操作类型,每种操作类型都对应了相应的事件,通过监听这几种不同的事件,在触发该事件时表示所对应的操作已准备就绪:

操作类型 值 描述
OP_READ 1 << 0 读操作
OP_WRITE 1 << 2 写操作
OP_CONNECT 1 << 3 连接socket操作
OP_ACCEPT 1 << 4 接受socket操作
  这里得提一句,所有继承SelectableChannel的通道都会定义自己能够支持的操作类型,可以通过具体通道的 validOps() 方法查看,例如SocketChannel支持read、write、connect这几种操作:

// SocketChannel类
public final int validOps() {
return (SelectionKey.OP_READ | SelectionKey.OP_WRITE | SelectionKey.OP_CONNECT);
}
interestOps & readyOps
  这是SelectionKey的实现类中定义的变量:
private volatile int interestOps;
private int readyOps;
  interestOps用来存储感兴趣的操作集,readyOps用来存储已经就绪的操作集。
  其中 interestOps() 方法和 nioInterestOps() 都会返回interestOps,不同的是interestOps()会校验是否已执行 cancel() ,如果已经取消则会抛出 CancelledKeyException 异常。readyOps同样也有 readyOps() 和 nioReadyOps() 方法,逻辑与interestOps几乎一致。
  观察这段SelectionKey抽象类已经实现的代码:

// SelectionKey 类
public final boolean isAcceptable() {
return (readyOps() & OP_ACCEPT) != 0;
}
  当判断是否访问就绪的时候,只要 readyOps() 与相应的操作类型相与,非零就返回true,代表接受请求操作已就绪。这个是SelectionKey已提供的方法,但是SelectionKey并未提供同样返回boolean判断某个操作在interestOps集是否存在,我们可以自己实现这些方法:

private boolean isInterestRead(SelectionKey selectionKey){
return (selectionKey.interestOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_READ;
}
private boolean isInterestWrite(SelectionKey selectionKey){
return (selectionKey.interestOps() & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE;
}
private boolean isInterestConnect(SelectionKey selectionKey){
return (selectionKey.interestOps() & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT;
}
private boolean isInterestAccept(SelectionKey selectionKey){
return (selectionKey.interestOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
}
Channel、Selector
  在SelectionKey中获取通道或者选择器只需要调用其中的两个方法即可:
SelectableChannel selectableChannel = selectionKey.channel();
Selector sel = selectionKey.selector();

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值