NIO教程 ——檢視閱讀
簡(jiǎn)介
NIO中的N可以理解為Non-blocking 债朵,不單純是New 纵势。
不同點(diǎn):
- 標(biāo)準(zhǔn)的IO編程接口是面向字節(jié)流和字符流的绒净。而NIO是面向通道和緩沖區(qū)的贡耽,數(shù)據(jù)總是從通道中讀到buffer緩沖區(qū)內(nèi)枫绅,或者從buffer寫(xiě)入到通道中鲁捏。
- Java NIO使我們可以進(jìn)行非阻塞IO操作芯砸。比如說(shuō),單線程中從通道讀取數(shù)據(jù)到buffer碴萧,同時(shí)可以繼續(xù)做別的事情乙嘀,當(dāng)數(shù)據(jù)讀取到buffer中后,線程再繼續(xù)處理數(shù)據(jù)破喻。寫(xiě)數(shù)據(jù)也是一樣的虎谢。
- NIO中有一個(gè)“slectors”的概念。selector可以檢測(cè)多個(gè)通道的事件狀態(tài)(例如:鏈接打開(kāi)曹质,數(shù)據(jù)到達(dá))這樣單線程就可以操作多個(gè)通道的數(shù)據(jù)婴噩。
概覽
NIO包含下面3個(gè)核心的組件,Channel,Buffer和Selector組成了這個(gè)核心的API:
- Channels ——通道
- Buffers ——緩沖區(qū)
- Selectors ——選擇器
通常來(lái)說(shuō)NIO中的所有IO都是從Channel開(kāi)始的羽德。Channel和流有點(diǎn)類似几莽。通過(guò)Channel,我們即可以從Channel把數(shù)據(jù)寫(xiě)到Buffer中宅静,也可以把數(shù)據(jù)沖Buffer寫(xiě)入到Channel 章蚣。
有很多的Channel,Buffer類型姨夹。下面列舉了主要的幾種:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
正如你看到的纤垂,這些channel基于于UDP和TCP的網(wǎng)絡(luò)IO矾策,以及文件IO。 和這些類一起的還有其他一些比較有趣的接口峭沦,在本節(jié)中暫時(shí)不多介紹贾虽。為了簡(jiǎn)潔起見(jiàn),我們會(huì)在必要的時(shí)候引入這些概念吼鱼。 下面是核心的Buffer實(shí)現(xiàn)類的列表:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
這些Buffer涵蓋了可以通過(guò)IO操作的基礎(chǔ)類型:byte,short,int,long,float,double以及characters. NIO實(shí)際上還包含一種MappedBytesBuffer,一般用于和內(nèi)存映射的文件蓬豁。
選擇器允許單線程操作多個(gè)通道。如果你的程序中有大量的鏈接菇肃,同時(shí)每個(gè)鏈接的IO帶寬不高的話地粪,這個(gè)特性將會(huì)非常有幫助。比如聊天服務(wù)器巷送。 下面是一個(gè)單線程中Slector維護(hù)3個(gè)Channel的示意圖:
要使用Selector的話驶忌,我們必須把Channel注冊(cè)到Selector上矛辕,然后就可以調(diào)用Selector的select()方法笑跛。這個(gè)方法會(huì)進(jìn)入阻塞,直到有一個(gè)channel的狀態(tài)符合條件聊品。當(dāng)方法返回后飞蹂,線程可以處理這些事件。
Java NIO Channel通道
Java NIO Channel通道和流非常相似翻屈,主要有以下3點(diǎn)區(qū)別:
- 通道可以讀也可以寫(xiě)陈哑,流一般來(lái)說(shuō)是單向的(只能讀或者寫(xiě))。
- 通道可以異步讀寫(xiě)伸眶。
- 通道總是基于緩沖區(qū)Buffer來(lái)讀寫(xiě)惊窖。
Channel的實(shí)現(xiàn)
下面列出Java NIO中最重要的集中Channel的實(shí)現(xiàn):
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
FileChannel用于文件的數(shù)據(jù)讀寫(xiě)。 DatagramChannel用于UDP的數(shù)據(jù)讀寫(xiě)厘贼。 SocketChannel用于TCP的數(shù)據(jù)讀寫(xiě)界酒。 ServerSocketChannel允許我們監(jiān)聽(tīng)TCP鏈接請(qǐng)求,每個(gè)請(qǐng)求會(huì)創(chuàng)建會(huì)一個(gè)SocketChannel.
RandomAccessFile擴(kuò)展:
RandomAccessFile(隨機(jī)訪問(wèn)文件)類嘴秸。該類是Java語(yǔ)言中功能最為豐富的文件訪問(wèn)類 毁欣。RandomAccessFile類支持“隨機(jī)訪問(wèn)”方式,這里“隨機(jī)”是指可以跳轉(zhuǎn)到文件的任意位置處讀寫(xiě)數(shù)據(jù)岳掐。在訪問(wèn)一個(gè)文件的時(shí)候凭疮,不必把文件從頭讀到尾,而是希望像訪問(wèn)一個(gè)數(shù)據(jù)庫(kù)一樣“隨心所欲”地訪問(wèn)一個(gè)文件的某個(gè)部分串述,這時(shí)使用RandomAccessFile類就是最佳選擇执解。
四種模式:R RW RWD RWS
r | 以只讀的方式打開(kāi)文本,也就意味著不能用write來(lái)操作文件 |
---|---|
rw | 讀操作和寫(xiě)操作都是允許的 |
rws | 每當(dāng)進(jìn)行寫(xiě)操作纲酗,同步的刷新到磁盤衰腌,刷新內(nèi)容和元數(shù)據(jù) |
rwd | 每當(dāng)進(jìn)行寫(xiě)操作逝淹,同步的刷新到磁盤,刷新內(nèi)容 |
RandomAccessFile的用處:
1桶唐、大型文本日志類文件的快速定位獲取數(shù)據(jù):
得益于seek的巧妙設(shè)計(jì)栅葡,我認(rèn)為我們可以從超大的文本中快速定位我們的游標(biāo),例如每次存日志的時(shí)候尤泽,我們可以建立一個(gè)索引緩存欣簇,索引是日志的起始日期,value是文本的poiniter 也就是光標(biāo)坯约,這樣我們可以快速定位某一個(gè)時(shí)間段的文本內(nèi)容
2熊咽、并發(fā)讀寫(xiě)
也是得益于seek的設(shè)計(jì),我認(rèn)為多線程可以輪流操作seek控制光標(biāo)的位置闹丐,從未達(dá)到不同線程的并發(fā)寫(xiě)操作横殴。
3、更方便的獲取二進(jìn)制文件
通過(guò)自帶的讀寫(xiě)轉(zhuǎn)碼(readDouble卿拴、writeLong等)衫仑,我認(rèn)為可以快速的完成字節(jié)碼到字符的轉(zhuǎn)換功能,對(duì)使用者來(lái)說(shuō)比較友好堕花。
RandomAccessFile參考
實(shí)例:
public class FileChannelTest {
public static void main(String[] args) throws IOException {
RandomAccessFile file = new RandomAccessFile("D:\\text\\1_loan.sql", "r");
//mode只有4中文狱,如果不是讀寫(xiě)的mode或者給的不是4種中的,就會(huì)報(bào)錯(cuò)缘挽。
RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "r");
try {
FileChannel fileChannel = file.getChannel();
FileChannel copyFileChannel = copyFile.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int read = fileChannel.read(byteBuffer);
while (read != -1) {
System.out.println("read:" + read);
//byteBuffer緩沖區(qū)切換為讀模式
byteBuffer.flip();
copyFileChannel.write(byteBuffer);
//“清空”byteBuffer緩沖區(qū)瞄崇,以滿足后續(xù)寫(xiě)入操作
byteBuffer.clear();
//注意,每次讀時(shí)都要返回讀后的狀態(tài)read值賦值給循環(huán)判斷體read壕曼,否則會(huì)陷入死循環(huán)true
read = fileChannel.read(byteBuffer);
}
} finally {
file.close();
copyFile.close();
}
}
}
報(bào)錯(cuò):
RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "w");
//因?yàn)闆](méi)有"w"的mode
Exception in thread "main" java.lang.IllegalArgumentException: Illegal mode "w" must be one of "r", "rw", "rws", or "rwd"
at java.io.RandomAccessFile.<init>(RandomAccessFile.java:221)
RandomAccessFile copyFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "r");
//因?yàn)闆](méi)有"w"的權(quán)限
Exception in thread "main" java.nio.channels.NonWritableChannelException
at sun.nio.ch.FileChannelImpl.write(FileChannelImpl.java:194)
at com.niotest.FileChannelTest.main(FileChannelTest.java:33)
NIO Buffer緩沖區(qū)
Java NIO Buffers用于和NIO Channel交互苏研。正如你已經(jīng)知道的,我們從channel中讀取數(shù)據(jù)到buffers里腮郊,從buffer把數(shù)據(jù)寫(xiě)入到channels.
buffer本質(zhì)上就是一塊內(nèi)存區(qū)摹蘑,可以用來(lái)寫(xiě)入數(shù)據(jù),并在稍后讀取出來(lái)伴榔。這塊內(nèi)存被NIO Buffer包裹起來(lái)纹蝴,對(duì)外提供一系列的讀寫(xiě)方便開(kāi)發(fā)的接口。
Buffer基本用法
利用Buffer讀寫(xiě)數(shù)據(jù)踪少,通常遵循四個(gè)步驟:
- 把數(shù)據(jù)寫(xiě)入buffer塘安;
- 調(diào)用flip;
- 從Buffer中讀取數(shù)據(jù)援奢;
- 調(diào)用buffer.clear()或者buffer.compact()
當(dāng)寫(xiě)入數(shù)據(jù)到buffer中時(shí)兼犯,buffer會(huì)記錄已經(jīng)寫(xiě)入的數(shù)據(jù)大小。當(dāng)需要讀數(shù)據(jù)時(shí),通過(guò)flip()方法把buffer從寫(xiě)模式調(diào)整為讀模式切黔;在讀模式下砸脊,可以讀取所有已經(jīng)寫(xiě)入的數(shù)據(jù)。
當(dāng)讀取完數(shù)據(jù)后纬霞,需要清空buffer凌埂,以滿足后續(xù)寫(xiě)入操作。清空buffer有兩種方式:調(diào)用clear()或compact()方法诗芜。clear會(huì)清空整個(gè)buffer瞳抓,compact則只清空已讀取的數(shù)據(jù),未被讀取的數(shù)據(jù)會(huì)被移動(dòng)到buffer的開(kāi)始位置伏恐,寫(xiě)入位置則緊跟著未讀數(shù)據(jù)之后孩哑。
Buffer的容量,位置翠桦,上限(Buffer Capacity, Position and Limit)
buffer緩沖區(qū)實(shí)質(zhì)上就是一塊內(nèi)存横蜒,用于寫(xiě)入數(shù)據(jù),也供后續(xù)再次讀取數(shù)據(jù)销凑。這塊內(nèi)存被NIO Buffer管理丛晌,并提供一系列的方法用于更簡(jiǎn)單的操作這塊內(nèi)存。
一個(gè)Buffer有三個(gè)屬性是必須掌握的闻鉴,分別是:
- capacity容量
- position位置
- limit限制
position和limit的具體含義取決于當(dāng)前buffer的模式茵乱。capacity在兩種模式下都表示容量。
下面有張示例圖孟岛,描訴了不同模式下position和limit的含義:
容量(Capacity)
作為一塊內(nèi)存,buffer有一個(gè)固定的大小督勺,叫做capacity容量渠羞。也就是最多只能寫(xiě)入容量值的字節(jié),整形等數(shù)據(jù)智哀。一旦buffer寫(xiě)滿了就需要清空已讀數(shù)據(jù)以便下次繼續(xù)寫(xiě)入新的數(shù)據(jù)次询。
位置(Position)
當(dāng)寫(xiě)入數(shù)據(jù)到Buffer的時(shí)候需要中一個(gè)確定的位置開(kāi)始,默認(rèn)初始化時(shí)這個(gè)位置position為0瓷叫,一旦寫(xiě)入了數(shù)據(jù)比如一個(gè)字節(jié)屯吊,整形數(shù)據(jù),那么position的值就會(huì)指向數(shù)據(jù)之后的一個(gè)單元摹菠,position最大可以到capacity-1.
當(dāng)從Buffer讀取數(shù)據(jù)時(shí)盒卸,也需要從一個(gè)確定的位置開(kāi)始。buffer從寫(xiě)入模式變?yōu)樽x取模式時(shí)次氨,position會(huì)歸零太雨,每次讀取后罐氨,position向后移動(dòng)乱凿。
上限(Limit)
在寫(xiě)模式芬骄,limit的含義是我們所能寫(xiě)入的最大數(shù)據(jù)量。它等同于buffer的容量抡驼。
一旦切換到讀模式,limit則代表我們所能讀取的最大數(shù)據(jù)量,他的值等同于寫(xiě)模式下position的位置外臂。
數(shù)據(jù)讀取的上限時(shí)buffer中已有的數(shù)據(jù),也就是limit的位置(原寫(xiě)模式下position所指的位置)律胀。
Buffer Types
Java NIO有如下具體的Buffer類型:
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
正如你看到的专钉,Buffer的類型代表了不同數(shù)據(jù)類型,換句話說(shuō)累铅,Buffer中的數(shù)據(jù)可以是上述的基本類型跃须;
分配一個(gè)Buffer(Allocating a Buffer)
為了獲取一個(gè)Buffer對(duì)象,你必須先分配娃兽。每個(gè)Buffer實(shí)現(xiàn)類都有一個(gè)allocate()方法用于分配內(nèi)存菇民。
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
CharBuffer charBuffer = CharBuffer.allocate(48);
寫(xiě)入數(shù)據(jù)到Buffer(Writing Data to a Buffer)
寫(xiě)數(shù)據(jù)到Buffer有兩種方法:
- 從Channel中寫(xiě)數(shù)據(jù)到Buffer
- 手動(dòng)寫(xiě)數(shù)據(jù)到Buffer,調(diào)用put方法
//從Channel中寫(xiě)數(shù)據(jù)到Buffer
int read = fileChannel.read(byteBuffer);
//調(diào)用put方法寫(xiě)
buf.put(3);
//把數(shù)據(jù)寫(xiě)到特定的位置
public ByteBuffer put(int i, byte x);
//把一個(gè)具體類型數(shù)據(jù)寫(xiě)入buffer
public ByteBuffer putInt(int x)投储;
flip()——翻轉(zhuǎn)
flip()方法可以把Buffer從寫(xiě)模式切換到讀模式第练。調(diào)用flip方法會(huì)把position歸零,并設(shè)置limit為之前的position的值玛荞。 也就是說(shuō)娇掏,現(xiàn)在position代表的是讀取位置,limit標(biāo)示的是已寫(xiě)入的數(shù)據(jù)位置勋眯。
從Buffer讀取數(shù)據(jù)(Reading Data from a Buffer)
從Buffer讀數(shù)據(jù)也有兩種方式婴梧。
- 從buffer讀數(shù)據(jù)到channel。
- 從buffer直接讀取數(shù)據(jù)客蹋,調(diào)用get方法塞蹭。
//讀取數(shù)據(jù)到channel的例子:
int bytesWritten = inChannel.write(buf);
//調(diào)用get讀取數(shù)據(jù)的例子:
byte aByte = buf.get();
rewind()——倒帶
Buffer.rewind()方法將position置為0,這樣我們可以重復(fù)讀取buffer中的數(shù)據(jù)讶坯。limit保持不變番电。
clear() and compact()
一旦我們從buffer中讀取完數(shù)據(jù),需要復(fù)用buffer為下次寫(xiě)數(shù)據(jù)做準(zhǔn)備辆琅。只需要調(diào)用clear或compact方法漱办。
clear方法會(huì)重置position為0,limit為capacity婉烟,也就是整個(gè)Buffer清空娩井。實(shí)際上Buffer中數(shù)據(jù)并沒(méi)有清空,我們只是把標(biāo)記為修改了隅很。(重新寫(xiě)入的時(shí)候這些存在的數(shù)據(jù)就會(huì)被新的數(shù)據(jù)覆蓋)
如果Buffer還有一些數(shù)據(jù)沒(méi)有讀取完撞牢,調(diào)用clear就會(huì)導(dǎo)致這部分?jǐn)?shù)據(jù)被“遺忘”率碾,因?yàn)槲覀儧](méi)有標(biāo)記這部分?jǐn)?shù)據(jù)未讀。
針對(duì)這種情況屋彪,如果需要保留未讀數(shù)據(jù)所宰,那么可以使用compact()。 因此compact()和clear()的區(qū)別就在于對(duì)未讀數(shù)據(jù)的處理畜挥,是保留這部分?jǐn)?shù)據(jù)還是一起清空仔粥。
mark() and reset()
通過(guò)mark方法可以標(biāo)記當(dāng)前的position,通過(guò)reset來(lái)恢復(fù)mark的位置蟹但,這個(gè)非常像canva的save和restore:
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
equals() and compareTo()
可以用eqauls和compareTo比較兩個(gè)buffer
equals()
判斷兩個(gè)buffer相對(duì)躯泰,需滿足:
- 類型相同
- buffer中剩余字節(jié)數(shù)相同
- 所有剩余字節(jié)相等
從上面的三個(gè)條件可以看出,equals只比較buffer中的部分內(nèi)容华糖,并不會(huì)去比較每一個(gè)元素麦向。
compareTo()
compareTo也是比較buffer中的剩余元素,只不過(guò)這個(gè)方法適用于比較排序的:
NIO Scatter (分散)/ Gather(聚集)
——分散讀和聚集寫(xiě)的場(chǎng)景客叉。
Java NIO發(fā)布時(shí)內(nèi)置了對(duì)scatter / gather的支持诵竭。scatter / gather是通過(guò)通道讀寫(xiě)數(shù)據(jù)的兩個(gè)概念。
Scattering read指的是從通道讀取的操作能把數(shù)據(jù)寫(xiě)入多個(gè)buffer兼搏,也就是scatters代表了數(shù)據(jù)從一個(gè)channel到多個(gè)buffer的過(guò)程卵慰。
gathering write則正好相反,表示的是從多個(gè)buffer把數(shù)據(jù)寫(xiě)入到一個(gè)channel中佛呻。
Scatter/gather在有些場(chǎng)景下會(huì)非常有用裳朋,比如需要處理多份分開(kāi)傳輸?shù)臄?shù)據(jù)。舉例來(lái)說(shuō)吓著,假設(shè)一個(gè)消息包含了header和body鲤嫡,我們可能會(huì)把header和body保存在不同獨(dú)立buffer中,這種分開(kāi)處理header與body的做法會(huì)使開(kāi)發(fā)更簡(jiǎn)明夜矗。
Scattering Reads
"scattering read"是把數(shù)據(jù)從單個(gè)Channel寫(xiě)入到多個(gè)buffer泛范,下面是示意圖:
觀察代碼可以發(fā)現(xiàn),我們把多個(gè)buffer寫(xiě)在了一個(gè)數(shù)組中紊撕,然后把數(shù)組傳遞給channel.read()方法。read()方法內(nèi)部會(huì)負(fù)責(zé)把數(shù)據(jù)按順序?qū)戇M(jìn)傳入的buffer數(shù)組內(nèi)赡突。一個(gè)buffer寫(xiě)滿后对扶,接著寫(xiě)到下一個(gè)buffer中。
實(shí)際上惭缰,scattering read內(nèi)部必須寫(xiě)滿一個(gè)buffer后才會(huì)向后移動(dòng)到下一個(gè)buffer浪南,因此這并不適合消息大小會(huì)動(dòng)態(tài)改變的部分,也就是說(shuō)漱受,如果你有一個(gè)header和body络凿,并且header有一個(gè)固定的大小(比如128字節(jié)),這種情形下可以正常工作。
gathering Writes
"gathering write"把多個(gè)buffer的數(shù)據(jù)寫(xiě)入到同一個(gè)channel中.
傳入一個(gè)buffer數(shù)組給write,內(nèi)部會(huì)按順序?qū)?shù)組內(nèi)的內(nèi)容寫(xiě)進(jìn)channel絮记,這里需要注意摔踱,寫(xiě)入的時(shí)候針對(duì)的是buffer中position到limit之間的數(shù)據(jù)。也就是如果buffer的容量是128字節(jié)怨愤,但它只包含了58字節(jié)數(shù)據(jù)派敷,那么寫(xiě)入的時(shí)候只有58字節(jié)會(huì)真正寫(xiě)入。因此gathering write是可以適用于可變大小的message的撰洗,這和scattering reads不同篮愉。
NIO Channel to Channel Transfers通道傳輸接口
在Java NIO中如果一個(gè)channel是FileChannel類型的,那么他可以直接把數(shù)據(jù)傳輸?shù)搅硪粋€(gè)channel差导。這個(gè)特性得益于FileChannel包含的transferTo和transferFrom兩個(gè)方法试躏。
transferFrom()——目標(biāo)channel用,參數(shù)為源數(shù)據(jù)channel设褐。
transferFrom的參數(shù)position和count表示目標(biāo)文件的寫(xiě)入位置和最多寫(xiě)入的數(shù)據(jù)量颠蕴。如果通道源的數(shù)據(jù)小于count那么就傳實(shí)際有的數(shù)據(jù)量。 另外络断,有些SocketChannel的實(shí)現(xiàn)在傳輸時(shí)只會(huì)傳輸哪些處于就緒狀態(tài)的數(shù)據(jù)裁替,即使SocketChannel后續(xù)會(huì)有更多可用數(shù)據(jù)。因此貌笨,這個(gè)傳輸過(guò)程可能不會(huì)傳輸整個(gè)的數(shù)據(jù)弱判。
transferTo()——源數(shù)據(jù)用,參數(shù)為目標(biāo)channel
SocketChannel的問(wèn)題也存在于transferTo.SocketChannel的實(shí)現(xiàn)可能只在發(fā)送的buffer填充滿后才發(fā)送锥惋,并結(jié)束昌腰。
實(shí)例:
public class ChannelTransferTest {
public static void main(String[] args) throws IOException {
RandomAccessFile fromfile = new RandomAccessFile("D:\\text\\1_loan.sql", "rw");
//mode只有4中,如果不是讀寫(xiě)的mode或者給的不是4種中的膀跌,就會(huì)報(bào)錯(cuò)遭商。
RandomAccessFile toFile = new RandomAccessFile("D:\\text\\1_loan_copy.sql", "rw");
FileChannel fromfileChannel = fromfile.getChannel();
FileChannel toFileChannel = toFile.getChannel();
//==========================transferTo=================================
//transferTo方法把fromfileChannel數(shù)據(jù)傳輸?shù)搅硪粋€(gè)toFileChannel
//long transferSize = fromfileChannel.transferTo(0, fromfileChannel.size(), toFileChannel);
//System.out.println(transferSize);
//=============================transferFrom==============================
//把數(shù)據(jù)從通道源傳輸?shù)絫oFileChannel,相比通過(guò)buffer讀寫(xiě)更加的便捷
long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size());
//參數(shù)position和count表示目標(biāo)文件的寫(xiě)入位置和最多寫(xiě)入的數(shù)據(jù)量
//long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size()-1000);
//如果通道源的數(shù)據(jù)小于count那么就傳實(shí)際有的數(shù)據(jù)量。
//long transferSize1 = toFileChannel.transferFrom(fromfileChannel, 0, fromfileChannel.size()+1000);
System.out.println(transferSize1);
}
}
NIO Selector選擇器
Selector是Java NIO中的一個(gè)組件捅伤,用于檢查一個(gè)或多個(gè)NIO Channel的狀態(tài)是否處于可讀劫流、可寫(xiě)。如此可以實(shí)現(xiàn)單線程管理多個(gè)channels,也就是可以管理多個(gè)網(wǎng)絡(luò)鏈接丛忆。
為什么使用Selector
用單線程處理多個(gè)channels的好處是我需要更少的線程來(lái)處理channel祠汇。實(shí)際上,你甚至可以用一個(gè)線程來(lái)處理所有的channels熄诡。從操作系統(tǒng)的角度來(lái)看可很,切換線程開(kāi)銷是比較昂貴的,并且每個(gè)線程都需要占用系統(tǒng)資源凰浮,因此暫用線程越少越好我抠。
需要留意的是苇本,現(xiàn)代操作系統(tǒng)和CPU在多任務(wù)處理上已經(jīng)變得越來(lái)越好,所以多線程帶來(lái)的影響也越來(lái)越小菜拓。如果一個(gè)CPU是多核的瓣窄,如果不執(zhí)行多任務(wù)反而是浪費(fèi)了機(jī)器的性能。不過(guò)這些設(shè)計(jì)討論是另外的話題了尘惧。簡(jiǎn)而言之康栈,通過(guò)Selector我們可以實(shí)現(xiàn)單線程操作多個(gè)channel。
創(chuàng)建Selector
創(chuàng)建一個(gè)Selector可以通過(guò)Selector.open()方法:
Selector selector = Selector.open();
注冊(cè)Channel到Selector上
先把Channel注冊(cè)到Selector上喷橙,這個(gè)操作使用SelectableChannel的register()啥么。SocketChannel等都有繼承此抽象類。
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
Channel必須是非阻塞的贰逾。所以FileChannel不適用Selector悬荣,因?yàn)?strong>FileChannel不能切換為非阻塞模式。Socket channel可以正常使用疙剑。
注意register的第二個(gè)參數(shù)氯迂,這個(gè)參數(shù)是一個(gè)“關(guān)注集合”,代表我們關(guān)注的channel狀態(tài)言缤,有四種基礎(chǔ)類型可供監(jiān)聽(tīng):
- Connect——連接就緒(連接成功后)
- Accept——可連接就緒(接受請(qǐng)求連接時(shí))
- Read——讀就緒
- Write——寫(xiě)就緒
一個(gè)channel觸發(fā)了一個(gè)事件也可視作該事件處于就緒狀態(tài)嚼蚀。因此當(dāng)channel與server連接成功后,那么就是“連接就緒”狀態(tài)管挟。server channel接收請(qǐng)求連接時(shí)處于“可連接就緒”狀態(tài)轿曙。channel有數(shù)據(jù)可讀時(shí)處于“讀就緒”狀態(tài)。channel可以進(jìn)行數(shù)據(jù)寫(xiě)入時(shí)處于“寫(xiě)就緒”狀態(tài)僻孝。
上述的四種就緒狀態(tài)用SelectionKey中的常量表示如下:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
如果對(duì)多個(gè)事件感興趣可利用位的或運(yùn)算結(jié)合多個(gè)常量导帝,比如:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
SelectionKey's
在上一小節(jié)中,我們利用register方法把Channel注冊(cè)到了Selectors上穿铆,這個(gè)方法的返回值是SelectionKeys您单,這個(gè)返回的對(duì)象包含了一些比較有價(jià)值的屬性:
- The interest set
- The ready set
- The Channel
- The Selector
- An attached object (optional)
Interest Set
這個(gè)“關(guān)注集合”實(shí)際上就是我們希望處理的事件的集合,它的值就是注冊(cè)時(shí)傳入的參數(shù)荞雏,我們可以用按為與運(yùn)算把每個(gè)事件取出來(lái):
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
Ready Set
"就緒集合"中的值是當(dāng)前channel處于就緒的值虐秦,一般來(lái)說(shuō)在調(diào)用了select方法后都會(huì)需要用到就緒狀態(tài)
int readySet = selectionKey.readyOps();
從“就緒集合”中取值的操作類似于“關(guān)注集合”的操作,當(dāng)然還有更簡(jiǎn)單的方法凤优,SelectionKey提供了一系列返回值為boolean的的方法:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
Channel + Selector
從SelectionKey操作Channel和Selector非常簡(jiǎn)單:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
Attaching Objects
我們可以給一個(gè)SelectionKey附加一個(gè)Object羡疗,這樣做一方面可以方便我們識(shí)別某個(gè)特定的channel,同時(shí)也增加了channel相關(guān)的附加信息别洪。例如,可以把用于channel的buffer附加到SelectionKey上:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
附加對(duì)象的操作也可以在register的時(shí)候就執(zhí)行:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
從Selector中選擇channel
一旦我們向Selector注冊(cè)了一個(gè)或多個(gè)channel后柳刮,就可以調(diào)用select來(lái)獲取channel挖垛。select方法會(huì)返回所有處于就緒狀態(tài)的channel痒钝。 select方法具體如下:
- int select()
- int select(long timeout)
- int selectNow()
select()方法在返回channel之前處于阻塞狀態(tài)。 select(long timeout)和select做的事一樣痢毒,不過(guò)他的阻塞有一個(gè)超時(shí)限制送矩。
selectNow()不會(huì)阻塞,根據(jù)當(dāng)前狀態(tài)立刻返回合適的channel哪替。
select()方法的返回值是一個(gè)int整形栋荸,代表有多少channel處于就緒了。也就是自上一次select后有多少channel進(jìn)入就緒凭舶。舉例來(lái)說(shuō)晌块,假設(shè)第一次調(diào)用select時(shí)正好有一個(gè)channel就緒,那么返回值是1帅霜,并且對(duì)這個(gè)channel做任何處理匆背,接著再次調(diào)用select,此時(shí)恰好又有一個(gè)新的channel就緒身冀,那么返回值還是1钝尸,現(xiàn)在我們一共有兩個(gè)channel處于就緒,但是在每次調(diào)用select時(shí)只有一個(gè)channel是就緒的搂根。
selectedKeys()
在調(diào)用select并返回了有channel就緒之后珍促,可以通過(guò)選中的key集合來(lái)獲取channel,這個(gè)操作通過(guò)調(diào)用selectedKeys()方法:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
還記得在register時(shí)的操作吧剩愧,我們r(jià)egister后的返回值就是SelectionKey實(shí)例猪叙,也就是我們現(xiàn)在通過(guò)selectedKeys()方法所返回的SelectionKey。
遍歷這些SelectionKey可以通過(guò)如下方法:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
上述循環(huán)會(huì)迭代key集合隙咸,針對(duì)每個(gè)key我們單獨(dú)判斷他是處于何種就緒狀態(tài)沐悦。
注意:keyIterater.remove()方法的調(diào)用,Selector本身并不會(huì)移除SelectionKey對(duì)象五督,這個(gè)操作需要我們手動(dòng)執(zhí)行藏否。當(dāng)下次channel處于就緒是,Selector仍然會(huì)把這些key再次加入進(jìn)來(lái)充包。
SelectionKey.channel返回的channel實(shí)例需要強(qiáng)轉(zhuǎn)為我們實(shí)際使用的具體的channel類型副签,例如ServerSocketChannel或SocketChannel.
wakeUp()
由于調(diào)用select而被阻塞的線程,可以通過(guò)調(diào)用Selector.wakeup()來(lái)喚醒即便此時(shí)已然沒(méi)有channel處于就緒狀態(tài)基矮。具體操作是淆储,在另外一個(gè)線程調(diào)用wakeup,被阻塞與select方法的線程就會(huì)立刻返回家浇。
close()
當(dāng)操作Selector完畢后本砰,需要調(diào)用close方法。close的調(diào)用會(huì)關(guān)閉Selector并使相關(guān)的SelectionKey都無(wú)效钢悲。channel本身不會(huì)被關(guān)閉点额。
示例:首先打開(kāi)一個(gè)Selector,然后注冊(cè)channel舔株,最后監(jiān)聽(tīng)Selector的狀態(tài)。
public class NIOServer {
public static void main(String[] args) throws IOException {
// 1.獲取通道
ServerSocketChannel server = ServerSocketChannel.open();
// 2.切換成非阻塞模式
server.configureBlocking(false);
// 3. 綁定連接
server.bind(new InetSocketAddress(6666));
// 4. 獲取選擇器
Selector selector = Selector.open();
// 4.1將通道注冊(cè)到選擇器上还棱,指定接收“監(jiān)聽(tīng)通道”事件
server.register(selector, SelectionKey.OP_ACCEPT);
// 5. 輪訓(xùn)地獲取選擇器上已“就緒”的事件--->只要select()>0载慈,說(shuō)明已就緒
while (selector.select() > 0) {
// 6. 獲取當(dāng)前選擇器所有注冊(cè)的“選擇鍵”(已就緒的監(jiān)聽(tīng)事件)
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
// 7. 獲取已“就緒”的事件,(不同的事件做不同的事)
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 接收事件就緒
if (selectionKey.isAcceptable()) {
// 8. 獲取客戶端的鏈接
SocketChannel client = server.accept();
// 8.1 切換成非阻塞狀態(tài)
client.configureBlocking(false);
// 8.2 注冊(cè)到選擇器上-->拿到客戶端的連接為了讀取通道的數(shù)據(jù)(監(jiān)聽(tīng)讀就緒事件)
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) { // 讀事件就緒
// 9. 獲取當(dāng)前選擇器讀就緒狀態(tài)的通道
SocketChannel client = (SocketChannel) selectionKey.channel();
// 9.1讀取數(shù)據(jù)
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 9.2得到文件通道珍手,將客戶端傳遞過(guò)來(lái)的圖片寫(xiě)到本地項(xiàng)目下(寫(xiě)模式办铡、沒(méi)有則創(chuàng)建)
FileChannel outChannel = FileChannel.open(Paths.get("2_loan.sql"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
while (client.read(buffer) > 0) {
// 在讀之前都要切換成讀模式
buffer.flip();
outChannel.write(buffer);
// 讀完切換成寫(xiě)模式,能讓管道繼續(xù)讀取文件的數(shù)據(jù)
buffer.clear();
}
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
byteBuffer.put("yeah,i know,i got your message!".getBytes());
byteBuffer.flip();
client.write(byteBuffer);
}
// 10. 取消選擇鍵(已經(jīng)處理過(guò)的事件琳要,就應(yīng)該取消掉了)
iterator.remove();
}
}
}
}
public class NIOClientTwo {
public static void main(String[] args) throws IOException {
// 1. 獲取通道
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
// 1.1切換成非阻塞模式
socketChannel.configureBlocking(false);
// 1.2獲取選擇器
Selector selector = Selector.open();
// 1.3將通道注冊(cè)到選擇器中寡具,獲取服務(wù)端返回的數(shù)據(jù)
socketChannel.register(selector, SelectionKey.OP_READ);
// 2. 發(fā)送一張圖片給服務(wù)端吧
FileChannel fileChannel = FileChannel.open(Paths.get("D:\\text\\1_loan.sql"), StandardOpenOption.READ);
// 3.要使用NIO,有了Channel焙蹭,就必然要有Buffer晒杈,Buffer是與數(shù)據(jù)打交道的呢
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4.讀取本地文件(圖片),發(fā)送到服務(wù)器
while (fileChannel.read(buffer) != -1) {
// 在讀之前都要切換成讀模式
buffer.flip();
socketChannel.write(buffer);
// 讀完切換成寫(xiě)模式孔厉,能讓管道繼續(xù)讀取文件的數(shù)據(jù)
buffer.clear();
}
// 5. 輪訓(xùn)地獲取選擇器上已“就緒”的事件--->只要select()>0拯钻,說(shuō)明已就緒
while (selector.select() > 0) {
// 6. 獲取當(dāng)前選擇器所有注冊(cè)的“選擇鍵”(已就緒的監(jiān)聽(tīng)事件)
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
// 7. 獲取已“就緒”的事件,(不同的事件做不同的事)
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 8. 讀事件就緒
if (selectionKey.isReadable()) {
// 8.1得到對(duì)應(yīng)的通道
SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
// 9. 知道服務(wù)端要返回響應(yīng)的數(shù)據(jù)給客戶端撰豺,客戶端在這里接收
int readBytes = channel.read(responseBuffer);
if (readBytes > 0) {
// 切換讀模式
responseBuffer.flip();
System.out.println(new String(responseBuffer.array(), 0, readBytes));
}
}
// 10. 取消選擇鍵(已經(jīng)處理過(guò)的事件粪般,就應(yīng)該取消掉了)
iterator.remove();
}
}
}
}
NIO FileChannel文件通道
Java NIO中的FileChannel是用于連接文件的通道。通過(guò)文件通道可以讀污桦、寫(xiě)文件的數(shù)據(jù)亩歹。Java NIO的FileChannel是相對(duì)標(biāo)準(zhǔn)Java IO API的可選接口。
FileChannel不可以設(shè)置為非阻塞模式凡橱,他只能在阻塞模式下運(yùn)行小作。
打開(kāi)文件通道
在使用FileChannel前必須打開(kāi)通道,打開(kāi)一個(gè)文件通道需要通過(guò)輸入/輸出流或者RandomAccessFile稼钩,下面是通過(guò)RandomAccessFile打開(kāi)文件通道的案例:
RandomAccessFile aFile = new RandomAccessFile("D:\text\1_loan.sql", "rw");
FileChannel inChannel = aFile.getChannel();
從文件通道內(nèi)讀取數(shù)據(jù)
讀取文件通道的數(shù)據(jù)可以通過(guò)read方法:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
首先開(kāi)辟一個(gè)Buffer顾稀,從通道中讀取的數(shù)據(jù)會(huì)寫(xiě)入Buffer內(nèi)。接著就可以調(diào)用read方法坝撑,read的返回值代表有多少字節(jié)被寫(xiě)入了Buffer静秆,返回-1則表示已經(jīng)讀取到文件結(jié)尾了。
向文件通道寫(xiě)入數(shù)據(jù)
寫(xiě)數(shù)據(jù)用write方法巡李,入?yún)⑹荁uffer:
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
注意這里的write調(diào)用寫(xiě)在了wihle循環(huán)匯總抚笔,這是因?yàn)閣rite不能保證有多少數(shù)據(jù)真實(shí)被寫(xiě)入,因此需要循環(huán)寫(xiě)入直到?jīng)]有更多數(shù)據(jù)侨拦。
關(guān)閉通道
操作完畢后殊橙,需要把通道關(guān)閉:
channel.close();
FileChannel Position
當(dāng)操作FileChannel的時(shí)候讀和寫(xiě)都是基于特定起始位置的(position),獲取當(dāng)前的位置可以用FileChannel的position()方法,設(shè)置當(dāng)前位置可以用帶參數(shù)的position(long pos)方法蛀柴。
//獲取當(dāng)前的位置
long position = fileChannel.position();
//設(shè)置當(dāng)前位置為pos +123
fileChannel.position(pos +123);
假設(shè)我們把當(dāng)前位置設(shè)置為文件結(jié)尾之后螃概,那么當(dāng)我們視圖從通道中讀取數(shù)據(jù)時(shí)就會(huì)發(fā)現(xiàn)返回值是-1,表示已經(jīng)到達(dá)文件結(jié)尾了鸽疾。 如果把當(dāng)前位置設(shè)置為文件結(jié)尾之后,再向通道中寫(xiě)入數(shù)據(jù)训貌,文件會(huì)自動(dòng)擴(kuò)展以便寫(xiě)入數(shù)據(jù)制肮,但是這樣會(huì)導(dǎo)致文件中出現(xiàn)類似空洞,即文件的一些位置是沒(méi)有數(shù)據(jù)的递沪。
FileChannel Size
size()方法可以返回FileChannel對(duì)應(yīng)的文件的文件大胁虮恰:
long fileSize = channel.size();
FileChannel Truncate
利用truncate方法可以截取指定長(zhǎng)度的文件:
FileChannel truncateFile = fileChannel.truncate(1024);
FileChannel Force
force方法會(huì)把所有未寫(xiě)磁盤的數(shù)據(jù)都強(qiáng)制寫(xiě)入磁盤催植。這是因?yàn)樵诓僮飨到y(tǒng)中出于性能考慮回把數(shù)據(jù)放入緩沖區(qū)噩斟,所以不能保證數(shù)據(jù)在調(diào)用write寫(xiě)入文件通道后就及時(shí)寫(xiě)到磁盤上了,除非手動(dòng)調(diào)用force方法诫惭。 force方法需要一個(gè)布爾參數(shù)檩奠,代表是否把meta data也一并強(qiáng)制寫(xiě)入桩了。
channel.force(true);
NIO SocketChannel套接字通道
在Java NIO體系中,SocketChannel是用于TCP網(wǎng)絡(luò)連接的套接字接口埠戳,相當(dāng)于Java網(wǎng)絡(luò)編程中的Socket套接字接口井誉。創(chuàng)建SocketChannel主要有兩種方式,如下:
- 打開(kāi)一個(gè)SocketChannel并連接網(wǎng)絡(luò)上的一臺(tái)服務(wù)器整胃。
- 當(dāng)ServerSocketChannel接收到一個(gè)連接請(qǐng)求時(shí)颗圣,會(huì)創(chuàng)建一個(gè)SocketChannel。
建立一個(gè)SocketChannel連接
打開(kāi)一個(gè)SocketChannel可以這樣操作:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("http://www.google.com", 80));
關(guān)閉一個(gè)SocketChannel連接
關(guān)閉一個(gè)SocketChannel只需要調(diào)用他的close方法屁使,如下:
socketChannel.close();
從SocketChannel中讀數(shù)據(jù)
從一個(gè)SocketChannel連接中讀取數(shù)據(jù)在岂,可以通過(guò)read()方法,如下:
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = socketChannel.read(buf);
首先需要開(kāi)辟一個(gè)Buffer蛮寂。從SocketChannel中讀取的數(shù)據(jù)將放到Buffer中蔽午。
接下來(lái)就是調(diào)用SocketChannel的read()方法.這個(gè)read()會(huì)把通道中的數(shù)據(jù)讀到Buffer中。read()方法的返回值是一個(gè)int數(shù)據(jù)共郭,代表此次有多少字節(jié)的數(shù)據(jù)被寫(xiě)入了Buffer中祠丝。如果返回的是-1,那么意味著通道內(nèi)的數(shù)據(jù)已經(jīng)讀取完畢,到底了(鏈接關(guān)閉)除嘹。
向SocketChannel寫(xiě)數(shù)據(jù)
向SocketChannel中寫(xiě)入數(shù)據(jù)是通過(guò)write()方法写半,write也需要一個(gè)Buffer作為參數(shù)。下面看一下具體的示例:
String newData = "New String to write to file..." + System.currentTimeMillis();
ByteBuffer buf = ByteBuffer.allocate(48);
buf.clear();
buf.put(newData.getBytes());
buf.flip();
while(buf.hasRemaining()) {
channel.write(buf);
}
非阻塞模式
我們可以把SocketChannel設(shè)置為non-blocking(非阻塞)模式尉咕。這樣的話在調(diào)用connect(), read(), write()時(shí)都是異步的叠蝇。
socketChannel.configureBlocking(false);
connect()
如果我們?cè)O(shè)置了一個(gè)SocketChannel是非阻塞的,那么調(diào)用connect()后年缎,方法會(huì)在鏈接建立前就直接返回悔捶。為了檢查當(dāng)前鏈接是否建立成功铃慷,我們可以調(diào)用finishConnect(),如下:
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("http://www.google.com", 80));
while(! socketChannel.finishConnect() ){
//wait, or do something else...
}
write()
在非阻塞模式下,調(diào)用write()方法不能確保方法返回后寫(xiě)入操作一定得到了執(zhí)行蜕该。因此我們需要把write()調(diào)用放到循環(huán)內(nèi)犁柜。這和前面在講write()時(shí)是一樣的,此處就不在代碼演示堂淡。
read()
在非阻塞模式下馋缅,調(diào)用read()方法也不能確保方法返回后,確實(shí)讀到了數(shù)據(jù)绢淀。因此我們需要自己檢查的整型返回值萤悴,這個(gè)返回值會(huì)告訴我們實(shí)際讀取了多少字節(jié)的數(shù)據(jù)。
Selector結(jié)合非阻塞模式
SocketChannel的非阻塞模式可以和Selector很好的協(xié)同工作皆的。把一個(gè)或多個(gè)SocketChannel注冊(cè)到一個(gè)Selector后覆履,我們可以通過(guò)Selector指導(dǎo)哪些channels通道是處于可讀,可寫(xiě)等等狀態(tài)的费薄。
NIO ServerSocketChannel服務(wù)端套接字通道
在Java NIO中硝全,ServerSocketChannel是用于監(jiān)聽(tīng)TCP鏈接請(qǐng)求的通道,正如Java網(wǎng)絡(luò)編程中的ServerSocket一樣义锥。
ServerSocketChannel實(shí)現(xiàn)類位于java.nio.channels包下面柳沙。
void test() throws IOException {
//打開(kāi)一個(gè)ServerSocketChannel我們需要調(diào)用他的open()方法
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
while(true) {
SocketChannel socketChannel = serverSocketChannel.accept();
//do something with socketChannel...
if (socketChannel.isConnected()) {
break;
}
}
//關(guān)閉一個(gè)ServerSocketChannel我們需要調(diào)用close()方法
serverSocketChannel.close();
}
監(jiān)聽(tīng)鏈接
通過(guò)調(diào)用accept()方法,我們就開(kāi)始監(jiān)聽(tīng)端口上的請(qǐng)求連接拌倍。當(dāng)accept()返回時(shí)赂鲤,他會(huì)返回一個(gè)SocketChannel連接實(shí)例,實(shí)際上accept()是阻塞操作柱恤,他會(huì)阻塞帶去線程知道返回一個(gè)連接数初; 很多時(shí)候我們是不滿足于監(jiān)聽(tīng)一個(gè)連接的,因此我們會(huì)把a(bǔ)ccept()的調(diào)用放到循環(huán)中梗顺,就像這樣:
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();
//do something with socketChannel...
}
當(dāng)然我們可以在循環(huán)體內(nèi)加上合適的中斷邏輯泡孩,而不是單純的在while循環(huán)中寫(xiě)true,以此來(lái)結(jié)束循環(huán)監(jiān)聽(tīng)寺谤;
非阻塞模式
實(shí)際上ServerSocketChannel是可以設(shè)置為非阻塞模式的仑鸥。在非阻塞模式下,調(diào)用accept()函數(shù)會(huì)立刻返回变屁,如果當(dāng)前沒(méi)有請(qǐng)求的鏈接眼俊,那么返回值為空null。因此我們需要手動(dòng)檢查返回的SocketChannel是否為空粟关,例如:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(9999));
//設(shè)置為非阻塞模式
serverSocketChannel.configureBlocking(false);
while(true){
SocketChannel socketChannel = serverSocketChannel.accept();