一、簡介
比如我們常用的 SD 卡、U 盤、移動硬盤等等存儲文件的硬件設(shè)備,當(dāng)我們將其插入電腦的 usb 硬件接口時,我們就可以從電腦中讀取設(shè)備中的信息或者寫入信息,這個過程就涉及到 I/O 的操作。 當(dāng)然,涉及 I/O 的操作,不僅僅局限于硬件設(shè)備的讀寫,還要網(wǎng)絡(luò)數(shù)據(jù)的傳輸,比如,我們在電腦上用瀏覽器搜索互聯(lián)網(wǎng)上的信息,這個過程也涉及到 I/O 的操作。 無論是從磁盤中讀寫文件,還是在網(wǎng)絡(luò)中傳輸數(shù)據(jù),可以說 I/O 主要為處理人機(jī)交互、機(jī)與機(jī)交互中獲取和交換信息提供的一套解決方案。 在 Java 的 IO 體系中,類將近有 80 個,位于java.io包下,感覺很復(fù)雜,但是這些類大致可以分成四組:
前兩組主要從傳輸數(shù)據(jù)的數(shù)據(jù)格式不同,進(jìn)行分組;后兩組主要從傳輸數(shù)據(jù)的方式不同,進(jìn)行分組。 雖然 Socket 類并不在java.io包下,但是我們?nèi)匀话阉鼈儎澐衷谝黄?,因?yàn)?I/O 的核心問題,要么是數(shù)據(jù)格式影響 I/O 操作,要么是傳輸方式影響 I/O 操作,也就是將什么樣的數(shù)據(jù)寫到什么地方的問題,I/O 只是人與機(jī)器或者機(jī)器與機(jī)器交互的手段,除了在它們能夠完成這個交互功能外,我們關(guān)注的就是如何提高它的運(yùn)行效率了,而數(shù)據(jù)格式和傳輸方式是影響效率最關(guān)鍵的因素。 本文后面,也是基于這兩個點(diǎn)進(jìn)行深入展開分析。 二、基于字節(jié)操作的接口基于字節(jié)的輸入和輸出操作接口分別是:InputStream 和 OutputStream 。 2.1、字節(jié)輸入流InputStream 輸入流的類繼承層次如下圖所示: 輸入流根據(jù)數(shù)據(jù)節(jié)點(diǎn)類型和處理方式,分別可以劃分出了若干個子類,如下圖: OutputStream 輸出流的類層次結(jié)構(gòu)也是類似。 2.2、字節(jié)輸出流OutputStream 輸出流的類繼承層次如下圖所示: 輸出流根據(jù)數(shù)據(jù)節(jié)點(diǎn)類型和處理方式,也分別可以劃分出了若干個子類,如下圖: 在這里就不詳細(xì)的介紹各個子類的使用方法,有興趣的朋友可以查看 JDK 的 API 說明文檔,筆者也會在后期的文章會進(jìn)行詳細(xì)的介紹,這里只是重點(diǎn)想說一下,無論是輸入還是輸出,操作數(shù)據(jù)的方式可以組合使用,各個處理流的類并不是只操作固定的節(jié)點(diǎn)流,比如如下輸出方式: //將文件輸出流包裝到序列化輸出流中,再將序列化輸出流包裝到緩沖中OutputStream out = new BufferedOutputStream(new ObjectOutputStream(new FileOutputStream(new File('fileName'))); 另外,輸出流最終寫到什么地方必須要指定,要么是寫到硬盤中,要么是寫到網(wǎng)絡(luò)中,從圖中可以發(fā)現(xiàn),寫網(wǎng)絡(luò)實(shí)際上也是寫文件,只不過寫到網(wǎng)絡(luò)中,需要經(jīng)過底層操作系統(tǒng)將數(shù)據(jù)發(fā)送到其他的計(jì)算機(jī)中,而不是寫入到本地硬盤中。 三、基于字符操作的接口不管是磁盤還是網(wǎng)絡(luò)傳輸,最小的存儲單元都是字節(jié),而不是字符,所以 I/O 操作的都是字節(jié)而不是字符,但是為什么要有操作字符的 I/O 接口呢? 這是因?yàn)槲覀兊某绦蛑型ǔ2僮鞯臄?shù)據(jù)都是以字符形式,為了程序操作更方便而提供一個直接寫字符的 I/O 接口,僅此而已。 基于字符的輸入和輸出操作接口分別是:Reader 和 Writer ,下圖是字符的 I/O 操作接口涉及到的類結(jié)構(gòu)圖。 3.1、字符輸入流Reader 輸入流的類繼承層次如下圖所示: 同樣的,輸入流根據(jù)數(shù)據(jù)節(jié)點(diǎn)類型和處理方式,分別可以劃分出了若干個子類,如下圖: 3.2、字符輸出流Writer 輸出流的類繼承層次如下圖所示: 同樣的,輸出流根據(jù)數(shù)據(jù)節(jié)點(diǎn)類型和處理方式分類,分別可以劃分出了若干個子類,如下圖: 不管是 Reader 還是 Writer 類,它們都只定義了讀取或?qū)懭霐?shù)據(jù)字符的方式,也就是說要么是讀要么是寫,但是并沒有規(guī)定數(shù)據(jù)要寫到哪去,寫到哪去就是我們后面要討論的基于磁盤或網(wǎng)絡(luò)的工作機(jī)制。 四、字節(jié)與字符的轉(zhuǎn)化剛剛我們說到,不管是磁盤還是網(wǎng)絡(luò)傳輸,最小的存儲單元都是字節(jié),而不是字符,設(shè)計(jì)字符的原因是為了程序操作更方便,那么怎么將字符轉(zhuǎn)化成字節(jié)或者將字節(jié)轉(zhuǎn)化成字符呢? InputStreamReader 和 OutputStreamWriter 就是轉(zhuǎn)化橋梁。 4.1、輸入流轉(zhuǎn)化過程輸入流字符解碼相關(guān)類結(jié)構(gòu)的轉(zhuǎn)化過程如下圖所示: 從圖上可以看到,InputStreamReader 類是字節(jié)到字符的轉(zhuǎn)化橋梁, 其中StreamDecoder指的是一個解碼操作類,Charset指的是字符集。 InputStream 到 Reader 的過程需要指定編碼字符集,否則將采用操作系統(tǒng)默認(rèn)字符集,很可能會出現(xiàn)亂碼問題,StreamDecoder 則是完成字節(jié)到字符的解碼的實(shí)現(xiàn)類。 打開源碼部分,InputStream 到 Reader 轉(zhuǎn)化過程,如下圖: 4.1、輸出流轉(zhuǎn)化過程輸出流轉(zhuǎn)化過程也是類似,如下圖所示: 通過 OutputStreamWriter 類完成字符到字節(jié)的編碼過程,由 StreamEncoder 完成編碼過程。 源碼部分,Writer 到 OutputStream 轉(zhuǎn)化過程,如下圖: 五、基于磁盤操作的接口前面介紹了 Java I/O 的操作接口,這些接口主要定義了如何操作數(shù)據(jù),以及介紹了操作數(shù)據(jù)格式的方式:字節(jié)流和字符流。 還有一個關(guān)鍵問題就是數(shù)據(jù)寫到何處,其中一個主要的處理方式就是將數(shù)據(jù)持久化到物理磁盤。 我們知道數(shù)據(jù)在磁盤的唯一最小描述就是文件,也就是說上層應(yīng)用程序只能通過文件來操作磁盤上的數(shù)據(jù),文件也是操作系統(tǒng)和磁盤驅(qū)動器交互的一個最小單元。 在 Java I/O 體系中,F(xiàn)ile 類是唯一代表磁盤文件本身的對象。 File 類定義了一些與平臺無關(guān)的方法來操作文件,包括檢查一個文件是否存在、創(chuàng)建、刪除文件、重命名文件、判斷文件的讀寫權(quán)限是否存在、設(shè)置和查詢文件的最近修改時間等等操作。 值得注意的是 Java 中通常的 File 并不代表一個真實(shí)存在的文件對象,當(dāng)你通過指定一個路徑描述符時,它就會返回一個代表這個路徑相關(guān)聯(lián)的一個虛擬對象,這個可能是一個真實(shí)存在的文件或者是一個包含多個文件的目錄。 例如,讀取一個文件內(nèi)容,程序如下: 以上面的程序?yàn)槔?,從硬盤中讀取一段文本字符,操作流程如下圖: 我們再來看看源碼執(zhí)行流程。 當(dāng)我們傳入一個指定的文件名來創(chuàng)建 File 對象,通過 FileReader 來讀取文件內(nèi)容時,會自動創(chuàng)建一個FileInputStream對象來讀取文件內(nèi)容,也就是我們上文中所說的字節(jié)流來讀取文件。 緊接著,會創(chuàng)建一個FileDescriptor的對象,其實(shí)這個對象就是真正代表一個存在的文件對象的描述??梢酝ㄟ^FileInputStream對象調(diào)用getFD()方法獲取真正與底層操作系統(tǒng)關(guān)聯(lián)的文件描述。 由于我們需要讀取的是字符格式,所以需要 StreamDecoder 類將byte解碼為char格式,至于如何從磁盤驅(qū)動器上讀取一段數(shù)據(jù),由操作系統(tǒng)幫我們完成。 六、基于網(wǎng)絡(luò)操作的接口繼續(xù)來說說數(shù)據(jù)寫到何處的另一種處理方式:將數(shù)據(jù)寫入互聯(lián)網(wǎng)中以供其他電腦能訪問。 6.1、Socket 簡介在現(xiàn)實(shí)中,Socket 這個概念沒有一個具體的實(shí)體,它是描述計(jì)算機(jī)之間完成相互通信一種抽象定義。 打個比方,可以把 Socket 比作為兩個城市之間的交通工具,有了它,就可以在城市之間來回穿梭了。并且,交通工具有多種,每種交通工具也有相應(yīng)的交通規(guī)則。Socket 也一樣,也有多種。大部分情況下我們使用的都是基于 TCP/IP 的流套接字,它是一種穩(wěn)定的通信協(xié)議。 典型的基于 Socket 通信的應(yīng)用程序場景,如下圖: 主機(jī) A 的應(yīng)用程序要想和主機(jī) B 的應(yīng)用程序通信,必須通過 Socket 建立連接,而建立 Socket 連接必須需要底層 TCP/IP 協(xié)議來建立 TCP 連接。 6.2、建立通信鏈路我們知道網(wǎng)絡(luò)層使用的 IP 協(xié)議可以幫助我們根據(jù) IP 地址來找到目標(biāo)主機(jī),但是一臺主機(jī)上可能運(yùn)行著多個應(yīng)用程序,如何才能與指定的應(yīng)用程序通信就要通過 TCP 或 UPD 的地址也就是端口號來指定。這樣就可以通過一個 Socket 實(shí)例代表唯一一個主機(jī)上的一個應(yīng)用程序的通信鏈路了。 為了準(zhǔn)確無誤地把數(shù)據(jù)送達(dá)目標(biāo)處,TCP 協(xié)議采用了三次握手策略,如下圖: 其中,SYN 全稱為 Synchronize Sequence Numbers,表示同步序列編號,是 TCP/IP 建立連接時使用的握手信號。 ACK 全稱為 Acknowledge character,即確認(rèn)字符,表示發(fā)來的數(shù)據(jù)已確認(rèn)接收無誤。 在客戶機(jī)和服務(wù)器之間建立正常的 TCP 網(wǎng)絡(luò)連接時,客戶機(jī)首先發(fā)出一個 SYN 消息,服務(wù)器使用 SYN + ACK 應(yīng)答表示接收到了這個消息,最后客戶機(jī)再以 ACK 消息響應(yīng)。 這樣在客戶機(jī)和服務(wù)器之間才能建立起可靠的 TCP 連接,數(shù)據(jù)才可以在客戶機(jī)和服務(wù)器之間傳遞。 簡單流程如下:
完成三次握手之后,客戶端應(yīng)用程序與服務(wù)器應(yīng)用程序就可以開始傳送數(shù)據(jù)了。 傳輸數(shù)據(jù)是我們建立連接的主要目的,如何通過 Socket 傳輸數(shù)據(jù)呢? 6.3、傳輸數(shù)據(jù)當(dāng)客戶端要與服務(wù)端通信時,客戶端首先要創(chuàng)建一個 Socket 實(shí)例,默認(rèn)操作系統(tǒng)將為這個 Socket 實(shí)例分配一個沒有被使用的本地端口號,并創(chuàng)建一個包含本地、遠(yuǎn)程地址和端口號的套接字?jǐn)?shù)據(jù)結(jié)構(gòu),這個數(shù)據(jù)結(jié)構(gòu)將一直保存在系統(tǒng)中直到這個連接關(guān)閉。 與之對應(yīng)的服務(wù)端,也將創(chuàng)建一個 ServerSocket 實(shí)例,ServerSocket 創(chuàng)建比較簡單,只要指定的端口號沒有被占用,一般實(shí)例創(chuàng)建都會成功,同時操作系統(tǒng)也會為 ServerSocket 實(shí)例創(chuàng)建一個底層數(shù)據(jù)結(jié)構(gòu),這個數(shù)據(jù)結(jié)構(gòu)中包含指定監(jiān)聽的端口號和包含監(jiān)聽地址的通配符,通常情況下都是*即監(jiān)聽所有地址。 之后當(dāng)調(diào)用 accept() 方法時,將進(jìn)入阻塞狀態(tài),等待客戶端的請求。 我們先啟動服務(wù)端程序,再運(yùn)行客戶端,服務(wù)端收到客戶端發(fā)送的信息,服務(wù)端打印結(jié)果如下: 注意,客戶端只有與服務(wù)端建立三次握手成功之后,才會發(fā)送數(shù)據(jù),而 TCP/IP 握手過程,底層操作系統(tǒng)已經(jīng)幫我們實(shí)現(xiàn)了! 當(dāng)連接已經(jīng)建立成功,服務(wù)端和客戶端都會擁有一個 Socket 實(shí)例,每個 Socket 實(shí)例都有一個 InputStream 和 OutputStream,正如我們前面所說的,網(wǎng)絡(luò) I/O 都是以字節(jié)流傳輸?shù)模琒ocket 正是通過這兩個對象來交換數(shù)據(jù)。 當(dāng) Socket 對象創(chuàng)建時,操作系統(tǒng)將會為 InputStream 和 OutputStream 分別分配一定大小的緩沖區(qū),數(shù)據(jù)的寫入和讀取都是通過這個緩存區(qū)完成的。 寫入端將數(shù)據(jù)寫到 OutputStream 對應(yīng)的 SendQ 隊(duì)列中,當(dāng)隊(duì)列填滿時,數(shù)據(jù)將被發(fā)送到另一端 InputStream 的 RecvQ 隊(duì)列中,如果這時 RecvQ 已經(jīng)滿了,那么 OutputStream 的 write 方法將會阻塞直到 RecvQ 隊(duì)列有足夠的空間容納 SendQ 發(fā)送的數(shù)據(jù)。 值得特別注意的是,緩存區(qū)的大小以及寫入端的速度和讀取端的速度非常影響這個連接的數(shù)據(jù)傳輸效率,由于可能會發(fā)生阻塞,所以網(wǎng)絡(luò) I/O 與磁盤 I/O 在數(shù)據(jù)的寫入和讀取還要有一個協(xié)調(diào)的過程,如果兩邊同時傳送數(shù)據(jù)時可能會產(chǎn)生死鎖的問題。 如何提高網(wǎng)絡(luò) IO 傳輸效率、保證數(shù)據(jù)傳輸?shù)目煽?,已?jīng)成了工程師們急需解決的問題。 6.4、IO 工作方式在計(jì)算機(jī)中,IO 傳輸數(shù)據(jù)有三種工作方式,分別是 BIO、NIO、AIO。 在講解 BIO、NIO、AIO 之前,我們先來回顧一下這幾個概念:同步與異步,阻塞與非阻塞。 同步與異步的區(qū)別
阻塞和非阻塞的區(qū)別
而我們要講的 BIO、NIO、AIO 就是同步與異步、阻塞與非阻塞的組合。
6.4.1、BIOBIO 俗稱同步阻塞 IO,一種非常傳統(tǒng)的 IO 模型,比如我們上面所舉的那個程序例子,就是一個典型的**同步阻塞 IO **的工作方式。 采用 BIO 通信模型的服務(wù)端,通常由一個獨(dú)立的 Acceptor 線程負(fù)責(zé)監(jiān)聽客戶端的連接。 我們一般在服務(wù)端通過while(true)循環(huán)中會調(diào)用accept()方法等待監(jiān)聽客戶端的連接,一旦接收到一個連接請求,就可以建立通信套接字進(jìn)行讀寫操作,此時不能再接收其他客戶端連接請求,只能等待同當(dāng)前連接的客戶端的操作執(zhí)行完成, 不過可以通過多線程來支持多個客戶端的連接。 客戶端多線程操作,程序如下: 服務(wù)端多線程操作,程序如下: 服務(wù)端運(yùn)行結(jié)果,如下: 如果要讓 BIO 通信模型能夠同時處理多個客戶端請求,就必須使用多線程,也就是說它在接收到客戶端連接請求之后為每個客戶端創(chuàng)建一個新的線程進(jìn)行鏈路處理,處理完成之后,通過輸出流返回應(yīng)答給客戶端,線程銷毀。 這就是典型的一請求一應(yīng)答通信模型 。 如果出現(xiàn) 100、1000、甚至 10000 個用戶同時訪問服務(wù)器,這個時候,如果使用這種模型,那么服務(wù)端也會創(chuàng)建與之相同的線程數(shù)量,線程數(shù)急劇膨脹可能會導(dǎo)致線程堆棧溢出、創(chuàng)建新線程失敗等問題,最終導(dǎo)致進(jìn)程宕機(jī)或者僵死,不能對外提供服務(wù)。 當(dāng)然,我們可以通過使用 Java 中 ThreadPoolExecutor 線程池機(jī)制來改善,讓線程的創(chuàng)建和回收成本相對較低,保證了系統(tǒng)有限的資源的控制,實(shí)現(xiàn)了 N (客戶端請求數(shù)量)大于 M (處理客戶端請求的線程數(shù)量)的偽異步 I/O 模型。 6.4.2、偽異步 BIO為了解決同步阻塞 I/O 面臨的一個鏈路需要一個線程處理的問題,后來有人對它的線程模型進(jìn)行了優(yōu)化,后端通過一個線程池來處理多個客戶端的請求接入,形成客戶端個數(shù) M:線程池最大線程數(shù) N 的比例關(guān)系,其中 M 可以遠(yuǎn)遠(yuǎn)大于 N,通過線程池可以靈活地調(diào)配線程資源,設(shè)置線程的最大值,防止由于海量并發(fā)接入導(dǎo)致資源耗盡。 偽異步 IO 模型圖,如下圖: 采用線程池和任務(wù)隊(duì)列可以實(shí)現(xiàn)一種叫做偽異步的 I/O 通信框架,當(dāng)有新的客戶端接入時,將客戶端的 Socket 封裝成一個 Task 投遞到后端的線程池中進(jìn)行處理。 Java 的線程池維護(hù)一個消息隊(duì)列和 N 個活躍線程,對消息隊(duì)列中的任務(wù)進(jìn)行處理。 客戶端,程序如下: 服務(wù)端,程序如下: 先啟動服務(wù)端程序,再啟動客戶端程序,看看運(yùn)行結(jié)果! 服務(wù)端,運(yùn)行結(jié)果如下: 客戶端,運(yùn)行結(jié)果如下: 本例中測試的客戶端數(shù)量是 30,服務(wù)端使用 java 線程池來處理任務(wù),線程數(shù)量為 5 個,服務(wù)端不用為每個客戶端都創(chuàng)建一個線程,由于線程池可以設(shè)置消息隊(duì)列的大小和最大線程數(shù),因此,它的資源占用是可控的,無論多少個客戶端并發(fā)訪問,都不會導(dǎo)致資源的耗盡和宕機(jī)。 在活動連接數(shù)不是特別高的情況下,這種模型是還不錯,可以讓每一個連接專注于自己的 I/O 并且編程模型簡單,也不用過多考慮系統(tǒng)的過載、限流等問題。 但是,它的底層仍然是同步阻塞的 BIO 模型,當(dāng)面對十萬甚至百萬級連接的時候,傳統(tǒng)的 BIO 模型真的是無能為力的,我們需要一種更高效的 I/O 處理模型來應(yīng)對更高的并發(fā)量。 6.4.3、NIONIO 中的 N 可以理解為 Non-blocking,一種同步非阻塞的 I/O 模型,在 Java 1.4 中引入,對應(yīng)的在java.nio包下。 NIO 新增了 Channel、Selector、Buffer 等抽象概念,支持面向緩沖、基于通道的 I/O 操作方法。 NIO 提供了與傳統(tǒng) BIO 模型中的 Socket 和 ServerSocket 相對應(yīng)的 SocketChannel和 ServerSocketChannel 兩種不同的套接字通道實(shí)現(xiàn)。 NIO 這兩種通道都支持阻塞和非阻塞兩種模式。阻塞模式使用就像傳統(tǒng)中的支持一樣,比較簡單,但是性能和可靠性都不好;非阻塞模式正好與之相反。 對于低負(fù)載、低并發(fā)的應(yīng)用程序,可以使用同步阻塞 I/O 來提升開發(fā)效率和更好的維護(hù)性;對于高負(fù)載、高并發(fā)的(網(wǎng)絡(luò))應(yīng)用,應(yīng)使用 NIO 的非阻塞模式來開發(fā)。 我們先看一下 NIO 涉及到的核心關(guān)聯(lián)類圖,如下: 上圖中有三個關(guān)鍵類:Channel 、Selector 和 Buffer,它們是 NIO 中的核心概念。
我們還是用前面的城市交通工具來繼續(xù)形容 NIO 的工作方式,這里的 Channel 要比 Socket 更加具體,它可以比作為某種具體的交通工具,如汽車或是高鐵、飛機(jī)等,而 Selector 可以比作為一個車站的車輛運(yùn)行調(diào)度系統(tǒng),它將負(fù)責(zé)監(jiān)控每輛車的當(dāng)前運(yùn)行狀態(tài):是已經(jīng)出站還是在路上等等,也就是說它可以輪詢每個 Channel 的狀態(tài)。 還有一個 Buffer 類,你可以將它看作為 IO 中 Stream,但是它比 IO 中的 Stream 更加具體化,我們可以將它比作為車上的座位,Channel 如果是汽車的話,那么 Buffer 就是汽車上的座位,Channel 如果是高鐵上,那么 Buffer 就是高鐵上的座位,它始終是一個具體的概念,這一點(diǎn)與 Stream 不同。 Socket 中的 Stream 只能代表是一個座位,至于是什么座位由你自己去想象,也就是說你在上車之前并不知道這個車上是否還有沒有座位,也不知道上的是什么車,因?yàn)槟悴⒉荒苓x擇,這些信息都已經(jīng)被封裝在了運(yùn)輸工具(Socket)里面了。 NIO 引入了 Channel、Buffer 和 Selector 就是想把 IO 傳輸過程中涉及到的信息具體化,讓程序員有機(jī)會去控制它們。 當(dāng)我們進(jìn)行傳統(tǒng)的網(wǎng)絡(luò) IO 操作時,比如調(diào)用 write() 往 Socket 中的 SendQ 隊(duì)列寫數(shù)據(jù)時,當(dāng)一次寫的數(shù)據(jù)超過 SendQ 長度時,操作系統(tǒng)會按照 SendQ 的長度進(jìn)行分割的,這個過程中需要將用戶空間數(shù)據(jù)和內(nèi)核地址空間進(jìn)行切換,而這個切換不是程序員可以控制的,由底層操作系統(tǒng)來幫我們處理。 而在 Buffer 中,我們可以控制 Buffer 的 capacity(容量),并且是否擴(kuò)容以及如何擴(kuò)容都可以控制。 理解了這些概念后我們看一下,實(shí)際上它們是如何工作的呢? 還是以上面的操作為例子,為了方便觀看結(jié)果,本次的客戶端線程請求數(shù)改成 15 個。 客戶端,程序如下: 服務(wù)端,程序如下: 先啟動服務(wù)端程序,再啟動客戶端程序,看看運(yùn)行結(jié)果! 服務(wù)端,運(yùn)行結(jié)果如下: 客戶端,運(yùn)行結(jié)果如下: 當(dāng)然,客戶端也不僅僅只限制于 IO 的寫法,還可以使用SocketChannel來操作客戶端,程序如下: 一樣的,先啟動服務(wù)端,再啟動客戶端,客戶端運(yùn)行結(jié)果如下: 從操作上可以看到,NIO 的操作比傳統(tǒng)的 IO 操作要復(fù)雜的多! Selector 被稱為選擇器 ,當(dāng)然你也可以翻譯為多路復(fù)用器 。它是 Java NIO 核心組件中的一個,用于檢查一個或多個 Channel(通道)的狀態(tài)是否處于連接就緒、接受就緒、可讀就緒、可寫就緒。 如此可以實(shí)現(xiàn)單線程管理多個 channels,也就是可以管理多個網(wǎng)絡(luò)連接。 使用 Selector 的好處在于: 相比傳統(tǒng)方式使用多個線程來管理 IO,Selector 使用了更少的線程就可以處理通道了,并且實(shí)現(xiàn)網(wǎng)絡(luò)高效傳輸! 雖然 java 中的 nio 傳輸比較快,為什么大家都不愿意用 JDK 原生 NIO 進(jìn)行開發(fā)呢? 從上面的代碼中大家都可以看出來,除了編程復(fù)雜、編程模型難之外,還有幾個讓人詬病的問題:
但是,Google 的 Netty 框架的出現(xiàn),很大程度上改善了 JDK 原生 NIO 所存在的一些讓人難以忍受的問題,關(guān)于 Netty 框架,會在后期的文章里進(jìn)行介紹。 6.4.4、AIO最后就是 AIO 了,全稱 Asynchronous I/O,可以理解為異步 IO,也被稱為 NIO 2,在 Java 7 中引入了 NIO 的改進(jìn)版 NIO 2,它是異步非阻塞的 IO 模型,也就是我們現(xiàn)在所說的 AIO。 異步 IO 是基于事件和回調(diào)機(jī)制實(shí)現(xiàn)的,也就是應(yīng)用操作之后會直接返回,不會堵塞在那里,當(dāng)后臺處理完成,操作系統(tǒng)會通知相應(yīng)的線程進(jìn)行后續(xù)的操作。 客戶端,程序示例: 服務(wù)端,程序示例: 同樣的,先啟動服務(wù)端程序,再啟動客戶端程序,看看運(yùn)行結(jié)果! 服務(wù)端,運(yùn)行結(jié)果如下: 客戶端端,運(yùn)行結(jié)果如下: 這種組合方式用起來比較復(fù)雜,只有在一些非常復(fù)雜的分布式情況下使用,像集群之間的消息同步機(jī)制一般用這種 I/O 組合方式。如 Cassandra 的 Gossip 通信機(jī)制就是采用異步非阻塞的方式。 Netty 之前也嘗試使用過 AIO,不過又放棄了! 七、總結(jié)本文闡述的內(nèi)容較多,從 Java 基本 I/O 類庫結(jié)構(gòu)開始說起,主要介紹了 IO 的傳輸格式和傳輸方式,以及磁盤 I/O 和網(wǎng)絡(luò) I/O 的基本工作方式。 本篇文章主要對 Java 的 IO 體系以及計(jì)算機(jī)部分網(wǎng)絡(luò)基礎(chǔ)知識做了些簡單的介紹,其實(shí)每一個模塊涉及到的知識都非常非常多,在后期的文章中,會對各個模塊進(jìn)行詳細(xì)的介紹,如果有理解不到的位置,歡迎指出! 覺得文章不錯就給小老弟點(diǎn)個關(guān)注吧,更多內(nèi)容陸續(xù)奉上。
|
|