Java NIO概述

作者: 一字馬胡
轉(zhuǎn)載標(biāo)志 【2017-11-24】

更新日志

日期 更新內(nèi)容 備注
2017-11-24 新建文章 以前學(xué)習(xí)java NI/O的時(shí)候?qū)懙奈恼拢瑥?fù)制過來的,格式改了不少,所以難免有錯(cuò)誤,不斷更新

一、Java OIO

Java OIO (Java Old I/O)代表著的是一種阻塞I/O,所謂阻塞I/O,就是函數(shù)調(diào)用之后會(huì)一直阻塞直到函數(shù)返回正確值或者出錯(cuò)或者被中斷,而在函數(shù)返回之前,該調(diào)用之后的代碼將不會(huì)被執(zhí)行,也就是說,你必須要等到這個(gè)函數(shù)返回(無論多久),你才能繼續(xù)做接下來的事情。有時(shí)候這樣的編程模型是必須的,比如我們必須依賴從數(shù)據(jù)庫中讀取到的數(shù)據(jù)以作為依據(jù)去執(zhí)行接下來的代碼邏輯,這樣的編程模型是在一個(gè)假設(shè)下成立的,這個(gè)假設(shè)就是:認(rèn)為阻塞等待函數(shù)返回是值得的,后面的代碼就好像被鎖住了一樣,需要獲取到一把鑰匙才能打開鎖以繼續(xù)執(zhí)行,而獲取這把鎖的唯一方法就是從阻塞中返回一種結(jié)果,然后根據(jù)不同的結(jié)果來打開不同的鎖。這種I/O編程模型是簡單的,你不需要為如何編寫代碼而緊皺眉頭,但是這種編程模型的缺陷也是很明顯的,因?yàn)椋芏嗲闆r下,我們并不需要等待結(jié)果立刻返回,我們更希望提前提交任務(wù),然后去做一些其他的事情,然后在必須獲取結(jié)果才能繼續(xù)的時(shí)候才阻塞等待獲取,而這個(gè)時(shí)候可能函數(shù)早已返回,已經(jīng)不需要阻塞了,這樣的編程模型使得我們的工作更加高效,這其實(shí)也是并發(fā)編程的模型,這樣的模型確實(shí)可以提高我們的代碼的效率,但是寫代碼的難度就上升了一些,可能我們需要非常小心的安排代碼的順序,并且在必要的時(shí)候釋放一些資源等。但是為了提高效率解決編程的復(fù)雜性是值得的。有必要清晰一下下面的概念:

  • 阻塞I/O
  • 非阻塞I/O
  • 同步I/O
  • 異步I/O

每一次I/O操作都會(huì)涉及下面的兩個(gè)過程:

  1. 數(shù)據(jù)被copy到操作系統(tǒng)內(nèi)核的緩沖區(qū)中
  2. 數(shù)據(jù)從操作系統(tǒng)內(nèi)核緩存區(qū)copy到用戶進(jìn)程空間中

而這兩個(gè)過程分別對應(yīng)著下面的兩個(gè)過程:

  1. 內(nèi)核等待IO數(shù)據(jù)準(zhǔn)備完成
  2. 進(jìn)程將數(shù)據(jù)從內(nèi)核copy到自己的地址空間內(nèi)

上面四個(gè)概念的區(qū)別,可以通過下面的準(zhǔn)則區(qū)分:

  1. 調(diào)用函數(shù)之后如果函數(shù)立即返回?zé)o論數(shù)據(jù)準(zhǔn)備完成與否,則為非阻塞IO,否則為阻塞IO(重點(diǎn)在于調(diào)用線程是否會(huì)被阻塞)
  2. 在做真正的IO操作的時(shí)候如果會(huì)阻塞調(diào)用線程,則為同步IO,否則為異步IO(重點(diǎn)在于真正執(zhí)行IO操作的時(shí)候?qū)φ{(diào)用線程是否可感知)

