一、傳統(tǒng)的BIO
1.網(wǎng)絡(luò)編程的基本模型是Client/Server模型惶翻,也就是兩個(gè)進(jìn)程之間進(jìn)行相互通信蝶溶,其中服務(wù)端提供位置信息(綁定的IP地址和監(jiān)聽端口),客戶端通過連接操作向服務(wù)端監(jiān)聽的地址發(fā)起連接請(qǐng)求俱济,通過三次握手建立連接,如果連接建立成功钙勃,雙方就可以通過網(wǎng)絡(luò)套接字(Socket)進(jìn)行通信蛛碌。在基于傳統(tǒng)同步阻塞模型開發(fā)中,ServerSocket負(fù)責(zé)綁定IP地址辖源,啟動(dòng)監(jiān)聽端口蔚携;Socket負(fù)責(zé)發(fā)起連接操作。連接成功之后克饶,雙方通過輸入和輸出流進(jìn)行同步阻塞式通信酝蜒。
該模型最大的問題就是缺乏彈性伸縮能力,當(dāng)客戶端并發(fā)訪問量增加后矾湃,服務(wù)端的線程個(gè)數(shù)和客戶端并發(fā)訪問數(shù)呈1:1 的正比關(guān)系亡脑,猶豫線程是Java虛擬機(jī)非常寶貴的系統(tǒng)資源,當(dāng)線程數(shù)膨脹之后邀跃,系統(tǒng)的性能將急劇下降霉咨,隨著并發(fā)訪問量的繼續(xù)增大,系統(tǒng)會(huì)發(fā)生線程堆棧溢出拍屑、創(chuàng)建新線程失敗等問題途戒,并最終導(dǎo)致進(jìn)程宕機(jī)或者僵死,不能對(duì)外提供服務(wù)丽涩。
我們發(fā)現(xiàn)棺滞,BIO主要的問題在于每當(dāng)有一個(gè)新的客戶端請(qǐng)求接入時(shí),服務(wù)端必須創(chuàng)建一個(gè)新的線程處理新接入的客戶端鏈路矢渊,一個(gè)線程只能吃力一個(gè)客戶端連接继准。在高性能服務(wù)器應(yīng)用領(lǐng)域,往往需要面向成千上萬(wàn)個(gè)客戶端的并發(fā)連接矮男,這種模型顯然無(wú)法滿足高性能移必、高并發(fā)接入的場(chǎng)景。
為了改進(jìn)一線程一連接模型毡鉴,后來又演進(jìn)出了一種通過線程池或者消息隊(duì)列實(shí)現(xiàn)1個(gè)或者多個(gè)線程處理N個(gè)客戶端的模型崔泵,由于它的底層通信機(jī)制依然使用同步阻塞I/O秒赤,所以被稱為“偽異步”。后面我們將通過對(duì)偽異步代碼的分析憎瘸,看看偽異步能否滿足我們對(duì)高性能入篮、高并發(fā)接入的訴求。
二幌甘、偽異步IO編程
1.采用線程池和任務(wù)隊(duì)列可以實(shí)現(xiàn)一種叫做偽異步的I/O通信框架潮售,它的模型圖如下所示。
當(dāng)有新的客戶端接入的時(shí)候锅风,將客戶端的Socket封裝成一個(gè)Task(該任務(wù)實(shí)現(xiàn)java.lang.Runnable接口)投遞到后端的線程池中進(jìn)行處理酥诽,JDK的線程池維護(hù)一個(gè)消息隊(duì)列和N個(gè)活躍線程對(duì)消息隊(duì)列中的任務(wù)進(jìn)行處理。由于線程池可以設(shè)置消息隊(duì)列的大小和最大線程數(shù)皱埠。因此肮帐,它的資源占用是可控的,無(wú)論多少個(gè)客戶端并發(fā)訪問边器,都不會(huì)導(dǎo)致資源的耗盡和宕機(jī)训枢。
學(xué)習(xí)過TCP/IP相關(guān)知識(shí)的人都知道,當(dāng)消息的接收方處理緩慢的時(shí)候忘巧,將不能及時(shí)地從TCP緩沖區(qū)讀取數(shù)據(jù)肮砾,這將會(huì)導(dǎo)致發(fā)送方的TCP window size不斷減小,直到為0袋坑,雙方處于Keep-Alive狀態(tài),消息發(fā)送方將不能再向TCP緩沖區(qū)寫入消息眯勾,這是如果采用的是同步阻塞I/O枣宫,write操作將會(huì)被無(wú)限期阻塞,直到TCP window size大于0或者發(fā)生I/O異常吃环。
? 通過對(duì)輸入和輸出流的API文檔進(jìn)行分析也颤,我們了解到讀和寫操作都是同步阻塞的,阻塞的時(shí)間取決于對(duì)方I/O線程的處理速度和網(wǎng)絡(luò)I/O傳輸速度郁轻。本質(zhì)上來講翅娶,我們無(wú)法保證生產(chǎn)環(huán)境的網(wǎng)絡(luò)狀況和對(duì)端的應(yīng)用程序能夠足夠快,如果我們的應(yīng)用程序依賴對(duì)方的處理速度好唯,它的可靠性就非常差竭沫。
偽異步I/O實(shí)際上僅僅只是對(duì)之前I/O線程模型的一個(gè)簡(jiǎn)單優(yōu)化,它無(wú)法從根本上解決同步I/O導(dǎo)致的通信線程阻塞問題骑篙。下面我們就簡(jiǎn)單分析下如果通信對(duì)方返回應(yīng)答時(shí)間過長(zhǎng)蜕提,會(huì)引起的級(jí)聯(lián)故障。
服務(wù)端處理緩慢靶端,返回應(yīng)答消息耗費(fèi)60s谎势,平時(shí)只需要10ms凛膏。
? ?采用偽異步I/O的線程正在讀取故障服務(wù)節(jié)點(diǎn)的響應(yīng),由于讀取輸入流是阻塞的脏榆,因此猖毫,它將會(huì)被同步阻塞60s。
? ?假如所有的可用線程都被故障服務(wù)器阻塞须喂,那后續(xù)所有的I/O消息都將在隊(duì)里中排隊(duì)吁断。
? ?由于線程池采用阻塞隊(duì)里實(shí)現(xiàn),當(dāng)隊(duì)列積滿之后镊折,后續(xù)入隊(duì)的操作將被阻塞胯府。
? ?由于前端只有一個(gè)Accptor線程接收客戶端接入,它被阻塞在線程池的同步阻塞隊(duì)列之后恨胚,新的客戶端請(qǐng)求消息將被拒絕骂因, ? ? ?客戶端會(huì)發(fā)生大量的連接超時(shí)。
? ?由于幾乎所有的連接都超時(shí)赃泡,調(diào)用者會(huì)認(rèn)為系統(tǒng)已經(jīng)崩潰寒波,無(wú)法接收新的請(qǐng)求消息。
三升熊、NIO
新的輸入/輸出(NIO)庫(kù)是在JDK1.4中引入的俄烁。NIO彌補(bǔ)了原來同步阻塞I/O的不足,它在標(biāo)準(zhǔn)Java代碼中提供了高速的级野、面向塊的I/O页屠。通過定義包含數(shù)據(jù)的類,以及通過以塊的形式處理這些數(shù)據(jù)蓖柔,NIO不使用本機(jī)代碼就可以利用低級(jí)優(yōu)化辰企,這是原來的I/O包所無(wú)法做到的。下面對(duì)NIO的一些概念和功能做下簡(jiǎn)單介紹况鸣,以便大家能夠快速地了解NIO類庫(kù)和相關(guān)概念牢贸。
1.緩沖區(qū)Buffer
Buffer是一個(gè)對(duì)象,它包含一些要寫入或者要讀出的數(shù)據(jù)镐捧。在NIO類庫(kù)中加入Buffer對(duì)象潜索,體現(xiàn)了新庫(kù)與原I/O的一個(gè)重要區(qū)別。在面向流的I/O中懂酱,可以將數(shù)據(jù)直接寫入或者將數(shù)據(jù)直接讀到Stream對(duì)象中竹习。
在NIO庫(kù)中,所有數(shù)據(jù)都是用緩沖區(qū)處理的列牺。在讀取數(shù)據(jù)時(shí)由驹,它是直接讀到緩沖區(qū)中的;在寫入數(shù)據(jù)時(shí),寫入到緩沖區(qū)中蔓榄。任何時(shí)候訪問NIO中的數(shù)據(jù)并炮,都是通過緩沖區(qū)進(jìn)行操作。
緩沖區(qū)實(shí)質(zhì)上是一個(gè)數(shù)組甥郑。通常它是一個(gè)字節(jié)數(shù)組(ByteBuffer)逃魄,也可以使用其他種類的數(shù)組。但是緩沖區(qū)不僅僅是一個(gè)數(shù)組澜搅,緩沖區(qū)提供了對(duì)數(shù)據(jù)的結(jié)構(gòu)化訪問以及維護(hù)讀寫位置(limit)等信息伍俘。
最常用的緩沖區(qū)是ByteBuffer,一個(gè)ByteBuffer提供了一組功能用于操作byte數(shù)組勉躺。除了ByteBuffer癌瘾,還有其他的一些緩沖區(qū),事實(shí)上饵溅,每一種Java基本類型(除了Boolean類型)都對(duì)應(yīng)有一種緩沖區(qū)妨退,具體如下:
ByteBuffer:字節(jié)緩沖區(qū)
CharBuffer:字符緩沖區(qū)
ShortBuffer:短整型緩沖區(qū)
IntBuffer:整型緩沖區(qū)
LongBuffer:長(zhǎng)整型緩沖區(qū)
FloatBuffer:浮點(diǎn)型緩沖區(qū)
DoubleBuffer:雙精度浮點(diǎn)型緩沖區(qū)
?每一個(gè)Buffer類都是Buffer接口的一個(gè)子實(shí)例。除了ByteBuffer,每一個(gè)Buffer類都有完全一樣的操作蜕企,只是它們所處理的數(shù)據(jù)類型不一樣咬荷。因?yàn)榇蠖鄶?shù)標(biāo)準(zhǔn)I/O操作都是使用ByteBuffer,所以它除了具有一般緩沖區(qū)的操作之外還提供一些特有的操作轻掩,方便網(wǎng)絡(luò)讀寫幸乒。
2.通道Channel
Channel是一個(gè)通道,可以通過它讀取和寫入數(shù)據(jù)唇牧,它就像自來水管一樣罕扎,網(wǎng)絡(luò)數(shù)據(jù)通過Channel讀取和寫入。通道與流的不同之處在于通道是雙向的丐重,流只是在一個(gè)方向上移動(dòng)(一個(gè)流必須是InputStream或者OutputStream的子類)壳影,而且通道可以用于讀、寫或者同時(shí)讀寫弥臼。因?yàn)镃hannel是全雙工的,所以它可以比流更好地映射底層操作系統(tǒng)的API根灯。
3.多路復(fù)用器Selector
多路復(fù)用器Selector是Java NIO編程的基礎(chǔ)径缅,熟練地掌握Selector對(duì)于掌握NIO編程至關(guān)重要。多路復(fù)用器提供選擇已經(jīng)就緒的任務(wù)的能力烙肺。簡(jiǎn)單來講纳猪,Selector會(huì)不斷地輪詢注冊(cè)在其上的Channel,如果某個(gè)Channel上面有新的TCP連接接入桃笙、讀和寫事件氏堤,這個(gè)Channel就處于就緒狀態(tài),會(huì)被Selector輪詢出來,然后通過SelectionKey可以獲取就緒Channel的集合鼠锈,進(jìn)行后續(xù)的I/O操作闪檬。
一個(gè)多路復(fù)用器Selector可以同時(shí)輪詢多個(gè)Channel,由于JDK使用了epoll()代替?zhèn)鹘y(tǒng)的select實(shí)現(xiàn)购笆,所以它并沒有最大連接句柄1024/2048的限制粗悯。這也就意味著只需要一個(gè)線程負(fù)責(zé)Selector的輪詢,就可以接入成千上萬(wàn)的客戶端同欠,這確實(shí)是個(gè)非常巨大的進(jìn)步样傍。
2.NIO服務(wù)端序列圖
NIO服務(wù)端通信序列圖如下圖所示:
3.NIO客戶端序列圖
NIO客戶端創(chuàng)建序列圖如圖所示。
通過源碼對(duì)比分析發(fā)現(xiàn)铺遂,NIO編程難度確實(shí)比同步阻塞BIO大很多衫哥,此處我們的NIO例程并沒有考慮“半包讀”和“半包寫”,如果加上這些襟锐,代碼會(huì)更加復(fù)雜撤逢。NIO代碼既然這么復(fù)雜,為什么它的應(yīng)用卻越來越廣泛呢捌斧,使用NIO編程的優(yōu)點(diǎn)總結(jié)如下:
客戶端發(fā)起的連接操作是異步的笛质,可以通過多路復(fù)用器注冊(cè)O(shè)P_CONNECT等待后續(xù)結(jié)果,不需要像之前的客戶端那樣被同步阻塞捞蚂。
SocketChannel的讀寫操作都是異步的妇押,如果沒有可讀寫的數(shù)據(jù)它不會(huì)同步等待,直接返回姓迅,這樣I/O通信線程就可以處理其他的鏈路敲霍,不需要同步等待這個(gè)鏈路可用。
線程模型的優(yōu)化:由于JDK的Selector在Linux等主流操作系統(tǒng)上通過epoll實(shí)現(xiàn)丁存,它沒有連接句柄數(shù)的限制(只受限于操作系統(tǒng)的最大句柄數(shù)或者對(duì)單個(gè)進(jìn)程的句柄限制)肩杈,這意味著一個(gè)Selector線程可以同時(shí)處理成千上萬(wàn)個(gè)客戶端連接,而且性能不會(huì)隨著客戶端的增加而線性下降解寝,因此扩然,它非常適合做高性能、高負(fù)載的網(wǎng)絡(luò)服務(wù)器聋伦。
JDK1.7升級(jí)了NIO類庫(kù)夫偶,升級(jí)后的NIO類庫(kù)被稱為NIO 2.0。引入注目的是觉增,Java正式提供了異步文件I/O操作兵拢,同時(shí)提供了與UNIX網(wǎng)絡(luò)編程事件驅(qū)動(dòng)I/O對(duì)應(yīng)的AIO。
四逾礁、AIO
NIO2.0引入了新的異步通道的概念说铃,并提供了異步文件通道和異步套接字通道的實(shí)現(xiàn)。異步通道提供兩種方式獲取操作結(jié)果。
通過java.util.concurrent.Future類來表示異步操作的結(jié)果腻扇;
在執(zhí)行異步操作的時(shí)候傳入一個(gè)java.nio.channels债热。
CompletionHandler接口的實(shí)現(xiàn)類作為操作完成的回調(diào)。
NIO2.0的異步套接字通道是真正的異步非阻塞I/O衙解,它對(duì)UNIX網(wǎng)絡(luò)編程中的事件驅(qū)動(dòng)I/O(AIO)阳柔,它不需要通過多路復(fù)用器(Selector)對(duì)注冊(cè)的通道進(jìn)行輪詢操作即可實(shí)現(xiàn)異步讀寫,從而簡(jiǎn)化了NIO的編程模型蚓峦。
異步SocketChannel是被動(dòng)執(zhí)行對(duì)象舌剂,我們不需要像NIO編程那樣創(chuàng)建一個(gè)獨(dú)立I/O線程來處理讀寫操作。對(duì)于AsynchronousServerSocketChannel和?AsynchronousSocketChannel暑椰,它們都由JDK底層的線程池負(fù)責(zé)回調(diào)并驅(qū)動(dòng)讀寫操作霍转。正因?yàn)槿绱耍贜IO2.0新的異步非阻塞Channel進(jìn)行編程比NIO編程更為簡(jiǎn)單一汽。