IO模型到netty的NIO處理

網絡I/O有兩個交互過程:

階段1 wait for data 等待數據準備

階段2 copy data from kernel to user 將數據從內核拷貝到用戶進程中

(1)blocking IO - 阻塞IO:阻塞等待數據準備及拷貝返回

(2)nonblocking IO :循環訪問數據是否準備好

(3)IO multiplexing - IO多路複用:輪循多個socekt連接數據是否就緒

(4)signal driven IO - 信號驅動IO:數據準備好再響應

(5)asynchronous IO - 異步IO:數據準備及拷貝工作都就緒才響應

一、基本概念描述
1.1 I/O簡介
I/O即輸入輸出,是計算機與外界世界的一個藉口。IO操作的實際主題是操作系統。在java編程中,一般使用流的方式來處理IO,所有的IO都被視作是單個字節的移動,通過stream對象一次移動一個字節。流IO負責把對象轉換爲字節,然後再轉換爲對象。

關於Java IO相關知識請參考我的另一篇文章:Java IO 詳解

1.2 什麼是NIO
NIO即New IO,這個庫是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但實現方式不同,NIO主要用到的是塊,所以NIO的效率要比IO高很多。

在Java API中提供了兩套NIO,一套是針對標準輸入輸出NIO,另一套就是網絡編程NIO,本篇文章重點介紹標NIO,關於網絡編程NIO請見Java NIO詳解(二)。

1.3 流與塊的比較
NIO和IO最大的區別是數據打包和傳輸方式。IO是以流的方式處理數據,而NIO是以塊的方式處理數據。

面向流的IO一次一個字節的處理數據,一個輸入流產生一個字節,一個輸出流就消費一個字節。爲流式數據創建過濾器就變得非常容易,鏈接幾個過濾器,以便對數據進行處理非常方便而簡單,但是面向流的IO通常處理的很慢。

面向塊的IO系統以塊的形式處理數據。每一個操作都在一步中產生或消費一個數據塊。按塊要比按流快的多,但面向塊的IO缺少了面向流IO所具有的有雅興和簡單性。

二、NIO基礎
Buffer和Channel是標準NIO中的核心對象(網絡NIO中還有個Selector核心對象,具體請參考Java NIO詳解(二)),幾乎每一個IO操作中都會用到它們。

Channel是對原IO中流的模擬,任何來源和目的數據都必須通過一個Channel對象。一個Buffer實質上是一個容器對象,發給Channel的所有對象都必須先放到Buffer中;同樣的,從Channel中讀取的任何數據都要讀到Buffer中。

2.1 關於Buffer
Buffer是一個對象,它包含一些要寫入或讀出的數據。在NIO中,數據是放入buffer對象的,而在IO中,數據是直接寫入或者讀到Stream對象的。應用程序不能直接對 Channel 進行讀寫操作,而必須通過 Buffer 來進行,即 Channel 是通過 Buffer 來讀寫數據的。

在NIO中,所有的數據都是用Buffer處理的,它是NIO讀寫數據的中轉池。Buffer實質上是一個數組,通常是一個字節數據,但也可以是其他類型的數組。但一個緩衝區不僅僅是一個數組,重要的是它提供了對數據的結構化訪問,而且還可以跟蹤系統的讀寫進程。

使用 Buffer 讀寫數據一般遵循以下四個步驟:

寫入數據到 Buffer;
調用 flip() 方法;
從 Buffer 中讀取數據;
調用 clear() 方法或者 compact() 方法。
當向 Buffer 寫入數據時,Buffer 會記錄下寫了多少數據。一旦要讀取數據,需要通過 flip() 方法將 Buffer 從寫模式切換到讀模式。在讀模式下,可以讀取之前寫入到 Buffer 的所有數據。

一旦讀完了所有的數據,就需要清空緩衝區,讓它可以再次被寫入。有兩種方式能清空緩衝區:調用 clear() 或 compact() 方法。clear() 方法會清空整個緩衝區。compact() 方法只會清除已經讀過的數據。任何未讀的數據都被移到緩衝區的起始處,新寫入的數據將放到緩衝區未讀數據的後面。