根據(jù)上面的判斷準(zhǔn)則,OIO是阻塞的同步IO,而NIO是非阻塞的同步IO,NIO依然不是異步的,因?yàn)檎嬲龍?zhí)行IO操作(比如read)的時(shí)候調(diào)用線程依然會(huì)被阻塞以等待結(jié)果(當(dāng)內(nèi)核數(shù)據(jù)還沒有準(zhǔn)備好的時(shí)候,是不會(huì)阻塞線程的,但是當(dāng)內(nèi)核已經(jīng)準(zhǔn)備好數(shù)據(jù)之后,進(jìn)程需要將數(shù)據(jù)從內(nèi)核拷貝到自己的地址空間這個(gè)步驟是阻塞的),Netty框架則基于NIO使得IO操作變成了異步的,所以Netty是一個(gè)異步的IO框架。

二、I/O多路復(fù)用技術(shù)

說到IO多路復(fù)用,馬上應(yīng)該想select、poll、epoll等機(jī)制。多路復(fù)用技術(shù)說的是,一個(gè)線程可以監(jiān)聽多個(gè)文件描述符,如果那個(gè)準(zhǔn)備好了就處理哪個(gè),這和傳統(tǒng)的線程模型是有顯著的區(qū)別的。傳統(tǒng)的IO處理做法是,使用一個(gè)線程監(jiān)聽端口,進(jìn)來一個(gè)請求,則新建一個(gè)線程處理該請求。這樣的線程模型非常簡單,弊端也是非常明顯的,比如一個(gè)流量非常大的服務(wù)使用這樣的線程模型來承接請求,那么服務(wù)的可用性是非常差的,當(dāng)然,有一個(gè)方案可能比這個(gè)好一些,那就是使用線程池,并且設(shè)置等待隊(duì)列,這樣的話,線程不需要頻繁的被創(chuàng)建,當(dāng)一個(gè)請求完成處理之后,線程就可以空閑出來接收新的請求,當(dāng)線程池里的線程都被占用了之后,請求會(huì)被放到等待隊(duì)列,等待線程來拉取,這樣的解決方案貌似非常先進(jìn),確實(shí),這樣的方案比起一開始的方案好很多,對于業(yè)務(wù)非常簡單的服務(wù),使用這樣的方案應(yīng)該可以承接不小的流量,但是對于業(yè)務(wù)足夠復(fù)雜的場景來說,這樣的方案依然會(huì)有風(fēng)險(xiǎn),因?yàn)榫€程池滿了之后,請求會(huì)被緩存起來啊,那緩存就需要空間來存放啊,那么這個(gè)隊(duì)列的大小就是有約束的啊,不可能無限大啊,那如果緩存隊(duì)列被打滿了呢?那么接下來的請求將會(huì)被丟棄,對于用戶而言就是,我明明點(diǎn)擊了屏幕,但是沒有任何動(dòng)靜????。∵@樣的后果就是,用戶會(huì)再次點(diǎn)擊,再次點(diǎn)擊,再次點(diǎn)擊....這樣的后果對于服務(wù)端來說就是請求越來越多,對于用戶來說就是,“多么垃圾的app啊”。所以,這樣的方案依然得慎用。對于業(yè)務(wù)足夠復(fù)雜,流量足夠大的場景來說,選擇多路復(fù)用技術(shù)是必須的。

2.1 select

下面是select的處理流程,select的具體操作步驟:

  • 1、拷貝nfds、readfds、writefds和exceptfds到內(nèi)核(自己感興趣的描述符)
  • 2、遍歷[0,nfds)范圍內(nèi)的每個(gè)流,調(diào)用流所對應(yīng)的設(shè)備的驅(qū)動(dòng)poll函數(shù)
  • 3、檢查是否有流發(fā)生,如果有發(fā)生,把流設(shè)置對應(yīng)的類別,并執(zhí)行4,如果沒有流發(fā)生,執(zhí)行5。或者timeout=0,執(zhí)行4
  • 4、select返回
  • 5、select阻塞當(dāng)前進(jìn)程,等待被流對應(yīng)的設(shè)備喚醒,當(dāng)被喚醒時(shí),執(zhí)行2?;蛘遲imeout到期,執(zhí)行4

select的缺陷:

  • (1)每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài)
  • (2)同時(shí)每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來的所有fd
  • (3)select支持的文件描述符數(shù)量很小,默認(rèn)是1024

2.2 poll 和epoll

