nio和netty(上)

客戶端和服務(wù)器的通信有bio和nio之分,bio一般被描述為同步阻塞io。其實(shí)現(xiàn)一般如下:服務(wù)端會監(jiān)聽一個(gè)端口,從而等待客戶端發(fā)起請求連接服務(wù)端,此時(shí)服務(wù)器會阻塞在那里,以直等到有請求進(jìn)來才會繼續(xù)執(zhí)行下面流程。而這個(gè)acceptor會處于while循環(huán)內(nèi)一直等待接收客戶端的請求,每當(dāng)客戶端有請求進(jìn)來,服務(wù)端就分配其一個(gè)線程去處理客戶端socket,這樣一來一個(gè)客戶端請求對應(yīng)一個(gè)服務(wù)端線程,當(dāng)客戶端并發(fā)非常大時(shí)服務(wù)端線程數(shù)也非常大,很容易造成線程堆oom,內(nèi)存撐爆。所以這種模型只是初識io時(shí)使用的io通信的方式,在真實(shí)環(huán)境中不可能用此方式來實(shí)現(xiàn)io。

基于同步io中的線程會不斷增加,導(dǎo)致內(nèi)存溢出,一種線程池模型的同步io就理所應(yīng)當(dāng)?shù)爻霈F(xiàn)了,其不同于同步io的地方就是在服務(wù)端accept客戶端socket后不是立即創(chuàng)建新線程,而是把這個(gè)socket封裝成task丟入線程池,用jdk線程池去execute新的任務(wù),這樣就引入了消息隊(duì)列即阻塞隊(duì)列的概念。這種方式能解決線程無限擴(kuò)容的問題,但他的io實(shí)現(xiàn)還是同步的,就避免不了阻塞的問題,先來看下jdk的兩個(gè)阻塞read和write方法的api說明。read方法會阻塞讀,直到有可用數(shù)據(jù)進(jìn)行讀取,或者數(shù)據(jù)已讀完,或者發(fā)生空指針或io異常才會退出這個(gè)方法,那就意味著如果read方法的線程的io能力差,或者網(wǎng)絡(luò)差,那么整個(gè)讀方法就會阻塞很久。設(shè)想一下客戶端并發(fā)寫入服務(wù)器數(shù)據(jù),而如果服務(wù)端線程池內(nèi)所有正在執(zhí)行read方法的線程都阻塞了將近一分鐘,那么消息隊(duì)列中的其他任務(wù)就不得不等待這么長時(shí)間,其他客戶端的寫入請求將得不到及時(shí)響應(yīng),直至服務(wù)端阻塞讀取完成才能最終得到read完成的反饋,那么這種網(wǎng)絡(luò)通信系統(tǒng)的性能實(shí)在時(shí)令人堪憂的。write方法api也是如此,如果服務(wù)端寫出數(shù)據(jù)到客戶端,那么它將會被阻塞,知道所有要發(fā)送的字節(jié)全部寫入完畢,或者發(fā)生異常,如果服務(wù)端寫出數(shù)據(jù)的速度受網(wǎng)絡(luò)影響比較嚴(yán)重,同樣會產(chǎn)生超時(shí)嚴(yán)重的問題。所以這種俗稱偽異步的io模型同樣是阻塞的,在一端網(wǎng)絡(luò)延遲的情況下,很容易產(chǎn)生眾多io線程被阻塞,影響整個(gè)服務(wù)器和客戶端的并發(fā)通信能力。

同步阻塞io的好處是使用簡單,所以在并發(fā)量不高的簡單場景下,使用同步阻塞io來處理網(wǎng)絡(luò)通信也是足夠的。但作為一個(gè)支持大量請求的服務(wù)器開發(fā)者來說,了解并使用nio是很有必要的。nio顧名思義就是非阻塞io,也可以叫做new io,這種io會有個(gè)讀取和寫入塊的概念,我想主要是通過其緩沖區(qū)來實(shí)現(xiàn)的。nio有三個(gè)基本組件,buffer,channel和selector,buffer是處于消息發(fā)送兩端的緩沖組件,有bytebuffer,charbuffer,longbuffer等,本質(zhì)都是通過把傳輸?shù)淖址D(zhuǎn)化為byte來實(shí)現(xiàn)的,所以最根本的是bytebuffer,buffer中有很多操作,比如position,mark,limit,capacity,用來控制buffer的指針,方便對buffer進(jìn)行讀寫。channel是進(jìn)行數(shù)據(jù)傳輸?shù)耐ǖ溃侨p工通信的,即可以同時(shí)在一個(gè)channel上進(jìn)行讀寫,而流因?yàn)楸仨毷荌nputStream或OutputStream的子類,所以流只能進(jìn)行單向傳輸。Selector是一個(gè)多路復(fù)用器,多個(gè)channel的互動是通過selector來協(xié)調(diào)的,一般一個(gè)selector可以持有多個(gè)channel信息的引用,即不同的channel可以注冊到同一個(gè)selector上,注冊時(shí)會同時(shí)注冊selector所監(jiān)聽的channel事件,之后selector可以在一個(gè)while循環(huán)里不斷執(zhí)行select方法,來獲得注冊在其上的channel事件,這種事件抽象到了SelectionKey上。一個(gè)典型的服務(wù)器場景如下:selector和serverChannel開啟服務(wù),serverChannel監(jiān)聽了某個(gè)端口,等待客戶端接入,并把channel模式設(shè)為非阻塞,則之后的讀寫操作就都是非阻塞的了,然后把serverChannel的accept事件注冊到selector上。接下來在while循環(huán)里selector就可以遍歷其內(nèi)部的SelectionKey,當(dāng)有客戶端請求時(shí),select方法就會返回1,代表此時(shí)selector捕捉到一個(gè)事件,之后匹配事件時(shí)就會知道時(shí)accept事件,接下來serverChannel執(zhí)行accept方法的到socketChannel,客戶端channel就會注冊讀事件到selector上,之后有客戶端寫入數(shù)據(jù)時(shí),selector就會意識到channel的讀事件發(fā)生,進(jìn)行數(shù)據(jù)讀取。此時(shí)的read方法是非阻塞的,他有三種返回情況,當(dāng)返回值大于0,都到了字節(jié),對字節(jié)進(jìn)行編碼等后續(xù)操作;等于0,沒有讀到字節(jié),屬于正常場景,忽略(這里就是非阻塞的表現(xiàn),并不會阻塞在此一直等待有數(shù)據(jù)讀取);等于-1,表示鏈路已關(guān)閉,需要關(guān)閉客戶端channel釋放資源。寫操作也類似。

