学习NIO的过程中,对selector选择器的知识产生了兴趣,尤其是关于SelectionKey的轮询后remove()的问题,博主尝试简单地解释一下NIO如何实现非阻塞的。

首先是客户端的代码:

public void testNonBlockingNIOClient() throws IOException{
		//客户端
		//1.获取通道
		SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1",9899));
		
		//2.切换成非阻塞模式
		sChannel.configureBlocking(false);//key
		
		//3.缓冲区
		ByteBuffer buf = ByteBuffer.allocate(1024);
		
		//4.读文件发送至服务端
		buf.put(new Date().toString().getBytes());
		buf.flip();
		sChannel.write(buf);
		buf.clear();
		
		sChannel.close();
		
	}

再者是服务端代码:

public void testNonBlockingNIOServer() throws IOException{
		//服务端
		//1.获取通道
		ServerSocketChannel ssChannel = ServerSocketChannel.open();
		
		//2.切换成非阻塞模式
		ssChannel.configureBlocking(false);
		
		//3.绑定连接
		ssChannel.bind(new InetSocketAddress(9899));
		
		//4.获取选择器selector
		Selector selector = Selector.open();
		
		//5.将通道注册到选择器上
		ssChannel.register(selector, SelectionKey.OP_ACCEPT);//ops选择键,监控通道什么状态
		//return SelectionKey
		                                  //1 读        OP_READ
		                                  //4 写        OP_WRITE
		                                  //8 连接    OP_CONNECT
		                                  //16接受  OP_ACCEPT
		                                  //|连接符监控多个
		//6.轮询式地获取选择器上准备就绪的事件
		while(selector.select()>0){
			//7.获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
			Iterator<SelectionKey> it = selector.selectedKeys().iterator();
			while(it.hasNext()){
				//8.获取准备就绪的事件
				SelectionKey sk = it.next();
				
				//9.判断具体是什么事件准备就绪
				if(sk.isAcceptable()){
					//若“接收就绪”
					SocketChannel SChannel = ssChannel.accept();
					//切换非阻塞模式
					SChannel.configureBlocking(false);
					
					SChannel.register(selector, SelectionKey.OP_READ);
				}else if(sk.isReadable()){
					//若读就绪
					SocketChannel SChannel = (SocketChannel) sk.channel();
					
					ByteBuffer buf = ByteBuffer.allocate(1024);
					
					int len = 0;
					while((len = SChannel.read(buf))>0){
						buf.flip();
						System.out.println(new String(buf.array(),0,len));
						buf.clear();
					}
				}
				
				//10.重点,取消选择键
				it.remove();
			}
		}
	}

可以发现服务端的最后进行了remove()操作,将SelectionKey从迭代器中删除了,博主一开始总觉得很纳闷,SelectionKey中可是记录了相关的channel信息,如果将SelectionKey删除了,那不就代表着将通道信息也抹除了吗,那后续还怎么继续获取通道,说来惭愧,这问题问的确实缺乏水准。

后来博主理了理selector的思路,要知道,一码事归一码事,channel是注册在selector中的,在后面的轮询中,是先将已准备好的channel挑选出来,即selector.select(),再通过selectedKeys()生成的一个SelectionKey迭代器进行轮询的,一次轮询会将这个迭代器中的每个SelectionKey都遍历一遍,每次访问后都remove()相应的SelectionKey,但是移除了selectedKeys中的SelectionKey不代表移除了selector中的channel信息(这点很重要),注册过的channel信息会以SelectionKey的形式存储在selector.keys()中,也就是说每次select()后的selectedKeys迭代器中是不能还有成员的,但keys()中的成员是不会被删除的(以此来记录channel信息)。

那么为什么要删除呢,要知道,迭代器如果只需要访问的话,直接访问就好了,完全没必要remove()其中的元素啊,查询了相关资料,一致的回答是为了防止重复处理(大雾),后来又有信息说明:每次循环调用remove()是因为selector不会自己从已选择集合中移除selectionKey实例,必须在处理完通道时自己移除,这样,在下次select时,会将这个就绪通道添加到已选择通道集合中,其实到这里就已经可以理解了,selector不会自己删除selectedKeys()集合中的selectionKey,那么如果不人工remove(),将导致下次select()的时候selectedKeys()中仍有上次轮询留下来的信息,这样必然会出现错误,假设这次轮询时该通道并没有准备好,却又由于上次轮询未被remove()的原因被认为已经准备好了,这样能不出错吗?

即selector.select()会将准备好的channel以SelectionKey的形式放置于selector的selectedKeys()中供使用者迭代,使用的过程中需将selectedKeys清空,这样下次selector.select()时就不会出现错误了。