poll和select差不多,但是poll不再告訴內(nèi)核文件描述符的范圍,而是告訴內(nèi)核自己感興趣的文件描述符集合,這樣的話就沒必要去詢問自己不感興趣的文件描述符了。epoll既然是對select和poll的改進(jìn),就應(yīng)該能避免上述的三個(gè)缺點(diǎn)。那epoll都是怎么解決的呢?在此之前,我們先看一下epoll和select和poll的調(diào)用接口上的不同,select和poll都只提供了一個(gè)函數(shù)——select或者poll函數(shù)。而epoll提供了三個(gè)函數(shù),epoll_create,epoll_ctl和epoll_wait,epoll_create是創(chuàng)建一個(gè)epoll句柄;epoll_ctl是注冊要監(jiān)聽的事件類型;epoll_wait則是等待事件的產(chǎn)生。那我們從select/poll的三個(gè)缺點(diǎn)的解決方案來看下epoll的實(shí)現(xiàn):

  1. 缺點(diǎn)1:每次調(diào)用select,都需要把fd集合從用戶態(tài)拷貝到內(nèi)核態(tài),這個(gè)開銷在fd很多時(shí)會(huì)很大
    epoll的解決方案:對于第一個(gè)缺點(diǎn),epoll的解決方案在epoll_ctl函數(shù)中。每次注冊新的事件到epoll句柄中時(shí)(在epoll_ctl中指定EPOLL_CTL_ADD),會(huì)把所有的fd拷貝進(jìn)內(nèi)核,而不是在epoll_wait的時(shí)候重復(fù)拷貝。epoll保證了每個(gè)fd在整個(gè)過程中只會(huì)拷貝一次。
  2. 缺點(diǎn)2:同時(shí)每次調(diào)用select都需要在內(nèi)核遍歷傳遞進(jìn)來的所有fd,這個(gè)開銷在fd很多時(shí)也很大
    epoll的解決方案: 對于第二個(gè)缺點(diǎn),epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應(yīng)的設(shè)備等待隊(duì)列中,而只在epoll_ctl時(shí)把current掛一遍(這一遍必不可少)并為每個(gè)fd指定一個(gè)回調(diào)函數(shù),當(dāng)設(shè)備就緒,喚醒等待隊(duì)列上的等待者時(shí),就會(huì)調(diào)用這個(gè)回調(diào)函數(shù),而這個(gè)回調(diào)函數(shù)會(huì)把就緒的fd加入一個(gè)就緒鏈表)。epoll_wait的工作實(shí)際上就是在這個(gè)就緒鏈表中查看有沒有就緒的fd
  3. 缺點(diǎn)3:select支持的文件描述符數(shù)量太小了,默認(rèn)是1024

epoll的解決方案:epoll沒有這個(gè)限制,它所支持的FD上限是最大可以打開文件的數(shù)目,這個(gè)數(shù)字一般遠(yuǎn)大于2048,舉個(gè)例子,在1GB內(nèi)存的機(jī)器上大約是10萬左右,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個(gè)數(shù)目和系統(tǒng)內(nèi)存關(guān)系很大。

2.3 select、poll、epoll總結(jié)

概括:

type desc
Select select本質(zhì)上是通過設(shè)置或者檢查存放fd標(biāo)志位的數(shù)據(jù)結(jié)構(gòu)來進(jìn)行下一步處理。這樣所帶來的缺點(diǎn)是:(1) 單個(gè)進(jìn)程可監(jiān)視的fd數(shù)量被限制 (2) 需要維護(hù)一個(gè)用來存放大量fd的數(shù)據(jù)結(jié)構(gòu),這樣會(huì)使得用戶空間和內(nèi)核空間在傳遞該結(jié)構(gòu)時(shí)復(fù)制開銷大 (3) 對socket進(jìn)行掃描時(shí)是線性掃描
Poll poll本質(zhì)上和select沒有區(qū)別,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個(gè)fd對應(yīng)的設(shè)備狀態(tài),如果設(shè)備就緒則在設(shè)備等待隊(duì)列中加入一項(xiàng)并繼續(xù)遍歷,如果遍歷完所有fd后沒有發(fā)現(xiàn)就緒設(shè)備,則掛起當(dāng)前進(jìn)程,直到設(shè)備就緒或者主動(dòng)超時(shí),被喚醒后它又要再次遍歷fd。這個(gè)過程經(jīng)歷了多次無謂的遍歷。它沒有最大連接數(shù)的限制,原因是它是基于鏈表來存儲(chǔ)的,但是同樣有一個(gè)缺點(diǎn):大量的fd的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核地址空間之間,而不管這樣的復(fù)制是不是有意義。poll還有一個(gè)特點(diǎn)是“水平觸發(fā)”,如果報(bào)告了fd后,沒有被處理,那么下次poll時(shí)會(huì)再次報(bào)告該fd。
Epoll epoll支持水平觸發(fā)和邊緣觸發(fā),最大的特點(diǎn)在于邊緣觸發(fā),它只告訴進(jìn)程哪些fd剛剛變?yōu)榫托钁B(tài),并且只會(huì)通知一次。在前面說到的復(fù)制問題上,epoll使用mmap減少復(fù)制開銷。還有一個(gè)特點(diǎn)是,epoll使用“事件”的就緒通知方式,通過epoll_ctl注冊fd,一旦該fd就緒,內(nèi)核就會(huì)采用類似callback的回調(diào)機(jī)制來激活該fd,epoll_wait便可以收到通知

