当前位置: 首页 > 科技观察

使用_0

时间:2023-03-16 19:12:35 科技观察

谈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();不会再阻塞,会继续运行,即:Iteratoriterator=selector.selectedKeys().iterator();其中,还可以看到名字selectedKeys,表示选中的SelectionKey。上面我们讨论了Selector维护的一个集合——SelectionKey集合,接下来我们再讨论另一个集合——SelectedKey集合。selectedkey集合当Channel有readyIO事件时,会把对应的Key添加到SelectedKey集合中,然后这次While循环会依次处理所有选中的Key。但是选择的Key可能会触发不同的IO事件,所以我们需要区分Key。代码中对OP_ACCEPT和OP_READ进行了区分,分别讨论。ServerSocketChannel第一次注册时,只设置关注OP_ACCEPT事件,所以第一次循环只会进入IsAcceptable分支,所以这里通过iterator.next()迭代器得到的SelectionKey就是serverSocketChannel注册后返回的Key。同样,take要接收的通道是先调用ServerSocketChannel.open()创建的通道;拿到ServerSocketChannel之后,我们就可以调用它的accept()方法来处理连接建立请求了。这里值得注意的是,连接建立后,SocketChannel也需要向Selector注册,因为这些SocketChannel也需要将控制权交给Selector,以便后续就绪的IO事件交由Selector处理。这里我们只关注这个SocketChannel的OP_READ事件。相当于将所有后续连接都关联到Selector。Accept事件处理成功后,服务端会继续循环,然后再次阻塞在selector.select();客户端会继续调用write方法向通道写入数据。数据准备好后,会触发OP_READ事件,然后继续往下走。这次因为事件是OP_READ,所以会进入key.isReadable()分支。进入该分支后,会获取对应的SocketChannel,并从中读取客户端发送的数据。另外值得关注的是iterator.remove();,每次迭代都需要移除当前处理的SelectedKey,这是为什么呢?因为进入SelectedKey集合remove后,NIO中的机制不会给出对应的Key。如果我们不删除它,那么下次调用selector.selectedKeys().iterator();会发现上次处理OP_ACCEPT事件的SelectionKey还在,这就会导致上面的服务端程序抛出空指针异常。你可以注释掉iterator.remove();然后再试一次。客户端的代码很简单,直接给出:publicstaticvoidmain(String[]args)throwsIOException{SocketChannelsocketChannel=SocketChannel.open();socketChannel.connect(newInetSocketAddress("localhost",8080));ByteBufferbuffer=ByteBuffer.allocate(16);buffer.put("test".getBytes(StandardCharsets.UTF_8));缓冲区.翻转();socketChannel.write(buffer);}如果你不删除它,服务器将在下一行中NPE。socketChannel.configureBlocking(false);为什么?因为此时SelectionKey还在,ServerSocketChannel也能拿到,但是当channel.accept();被调用时,实际上并没有client在发起连接(之前的循环已经处理过真正的连接请求,但是Key还没有从SelectedKey中移除)。所以channel.accept();会返回一个null,我们在这个null上调用configureBlocking方法,自然是NPE了。