JAVA IO

Linux網(wǎng)絡I/O模型

? ? ? ?Linux的內(nèi)核將所有外部設備都看作一個文件來操作,對一個文件的讀寫操作會調(diào)用內(nèi)核提供的系統(tǒng)命令,返回一個file descriptor(fd,文件描述符)。而對一個socket的讀寫也會有相應的描述符,稱為socketfd(socket描述符),描述符就是一個數(shù)字,它指向內(nèi)核中的一個結構體(文件路徑,數(shù)據(jù)區(qū)等一些屬性)。
? ? ? ?根據(jù)UNIX網(wǎng)絡編程對I/O模型的分類,UNIX提供了5種I/O模型:

  • 阻塞I/O模型:最常用的I/O模型,缺省條件下,所有文件操作都是阻塞的。以套接字接口為例:在進程空間中調(diào)用recvfrom,其系統(tǒng)調(diào)用直到數(shù)據(jù)包到達且被復制到應用進程的緩沖區(qū)中或者發(fā)生錯誤時才返回,在此期間都是被阻塞的。

  • 非阻塞I/O模型:recvfrom從應用層到內(nèi)核的時候,如果該緩沖區(qū)沒有數(shù)據(jù),就直接返回一個EWOULDBLOCK錯誤,一般都對非阻塞I/O模型進行輪詢檢查這個狀態(tài),看內(nèi)核是否有數(shù)據(jù)到來。

  • I/O復用模型:Linux提供select/poll,進程通過將一個或多個fd傳遞給select或poll系統(tǒng)調(diào)用,阻塞在select操作上,select/poll可以通過順序掃描偵測多個fd是否處于就緒狀態(tài),不過支持的fd數(shù)量有限。Linux還提供了epoll,基于事件驅動方式代替順序掃描,性能更高,當有fd就緒時,立即回調(diào)函數(shù)rollback。

  • 信號驅動I/O模型:首先開啟套接字信號驅動I/O功能,并通過系統(tǒng)調(diào)用sigaction執(zhí)行信號處理函數(shù)(此系統(tǒng)調(diào)用立即返回,非阻塞)。當數(shù)據(jù)準備就緒時,為該進程生成一個SIGIO信號,通過信號回調(diào)通知應用程序調(diào)用recvfrom來讀取數(shù)據(jù),并通知主循環(huán)函數(shù)處理數(shù)據(jù)。

  • 異步I/O模型:告知內(nèi)核啟動某個操作,并讓內(nèi)核在整個操作完成后(包括數(shù)據(jù)從內(nèi)核復制到用戶自己的緩沖區(qū))進行通知。此模型與信號驅動模型的主要區(qū)別是:信號驅動I/O模型由內(nèi)核通知我們何時開始一個I/O操作;異步I/O模型由內(nèi)核通知我們I/O操作何時已經(jīng)完成。

I/O多路復用技術

? ? ? ?I/O多路復用通過把多個I/O的阻塞復用到同一個select的阻塞上,從而使得系統(tǒng)在單線程的情況下可以同時處理多個客戶端請求。優(yōu)勢是:系統(tǒng)開銷小,無需創(chuàng)建和維護額外線程,降低了系統(tǒng)維護工作量,節(jié)省了系統(tǒng)資源。主要應用場景如下:

  • 服務器需要同時處理多個處于監(jiān)聽狀態(tài)或者多個連接狀態(tài)的套接字;
  • 服務器需要同時處理多種網(wǎng)絡協(xié)議的套接字;

? ? ? ?目前支持I/O多路復用的系統(tǒng)調(diào)用有select、pselect、poll、epoll,然而select有一些固有缺陷,為了克服select的缺點,epoll做了很多重大改進:

  • 支持一個進程打開的socket描述符(FD)不受限制(僅受限于操作系統(tǒng)的最大文件句柄數(shù));

傳統(tǒng)的BIO