注:水平觸發(fā)(level-triggered)——只要滿足條件,就觸發(fā)一個(gè)事件(只要有數(shù)據(jù)沒有被獲取,內(nèi)核就不斷通知你);邊緣觸發(fā)(edge-triggered)——每當(dāng)狀態(tài)變化時(shí),觸發(fā)一個(gè)事件。

區(qū)別:

type Select Poll EPoll
支持最大連接數(shù) 1024(x86) or 2048(x64) 無上限 無上限
IO效率 每次調(diào)用進(jìn)行線性遍歷,時(shí)間復(fù)雜度為O(N) 每次調(diào)用進(jìn)行線性遍歷,時(shí)間復(fù)雜度為O(N) 使用“事件”通知方式,每當(dāng)fd就緒,系統(tǒng)注冊的回調(diào)函數(shù)就會(huì)被調(diào)用,將就緒fd放到rdllist里面,這樣epoll_wait返回的時(shí)候我們就拿到了就緒的fd。時(shí)間發(fā)復(fù)雜度O(1)
fd拷貝 每次select都拷貝 每次poll都拷貝 調(diào)用epoll_ctl時(shí)拷貝進(jìn)內(nèi)核并由內(nèi)核保存,之后每次epoll_wait不拷貝
  1. select,poll實(shí)現(xiàn)需要自己不斷輪詢所有fd集合,直到設(shè)備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實(shí)也需要調(diào)用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設(shè)備就緒時(shí),調(diào)用回調(diào)函數(shù),把就緒fd放入就緒鏈表中,并喚醒在epoll_wait中進(jìn)入睡眠的進(jìn)程。雖然都要睡眠和交替,但是select和poll在“醒著”的時(shí)候要遍歷整個(gè)fd集合,而epoll在“醒著”的時(shí)候只要判斷一下就緒鏈表是否為空就行了,這節(jié)省了大量的CPU時(shí)間。這就是回調(diào)機(jī)制帶來的性能提升。

  2. select,poll每次調(diào)用都要把fd集合從用戶態(tài)往內(nèi)核態(tài)拷貝一次,并且要把current往設(shè)備等待隊(duì)列中掛一次,而epoll只要一次拷貝,而且把current往等待隊(duì)列上掛也只掛一次(在epoll_wait的開始,注意這里的等待隊(duì)列并不是設(shè)備等待隊(duì)列,只是一個(gè)epoll內(nèi)部定義的等待隊(duì)列)。這也能節(jié)省不少的開銷。

三、Channel

channel是什么?NIO的channel類似于一種流,可以從channel讀取數(shù)據(jù),也可以向channel寫數(shù)據(jù),Channel在NIO中扮演著傳輸數(shù)據(jù)的角色,而接下來介紹的Buffer則扮演著存儲(chǔ)數(shù)據(jù)的角色。NIO提供了很多的channel。

  • FileChannel:從文件中讀寫數(shù)據(jù)(阻塞)
  • DatagramChannel:通過UDP讀寫網(wǎng)絡(luò)中的數(shù)據(jù)
  • SocketChannel:通過TCP讀寫網(wǎng)絡(luò)中的數(shù)據(jù)
  • ServerSocketChannel:可以監(jiān)聽新進(jìn)來的TCP連接,像Web服務(wù)器那樣。對每一個(gè)新進(jìn)來的連接都會(huì)創(chuàng)建一個(gè)SocketChannel

