前言 Java里面的IO模型種類較多,主要包括BIO,NIO和AIO,每個IO模型都有不一樣的地方,那么這些IO模型是如何演變呢,底層的原理又是怎樣的呢? 本文我們就來聊聊。 BIO BIO全稱是Blocking IO,是JDK1.4之前的傳統(tǒng)IO模型,本身是同步阻塞模式,針對網(wǎng)絡(luò)通信都是一請求一應(yīng)答的方式,雖然簡化了上層的應(yīng)用開發(fā),但在性能和可靠性方面存在著巨大瓶頸,試想一下如果每個請求都需要新建一個線程來專門處理,那么在高并發(fā)的場景下,機器資源很快就會被耗盡,當然,我們可以通過線程池來優(yōu)化這種情況,但即使是這樣,仍然改變不了阻塞IO的根本問題,就是在IO執(zhí)行的兩個階段都被block了。拿一個read操作來舉例子,在linux中,應(yīng)用程序向linux發(fā)起read操作,會經(jīng)歷兩個步驟: 第一個階段linux內(nèi)核首先會把需要讀取的數(shù)據(jù)加載到操作系統(tǒng)內(nèi)核的緩沖區(qū)中(Linux文件系統(tǒng)是緩存IO,也稱標準IO) 第二個階段應(yīng)用程序拷貝內(nèi)核里面的數(shù)據(jù)到自己的用戶空間中 如果是socket操作,類似也會經(jīng)歷兩個步驟: 第一個階段:通常涉及等待網(wǎng)絡(luò)上的數(shù)據(jù)分組包到達,然后被復(fù)制到內(nèi)核的緩沖區(qū) 第二個階段:把數(shù)據(jù)從內(nèi)核緩沖區(qū),從內(nèi)核緩沖區(qū)拷貝到用戶進程的內(nèi)存空間里面 同步阻塞IO之所以效率低下,就是因為在這兩個階段,用戶的線程或者進程都是阻塞的,期間雖然不占cpu資源,但也意味著該線程也不能再干其他事。有點站著茅坑不拉屎的感覺,自己暫時不用了,也不讓別人用。 圖示如下: NIO 由于BIO的缺點,導(dǎo)致Java在JDK1.0至JDK3.0中,網(wǎng)絡(luò)通信模塊的性能一直是短板,所以很多人更傾向于使用C/C 開發(fā)高性能服務(wù)端。為了強化Java在服務(wù)端的市場,終于在JSR-51也就是JDK4.0的時候發(fā)布了Java NIO,可以支持非阻塞IO。并新增了java.nio的包,提供很多異步開發(fā)的API和類庫。 主要的類和接口如下: (1)進行異步IO操作的緩沖區(qū)ByteBuffer (2)進行異步IO操作的管道Pipe (3)進行各種IO操作的Channel,主要包括ServerSocketChannel和SocketChannel (4)實現(xiàn)非阻塞IO的多路復(fù)用器Selector NIO主要有buffer、channel、selector三種技術(shù)的整合,通過零拷貝的buffer取得數(shù)據(jù),每一個客戶端通過channel在selector(多路復(fù)用器)上進行注冊。服務(wù)端不斷輪詢channel來獲取客戶端的信息。channel上有connect,accept(阻塞)、read(可讀)、write(可寫)四種狀態(tài)標識。根據(jù)標識來進行后續(xù)操作。所以一個服務(wù)端可接收無限多的channel。不需要新開一個線程。大大提升了性能。 新的nio類庫,促進了異步非阻塞編程的發(fā)展和應(yīng)用,但仍然有一些不足之處: (1)沒有統(tǒng)一的文件屬性,例如讀寫權(quán)限 (2)api能力比較弱,例如目錄的及聯(lián)創(chuàng)建和遞歸遍歷,往往需要自己完成。 (3)底層操作系統(tǒng)的一些高級API無法使用 (4)所有的文件操作都是同步阻塞調(diào)用,在操作系統(tǒng)層面上并不是異步文件讀寫操作。 Java里面的NIO其實采用了多路復(fù)用的IO模式,多路復(fù)用的模式在Linux底層其實是采用了select,poll,epoll的機制,這種機制可以用單個線程同時監(jiān)聽多個io端口,當其中任何一個socket的數(shù)據(jù)準備好了,就能返回通知用戶線程進行讀取操作,與阻塞IO阻塞的是每一個用戶的線程不一樣的地方是,多路復(fù)用只需要阻塞一個用戶線程即可,這個用戶線程通常我們叫它Selector,其實底層調(diào)用的是內(nèi)核的select,這里面只要任何一個IO操作就緒,就可以喚醒select,然后交由用戶線程處理。用戶線程讀取數(shù)據(jù)這個過程仍然是阻塞的,多路復(fù)用技術(shù)只是在第一個階段可以變?yōu)榉亲枞{(diào)用,但在第二個階段拷貝數(shù)據(jù)到用戶空間,其實還是阻塞的,多路復(fù)用技術(shù)的最大特點是使用一個線程就可以處理很多的socket連接,盡管性能上不一定提升,但支持并發(fā)能力卻大大增強了。 圖示如下: AIO AIO,其實是NIO的改進優(yōu)化,也被稱為NIO2.0,在2011年7月,也就是JDK7的版本中發(fā)布,它主要提供了三個方面的改進: (1)提供了能夠批量獲取文件屬性的api,通過SPI服務(wù),使得這些API具有平臺無關(guān)性。 (2)提供了AIO的功能,支持基于文件的異步IO操作和網(wǎng)絡(luò)套接字的異步操作 (3)完成了JSR-51定義的通道功能等。 AIO 通過調(diào)用accept方法,一個會話接入之后再次調(diào)用(遞歸)accept方法,監(jiān)聽下一次會話,讀取也不再阻塞,回調(diào)complete方法異步進行。不再需要selector 使用channel線程組來接收。 從NIO上面我們能看到,對于IO的兩個階段的阻塞,只是對于第一個階段有所改善,對于第二個階段在NIO里面仍然是阻塞的。而真正的理想的異步非阻塞IO(AAIO)要做的就是,將IO操作的兩個階段都全部交給內(nèi)核系統(tǒng)完成,用戶線程只需要告訴內(nèi)核,我要讀取一塊數(shù)據(jù),請你幫我讀取,讀取完了放在我給你的地址里面,然后告訴我一聲就可以了。 AIO可以做到真正的異步的操作,但實現(xiàn)起來比較復(fù)雜,支持純異步IO的操作系統(tǒng)非常少,目前也就windows是IOCP技術(shù)實現(xiàn)了,而在Linux上,目前有很多開源的異步IO庫,例如libevent、libev、libuv,但基本都不是純的異步IO操作,底層還是是使用的epoll實現(xiàn)的。 圖示如下: NIO與Netty 既然Java擁有了各種IO體系,那么為什么還會出現(xiàn)Netty這種框架呢? Netty出現(xiàn)的主要原因,如下: (1)Java NIO類庫和API繁雜眾多,使用麻煩。 (2)Java NIO封裝程度并不高,常常需要配合Java多線程編程來使用,這是因為NIO編程涉及到Reactor模式。 (3)Java NIO異常體系不完善,如客戶端面臨斷連,重連,網(wǎng)絡(luò)閃斷,半包讀寫,網(wǎng)絡(luò)阻塞,異常碼流等問題,雖然開發(fā)相對容易,但是可靠性和穩(wěn)定性并不高。 (4)Java NIO本身的bug,修復(fù)較慢。 注意,真正的異步非阻塞io,是需要操作系統(tǒng)層面支持的,在windows上通過IOCP實現(xiàn)了真正的異步io,所以Java的AIO的異步在windows平臺才算真正得到了支持,而在Linux系統(tǒng)中,仍然用的是epoll模式,所以在Linux層面上的AIO,并不是真正的或者純的異步IO,這也是Netty里面為什么采用Java的NIO實現(xiàn)的,而并非是AIO,主要原因如下: (1)AIO在linux上底層實現(xiàn)仍使用EPOLL,與NIO相同,因此在性能上沒有明顯的優(yōu)勢 (2)Windows的AIO底層實現(xiàn)良好,但Netty的開發(fā)者并沒有把Windows作為主要使用平臺,所以優(yōu)化考慮Linux 總結(jié) 本文主要介紹了Java里面IO模型的演變和發(fā)展,這也是Java在服務(wù)端領(lǐng)域大放異彩的一個重要原因,了解這些知識之后,我們再去學(xué)習(xí)高性能的Netty框架,將會更加容易 |
|