? ? ? ?網(wǎng)絡編程的基本模型是Client/Server模型,也就是兩個進程之間進行相互通信,其中服務端提供位置信息(綁定的IP地址和監(jiān)聽端口),客戶端通過連接操作向服務器端監(jiān)聽地址發(fā)起連接請求,通過三次握手建立連接,如果連接建立成功,雙方就可以通過網(wǎng)絡套接字(Socket)進行通信。
? ? ? ?采用BIO通信模型的服務端,通常由一個獨立的Acceptor線程負責監(jiān)聽客戶端的連接,接收到客戶端連接請求之后為每一個客戶端創(chuàng)建一個新的線程進行鏈路處理,處理完成之后,通過輸出流返回應答給客戶端,線程銷毀。此模型最大的問題就是缺乏彈性伸縮能力,當客戶端并發(fā)訪問量增加后,服務端的線程個數(shù)和客戶端并發(fā)訪問數(shù)呈1:1的正比關系,當線程數(shù)膨脹,系統(tǒng)性能將急劇下降。

偽異步I/O編程
? ? ? ?為解決同步阻塞I/O面臨的一個I/O鏈路需要一個線程處理的問題,通過一個線程池來處理多個客戶端的請求接入,形成客戶端個數(shù)M : 線程池最大線程數(shù)N的比例關系。通過線程池可以靈活地調(diào)配線程資源,設置線程的最大值,防止由于海量并發(fā)接入導致的線程耗盡。

弊端分析
? ? ? ?當對Socket的輸入流進行讀取操作時,會一直阻塞直到發(fā)生下列三種事件,意味著當對方發(fā)送請求或者應答消息比較緩慢,或者網(wǎng)絡傳輸較慢時,讀取輸入流一方的通信線程將被長時間阻塞:

  • 有數(shù)據(jù)可讀;
  • 可用數(shù)據(jù)已經(jīng)讀取完畢;
  • 發(fā)生空指針或者I/O異常;

? ? ? ?當寫輸出流時,將被阻塞直到所有要發(fā)送的字節(jié)全部寫入或者發(fā)生異常。但當消息接收方處理緩慢時,其不能及時地從TCP緩沖區(qū)讀取數(shù)據(jù),這將導致發(fā)送發(fā)的TCP發(fā)送窗口不斷減小,直到為0,雖然雙方處于Keep-Alive狀態(tài),但發(fā)送方已經(jīng)不能再向TCP緩沖區(qū)寫入消息,這時若采用的是同步阻塞I/O,write操作將被無限期的阻塞,直到TCP的發(fā)送窗口大于0或者發(fā)生I/O異常。

NIO編程

1.緩沖區(qū)Buffer
? ? ? ?Buffer是一個對象,它包含一些要寫入或者要讀出的數(shù)據(jù)。實質(zhì)是一個數(shù)組,通常為字節(jié)數(shù)組,并且提供了對數(shù)據(jù)的結構化訪問以及維護讀寫位置等信息。

2.通道Channel
? ? ? ?通道與流不同之處在于通道是雙向的,流只是在一個方向上移動,而通道可以用于讀、寫或者兩者同時進行。

3.多路復用器Selector
? ? ? ?Selector會不斷地輪詢注冊在其上的Channel,如果某個Channel上面發(fā)生讀或者寫事件,此Channel就處于就緒狀態(tài),然后通過SelectionKey獲取就緒Channel集合,進行后續(xù)的I/O操作。JDK使用epoll()代替?zhèn)鹘y(tǒng)的select實現(xiàn),所以它并沒有最大連接句柄1024/2048的限制,這意味著只需要一個線程負責Selector的輪詢,就可以接入成千上萬的客戶端。