需要特別注意的是,除了FileChannel之外,其他的Channel都可以設(shè)置為非阻塞模式,而FileChannel無法切換為非阻塞模式。

下面的代碼展示了如何新建一個(gè)FileChannel:


RandomAccessFile rf = new RandomAccessFile(file, mode); 
FileChannel inChannel = rf.getChannel();

獲取到Channel之后,我們就可以在Channel上做IO操作了。

四、Buffer

Buffer是一個(gè)緩沖區(qū),用于存儲(chǔ)從Channel中讀取到的數(shù)據(jù),或者將buffer作為參數(shù)傳遞給Channel來將buffer中的數(shù)據(jù)寫到Channel里面去。NIO提供了很多的Buffer:

  • ByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer
  • MappedByteBuffer
  • DirectByteBuffer

為了理解Buffer的工作原理,需要熟悉它的三個(gè)屬性:

  • capacity
  • position
  • limit

capacity

作為一個(gè)內(nèi)存塊,Buffer有一個(gè)固定的大小值,也叫“capacity”.你只能往里寫capacity個(gè)byte、long,char等類型。一旦Buffer滿了,需要將其清空(通過讀數(shù)據(jù)或者清除數(shù)據(jù))才能繼續(xù)寫數(shù)據(jù)往里寫數(shù)據(jù)。

position

當(dāng)你寫數(shù)據(jù)到Buffer中時(shí),position表示當(dāng)前的位置。初始的position值為0.當(dāng)一個(gè)byte、long等數(shù)據(jù)寫到Buffer后, position會(huì)向前移動(dòng)到下一個(gè)可插入數(shù)據(jù)的Buffer單元。position最大可為capacity – 1。當(dāng)讀取數(shù)據(jù)時(shí),也是從某個(gè)特定位置讀。當(dāng)將Buffer從寫模式切換到讀模式,position會(huì)被重置為0. 當(dāng)從Buffer的position處讀取數(shù)據(jù)時(shí),position向前移動(dòng)到下一個(gè)可讀的位置。在寫模式下,Buffer的limit表示你最多能往Buffer里寫多少數(shù)據(jù)。 寫模式下,limit等于Buffer的capacity。當(dāng)切換Buffer到讀模式時(shí), limit表示你最多能讀到多少數(shù)據(jù)。因此,當(dāng)切換Buffer到讀模式時(shí),limit會(huì)被設(shè)置成寫模式下的position值。換句話說,你能讀到之前寫入的所有數(shù)據(jù)(limit被設(shè)置成已寫數(shù)據(jù)的數(shù)量,這個(gè)值在寫模式下就是position)。

使用Buffer讀寫數(shù)據(jù)一般遵循以下四個(gè)步驟:

  1. 寫入數(shù)據(jù)到Buffer
  2. 調(diào)用flip()方法
  3. 從Buffer中讀取數(shù)據(jù)
  4. 調(diào)用clear()方法或者compact()方法

當(dāng)向buffer寫入數(shù)據(jù)時(shí),buffer會(huì)記錄下寫了多少數(shù)據(jù)。一旦要讀取數(shù)據(jù),需要通過flip()方法將Buffer從寫模式切換到讀模式。在讀模式下,可以讀取之前寫入到buffer的所有數(shù)據(jù)。一旦讀完了所有的數(shù)據(jù),就需要清空緩沖區(qū),讓它可以再次被寫入。有兩種方式能清空緩沖區(qū):調(diào)用clear()或compact()方法。clear()方法會(huì)清空整個(gè)緩沖區(qū)。compact()方法只會(huì)清除已經(jīng)讀過的數(shù)據(jù)。任何未讀的數(shù)據(jù)都被移到緩沖區(qū)的起始處,新寫入的數(shù)據(jù)將放到緩沖區(qū)未讀數(shù)據(jù)的后面。以下是所有Buffer共有的方法概要,具體的Buffer提供的接口可能稍有不同,可以參考jdk文檔來查看具體的操作。這里需要特別提到一下MappedByteBuffer和DirectByteBuffer,有什么特別的嘛?前者使用了一種類似于mmap(文件映射內(nèi)存)的技術(shù),而后者申請的內(nèi)存是堆外內(nèi)存,也就是申請的內(nèi)存不是jvm管理的,這樣的好處的明顯的,前者可以將文件的部分或者全部內(nèi)容映射到內(nèi)存中,實(shí)現(xiàn)了讀寫文件就好像是讀寫內(nèi)存一樣高效, 后者實(shí)現(xiàn)了所謂的“零拷貝”。

