Java NIO概述

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

更新日志

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

一庐椒、Java OIO

Java OIO (Java Old I/O)代表著的是一種阻塞I/O约谈,所謂阻塞I/O犁钟,就是函數(shù)調(diào)用之后會一直阻塞直到函數(shù)返回正確值或者出錯或者被中斷特纤,而在函數(shù)返回之前捧存,該調(diào)用之后的代碼將不會被執(zhí)行,也就是說镰官,你必須要等到這個函數(shù)返回(無論多久)泳唠,你才能繼續(xù)做接下來的事情笨腥。有時候這樣的編程模型是必須的脖母,比如我們必須依賴從數(shù)據(jù)庫中讀取到的數(shù)據(jù)以作為依據(jù)去執(zhí)行接下來的代碼邏輯闲孤,這樣的編程模型是在一個假設(shè)下成立的讼积,這個假設(shè)就是:認(rèn)為阻塞等待函數(shù)返回是值得的勤众,后面的代碼就好像被鎖住了一樣们颜,需要獲取到一把鑰匙才能打開鎖以繼續(xù)執(zhí)行,而獲取這把鎖的唯一方法就是從阻塞中返回一種結(jié)果边锁,然后根據(jù)不同的結(jié)果來打開不同的鎖茅坛。這種I/O編程模型是簡單的贡蓖,你不需要為如何編寫代碼而緊皺眉頭斥铺,但是這種編程模型的缺陷也是很明顯的坛善,因為,很多情況下肆饶,我們并不需要等待結(jié)果立刻返回驯镊,我們更希望提前提交任務(wù)板惑,然后去做一些其他的事情偎快,然后在必須獲取結(jié)果才能繼續(xù)的時候才阻塞等待獲取滨砍,而這個時候可能函數(shù)早已返回惋戏,已經(jīng)不需要阻塞了响逢,這樣的編程模型使得我們的工作更加高效舔亭,這其實也是并發(fā)編程的模型,這樣的模型確實可以提高我們的代碼的效率订雾,但是寫代碼的難度就上升了一些洼哎,可能我們需要非常小心的安排代碼的順序噩峦,并且在必要的時候釋放一些資源等识补。但是為了提高效率解決編程的復(fù)雜性是值得的凭涂。有必要清晰一下下面的概念:

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

每一次I/O操作都會涉及下面的兩個過程:

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

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

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

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

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

根據(jù)上面的判斷準(zhǔn)則祠挫,OIO是阻塞的同步IO等舔,而NIO是非阻塞的同步IO慌植,NIO依然不是異步的蝶柿,因為真正執(zhí)行IO操作(比如read)的時候調(diào)用線程依然會被阻塞以等待結(jié)果(當(dāng)內(nèi)核數(shù)據(jù)還沒有準(zhǔn)備好的時候交汤,是不會阻塞線程的蜻展,但是當(dāng)內(nèi)核已經(jīng)準(zhǔn)備好數(shù)據(jù)之后,進程需要將數(shù)據(jù)從內(nèi)核拷貝到自己的地址空間這個步驟是阻塞的)伍茄,Netty框架則基于NIO使得IO操作變成了異步的敷矫,所以Netty是一個異步的IO框架曹仗。

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

