一、BIO、NIO、AIO通訊機制理解
一、BIO的理解
首先我們通過通訊模型圖來熟悉下BIO的服務端通訊模型:採用BIO通訊模型的服務端,通常由一個獨立的Acceptor執行緒負責監聽客戶端的連線,它接收到客戶端的連線請求之後為每個客戶端建立一個新的執行緒進行鏈路處理,處理完成之後,通過輸出流返回應答給客戶端,執行緒銷燬。這就是典型的一請求一應答通訊模型。這個是在多執行緒情況下執行的。當在單執行緒環境下時,在while迴圈中服務端會呼叫accept方法等待接收客戶端的連線請求,一旦接收到一個連線請求,就可以建立socket,並在該socket上進行讀寫操作,此時不能再接收其它客戶端的連線請求,只能等待同當前連線的客戶端的操作執行完成。
該模型最大的問題就是缺乏彈性伸縮能力,當客戶端併發訪問量增加後,服務端的執行緒個數和客戶端併發訪問數呈1:1的正比關係,由於執行緒是Java虛擬機器非常寶貴的系統資源,當執行緒數膨脹之後,系統的效能將急劇下降,隨著併發訪問量的繼續增大,系統會發生執行緒堆疊溢位、建立新執行緒失敗等問題,並最終導致程序宕機或者僵死,不能對外提供服務。
二、偽非同步I/O程式設計
為了解決同步阻塞I/O面臨的一個鏈路需要一個執行緒處理的問題,後來有人對它的執行緒模型進行了優化,後端通過一個執行緒池來處理多個客戶端的請求接入,形成客戶端個數M:執行緒池最大執行緒數N的比例關係,其中M可以遠遠大於N,通過執行緒池可以靈活的調配執行緒資源。設定執行緒的最大值,防止由於海量併發接入導致執行緒耗盡。
採用執行緒池和任務佇列可以實現一種叫做偽非同步的I/O通訊框架。模型圖如下。
當有新的客戶端接入時,將客戶端的Socket封裝成一個Task(該任務實現Java.lang.Runnablle介面)投遞到後端的執行緒池中進行處理,JDK的執行緒池維護一個訊息佇列和N個活躍執行緒對訊息佇列中的任務進行處理。由於執行緒池可以設定訊息佇列的大小和最大執行緒數,因此,它的資源佔用是可控的,無論多少個客戶端併發訪問,都不會導致資源的耗盡和宕機。
由於執行緒池和訊息佇列都是有界的,因此,無論客戶端併發連線數多大,它都不會導致執行緒個數過於膨脹或者記憶體溢位,相對於傳統的一連線一執行緒模型,是一種改良。
偽非同步I/O通訊框架採用了執行緒池實現,因此避免了為每個請求都建立一個獨立執行緒造成的執行緒資源耗盡問題。但是由於它底層的通訊依然採用同步阻塞模型,因此無法從根本上解決問題。
通過對輸入和輸出流的API文件進行分析,我們瞭解到讀和寫操作都是同步阻塞的,阻塞的時間取決於對方IO執行緒的處理速度和網路IO的傳輸速度,本質上講,我們無法保證生產環境的網路狀況和對端的應用程式能足夠快,如果我們的應用程式依賴對方的處理速度,它的可靠性就會非常差。
三、NIO程式設計(非阻塞IO)
與Socket類和ServerSocket類相對應,NIO也提供了SocketChannel和ServerSocketChannel兩種不同的套接字通道實現,在JDK1.4中引入。這兩種新增的通道都支援阻塞和非阻塞兩種模式。阻塞模式使用非常簡單,但是效能和可靠性都不好,非阻塞模式則正好相反。我們可以根據自己的需求來選擇合適的模式,一般來說,低負載、低併發的應用程式可以選擇同步阻塞IO以降低程式設計複雜度,但是對於高負載、高併發的網路應用,需要使用NIO的非阻塞模式進行開發。
首先來了解一些概念
(1)緩衝區Buffer
Buffer是一個物件,它包含一些要寫入或者要讀出的資料,在NIO庫中,所有資料都是用緩衝區處理的。在讀取資料時,它是直接讀到緩衝區中的;在寫入資料時,寫入到緩衝區中,任何時候訪問NIO中的資料,都是通過緩衝區進行操作。
緩衝區實質上是一個數組。通常它是一個位元組陣列(ByteBuffer),也可以使用其他種類的陣列,但是一個緩衝區不僅僅是一個數組,緩衝區提供了對資料的結構化訪問以及維護讀寫位置(limit)等資訊。常用的有ByteBuffer,其它還有CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer
(2)通道Channel
Channel是一個通道,可以通過它讀取和寫入資料,它就像自來水管一樣,網路資料通過Channel讀取和寫入。通道與流的不同之處在於通道是雙向的,流只是一個方向上移動(一個流必須是InputStream或者OutputStream的子類),而且通道可以用於讀、寫或者用於讀寫。同時Channel是全雙工的,因此它可以比流更好的對映底層作業系統的API。特別是在Unix網路程式設計中,底層作業系統的通道都是全雙工的,同時支援讀寫操作。我們常用到的ServerSocketChannnel和SocketChannel都是SelectableChannel的子類。
(3)多路複用器Selector
多路複用器Selector是Java NIO程式設計的基礎,多路複用器提供選擇已經就緒的任務的能力,簡單的說,Selector會不斷的輪詢註冊在其上的Channel,如果某個Channel上面有新的TCP連線接入、讀和寫事件,這個Channel就處於就緒狀態,會被Selector輪詢出來,然後通過SelectionKey可以獲取就緒Channel的集合,進行後續的I/O操作。
一個多用複用器Selector可以同時輪詢多個Channel,由於JDK使用了epoll()代替傳統的select實現,所以它並沒有最大連線控制代碼1024/2048的限制,這也意味著只需要一個執行緒負責Selector的輪詢,就可以接入成千上萬的客戶端。
儘管NIO程式設計難度確實比同步阻塞BIO大很多,但是我們要考慮到它的優點:
(1)客戶端發起的連線操作是非同步的,可以通過在多路複用器註冊OP_CONNECT等後續結果,不需要像之前的客戶端那樣被同步阻塞。
(2)SocketChannel的讀寫操作都是非同步的,如果沒有可讀寫的資料它不會同步等待,直接返回,這樣IO通訊執行緒就可以處理其它的鏈路,不需要同步等待這個鏈路可用。
(3)執行緒模型的優化:由於JDK的Selector在Linux等主流作業系統上通過epoll實現,它沒有連線控制代碼數的限制(只受限於作業系統的最大控制代碼數或者對單個程序的控制代碼限制),這意味著一個Selector執行緒可以同時處理成千上萬個客戶端連線,而且效能不會隨著客戶端的增加而線性下降,因此,它非常適合做高效能、高負載的網路伺服器。
四、AIO(非同步非阻塞IO)
JDK1.7升級了NIO類庫,升級後的NIO類庫被稱為NIO2.0。也就是我們要介紹的AIO。NIO2.0引入了新的非同步通道的概念,並提供了非同步檔案通道和非同步套接字通道的實現。非同步通道提供兩種方式獲取操作結果。
(1)通過Java.util.concurrent.Future類來表示非同步操作的結果;
(2)在執行非同步操作的時候傳入一個Java.nio.channels.
CompletionHandler介面的實現類作為操作完成的回撥。
NIO2.0的非同步套接字通道是真正的非同步非阻塞IO,它對應UNIX網路程式設計中的事件驅動IO(AIO),它不需要通過多路複用器(Selector)對註冊的通道進行輪詢操作即可實現非同步讀寫,從而簡化了NIO的程式設計模型。
我們可以得出結論:非同步Socket Channel是被動執行物件,我們不需要想NIO程式設計那樣建立一個獨立的IO執行緒來處理讀寫操作。對於AsynchronousServerSocketChannel和AsynchronousSocketChannel,它們都由JDK底層的執行緒池負責回撥並驅動讀寫操作。正因為如此,基於NIO2.0新的非同步非阻塞Channel進行程式設計比NIO程式設計更為簡單。
總結:
由上述總結得出,並不意味著所有的Java網路程式設計都必須要選擇NIO和Netty,具體選擇什麼樣的IO模型或者NIO框架,完全基於業務的實際應用場景和效能訴求,如果客戶端併發連線數不多,周邊對接的網元不多,伺服器的負載也不重,那就完全沒必要選擇NIO做服務端;如果是相反情況,那就考慮選擇合適的NIO框架進行開發。