NIO网络编程(六)—— nio.Selector之基本使用
之前的博客里说到《Netty编程(一)—— BIO和NIO》提到NIO三大组件中有一个叫selector的组件,它能够对多个channel进行选择。而在上一篇博客《Netty编程(五)—— NIO模块下的阻塞和非阻塞模式》中提到了channel、buffer的非阻塞模式下会有cpu空转的问题,因此在这一篇博客中介绍selector,它能够解决非阻塞模式下的问题,使程序更加高效。
Selector 总述
单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用,对于多路复用,需要注意下面几点:
- 多路复用仅针对网络 IO,普通文件 IO 无法利用多路复用
- 如果不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证
- 有可连接事件时才去连接
- 有可读事件才去读取
- 有可写事件才去写入
- 限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件
Selector 使用方法
1、创建Selector
创建一个Selector对象是调用Selector的静态工厂方法Selector.open()来创建:
Selector selector = Selector.open();
2、建立ServerSocketChannel,并将通道设置为非阻塞模式,并注册到选择器中,并设置感兴趣的事件
channel必须工作在非阻塞模式下,而FileChannel没有非阻塞模式,所以不能配合selector一起使用
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
3、将ServerSocketChannel注册到选择器中,并设置感兴趣的事件
将通道注册在selector上,就可以令selecor对通道进行监听事件,当有事件发生时就处理事件,否则会阻塞,这种方式极大地提高CPU的利用率。Selector类没有增加通道的方法,SelectableChannel类里有register(),将selector传递给register方法,就可以向选择器注册这个通道。
SelectionKey ssckey = ssc.register(selector,0);
register方法的第一个参数是通道要向哪个selector注册,第二个参数是SelectionKey类的一个常量,他表示的是通道所注册的操作,这些操作一共四个:
- SelectionKey.OP_ACCEPT:服务器端成功接受连接时触发
- SelectionKey.OP_CONNECT:客户端连接成功时触发
- SelectionKey.OP_READ:数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
- SelectionKey.OP_WRITE:数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况
不过可以在register方法中设置成0(例如上面的代码),后面通过interestOps方法进行选择。
ssckey.interestOps(SelectionKey.OP_ACCEPT);
可以看到register方法会返回一个SelectionKey对象,selector使用SelectionKey对象就能得知是什么通道发生事件以及发生了什么事件,不过通常不需要保留register返回的这个SelectionKey对象,下面会介绍selectedKeys()方法可以在Set中再次返回相同的对象。
4、Selector监听事件
Selector有一个select方法可以监听事件,若没有通道就绪(即注册在这个Selector上没有通道触发事件),程序就会在这个方法处进行阻塞,当有事件发生时,会继续执行,select方法并会返回就绪的通道个数。
int channelnum = selector.select();
5、获取就绪事件并得到对应的通道,然后进行处理
当有事件发生了,就不会继续阻塞在selector.select()语句上,下面我们就需要对事件进行处理,可以使用下面的语句获得所有的事件集合:
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
这里使用的是迭代器,因为selector.selectedKeys()返回的是一个set集合,而之后会对这个set集合有删除操作,所以就得用迭代来遍历,不能循环遍历。可以看到迭代每次获得的是SelectionKey对象,使用这个SelectionKey就能够得到该事件的类型以及发生该事件的通道。
下面先以客户端连接服务器事件来讲解如何使用下面的代码模板对事件进行处理:
while(true)
{
int channelnum = selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();//拿到key,就拿到是什么事件以及哪个channel的事件
iter.remove();
//区分事件类型
if (key.isAcceptable()) {
ServerSocketChannel channel = ((ServerSocketChannel) key.channel());//通过key去拿到channel
SocketChannel sc = channel.accept();//建立连接
sc.configureBlocking(false);
SelectionKey sckey = sc.register(selector, 0, null);
sckey.interestOps(SelectionKey.OP_READ);
}
else if(key.isReadable()){
}
else if(key.isWritable()){
}
else if(key.isConnectable()){
}
}
}
首先使用语句SelectionKey key = iter.next()
就拿到了一个事件的SelectionKey 对象,使用这个对象就能得到这个事件的类型以及channel,具体来说,判断事件类型是使用SelectionKey的四个函数:
key.isAcceptable();
key.isReadable();
key.isWritable();
key.isConnectable();
而获得发生该事件的channel使用下面的语句,需要注意的是大部分时候需要对它进行类型转换:
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
新的SocketChannel连接服务端后,就需要将它注册到selector上,并且注册读、写事件,这样一个连接事件就处理完毕了。此外,可以注意到,每迭代一次都会执行一次删除操作iter.remove()
,这个会在下一篇博客中结合读事件一起加以讲解。
事件取消
上面说到当有事件发生时,就不再会阻塞在selector.select()
上了,之后会通过Iterator<SelectionKey> iter = selector.selectedKeys().iterator()
语句迭代处理,但是如果你不对发生的事件处理的话,selector就会认为这件事件一直存在,就会一直循环下去,比如下面代码:
while(true)
{
int channelnum = selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();//拿到key,就拿到是什么事件以及哪个channel的事件
iter.remove();
if (key.isAcceptable()) {
else if(key.isReadable()){
}
else if(key.isWritable()){
}
else if(key.isConnectable()){
}
}
}
在上面代码中,检测到发生事件后会遍历事件,但是不对事件进行处理,那么经过一轮迭代后回到select()
语句,此时不会阻塞住,会一直循环下去,因为你没有处理的话,selector就会认为这个事件还存在。此时有两种方式,一个处理事件,一个是使用cancel()
方法进行取消事件:
if (key.isAcceptable()) {
key.cancel();
}
因此事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则下次该事件仍会触发。
完整代码
下面给出处理连接事件的服务器完整代码:
public class SelectorServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
//selector可以管理多个channel,需要建立selector和channel的联系,把channnel注册在selector上
try (ServerSocketChannel ssc = ServerSocketChannel.open()) {
ssc.configureBlocking(false);
SelectionKey ssckey = ssc.register(selector,0,null);//将来事件发生后,可以通过这个key可以知道是哪个事件以及是哪个channel的事件
//关注哪几个事件,设置这个ssckey只关注accept事件
ssckey.interestOps(SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress(8080));
while(true)
{
int channelnum = selector.select();
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
SelectionKey key = iter.next();
iter.remove();
//区分事件类型
if (key.isAcceptable()) {
ServerSocketChannel channel = ((ServerSocketChannel) key.channel());//通过key去拿到channel
SocketChannel sc = channel.accept();//建立连接
sc.configureBlocking(false);
SelectionKey sckey = sc.register(selector, 0, null);
sckey.interestOps(SelectionKey.OP_READ);
}
else if(key.isReadable()){
}
else if(key.isWritable()){
}
else if(key.isConnectable()){
}
}
}
}
}
}
**这篇博客先是介绍了一下selector作用以及如何使用selector处理连接事件,下一篇博客会继续介绍selector的读写事件。**