谈JavaNIOSelector上一篇已经讲解了Java中NIO的Buffer和Channel。不太了解的可以回去看看。在这篇文章中,让我们谈谈Selector——选择器。首先,Selector是做什么用的?如果你对这个概念不熟悉,其实我们可以这样理解:选择器把它看成是SQL中的一条select语句,而在SQL中无非就是筛选出一组符合条件的结果。NIO中的Selector也有类似的用途,只不过它选择了一个有readyIO事件的Channel。IO事件代表Channel针对不同IO操作的不同状态,而不是对Channel进行IO操作。IO事件一共有4种定义:OP_READ可读,OP_WRITE可写,OP_CONNECT连接,OP_ACCEPT接收OP_READ等IO事件分类,其ready表示数据在内核态就绪,已经从内核态到用户态缓冲区,然后我们的应用程序可以读取数据,这称为可读。另一个例子是OP_CONNECT。当一个Channel完成握手连接后,Channel将处于OP_CONNECT状态。不知道用户态和内核态的可以看之前的文章《用户态和内核态的区别》。讲BIO模型的时候,用户态在发起read系统调用后会阻塞,直到数据准备好,在内核态复制。到用户模式缓冲区。如果只有一个用户,没关系,想屏蔽多久就屏蔽多久。但是如果此时有其他用户发送请求进来,就会卡在这里等待。这样的串行处理会导致系统的效率极低。这个问题也有解决办法。即为每个用户分配一个线程(ConnectionPerThread)。乍一看,这个想法可能没问题,但是使用线程需要占用系统资源。比如JVM中的一个线程会占用更多的资源,这是非常昂贵的。.如果系统并发稍微多一点(比如上千),你的系统会直接OOM。而且,频繁的创建、销毁、切换线程也是一个耗时的操作。而如果使用NIO,虽然不会阻塞,但是会一直轮询,让CPU空闲,也是一种不友好的方式。而如果使用Selector,只需要一个线程就可以监听多个Channel,而且这个数量可以是几千,几万甚至更多。那么这些Channel是如何与Selector相关联的呢?答案是通过注册,因为现在Selector决定什么时候处理Channel中的事件,注册操作相当于把Channel的控制权交给了Selector。注册后,当Channel有准备好的IO事件时,Selector会选择它们执行相应的操作。说了这么多,我们来看一个例子。客户端的代码比较简单。我们稍后再看。我们先看服务端:publicstaticvoidmain(String[]args)throwsIOException{//创建一个选择器并管理多个通道选择器selector=Selector.open();//创建ServerSocketChannel并绑定端口ServerSocketChannelserverSocketChannel=ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(新InetSocketAddress(8080));//注册通道以选择UpperSelectionKeyserverSocketChannelKey=serverSocketChannel.register(selector,0);//由于一共有4种事件,分别是accept、connect、read和write,//分别代表有连接请求时触发,客户端建立连接时触发,还有Read事件,writable事件//我们可以使用interestOps表示只处理有连接请求的事件serverSocketChannelKey.interestOps(SelectionKey.OP_ACCEPT);System.out.printf("serverSocketChannel%s\n",serverSocketChannelKey);while(true){//如果没有事件发生,线程将阻塞;如果有事件发生,线程会继续执行System.out.println("starttoselect...");选择器.select();//换句话说,如果有连接,会继续往下走//通过selectedKeys包含所有事件,可能包含READ或WRITEIterator<SelectionKey>iterator=selector.selectedKeys().iterator();while(iterator.hasNext()){SelectionKeykey=iterator.next();System.out.printf("选择键%s\n",key);//这里需要区分事件if(key.isAcceptable()){System.out.println("getacceptableevent");//触发该事件的通道在获取事件时必须进行处理,否则会进入非阻塞模式,空闲占用CPU//例如可以使用key.cancel()ServerSocketChannelchannel=(ServerSocketChannel)key。渠道();SocketChannelsocketChannel=channel.accept();socketChannel.configureBlocking(false);//这个socketChannel也需要注册到selector上,相当于把控制权交给了selectorSelectionKeysocketChannelKey=socketChannel.register(selector,0);socketChannelKey.interestOps(SelectionKey.OP_READ);System.out.printf("获取socketChannel%s\n",socketChannel);}elseif(key.isReadable()){System.out.println("getreadableevent");SocketChannel通道=(SocketChannel)key.c渠道();ByteBufferbuf=ByteBuffer.allocate(16);channel.read(buf);buf.翻转();ByteBufferUtil.debugRead(buf);键.取消();}迭代器.remove();}}}好像有点多,不过相应的注释都写了。你可以先看看。其实这里的很多代码都和前面玩Channel的代码差不多。下面是一些我认为值得一提的解释。首先是Selector.open(),类似于Channel的open方法,可以理解为创建一个选择器。第二个是SelectionKeyserverSocketChannelKey=serverSocketChannel.register(selector,0);我们调用了serverSocketChannel的注册方法后,返回了一个SelectionKey。这是什么概念?简单的说,你去商城注册就可以理解为SelectionKey,也就是说SelectionKey就是selector上当前serverSocketChannel的注册证书。选择器维护了一个SelectionKey的集合,用于统一管理。上面selectionkey集合中的每个Key代表一个特定的Channel。register的第二个参数,我们传入0,代表当前Selector需要关注这个Channel的哪些IO事件。0表示不关注任何事件。这里我们使用serverSocketChannelKey.interestOps(SelectionKey.OP_ACCEPT);告诉选择器只关注此通道的OP_ACCEPT事件。IO事件有4个,如果想同时监听多个IO事件怎么办?答案是通过or运算符。serverSocketChannelKey.interestOps(SelectionKey.OP_ACCEPT|SelectionKey.OP_READ);上面说到,NIO虽然不阻塞,但是会一直轮询占用CPU资源,而Selector解决了这个问题。调用selector.select();后,线程会阻塞在这里,而不是像NIO那样疯狂轮询,把CPU占满。因此Selector只有在有事件处理的时候才会执行,其余时间都处于阻塞状态,大大降低了CPU资源占用。当客户端调用connect发起连接时,Channel会处于OP_CONNECT就绪状态,selector.select();不会再阻塞,会继续运行,即:Iterator
