作者: 一字馬胡
轉(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操作都會涉及下面的兩個過程:
- 數(shù)據(jù)被copy到操作系統(tǒng)內(nèi)核的緩沖區(qū)中
- 數(shù)據(jù)從操作系統(tǒng)內(nèi)核緩存區(qū)copy到用戶進程空間中
而這兩個過程分別對應(yīng)著下面的兩個過程:
- 內(nèi)核等待IO數(shù)據(jù)準(zhǔn)備完成
- 進程將數(shù)據(jù)從內(nèi)核copy到自己的地址空間內(nèi)
上面四個概念的區(qū)別导盅,可以通過下面的準(zhǔn)則區(qū)分:
- 調(diào)用函數(shù)之后如果函數(shù)立即返回?zé)o論數(shù)據(jù)準(zhǔn)備完成與否白翻,則為非阻塞IO滤馍,否則為阻塞IO(重點在于調(diào)用線程是否會被阻塞)
- 在做真正的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:每次調(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:同時每次調(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: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不拷貝 |
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)機制帶來的性能提升克婶。
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ù)一般遵循以下四個步驟:
- 寫入數(shù)據(jù)到Buffer
- 調(diào)用
flip()
方法 - 從Buffer中讀取數(shù)據(jù)
- 調(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的四個常量來表示:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- 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门粪。