說到IO多路復(fù)用轨蛤,馬上應(yīng)該想select祥山、poll缝呕、epoll等機制供常。多路復(fù)用技術(shù)說的是话侧,一個線程可以監(jiān)聽多個文件描述符瞻鹏,如果那個準(zhǔn)備好了就處理哪個,這和傳統(tǒng)的線程模型是有顯著的區(qū)別的薪夕。傳統(tǒng)的IO處理做法是原献,使用一個線程監(jiān)聽端口姑隅,進來一個請求讲仰,則新建一個線程處理該請求鄙陡。這樣的線程模型非常簡單趁矾,弊端也是非常明顯的毫捣,比如一個流量非常大的服務(wù)使用這樣的線程模型來承接請求培漏,那么服務(wù)的可用性是非常差的牌柄,當(dāng)然珊佣,有一個方案可能比這個好一些咒锻,那就是使用線程池惑艇,并且設(shè)置等待隊列滨巴,這樣的話恭取,線程不需要頻繁的被創(chuàng)建,當(dāng)一個請求完成處理之后攒发,線程就可以空閑出來接收新的請求惠猿,當(dāng)線程池里的線程都被占用了之后紊扬,請求會被放到等待隊列餐屎,等待線程來拉取腹缩,這樣的解決方案貌似非常先進藏鹊,確實盘寡,這樣的方案比起一開始的方案好很多竿痰,對于業(yè)務(wù)非常簡單的服務(wù)影涉,使用這樣的方案應(yīng)該可以承接不小的流量蟹倾,但是對于業(yè)務(wù)足夠復(fù)雜的場景來說鲜棠,這樣的方案依然會有風(fēng)險夏哭,因為線程池滿了之后竖配,請求會被緩存起來啊进胯,那緩存就需要空間來存放啊胁镐,那么這個隊列的大小就是有約束的啊盯漂,不可能無限大啊就缆,那如果緩存隊列被打滿了呢空郊?那么接下來的請求將會被丟棄狞甚,對于用戶而言就是哼审,我明明點擊了屏幕棺蛛,但是沒有任何動靜啊椅野?>股痢!這樣的后果就是理朋,用戶會再次點擊嗽上,再次點擊兽愤,再次點擊....這樣的后果對于服務(wù)端來說就是請求越來越多浅萧,對于用戶來說就是洼畅,“多么垃圾的app啊”酱吝。所以,這樣的方案依然得慎用土思。對于業(yè)務(wù)足夠復(fù)雜务热,流量足夠大的場景來說,選擇多路復(fù)用技術(shù)是必須的己儒。

2.1 select

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

  • 1闪湾、拷貝nfds冲甘、readfds、writefds和exceptfds到內(nèi)核(自己感興趣的描述符)
  • 2途样、遍歷[0,nfds)范圍內(nèi)的每個流江醇,調(diào)用流所對應(yīng)的設(shè)備的驅(qū)動poll函數(shù)
  • 3、檢查是否有流發(fā)生何暇,如果有發(fā)生陶夜,把流設(shè)置對應(yīng)的類別,并執(zhí)行4裆站,如果沒有流發(fā)生条辟,執(zhí)行5『昕瑁或者timeout=0羽嫡,執(zhí)行4
  • 4、select返回
  • 5肩袍、select阻塞當(dāng)前進程杭棵,等待被流對應(yīng)的設(shè)備喚醒,當(dāng)被喚醒時氛赐,執(zhí)行2魂爪。或者timeout到期鹰祸,執(zhí)行4

select的缺陷:

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

2.2 poll 和epoll

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

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

epoll的解決方案:epoll沒有這個限制,它所支持的FD上限是最大可以打開文件的數(shù)目脾拆,這個數(shù)字一般遠大于2048,舉個例子,在1GB內(nèi)存的機器上大約是10萬左右馒索,具體數(shù)目可以cat /proc/sys/fs/file-max察看,一般來說這個數(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)來進行下一步處理。這樣所帶來的缺點是:(1) 單個進程可監(jiān)視的fd數(shù)量被限制 (2) 需要維護一個用來存放大量fd的數(shù)據(jù)結(jié)構(gòu)渠驼,這樣會使得用戶空間和內(nèi)核空間在傳遞該結(jié)構(gòu)時復(fù)制開銷大 (3) 對socket進行掃描時是線性掃描
Poll poll本質(zhì)上和select沒有區(qū)別蜈块,它將用戶傳入的數(shù)組拷貝到內(nèi)核空間,然后查詢每個fd對應(yīng)的設(shè)備狀態(tài),如果設(shè)備就緒則在設(shè)備等待隊列中加入一項并繼續(xù)遍歷百揭,如果遍歷完所有fd后沒有發(fā)現(xiàn)就緒設(shè)備爽哎,則掛起當(dāng)前進程,直到設(shè)備就緒或者主動超時器一,被喚醒后它又要再次遍歷fd课锌。這個過程經(jīng)歷了多次無謂的遍歷。它沒有最大連接數(shù)的限制祈秕,原因是它是基于鏈表來存儲的渺贤,但是同樣有一個缺點:大量的fd的數(shù)組被整體復(fù)制于用戶態(tài)和內(nèi)核地址空間之間,而不管這樣的復(fù)制是不是有意義请毛。poll還有一個特點是“水平觸發(fā)”志鞍,如果報告了fd后,沒有被處理获印,那么下次poll時會再次報告該fd述雾。
Epoll epoll支持水平觸發(fā)和邊緣觸發(fā),最大的特點在于邊緣觸發(fā)兼丰,它只告訴進程哪些fd剛剛變?yōu)榫托钁B(tài)玻孟,并且只會通知一次。在前面說到的復(fù)制問題上鳍征,epoll使用mmap減少復(fù)制開銷黍翎。還有一個特點是,epoll使用“事件”的就緒通知方式艳丛,通過epoll_ctl注冊fd匣掸,一旦該fd就緒,內(nèi)核就會采用類似callback的回調(diào)機制來激活該fd氮双,epoll_wait便可以收到通知

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

