NIO的一坑一惑小記

  • 前言

  不知不覺,已那麼長時間沒有更新東西了,說來真是汗顏啊。(主要是最近在技術上豁然開朗的感受愈來愈少了-_-|||)java

  最近一直在學習Linux相關的東西。又一次接觸到了I/O複用模型(select/poll/epoll),因爲很久沒在用NIO寫過代碼了,今天就小試寫個例子,以鞏固下對I/O複用模型的理解。這不,遇到了一個坑,也產生了一點疑惑。^_^。編程

  • 一坑

  簡單描述:Selector的select方法返回的key集合中有一個SelectionKey是可讀的,可是調用與此SelectionKey關聯的channel的read方法,老是返回讀取長度是-1。既然返回-1,能夠說明tcp連接已經斷開。在下次調用select方法不該再返回這個SelectionKey,也不該該此SelectionKey是可讀狀態的。但事實並不是如此:瀏覽器

public class NIOMain {

	public static void main(String[] args) throws Exception {
		Selector selector = Selector.open();
		ServerSocketChannel serverChannel = ServerSocketChannel.open();
		serverChannel.configureBlocking(false);
		serverChannel.socket().bind(new InetSocketAddress(9000), 10);
		serverChannel.register(selector, SelectionKey.OP_ACCEPT);
		
		doSelect(selector);
		
	}
	
	public static void doSelect(Selector selector)throws Exception{
		while (true) {
			int srt=selector.select();
			if(srt<=0){
				continue;
			}
			Set<SelectionKey> keys = selector.selectedKeys();
			Iterator<SelectionKey> iter = keys.iterator();
			
			while(iter.hasNext()){
				SelectionKey key = iter.next();
				if(key.isAcceptable()){
					ServerSocketChannel  sChannel= (ServerSocketChannel) key.channel();
					SocketChannel cChannel = sChannel.accept();
					cChannel.configureBlocking(false);
					cChannel.register(selector, SelectionKey.OP_READ);
				}else if(key.isReadable()){
					SocketChannel cChannel = (SocketChannel) key.channel();
					ByteBuffer bb = ByteBuffer.allocate(1024);
					int len =cChannel.read(bb);
					bb.flip();
					if(bb.hasArray() && len>0){
						System.out.println("from client "+":"+ new String(bb.array(),0,len));
						int newInterestOps = key.interestOps();
						newInterestOps |= SelectionKey.OP_WRITE;
						key.interestOps(newInterestOps);
					}else if(len==-1){
						System.out.println("no data");//在這裏不能忘記關閉channel
					}
					
					bb.clear();
				}
				iter.remove();
			}
		}
	}

}

  運行此代碼,而後在瀏覽器裏輸入127.0.0.1:9000,回車。結果是控制檯裏首先打印出http協議的信息。而後就是死循環打印no data。緣由可想而知,瀏覽器在發起http請求後,必定時間沒有獲得服務器端的相應,便會斷開tcp連接。此時channel的read方法就會返回-1。坑的是,連接都已經斷開了,Selector還能將它select出來,而且一直是可讀狀態。這就致使了一直死循環打印no data。若是這種事情發生在生產環境,後果然是不堪設想啊。服務器

  解決方式雖然比較簡單,但卻不能疏忽遺漏。當channel的read方法返回-1時。調用channel的close方法關閉channel。上邊代碼就是在打印no data的地方添加一行:cChannel.close()。這樣channel對應的SelectionKey也就不會再被select出來了。也就再也不發生死循環了。session

  • 一惑

  NIO編程中我一直有一個疑惑或者說不肯定,就是何時調用channel的write方法將數據返回給客戶端。框架

  在網上看到的一些例子代碼中無非兩種。socket

  1. 直接返回---服務器端讀取到客戶端發過來的數據後,直接調用channel的write方法將數據返回給客戶端。
  2. 註冊Writable事件,可寫事件發生後再返回---服務器讀取到客戶端發來的數據後,而後將channel註冊到selector對Writable感興趣。當可寫後,再調用channel.write寫數據。但這個方式必定得注意:當寫完數據後,必定取消對Writable事件的感興趣。不然服務器又得忙到崩潰。

  這兩個方式彷佛均可以工做,跑一些例子也都沒發現什麼問題。可是內心老是感受有一點不夠明確不夠開朗(可能就是由於對系統底層的實現不夠明確的緣由)。Java有一些成熟的開源的NIO框架,好比netty、mina。何不去看看他們是如何處理的呢?好,接下來就看看mina的實現方式。(我這裏看的是mina2.0.2版本)tcp

  接下來是我追蹤到AbstractPollingIoProcessor的flushNow方法的代碼學習

  

  因爲篇幅就不貼上writeBuffer方法的所有代碼,其關鍵調用:,writeBuffer方法也是將write方法返回的localWrittenBytes返回。接下來讓咱們抓緊看看write方法的實現吧。並看看到底返回的是什麼東西spa

  

  拋開其餘的細節無論,我們先看看如何實現向客戶返回數據的,mina直接從session中拿到關聯的SocketChannel,而後直接調用SocketChannel的write方法寫數據到客戶端,並將write寫出去數據的長度記錄下來。

  讓咱們返回到最開始flushNow方法:

  

  能夠看到,當channel寫出去的數據長度大於零,而且buff裏還有數據要寫時。調用了setInterestedInWrite方法,經過方法名也知道是在註冊對寫事件感興趣是吧,看下代碼明確下吧

  

  沒錯,確實是在註冊對寫事件感興趣。在flushNow方法後邊還有一個對localWrittenBytes等於零的判斷:

  

  經過源代碼裏的註釋,就知道,當localWrittenBytes等於零時,也就是調用channel的write沒有寫出任何數據,此時就是內核的Buufer滿了,是不可寫狀態。因此這裏也調用setInterestedInWrite方法註冊可寫感興趣,以待可寫事件發生後再發送數據到客戶端。

  總結一下mina的實現就是:讀取到客戶端請求的數據後,就調用channel的write方法向客戶返回數據,若是channel的write方法沒有把所要返回的數據所有發送完,就註冊對可寫感興趣,以待下次可寫事件觸發時再繼續發送。

 

 

 

 

  就寫到這吧,有啥說的不清楚,說的不許確的地方,還望高手不吝指教(*^__^*) ……

相關文章
相關標籤/搜索