.3 關於Channel
Channel是一個對象,可以通過它讀取和寫入數據。可以把它看做IO中的流。但是它和流相比還有一些不同:

Channel是雙向的,既可以讀又可以寫,而流是單向的
Channel可以進行異步的讀寫
對Channel的讀寫必須通過buffer對象
正如上面提到的,所有數據都通過Buffer對象處理,所以,您永遠不會將字節直接寫入到Channel中,相反,您是將數據寫入到Buffer中;同樣,您也不會從Channel中讀取字節,而是將數據從Channel讀入Buffer,再從Buffer獲取這個字節。

因爲Channel是雙向的,所以Channel可以比流更好地反映出底層操作系統的真實情況。特別是在Unix模型中,底層操作系統通常都是雙向的。

在Java NIO中Channel主要有如下幾種類型:

FileChannel:從文件讀取數據的
DatagramChannel:讀寫UDP網絡協議數據
SocketChannel:讀寫TCP網絡協議數據
ServerSocketChannel:可以監聽TCP連接
è¿éåå¾çæè¿°

異步IO
異步 I/O 是一種沒有阻塞地讀寫數據的方法。通常,在代碼進行 read() 調用時,代碼會阻塞直至有可供讀取的數據。同樣, write()調用將會阻塞直至數據能夠寫入,關於同步的IO請參考另一篇文章Java IO。

另一方面,異步 I/O 調用不但不會阻塞,相反,您可以註冊對特定 I/O 事件諸如數據可讀、新連接到來等等,而在發生這樣感興趣的事件時,系統將會告訴您。

異步 I/O 的一個優勢在於,它允許您同時根據大量的輸入和輸出執行 I/O。同步程序常常要求助於輪詢,或者創建許許多多的線程以處理大量的連接。使用異步 I/O,您可以監聽任何數量的通道上的事件,不用輪詢,也不用額外的線程。

Selector
在我的JavaNIO詳解(一)中已經詳細介紹了Java NIO三個核心對象中的Buffer和Channel,現在我們就重點介紹一下第三個核心對象Selector。Selector是一個對象,它可以註冊到很多個Channel上,監聽各個Channel上發生的事件,並且能夠根據事件情況決定Channel讀寫。這樣,通過一個線程管理多個Channel,就可以處理大量網絡連接了。

採用Selector模式的的好處
有了Selector,我們就可以利用一個線程來處理所有的channels。線程之間的切換對操作系統來說代價是很高的,並且每個線程也會佔用一定的系統資源。所以,對系統來說使用的線程越少越好。

但是,需要記住,現代的操作系統和CPU在多任務方面表現的越來越好,所以多線程的開銷隨着時間的推移,變得越來越小了。實際上,如果一個CPU有多個內核,不使用多任務可能是在浪費CPU能力。不管怎麼說,關於那種設計的討論應該放在另一篇不同的文章中。在這裏,只要知道使用Selector能夠處理多個通道就足夠了。

下面這幅圖展示了一個線程處理3個 Channel的情況:

è¿éåå¾çæè¿°

一、初步瞭解Netty
     
     Netty是一個NIO的編程框架,Netty是非常容易和快速開發出網絡應用程序的,它提供了一種全新的形式來方便你編寫網絡應用:它提供了對一些對複雜問題的抽象,提供了一套非常容易使用的api來把我們的業務邏輯和純粹處理網絡的代碼分離開來。因爲Betty是基於NIO,因此它的整個API都是異步的。
     Netty簡化了基於TCP和UDP的編程,但是你仍可以用它的底層的API做一些底層處理,因爲Netty提供了一系列高抽象的API。
     Netty有一系列豐富的特性,讓我們來看一下它的強大之處:
有一套統一的API來處理異步和同步編程模式
使用非常靈活
簡單但卻強大的線程機制
業務組件分離方便重用
極小的縮減不必要的Memory Copy
     Netty開始-理解Netty的設計理念NIONetty開始-理解Netty的設計理念NIO

