Linux網(wǎng)絡(luò)I/O模型
? ? ? ?Linux的內(nèi)核將所有外部設(shè)備都看作一個文件來操作瓷胧,對一個文件的讀寫操作會調(diào)用內(nèi)核提供的系統(tǒng)命令,返回一個file descriptor(fd,文件描述符)絮吵。而對一個socket的讀寫也會有相應(yīng)的描述符,稱為socketfd(socket描述符),描述符就是一個數(shù)字,它指向內(nèi)核中的一個結(jié)構(gòu)體(文件路徑儿惫,數(shù)據(jù)區(qū)等一些屬性)。
? ? ? ?根據(jù)UNIX網(wǎng)絡(luò)編程對I/O模型的分類伸但,UNIX提供了5種I/O模型:
-
阻塞I/O模型:最常用的I/O模型肾请,缺省條件下,所有文件操作都是阻塞的更胖。以套接字接口為例:在進(jìn)程空間中調(diào)用
recvfrom
铛铁,其系統(tǒng)調(diào)用直到數(shù)據(jù)包到達(dá)且被復(fù)制到應(yīng)用進(jìn)程的緩沖區(qū)中或者發(fā)生錯誤時才返回,在此期間都是被阻塞的却妨。
-
非阻塞I/O模型:recvfrom從應(yīng)用層到內(nèi)核的時候饵逐,如果該緩沖區(qū)沒有數(shù)據(jù),就直接返回一個
EWOULDBLOCK
錯誤彪标,一般都對非阻塞I/O模型進(jìn)行輪詢檢查這個狀態(tài)倍权,看內(nèi)核是否有數(shù)據(jù)到來。
-
I/O復(fù)用模型:Linux提供
select/poll
捞烟,進(jìn)程通過將一個或多個fd傳遞給select或poll系統(tǒng)調(diào)用薄声,阻塞在select操作上,select/poll可以通過順序掃描偵測多個fd是否處于就緒狀態(tài)题画,不過支持的fd數(shù)量有限默辨。Linux還提供了epoll
,基于事件驅(qū)動方式代替順序掃描苍息,性能更高缩幸,當(dāng)有fd就緒時,立即回調(diào)函數(shù)rollback
竞思。
-
信號驅(qū)動I/O模型:首先開啟套接字信號驅(qū)動I/O功能桌粉,并通過系統(tǒng)調(diào)用
sigaction
執(zhí)行信號處理函數(shù)(此系統(tǒng)調(diào)用立即返回,非阻塞)衙四。當(dāng)數(shù)據(jù)準(zhǔn)備就緒時铃肯,為該進(jìn)程生成一個SIGIO信號,通過信號回調(diào)通知應(yīng)用程序調(diào)用recvfrom來讀取數(shù)據(jù)传蹈,并通知主循環(huán)函數(shù)處理數(shù)據(jù)押逼。
-
異步I/O模型:告知內(nèi)核啟動某個操作步藕,并讓內(nèi)核在整個操作完成后(包括數(shù)據(jù)從內(nèi)核復(fù)制到用戶自己的緩沖區(qū))進(jìn)行通知。此模型與信號驅(qū)動模型的主要區(qū)別是:信號驅(qū)動I/O模型由內(nèi)核通知我們何時開始一個I/O操作挑格;異步I/O模型由內(nèi)核通知我們I/O操作何時已經(jīng)完成咙冗。
I/O多路復(fù)用技術(shù)
? ? ? ?I/O多路復(fù)用通過把多個I/O的阻塞復(fù)用到同一個select的阻塞上,從而使得系統(tǒng)在單線程的情況下可以同時處理多個客戶端請求漂彤。優(yōu)勢是:系統(tǒng)開銷小雾消,無需創(chuàng)建和維護(hù)額外線程,降低了系統(tǒng)維護(hù)工作量挫望,節(jié)省了系統(tǒng)資源立润。主要應(yīng)用場景如下:
- 服務(wù)器需要同時處理多個處于監(jiān)聽狀態(tài)或者多個連接狀態(tài)的套接字;
- 服務(wù)器需要同時處理多種網(wǎng)絡(luò)協(xié)議的套接字媳板;
? ? ? ?目前支持I/O多路復(fù)用的系統(tǒng)調(diào)用有select桑腮、pselect、poll蛉幸、epoll破讨,然而select有一些固有缺陷,為了克服select的缺點(diǎn)奕纫,epoll做了很多重大改進(jìn):
- 支持一個進(jìn)程打開的socket描述符(FD)不受限制(僅受限于操作系統(tǒng)的最大文件句柄數(shù))提陶;
傳統(tǒng)的BIO
? ? ? ?網(wǎng)絡(luò)編程的基本模型是Client/Server模型,也就是兩個進(jìn)程之間進(jìn)行相互通信匹层,其中服務(wù)端提供位置信息(綁定的IP地址和監(jiān)聽端口)隙笆,客戶端通過連接操作向服務(wù)器端監(jiān)聽地址發(fā)起連接請求,通過三次握手建立連接又固,如果連接建立成功,雙方就可以通過網(wǎng)絡(luò)套接字(Socket)進(jìn)行通信煤率。
? ? ? ?采用BIO通信模型的服務(wù)端仰冠,通常由一個獨(dú)立的Acceptor線程負(fù)責(zé)監(jiān)聽客戶端的連接,接收到客戶端連接請求之后為每一個客戶端創(chuàng)建一個新的線程進(jìn)行鏈路處理蝶糯,處理完成之后洋只,通過輸出流返回應(yīng)答給客戶端,線程銷毀昼捍。此模型最大的問題就是缺乏彈性伸縮能力识虚,當(dāng)客戶端并發(fā)訪問量增加后,服務(wù)端的線程個數(shù)和客戶端并發(fā)訪問數(shù)呈1:1的正比關(guān)系妒茬,當(dāng)線程數(shù)膨脹担锤,系統(tǒng)性能將急劇下降。
偽異步I/O編程
? ? ? ?為解決同步阻塞I/O面臨的一個I/O鏈路需要一個線程處理的問題乍钻,通過一個線程池來處理多個客戶端的請求接入肛循,形成客戶端個數(shù)M : 線程池最大線程數(shù)N的比例關(guān)系铭腕。通過線程池可以靈活地調(diào)配線程資源,設(shè)置線程的最大值多糠,防止由于海量并發(fā)接入導(dǎo)致的線程耗盡累舷。
弊端分析
? ? ? ?當(dāng)對Socket的輸入流進(jìn)行讀取操作時,會一直阻塞直到發(fā)生下列三種事件夹孔,意味著當(dāng)對方發(fā)送請求或者應(yīng)答消息比較緩慢被盈,或者網(wǎng)絡(luò)傳輸較慢時,讀取輸入流一方的通信線程將被長時間阻塞:
- 有數(shù)據(jù)可讀搭伤;
- 可用數(shù)據(jù)已經(jīng)讀取完畢只怎;
- 發(fā)生空指針或者I/O異常;
? ? ? ?當(dāng)寫輸出流時闷畸,將被阻塞直到所有要發(fā)送的字節(jié)全部寫入或者發(fā)生異常尝盼。但當(dāng)消息接收方處理緩慢時,其不能及時地從TCP緩沖區(qū)讀取數(shù)據(jù)佑菩,這將導(dǎo)致發(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ù)莲蜘。實(shí)質(zhì)是一個數(shù)組谭确,通常為字節(jié)數(shù)組,并且提供了對數(shù)據(jù)的結(jié)構(gòu)化訪問以及維護(hù)讀寫位置等信息票渠。
2.通道Channel
? ? ? ?通道與流不同之處在于通道是雙向的逐哈,流只是在一個方向上移動,而通道可以用于讀问顷、寫或者兩者同時進(jìn)行昂秃。
3.多路復(fù)用器Selector
? ? ? ?Selector會不斷地輪詢注冊在其上的Channel,如果某個Channel上面發(fā)生讀或者寫事件杜窄,此Channel就處于就緒狀態(tài)肠骆,然后通過SelectionKey獲取就緒Channel集合,進(jìn)行后續(xù)的I/O操作塞耕。JDK使用epoll()
代替?zhèn)鹘y(tǒng)的select
實(shí)現(xiàn)蚀腿,所以它并沒有最大連接句柄1024/2048的限制,這意味著只需要一個線程負(fù)責(zé)Selector的輪詢扫外,就可以接入成千上萬的客戶端唯咬。
4.NIO服務(wù)端序列圖
- 步驟一:打開
ServerSocketChannel
纱注,監(jiān)聽客戶端連接,它是所有客戶端連接的父管道胆胰; - 步驟二:綁定監(jiān)聽端口狞贱,設(shè)置連接為非阻塞模式;
- 步驟三:創(chuàng)建
Reactor
線程蜀涨,創(chuàng)建多路復(fù)用器并啟動線程瞎嬉; - 步驟四:將ServerSocketChannel注冊到Reactor線程的多路復(fù)用器
Selector
上,監(jiān)聽Accept
事件厚柳; - 步驟五:多路復(fù)用器在線程run方法的無限循環(huán)體內(nèi)輪詢準(zhǔn)備就緒的Key氧枣;
- 步驟六:多路復(fù)用器監(jiān)聽到新的客戶端接入請求,處理新的接入請求别垮,完成TCP三次握手便监,建立物理鏈路;
- 步驟七:設(shè)置客戶端鏈路模式為非阻塞碳想;
- 步驟八:將新接入的客戶端連接注冊到Rector線程的多路復(fù)用器上烧董,監(jiān)聽讀操作,讀取客戶端發(fā)送的網(wǎng)絡(luò)消息胧奔;
- 步驟九:異步讀取客戶端請求消息到緩沖區(qū)逊移;
- 步驟十:對ByteBuffer進(jìn)行編解碼,如果有半包消息龙填,指針reset胳泉,繼續(xù)讀取后續(xù)的報文,將解碼成功的消息封裝成Task岩遗,投遞到業(yè)務(wù)線程池中扇商,進(jìn)行業(yè)務(wù)邏輯編排;
- 步驟十一:將POJO對象encode成ByteBuffer宿礁,調(diào)用SocketChannel的異步write接口案铺,將消息異步發(fā)送給客戶端。
注意:如果發(fā)送區(qū)TCP緩沖區(qū)滿窘拯,會導(dǎo)致寫半包红且,此時需要注冊監(jiān)聽寫操作位坝茎,循環(huán)寫涤姊,知道整包消息寫入TCP緩沖區(qū)。
5.NIO客戶端序列圖
- 步驟一:打開SocketChannel嗤放,綁定客戶端本地地址(默認(rèn)會隨機(jī)分配一個可用的本地地址)思喊;
- 步驟二:設(shè)置SocketChannel為非阻塞模式,同時設(shè)置客戶端連接的TCP參數(shù)次酌;
- 步驟三:異步連接客戶端恨课;
- 步驟四:判斷是否連接成功舆乔,若成功,則直接注冊讀狀態(tài)位到多路復(fù)用器中剂公,若未連接成功(異步連接)希俩,說明客戶端已經(jīng)發(fā)送了sync包,但服務(wù)端還未返回ack包纲辽,物理鏈路還未建立颜武,則注冊連接狀態(tài)到多路復(fù)用器中,監(jiān)聽服務(wù)端的TCP ACK應(yīng)答拖吼;
- 步驟五:創(chuàng)建Reactor線程鳞上,創(chuàng)建多路復(fù)用器并啟動線程;
- 步驟六:多路復(fù)用器在線程run方法的無限循環(huán)體內(nèi)輪詢準(zhǔn)備就緒的Key吊档;
- 步驟七:接受connect事件進(jìn)行處理篙议;
- 步驟八:判斷連接結(jié)果,若連接成功怠硼,注冊讀事件到多路復(fù)用器鬼贱;
- 步驟九:異步讀客戶端請求消息到緩沖區(qū);
- 步驟十:將POJO對象encode成ByteBuffer拒名,調(diào)用SocketChannel的異步write接口吩愧,將消息異步發(fā)送到客戶端。
AIO編程
? ? ? ?NIO 2.0引入了新的異步通道的概念增显,并提供了異步文件通道和異步套接字通道的實(shí)現(xiàn)雁佳。提供以下兩種方式獲取操作結(jié)果:
- 通過
java.util.concurrent.Future
類來表示異步操作的結(jié)果; - 在執(zhí)行異步操作的時候傳入一個
java.nio.channels
同云;
? ? ? ?NIO 2.0的異步套接字通道是真正的異步非阻塞I/O糖权,對應(yīng)于UNIX網(wǎng)絡(luò)編程的事件驅(qū)動I/O(AIO)。不需要通過多路復(fù)用器(Selector)對注冊的通道進(jìn)行輪詢操作即可實(shí)現(xiàn)異步讀寫炸站,從而簡化了NIO的編程模型星澳。
? ? ? ?TCP以流的方式進(jìn)行數(shù)據(jù)傳輸箕速,上層的應(yīng)用程序?yàn)榱藢ο⑦M(jìn)行區(qū)分锁保,往往采用如下4種方式:
- 消息長度固定,累計(jì)讀取到長度總和為定長LEN的報文后排龄,就認(rèn)為讀取到了一個完整的消息阀坏;將計(jì)數(shù)器置位如暖,重新開始讀取下一個數(shù)據(jù)包;
- 將回車換行符作為消息結(jié)束符忌堂;
- 將特殊的分隔符作為消息的結(jié)束標(biāo)志盒至,如回車換行符;
- 通過在消息頭中定義長度字段來標(biāo)識消息的總長度;