“零拷貝”是指計(jì)算機(jī)操作的過程中,CPU不需要為數(shù)據(jù)在內(nèi)存之間的拷貝消耗資源。而它通常是指計(jì)算機(jī)在網(wǎng)絡(luò)上發(fā)送文件時(shí),不需要將文件內(nèi)容拷貝到用戶空間(User Space)而直接在內(nèi)核空間(Kernel Space)中傳輸?shù)骄W(wǎng)絡(luò)的方式。

Non-Zero Copy方式:

Zero Copy方式:

Zero Copy的模式中,避免了數(shù)據(jù)在用戶空間和內(nèi)存空間之間的拷貝,從而提高了系統(tǒng)的整體性能。Linux中的sendfile()以及Java NIO中的FileChannel.transferTo()方法都實(shí)現(xiàn)了零拷貝的功能。非直接內(nèi)存方式,數(shù)據(jù)需要在如下空間進(jìn)行復(fù)制:

JVM Heap <——> JVM用戶空間 <——> OS內(nèi)核空間 <——> 網(wǎng)卡驅(qū)動(dòng)空間;

直接內(nèi)存方式時(shí),數(shù)據(jù)需要在如下空間進(jìn)行復(fù)制:

JVM用戶空間 <——> OS內(nèi)核空間 <——> 網(wǎng)卡驅(qū)動(dòng)空間

所以當(dāng)進(jìn)行大量網(wǎng)絡(luò)通信時(shí)采用直接內(nèi)存方式,將減少一次復(fù)制,以及在Heap上對象的創(chuàng)建,將提高系統(tǒng)性能DirectByteBuffer屬于直接訪問內(nèi)存方式,其空間位于JVM用戶空間,不能由GC回收。java基于Cleaner和PhantomReference進(jìn)行存儲(chǔ)空間回收,也可以手動(dòng)調(diào)用Cleaner進(jìn)行回收。

五、Selector

Selector(選擇器)使得NIO中能夠監(jiān)聽一到多個(gè)通道,并且知道這些通道是否為讀寫做好準(zhǔn)備的組件,這樣一個(gè)線程可以通過管理多個(gè)Channel,進(jìn)而管理多個(gè)網(wǎng)絡(luò)連接。使用一個(gè)線程管理多個(gè)網(wǎng)絡(luò)連接的好處在于可以避免線程間切換的開銷。下面示范如何以一個(gè)Selector管理Channel。
首先是Selector的建立



//通過靜態(tài)的open()方法得到一個(gè)Selector

Selector selector = Selector.open();

然后是向Selector注冊一個(gè)ServerSocketChannel并監(jiān)聽連接事件:



//對于監(jiān)聽的端口打開一個(gè)ServerSocketChannel

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

//注冊到Selector的Channel必須設(shè)置為非阻塞模式,否則實(shí)現(xiàn)不了異步IO

serverSocketChannel.configureBlocking(false);

ServerSocket serverSocket = serverSocketChannel.socket();

InetSocketAddress address = new InetSocketAddress(8080);

serverSocket.bind(address);

//第二個(gè)參數(shù)是表明這個(gè)Channel感興趣的事件

serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

與Selector同時(shí)使用的Channel必須處于非阻塞模式,這意味著FileChannel不能用于Selector,因?yàn)樗荒芮袚Q到非阻塞通道;而套接字通道都是可以的。register的第二個(gè)參數(shù)表明了該Channel感興趣的事件,具體的事件分為四個(gè)類型:

1.Connect

2.Accept

3.Read

4.Write