二、異步編程模式設計

     一般來說網絡編程都會面臨着併發問題,那麼我們就來看一下爲何異步編程模式會很好的解決這個問題,其實這也就詮釋了netty背後的設計思想。
     在現在,瓶頸總是在IO處理上,異步的處理方式就是你可以不用一直等待任務(IO)處理完成,而是在任務進行的時候還可以做其它事情。那麼如何實現這個呢?讓我們來認識一下實現異步的兩種處理方式:
     CallBack:回調是異步處理經常用到的編程模式,回調函數通常被綁定到一個方法上,並且在方法完成之後才執行,這種處理方式在javascript當中得到了充分的運用。回調給我們帶來的難題是當一個問題處理過程中涉及很多回調時,代碼是很難讀的。
     Futures:Futures是一種抽象,它代表一個事情的執行過程中的一些關鍵點,我們通過Future就可以知道任務的執行情況,比如當任務沒完成時我們可以做一些其它事情。它給我們帶來的難題是我們需要去判斷future的值來確定任務的執行狀態。
     其實這兩者很難界定那個好或壞,其實Netty中這兩者都有用到。

異步模式(NIO)和非異步模式(BIO)(N可以理解爲non-blocking或new)

     Block IO會對每個連接創建一個線程,因此這極大限制了JVM創建線程的數量(當然線程池可緩解這個問題,但是也僅僅是緩解),如圖所示:
     
     

     NIO會通過專門的Selector來管理請求,然後可由一個線程來處理請求,如圖所示:


     
     在NIO中,不得不提到的是關於抽象的數據容器ByteBuffer,ByteBuffer允許相同的數據在不同ByteBuffer之間共享而不需要再拷貝一次來處理。Slicing-ByteBuffer允許創建一個新的ByteBuffer通過暴露一個子邊界的形式共享原ByteBuffer的數據,這就大大減少了在數據處理過程中內存的copy(Zero-copy)。ByteBuffer通過一些索引來記錄讀寫的信息,當你向其中寫數據時,它會自動跟蹤你寫到ByteBuffer的位置,類似,他也會自動跟蹤你讀的位置。而且ByteBuffer還提供了很多方法讓你切換讀寫模式(flip)或者清空(clear)或者壓縮(compact)等。
     關於Selectors,NIOAPI通過selector來處理網絡事件和數據。一個Channel代表一個網絡連接。Selector的作用就是來決定這些Channel是否已準備好讀或者寫操作,一個selector可以處理很多連接請求,這就解決了爲一個連接創建一個線程的問題。要想用一個selector需要以下步驟:
     1.創建一個或多個selector用來給打開的channels註冊。
     2.當一個channel註冊後,就需要來確定哪種事件需要你監聽,通常有四種事件:
     OP_ACCEPT 
     OP_CONNECT
     OP_READ
     OP_WRITE
     3.當channels被註冊後,你需要調用Selector.select()方法來阻塞直到上述事件發生。
     4.當方法沒有阻塞時,你會獲得所有SelectionKey實例,這些實例包含了已註冊channel的引用和其狀態,這樣你就可以做你的操作了。

三、Netty VS JavaNIO

1.跨平臺性和通用型
     NIO某些底層的操作依賴於操作系統,因此,你寫的NIO程序有可能在windows上運行良好,但到了Linux可能會出現問題。  Java6和Java7對NIO提供了不同的解決方案,兩個API是不通用的。
2.拓展了ByteBuffer
     Netty提供了對ByteBuffer的封裝類ByteBuf,拓展了JDK中ByteBuffer的功能,增強了易用性。
3.  數據拆分和聚集
     很多時候我們想把數據分割成獨立的Bytebuffer來處理,比如Http協議Header放到一個buffer中,而Body放到另一個buffer中。很不幸,對於這種處理方式直到Java7纔出現,而且如果處理不當,會極易造成OutOfMemoryError。
     Scattering And Gathering:


4.解決了著名的epoll bug



轉載請說明出處,原文鏈接:http://blog.csdn.net/suifeng3051/article/details/23348587
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章