4.NIO服務端序列圖

  • 步驟一:打開ServerSocketChannel,監(jiān)聽客戶端連接,它是所有客戶端連接的父管道;
  • 步驟二:綁定監(jiān)聽端口,設置連接為非阻塞模式;
  • 步驟三:創(chuàng)建Reactor線程,創(chuàng)建多路復用器并啟動線程;
  • 步驟四:將ServerSocketChannel注冊到Reactor線程的多路復用器Selector上,監(jiān)聽Accept事件;
  • 步驟五:多路復用器在線程run方法的無限循環(huán)體內(nèi)輪詢準備就緒的Key;
  • 步驟六:多路復用器監(jiān)聽到新的客戶端接入請求,處理新的接入請求,完成TCP三次握手,建立物理鏈路;
  • 步驟七:設置客戶端鏈路模式為非阻塞;
  • 步驟八:將新接入的客戶端連接注冊到Rector線程的多路復用器上,監(jiān)聽讀操作,讀取客戶端發(fā)送的網(wǎng)絡消息;
  • 步驟九:異步讀取客戶端請求消息到緩沖區(qū);
  • 步驟十:對ByteBuffer進行編解碼,如果有半包消息,指針reset,繼續(xù)讀取后續(xù)的報文,將解碼成功的消息封裝成Task,投遞到業(yè)務線程池中,進行業(yè)務邏輯編排;
  • 步驟十一:將POJO對象encode成ByteBuffer,調(diào)用SocketChannel的異步write接口,將消息異步發(fā)送給客戶端。

注意:如果發(fā)送區(qū)TCP緩沖區(qū)滿,會導致寫半包,此時需要注冊監(jiān)聽寫操作位,循環(huán)寫,知道整包消息寫入TCP緩沖區(qū)。

5.NIO客戶端序列圖

  • 步驟一:打開SocketChannel,綁定客戶端本地地址(默認會隨機分配一個可用的本地地址);
  • 步驟二:設置SocketChannel為非阻塞模式,同時設置客戶端連接的TCP參數(shù);
  • 步驟三:異步連接客戶端;
  • 步驟四:判斷是否連接成功,若成功,則直接注冊讀狀態(tài)位到多路復用器中,若未連接成功(異步連接),說明客戶端已經(jīng)發(fā)送了sync包,但服務端還未返回ack包,物理鏈路還未建立,則注冊連接狀態(tài)到多路復用器中,監(jiān)聽服務端的TCP ACK應答;
  • 步驟五:創(chuàng)建Reactor線程,創(chuàng)建多路復用器并啟動線程;
  • 步驟六:多路復用器在線程run方法的無限循環(huán)體內(nèi)輪詢準備就緒的Key;
  • 步驟七:接受connect事件進行處理;
  • 步驟八:判斷連接結果,若連接成功,注冊讀事件到多路復用器;
  • 步驟九:異步讀客戶端請求消息到緩沖區(qū);
  • 步驟十:將POJO對象encode成ByteBuffer,調(diào)用SocketChannel的異步write接口,將消息異步發(fā)送到客戶端。

AIO編程

? ? ? ?NIO 2.0引入了新的異步通道的概念,并提供了異步文件通道和異步套接字通道的實現(xiàn)。提供以下兩種方式獲取操作結果:

  • 通過java.util.concurrent.Future類來表示異步操作的結果;
  • 在執(zhí)行異步操作的時候傳入一個java.nio.channels;

? ? ? ?NIO 2.0的異步套接字通道是真正的異步非阻塞I/O,對應于UNIX網(wǎng)絡編程的事件驅動I/O(AIO)。不需要通過多路復用器(Selector)對注冊的通道進行輪詢操作即可實現(xiàn)異步讀寫,從而簡化了NIO的編程模型。

? ? ? ?TCP以流的方式進行數(shù)據(jù)傳輸,上層的應用程序為了對消息進行區(qū)分,往往采用如下4種方式:

  • 消息長度固定,累計讀取到長度總和為定長LEN的報文后,就認為讀取到了一個完整的消息;將計數(shù)器置位,重新開始讀取下一個數(shù)據(jù)包;
  • 將回車換行符作為消息結束符;
  • 將特殊的分隔符作為消息的結束標志,如回車換行符;
  • 通過在消息頭中定義長度字段來標識消息的總長度;
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容