具體來說某個(gè)channel成功連接到另一個(gè)服務(wù)器稱為“連接就緒”。一個(gè)server socket channel準(zhǔn)備好接收新進(jìn)入的連接稱為“接收就緒”。一個(gè)有數(shù)據(jù)可讀的通道可以說是“讀就緒”。等待寫數(shù)據(jù)的通道可以說是“寫就緒”。這些事件可以用SelectionKey的四個(gè)常量來表示:

  1. SelectionKey.OP_CONNECT
  2. SelectionKey.OP_ACCEPT
  3. SelectionKey.OP_READ
  4. SelectionKey.OP_WRITE

上面的Channel只是注冊了一個(gè)事件,但實(shí)際上是可以同時(shí)注冊多個(gè)事件的,比如可以像下面這樣同時(shí)注冊"接收就緒"和"讀就緒"兩個(gè)事件:


//使用"|"連接同時(shí)注冊多個(gè)事件

serverSocketChannel
     .register(selector, SelectionKey.OPACCEPT|SelectionKey.OPREAD);

SelectionKey

上面向Selector注冊Channel后返回了一個(gè)SelectionKey對象,這個(gè)對象包含了一些很有用的信息集:

  • interest集合
  • ready集合
  • Channel
  • Selector

interest集合即上面Channel注冊時(shí)添加的感興趣的事件集合,我們可以通過調(diào)用SelectionKey 的interestOps()方法得到一個(gè)int數(shù)字,然后通過“&”位操作來確定具體有哪些感興趣的集合:


int interestSet = key.interestOps();

//是否包含ACCEPT事件

boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;

//是否包含CONNECT事件

boolean isInterestedInConnect = (interestSet & SelectionKey.OP_CONNECT) == SelectionKey.OP_CONNECT;

boolean isInterestedInRead = (interestSet & SelectionKey.OP_READ) == SelectionKey.OP_READ;

boolean isInterestedInWrite = (interestSet & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE;


ready集合表明該Selector上已經(jīng)就緒的事件,可以通過key.readyOps()獲得一個(gè)數(shù)字,然后通過上面同樣的方式拿到就緒的集合;但是,也可以使用下面這些更加簡潔的方法判斷:


//四個(gè)返回boolean值的方法,可以用于判斷目前Selector上有哪些事件已經(jīng)就緒

selectionKey.isAcceptable();

selectionKey.isConnectable();

selectionKey.isReadable();

selectionKey.isWritable();


可以很簡單的拿到這個(gè)SelectinKey關(guān)聯(lián)的Selector和Channel,如下所示:


Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector();


監(jiān)聽Selector選擇通道

當(dāng)向Selector注冊了幾個(gè)Channel之后,就可以調(diào)用幾個(gè)重載的select()方法來檢測是否有通道已經(jīng)就緒了。具體的來說,Selector的select()方法有以下三種形式:


int select()

int select(long timeout)

int selectNow()

第一個(gè)方法會(huì)阻塞直到至少有一個(gè)通道就緒然后返回;第二個(gè)方法和第一個(gè)方法類似但不會(huì)一直阻塞而是至多會(huì)阻塞timeout時(shí)間;第三個(gè)方法不會(huì)阻塞,無論有無就緒的通道都會(huì)立即返回,如果沒有就緒的通道會(huì)返回0。這些方法返回的int值表明該Selector上就緒通道的數(shù)量,準(zhǔn)確的來說是自上次調(diào)用select()方法后有多少通道變成就緒狀態(tài)。如果調(diào)用select()方法,因?yàn)橛幸粋€(gè)通道變成就緒狀態(tài),返回了1,若再次調(diào)用select()方法,如果另一個(gè)通道就緒了,它會(huì)再次返回1。如果對第一個(gè)就緒的channel沒有做任何操作,現(xiàn)在就有兩個(gè)就緒的通道,但在每次select()方法調(diào)用之間,只有一個(gè)通道就緒了。如果調(diào)用select()方法表明至少有一個(gè)通道就緒了,那么就可以通過selector.selectedKeys()方法來獲得具體就緒的通道,這個(gè)方法的返回值是Set<SelectionKey>。如上面所介紹的我們可以很方便的通過SelectionKey找到就緒的事件以及對應(yīng)的Channel,下面的代碼示例了如何遍歷這個(gè)Set:


