1. 程式人生 > >java學習歷程:NIO為什麼SelectionKey在被輪詢後需要remove()

java學習歷程:NIO為什麼SelectionKey在被輪詢後需要remove()

學習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()時就不會出現錯誤了。