所以我們可以發(fā)現(xiàn)accept,connect,select,read,write等方法都沒有發(fā)生阻塞,要執(zhí)行accept必須在selector捕捉到了serverChannel這個(gè)事件后才會發(fā)生,所以是立即產(chǎn)生效果;客戶端發(fā)起的connect方法是異步的,如果沒有立即連接成功服務(wù)器,則客戶端channel會在selector上注冊自己的connect事件,等到服務(wù)器確認(rèn)和客戶端連接時(shí),selector監(jiān)測到此事件,就可以進(jìn)行連接;select方法也有有參和無參兩種方式,無參則在沒有事件后立即返回0,有參則是在參數(shù)(timeout)時(shí)間范圍內(nèi)沒有事件,則返回0,都不會發(fā)生很長事件的阻塞,在while循環(huán)內(nèi)返回后會立即執(zhí)行下一個(gè)select;read和write也是在selector捕捉到了客戶端channel的這些事件時(shí)才進(jìn)行真實(shí)操作,操作本身也不會發(fā)生阻塞。所以nio的這一機(jī)制的關(guān)鍵就是selector,主線程會分配一個(gè)線程去專門處理selector的注冊和輪詢 ,這個(gè)線程俗稱reactor線程,這種模式也被稱為reactor模式,即有個(gè)專門線程去接收客戶端的連接,讀寫等各種事件,也只是去接收這種事件,而把真正的事件處理工作分配給另一個(gè)線程去做,這樣一來如果具體的讀寫業(yè)務(wù)邏輯很復(fù)雜很費(fèi)事,也就不會影響reactor線程的吞吐量,這對于內(nèi)存執(zhí)行密集型的系統(tǒng)來說是很有必要的。那么bio和現(xiàn)在的nio有什么本質(zhì)區(qū)別嗎?注意到如果一個(gè)一步讀寫線程在處理復(fù)雜業(yè)務(wù)或者因?yàn)榫W(wǎng)絡(luò)原因執(zhí)行了很長時(shí)間,并不會影響其他客戶端請求的進(jìn)入,因?yàn)樽x寫線程是異步的,而主要的reactor線程還是在不斷的做selector輪詢,當(dāng)有新的請求發(fā)生,還是照樣可以捕捉,并把這些客戶端的讀寫交給新的子線程處理,當(dāng)然了這些子線程的分配肯定也有相應(yīng)線程池技術(shù)來保證線程不會撐爆內(nèi)存。

關(guān)于ByteBuffer的作用再多說一句,通過channel.read(byteBuffer),channel把客戶端數(shù)據(jù)通過channel讀入buffer,之后再把buffer內(nèi)容get至byte數(shù)組中,進(jìn)行輸出或操作;write操作也相似,先把數(shù)據(jù)寫入緩沖區(qū),然后把緩沖區(qū)內(nèi)容寫入channel。

AIO則是異步非阻塞IO,這里的異步是真正的異步,正因?yàn)橛辛水惒?,AIO去除了selector的協(xié)調(diào)。AIO中最關(guān)鍵的是CompletionHandler接口,其中有兩個(gè)方法completed和failed,nio內(nèi)部在方法執(zhí)行完時(shí)會自動執(zhí)行completed方法進(jìn)行回調(diào)。在異步服務(wù)器Channel的accept方法里會傳入當(dāng)前server作為attachment和AcceptCompletionHandler,在completed方法內(nèi)會繼續(xù)調(diào)用當(dāng)前server的accept方法去接收其他客戶端的連接。如果failed,則調(diào)用當(dāng)前server的latch countdown,使得外層latch的await通過,serverhandler結(jié)束。write方法也相似,在completed方法內(nèi)繼續(xù)看傳入的attachment還有沒有剩余字節(jié),如果有就繼續(xù)調(diào)用channel的write方法繼續(xù)走異步寫方法,傳進(jìn)來的attachment其實(shí)是一個(gè)byteBuffer,也是通過先寫入緩存,然后把緩存內(nèi)容寫入channel的方式進(jìn)行寫,只是多了個(gè)異步不斷寫的過程。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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