Set<SelectionKey> selectionKeySet  = selector.selectedKeys();

Iterator<SelectionKey> iterator = selectionKeySet.iterator();

while (iterator.hasNext()){

    SelectionKey selectionKey = iterator.next();

    if(selectionKey.isAcceptable()){

        // a connection was accepted by a ServerSocketChannel.

    }else if(selectionKey.isConnectable()){

        // a connection was established with a remote server.

    }else if(selectionKey.isWritable()){

        // a channel is ready for writing

    }else if(selectionKey.isReadable()){

        // a channel is ready for reading

    }

    iterator.remove();

}


注意末尾的remove()方法,當(dāng)處理完一個(gè)SelectionKey之后,必須手動(dòng)的將其從Set中移除,Selector本身不會(huì)進(jìn)行這個(gè)工作,所以需要我們手動(dòng)移除避免下一次重復(fù)處理。

ServerSocketChannel

其實(shí)從上面的代碼中我們已經(jīng)看到了,ServerSocketChannel和ServerSocket所起的作用是一致的,都是用來監(jiān)聽tcp連接的;值得注意的就是ServerSocketChannel是可以設(shè)置為非阻塞模式的,這時(shí)候它的accept()方法在沒有連接進(jìn)入的情況下總是返回null。下面的代碼示例了ServerSocketChannel的基本用法:


//ServerSocketChannel對象通過靜態(tài)方法獲取

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

//具體的端口綁定操作還是通過關(guān)聯(lián)的ServerSocket實(shí)現(xiàn)

ServerSocket ss = serverSocketChannel.socket();

InetSocketAddress address = new InetSocketAddress(8080);

ss.bind(address);

//ServerSocketChannel可以被設(shè)置成非阻塞的模式,這是和Selector配合使用的基礎(chǔ)

serverSocketChannel.configureBlocking(false);

while (true){

    //accept()方法用于監(jiān)聽進(jìn)來的連接,如果被設(shè)置為非阻塞模式,那么當(dāng)沒有連接時(shí)總是返回null

    SocketChannel socketChannel = serverSocketChannel.accept();

    if (socketChannel != null) {

        //do something with socketChannel...

    }

}

SocketChannel

Java NIO中的SocketChannel是一個(gè)連接到TCP網(wǎng)絡(luò)套接字的通道,和Socket是類似的??梢酝ㄟ^以下2種方式創(chuàng)建SocketChannel:

1、 打開一個(gè)SocketChannel并連接到互聯(lián)網(wǎng)上的某臺(tái)服務(wù)器。


    SocketChannel socketChannel = SocketChannel.open();

    socketChannel.connect(new InetSocketAddress("localhost",80));

2 、一個(gè)新連接到達(dá)ServerSocketChannel時(shí),會(huì)創(chuàng)建一個(gè)SocketChannel。如上面介紹ServerSocketChannel的代碼所示SocketChannel的數(shù)據(jù)讀寫和FileChannel沒有什么不同,都是需要借助Buffer;值得注意的是SocketChannel是可以工作在非阻塞模式下的,這時(shí)候的read()、write()方法都會(huì)直接返回,這種模式主要是為了配合Selector來實(shí)現(xiàn)異步非阻塞IO。

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

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

  • Java NIO 由以下幾個(gè)核心部分組成: Channels Buffers Selectors 雖然Java N...
    847d9ffdbd10閱讀 455評論 0 1
  • Java NIO(New IO)是從Java 1.4版本開始引入的一個(gè)新的IO API,可以替代標(biāo)準(zhǔn)的Java I...
    JackChen1024閱讀 7,928評論 1 143
  • Java NIO由下面幾個(gè)核心組件組成: Channel Buffer Selector Java NIO有更多的...
    kopshome閱讀 317評論 0 0
  • Java NIO(New IO)是從Java 1.4版本開始引入的一個(gè)新的IO API,可以替代標(biāo)準(zhǔn)的Java I...
    zhisheng_blog閱讀 1,197評論 0 7
  • 從去年初5月開始重新拾起丟失多年的bitcoin,到現(xiàn)在快一年了。雖然在2013年的時(shí)候也像現(xiàn)在很多人一樣喋喋...
    ngpisa閱讀 297評論 1 1

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