區(qū)別:

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

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

三睁宰、Channel

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

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

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

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


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

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

四拣技、Buffer

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

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

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

  • capacity
  • position
  • limit

capacity

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

position

當(dāng)你寫數(shù)據(jù)到Buffer中時蟆融,position表示當(dāng)前的位置。初始的position值為0.當(dāng)一個byte守呜、long等數(shù)據(jù)寫到Buffer后型酥, position會向前移動到下一個可插入數(shù)據(jù)的Buffer單元。position最大可為capacity – 1查乒。當(dāng)讀取數(shù)據(jù)時弥喉,也是從某個特定位置讀。當(dāng)將Buffer從寫模式切換到讀模式侣颂,position會被重置為0. 當(dāng)從Buffer的position處讀取數(shù)據(jù)時档桃,position向前移動到下一個可讀的位置。在寫模式下憔晒,Buffer的limit表示你最多能往Buffer里寫多少數(shù)據(jù)藻肄。 寫模式下,limit等于Buffer的capacity拒担。當(dāng)切換Buffer到讀模式時嘹屯, limit表示你最多能讀到多少數(shù)據(jù)。因此从撼,當(dāng)切換Buffer到讀模式時州弟,limit會被設(shè)置成寫模式下的position值。換句話說低零,你能讀到之前寫入的所有數(shù)據(jù)(limit被設(shè)置成已寫數(shù)據(jù)的數(shù)量婆翔,這個值在寫模式下就是position)。

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

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

當(dāng)向buffer寫入數(shù)據(jù)時掏婶,buffer會記錄下寫了多少數(shù)據(jù)啃奴。一旦要讀取數(shù)據(jù),需要通過flip()方法將Buffer從寫模式切換到讀模式雄妥。在讀模式下最蕾,可以讀取之前寫入到buffer的所有數(shù)據(jù)依溯。一旦讀完了所有的數(shù)據(jù),就需要清空緩沖區(qū)瘟则,讓它可以再次被寫入黎炉。有兩種方式能清空緩沖區(qū):調(diào)用clear()或compact()方法。clear()方法會清空整個緩沖區(qū)醋拧。compact()方法只會清除已經(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)存中逛尚,實現(xiàn)了讀寫文件就好像是讀寫內(nèi)存一樣高效, 后者實現(xiàn)了所謂的“零拷貝”刁愿。

“零拷貝”是指計算機操作的過程中绰寞,CPU不需要為數(shù)據(jù)在內(nèi)存之間的拷貝消耗資源。而它通常是指計算機在網(wǎng)絡(luò)上發(fā)送文件時铣口,不需要將文件內(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()方法都實現(xiàn)了零拷貝的功能。非直接內(nèi)存方式叔遂,數(shù)據(jù)需要在如下空間進行復(fù)制:

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

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

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

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

五、Selector

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



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

Selector selector = Selector.open();

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



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

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

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

serverSocketChannel.configureBlocking(false);

ServerSocket serverSocket = serverSocketChannel.socket();

InetSocketAddress address = new InetSocketAddress(8080);

serverSocket.bind(address);

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

serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

與Selector同時使用的Channel必須處于非阻塞模式片吊,這意味著FileChannel不能用于Selector绽昏,因為它不能切換到非阻塞通道;而套接字通道都是可以的俏脊。register的第二個參數(shù)表明了該Channel感興趣的事件全谤,具體的事件分為四個類型:

1.Connect

2.Accept

3.Read

4.Write

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

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

上面的Channel只是注冊了一個事件漫萄,但實際上是可以同時注冊多個事件的卷员,比如可以像下面這樣同時注冊"接收就緒"和"讀就緒"兩個事件:


//使用"|"連接同時注冊多個事件

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

SelectionKey

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

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

interest集合即上面Channel注冊時添加的感興趣的事件集合腾务,我們可以通過調(diào)用SelectionKey 的interestOps()方法得到一個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()獲得一個數(shù)字岩瘦,然后通過上面同樣的方式拿到就緒的集合未巫;但是,也可以使用下面這些更加簡潔的方法判斷:


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

selectionKey.isAcceptable();

selectionKey.isConnectable();

selectionKey.isReadable();

selectionKey.isWritable();


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


Channel  channel  = selectionKey.channel();

Selector selector = selectionKey.selector();


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

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


int select()

int select(long timeout)

int selectNow()

第一個方法會阻塞直到至少有一個通道就緒然后返回;第二個方法和第一個方法類似但不會一直阻塞而是至多會阻塞timeout時間苏遥;第三個方法不會阻塞饼拍,無論有無就緒的通道都會立即返回,如果沒有就緒的通道會返回0田炭。這些方法返回的int值表明該Selector上就緒通道的數(shù)量师抄,準(zhǔn)確的來說是自上次調(diào)用select()方法后有多少通道變成就緒狀態(tài)。如果調(diào)用select()方法教硫,因為有一個通道變成就緒狀態(tài)叨吮,返回了1辆布,若再次調(diào)用select()方法,如果另一個通道就緒了茶鉴,它會再次返回1锋玲。如果對第一個就緒的channel沒有做任何操作,現(xiàn)在就有兩個就緒的通道涵叮,但在每次select()方法調(diào)用之間惭蹂,只有一個通道就緒了。如果調(diào)用select()方法表明至少有一個通道就緒了割粮,那么就可以通過selector.selectedKeys()方法來獲得具體就緒的通道盾碗,這個方法的返回值是Set<SelectionKey>。如上面所介紹的我們可以很方便的通過SelectionKey找到就緒的事件以及對應(yīng)的Channel舀瓢,下面的代碼示例了如何遍歷這個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)處理完一個SelectionKey之后,必須手動的將其從Set中移除氢伟,Selector本身不會進行這個工作榜轿,所以需要我們手動移除避免下一次重復(fù)處理。

ServerSocketChannel

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


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

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

//具體的端口綁定操作還是通過關(guān)聯(lián)的ServerSocket實現(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)聽進來的連接,如果被設(shè)置為非阻塞模式,那么當(dāng)沒有連接時總是返回null

    SocketChannel socketChannel = serverSocketChannel.accept();

    if (socketChannel != null) {

        //do something with socketChannel...

    }

}

SocketChannel

Java NIO中的SocketChannel是一個連接到TCP網(wǎng)絡(luò)套接字的通道,和Socket是類似的∥芘耄可以通過以下2種方式創(chuàng)建SocketChannel:

1驹吮、 打開一個SocketChannel并連接到互聯(lián)網(wǎng)上的某臺服務(wù)器慧邮。


    SocketChannel socketChannel = SocketChannel.open();

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市烹困,隨后出現(xiàn)的幾起案子玄妈,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件拟蜻,死亡現(xiàn)場離奇詭異绎签,居然都是意外死亡,警方通過查閱死者的電腦和手機瞭郑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門辜御,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人屈张,你說我怎么就攤上這事「ぞ蓿” “怎么了阁谆?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長愉老。 經(jīng)常有香客問我场绿,道長,這世上最難降的妖魔是什么嫉入? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任焰盗,我火速辦了婚禮,結(jié)果婚禮上咒林,老公的妹妹穿的比我還像新娘熬拒。我一直安慰自己,他們只是感情好垫竞,可當(dāng)我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布澎粟。 她就那樣靜靜地躺著,像睡著了一般欢瞪。 火紅的嫁衣襯著肌膚如雪活烙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天遣鼓,我揣著相機與錄音啸盏,去河邊找鬼。 笑死骑祟,一個胖子當(dāng)著我的面吹牛回懦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播曾我,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼粉怕,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了抒巢?” 一聲冷哼從身側(cè)響起贫贝,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后稚晚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體崇堵,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年客燕,在試婚紗的時候發(fā)現(xiàn)自己被綠了鸳劳。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡也搓,死狀恐怖赏廓,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情傍妒,我是刑警寧澤幔摸,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站颤练,受9級特大地震影響既忆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜嗦玖,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一患雇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧宇挫,春花似錦苛吱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至娱局,卻和暖如春彰亥,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背衰齐。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工任斋, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人耻涛。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓废酷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親抹缕。 傳聞我的和親對象是個殘疾皇子澈蟆,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容

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