企業(yè)網(wǎng)站建設(shè)論文糕點(diǎn)烘焙專(zhuān)業(yè)培訓(xùn)學(xué)校
一、NIO基礎(chǔ)
Java New IO是從Java1.4版本開(kāi)始引入的一個(gè)新的IO api,可以替代以往的標(biāo)準(zhǔn)IO,NIO相比原來(lái)的IO有同樣的作用和目的,但是使用的方式完全不一樣,NIO是面向緩沖區(qū)的,基于通道的IO操作,這也讓它比傳統(tǒng)IO有著更為高效的讀寫(xiě)。
1.1 IO和NIO的主要區(qū)別
IO | NIO |
---|---|
面向流 | 面向緩沖 |
區(qū)阻塞IO | 非阻塞IO |
無(wú) | 選擇器 |
1.1.1 傳統(tǒng)IO的流
以下用圖來(lái)簡(jiǎn)單理解一下,在傳統(tǒng)IO中當(dāng)App要對(duì)網(wǎng)絡(luò),磁盤(pán)中的文件進(jìn)行讀寫(xiě)的時(shí)候,它們必須建立一個(gè)連接,流到底是一個(gè)什么樣的概念呢,我們可以先把它想象成自來(lái)水,家里要用自來(lái)水,需要有水管,讓水從水管過(guò)來(lái)到家里,起到一個(gè)運(yùn)輸?shù)淖饔谩?/p>
所以當(dāng)我們文件中的數(shù)據(jù)需要輸入到App里面時(shí),它們就會(huì)建立一個(gè)輸入的管道。而當(dāng)我們的App有數(shù)據(jù)需要寫(xiě)入到文件系統(tǒng)的時(shí)候,就會(huì)建立一個(gè)輸出的管道,這兩條管道就是我們的輸入流和輸出流。那水從來(lái)沒(méi)有逆流而上的呀,所以它們都是單向管道。這么一講,是不是就很好懂了呢😁?
1.1.2 NIO
也是同樣的文件系統(tǒng)和App,不過(guò)此時(shí)把流換成了一個(gè)channel,現(xiàn)在我們可以先認(rèn)為它就是一條鐵道,那我們知道鐵道本身是不能傳遞貨物的呀,所以我們需要一個(gè)載具—火車(chē)(也就是緩沖區(qū)),App需要的數(shù)據(jù)就由這個(gè)名叫緩沖區(qū)的載具運(yùn)輸過(guò)來(lái)。那火車(chē)是可以開(kāi)過(guò)來(lái),也可以開(kāi)回去的,所以NIO是雙向傳輸?shù)摹?br />
1.2 Buffer
NIO的核心在于,通道(channel)和緩沖區(qū)(buffer)兩個(gè)。通道是打開(kāi)到IO設(shè)備的連接。使用時(shí)需要獲取用于連接IO設(shè)備的通道以及用于容納數(shù)據(jù)的緩沖區(qū),然后通過(guò)操作緩沖區(qū)對(duì)數(shù)據(jù)進(jìn)行處理。(其實(shí)就是上面那張圖的事兒,或者一句話(huà)就是一個(gè)負(fù)責(zé)傳輸,一個(gè)負(fù)責(zé)存儲(chǔ))。
緩沖區(qū)是Java.nio包定義好的,所有緩沖區(qū)都是Buffer抽象類(lèi)的子類(lèi)。Buffer根據(jù)數(shù)據(jù)類(lèi)型不同,常用子類(lèi)分別是基本數(shù)據(jù)類(lèi)型除了Boolean外的xxxBuffer(IntBuffer,DoubleBuffer···等)。不同的Buffer類(lèi)它們的管理方式都是相同的,獲取對(duì)象的方法都是
// 創(chuàng)建一個(gè)容量為capacity的xxx類(lèi)型的Buffer對(duì)象
static xxxBuffer allocate(int capacity)
而且緩沖區(qū)提供了兩個(gè)核心方法:get()和put(),put方法是將數(shù)據(jù)存入到緩沖區(qū),而get是獲取緩沖區(qū)的數(shù)據(jù)。
此時(shí)我們用代碼看一下
public class BufferTest {@Testpublic void testBuffer(){// 創(chuàng)建緩沖區(qū)對(duì)象ByteBuffer byteBuffer = ByteBuffer.allocate(1024);}
}
點(diǎn)進(jìn)去ByteBuffer,會(huì)看到這個(gè)東西是繼承了Buffer類(lèi)的
public abstract class ByteBuffer extends Buffer implements Comparable<ByteBuffer>
此時(shí)繼續(xù)點(diǎn)進(jìn)去Buffer類(lèi),第一眼看到的是有幾個(gè)自帶的屬性
1.2.1 buffer的基本屬性
① capacity容量
表示Buffer的最大數(shù)據(jù)容量,這個(gè)值不能為負(fù)。而且創(chuàng)建后是不能更改的。
② limit限制
第一個(gè)不能讀取或?qū)懭氲臄?shù)據(jù)的索引,位于此索引后的數(shù)據(jù)不可讀寫(xiě)。這個(gè)數(shù)值不能為負(fù)且不能超過(guò)capacity,如上圖中第三個(gè)緩沖區(qū),在下標(biāo)為5之后的數(shù)據(jù)塊均不能讀寫(xiě),那limit為5
③ position位置
下一個(gè)要讀取或?qū)懭氲臄?shù)據(jù)的索引,這個(gè)數(shù)值不能為負(fù)且不能超過(guò)capacity,如圖中第二個(gè)緩沖區(qū),前面5塊寫(xiě)完成,此時(shí)第6個(gè)數(shù)據(jù)塊的下標(biāo)為5,所以position為5
④ mark標(biāo)記/reset重置
mark是一個(gè)索引,通過(guò)Buffer的mark()方法指定Buffer中一個(gè)特定的position后,可以通過(guò)reset()方法重置到這個(gè)position,這個(gè)通過(guò)代碼來(lái)解釋會(huì)比較好說(shuō)明
1.2.2 code部分(非常簡(jiǎn)單)
1.首先我們創(chuàng)建一個(gè)緩沖區(qū)對(duì)象,然后把它的屬性打印出來(lái)
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
System.out.println(byteBuffer.position());
System.out.println(byteBuffer.capacity());
System.out.println(byteBuffer.limit());運(yùn)行結(jié)果:0,10,10
2.執(zhí)行一個(gè)put()方法,來(lái)把一個(gè)字符丟進(jìn)去
String str = "abcde";
byteBuffer.put(str.getBytes());
System.out.println(byteBuffer.position());
System.out.println(byteBuffer.capacity());
System.out.println(byteBuffer.limit());運(yùn)行結(jié)果:5,10,10
"abcde"長(zhǎng)度為5,position已經(jīng)變化,其它不變
3.使用flip()切換為讀模式
byteBuffer.flip();
System.out.println(byteBuffer.position());
System.out.println(byteBuffer.capacity());
System.out.println(byteBuffer.limit());運(yùn)行結(jié)果:0,10,5
此時(shí)position變成為0了,因?yàn)橐婚_(kāi)始的5,是因?yàn)檫@時(shí)候要寫(xiě)的是下標(biāo)為5的數(shù)據(jù)塊,而轉(zhuǎn)換成讀模式后,第一個(gè)讀的明顯是下標(biāo)為0的數(shù)據(jù)塊呀。limit的數(shù)值也變成了5,因?yàn)楫?dāng)前能讀到的數(shù)據(jù)從下標(biāo)為5開(kāi)始就木有了,所以limit為5
4.簡(jiǎn)單獲取一下buffer中的數(shù)據(jù)
byte[] array = new byte[byteBuffer.limit()];
byteBuffer.get(array);
System.out.println(new String(array,0,array.length));運(yùn)行結(jié)果:abcde
5.mark() & reset()
byte[] array = new byte[byteBuffer.limit()];
byteBuffer.get(array,0,2);
System.out.println(new String(array,0,2));
System.out.println(byteBuffer.position());byteBuffer.mark();
byteBuffer.get(array,2,2);
System.out.println(new String(array,2,2));
System.out.println(byteBuffer.position());byteBuffer.reset();
System.out.println(byteBuffer.position());運(yùn)行結(jié)果:ab,2,cd,4,2
其實(shí)很簡(jiǎn)單,就是第一次讀取的時(shí)候,只是讀取了前面兩個(gè)字符,然后此時(shí)position的結(jié)果為2,然后再讀取后兩個(gè),position為4,可是因?yàn)槲以谧x取前面2個(gè)的時(shí)候進(jìn)行了一個(gè)mark操作,它就自動(dòng)回到我mark之前的那個(gè)讀取位置而已,就是這么簡(jiǎn)單
6.其他的一些方法
rewind()方法,可重復(fù)讀,clear()清空緩沖區(qū),不過(guò)這個(gè)方法的清空緩沖區(qū),是一種被遺忘的狀態(tài),就是說(shuō),數(shù)據(jù)仍然還存于緩沖區(qū)中,可是自動(dòng)忽略掉了。此時(shí)再次讀取數(shù)據(jù),是還是可以get()到的。hasRemaining()方法就是表示剩余可操作的數(shù)據(jù)量還有多少,比如剛剛的mark的那個(gè)例子中,我reset回去之后,剩余的可操作數(shù)據(jù)就是3,因?yàn)槲抑蛔x了ab,還有cde這三個(gè)。
1.2.3 直接緩沖區(qū)和非直接緩沖區(qū)
非直接緩沖區(qū):通過(guò)allocate()方法來(lái)分配緩沖區(qū)。將緩沖區(qū)建立在JVM的內(nèi)存中。
直接緩沖區(qū):通過(guò)allocateDirect()方法分配緩沖區(qū),將緩沖區(qū)建立在物理內(nèi)存中。效率更高。
① 非直接緩沖區(qū)
應(yīng)用程序想要在磁盤(pán)中讀取數(shù)據(jù)時(shí),首先它發(fā)起請(qǐng)求,讓物理磁盤(pán)先把它的數(shù)據(jù)讀到內(nèi)核地址空間當(dāng)中,之后這個(gè)內(nèi)核空間再將這個(gè)數(shù)據(jù)copy一份到用戶(hù)地址空間去。然后數(shù)據(jù)才能通過(guò)read()方法將數(shù)據(jù)返回個(gè)應(yīng)用程序。而應(yīng)用程序需要寫(xiě)數(shù)據(jù)進(jìn)去,也是同理,先寫(xiě)到用戶(hù)地址空間,然后copy到內(nèi)核地址空間,再寫(xiě)入磁盤(pán)。此時(shí)不難發(fā)現(xiàn),這個(gè)copy的操作顯得十分的多余,所以非直接緩沖區(qū)的效率相對(duì)來(lái)說(shuō)會(huì)低一些。
② 直接緩沖區(qū)
直接緩沖區(qū)就真的顧名思義非常直接了,寫(xiě)入的時(shí)候,寫(xiě)到物理內(nèi)存映射文件中,再由它寫(xiě)入物理磁盤(pán),讀取也是磁盤(pán)把數(shù)據(jù)讀到這個(gè)文件然后再由它讀取到應(yīng)用程序中即可。沒(méi)有了copy的中間過(guò)程。
1.3 channel
1.3.1 扯一下概念背景
由java.nio.channels包定義,表示IO源與目標(biāo)打開(kāi)的鏈接,它本身不存在直接訪(fǎng)問(wèn)數(shù)據(jù)的能力,只能和Buffer進(jìn)行交互
傳統(tǒng)的IO由cpu來(lái)全權(quán)負(fù)責(zé),此時(shí)這個(gè)設(shè)計(jì)在有大量文件讀取操作時(shí),CPU的利用率會(huì)被拉的非常低,因?yàn)镮O操作把CPU的資源都搶占了。
在這種背景下進(jìn)行了一些優(yōu)化,把對(duì)cpu的連接取消,轉(zhuǎn)為DMA(直接內(nèi)存存取)的方式。當(dāng)然DMA這個(gè)操作本身也是需要CPU進(jìn)行調(diào)度的。不過(guò)這個(gè)損耗自然就會(huì)比大量的IO要小的多。
此時(shí),就出現(xiàn)了通道這個(gè)概念,它是一個(gè)完全獨(dú)立的處理器。專(zhuān)門(mén)用來(lái)負(fù)責(zé)文件的IO操作。
1.3.2 常用通道
Java為Channel接口提供的主要實(shí)現(xiàn)類(lèi):
FileChannel:用于讀取,寫(xiě)入,映射和操作文件的通道
DatagramChannel:通過(guò)UDP讀寫(xiě)網(wǎng)絡(luò)中的數(shù)據(jù)通道
SocketChannel:通過(guò)TCP讀寫(xiě)網(wǎng)絡(luò)中的數(shù)據(jù)通道
ServerSocketChannel:可以監(jiān)聽(tīng)新進(jìn)來(lái)的TCP連接,對(duì)每一個(gè)新進(jìn)來(lái)的連接都會(huì)創(chuàng)建一個(gè)SocketChannel
獲取channel的一種方式是對(duì)支持通道的對(duì)象調(diào)用getChannel()方法,支持類(lèi)如下
FileInputStream
FileOutputStream
RandomAccessFile
DatagramSocket
Socket
ServerSocket
獲取的其他方式是使用Files類(lèi)的靜態(tài)方法newByteChannel()獲取字節(jié)通道。再或者是通過(guò)通道的靜態(tài)方法open()打開(kāi)并返回指定通道。
1.3.3 常用方法和簡(jiǎn)單使用
① 使用非直接緩沖區(qū)完成文件復(fù)制
// 創(chuàng)建輸入輸出流對(duì)象
FileInputStream fileInputStream = new FileInputStream("testPic.jpg");
FileOutputStream fileOutputStream = new FileOutputStream("testPic2.jpg");// 通過(guò)流對(duì)象獲取通道channel
FileChannel inChannel = fileInputStream.getChannel();
FileChannel outChannel = fileOutputStream.getChannel();// 創(chuàng)建指定大小的緩沖區(qū)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 將通道中的數(shù)據(jù)寫(xiě)入到緩沖區(qū)中
while (inChannel.read(byteBuffer) != -1){// 切換成讀取模式byteBuffer.flip();// 將緩沖區(qū)中的數(shù)據(jù)寫(xiě)到輸出通道outChannel.write(byteBuffer);// 清空緩沖區(qū)byteBuffer.clear();}
//回收資源(這里為了省時(shí)間直接拋出去了,反正這段不太重要)
outChannel.close();
inChannel.close();
fileInputStream.close();
fileOutputStream.close();運(yùn)行結(jié)果:就自然是復(fù)制了一個(gè)testPic2出來(lái)啦
因?yàn)榇a本身不難,注釋已經(jīng)寫(xiě)得比較詳細(xì),就不展開(kāi)了
② 使用直接緩沖區(qū)來(lái)完成文件的復(fù)制
注意這里的StandardOpenOption是一個(gè)枚舉,表示模式,很顯然這里是要選擇READ讀取模式。
FileChannel inChannel = FileChannel.open(Paths.get("testPic.jpg",StandardOpenOption.READ));
FileChannel outChannel = FileChannel.open(Paths.get("testPic2.jpg"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);
// 進(jìn)行內(nèi)存映射
MappedByteBuffer inMappedBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
MappedByteBuffer outMapBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());// 對(duì)緩沖區(qū)進(jìn)行數(shù)據(jù)的讀寫(xiě)操作
byte[] array = new byte[inMappedBuffer.limit()];
inMappedBuffer.get(array);
outMapBuffer.put(array);// 回收資源
inChannel.close();
outChannel.close();
如果需要看一下它們兩個(gè)的時(shí)間差,自己用最常規(guī)的系統(tǒng)時(shí)間來(lái)瞧瞧就好,在這里就不再加上了。
二、NIO非阻塞式網(wǎng)絡(luò)通信
傳統(tǒng)的IO流都是阻塞式的,當(dāng)一個(gè)線(xiàn)程調(diào)用read或者write時(shí),該線(xiàn)程被阻塞,直到數(shù)據(jù)被讀取或者寫(xiě)入,該線(xiàn)程在此期間都是不能執(zhí)行其他任務(wù)的,因此,在完成網(wǎng)絡(luò)通信進(jìn)行IO操作時(shí),線(xiàn)程被阻塞,所以服務(wù)器端必須為每個(gè)客戶(hù)端提供一個(gè)獨(dú)立線(xiàn)程進(jìn)行處理,當(dāng)服務(wù)器端需要處理大量客戶(hù)端時(shí),性能將會(huì)急劇下降。
NIO是非阻塞的,當(dāng)線(xiàn)程從某通道進(jìn)行讀寫(xiě)數(shù)據(jù)時(shí),若沒(méi)有數(shù)據(jù)可用,該線(xiàn)程可以進(jìn)行其他任務(wù)。線(xiàn)程通常將非阻塞IO的空閑時(shí)間用于在其他通道上執(zhí)行IO操作,所以單獨(dú)的線(xiàn)程可以管理多個(gè)輸入和輸出通道。因此NIO可以讓服務(wù)器端使用一個(gè)或有限幾個(gè)線(xiàn)程來(lái)同時(shí)處理連接到服務(wù)器端的所有客戶(hù)端。
2.1 Selector
這個(gè)選擇器其實(shí)就是在客戶(hù)端和服務(wù)端之間引入一個(gè)通道的注冊(cè)器,比如現(xiàn)在我的客戶(hù)端要像服務(wù)端傳輸數(shù)據(jù)了,客戶(hù)端會(huì)給選擇器去發(fā)送一個(gè)channel的注冊(cè)請(qǐng)求,注冊(cè)完成后,Selector就會(huì)去監(jiān)控這個(gè)channel的IO狀態(tài)(讀寫(xiě),連接)。只有當(dāng)通道中的數(shù)據(jù)完全準(zhǔn)備就緒,Selector才會(huì)將數(shù)據(jù)分配到服務(wù)端的某個(gè)線(xiàn)程去處理。
這種非阻塞性的流程就可以更好地去使用CPU的資源。提高CPU的工作效率。這個(gè)可以用收快遞來(lái)說(shuō)明。如果你一開(kāi)始就告訴我半小時(shí)后過(guò)來(lái)取快遞,而我在這時(shí)候已經(jīng)到目的地了,我有可能就原地不動(dòng)站著等半個(gè)小時(shí)。這個(gè)期間啥地都去不了,可是你是到了之后,才打電話(huà)告訴我過(guò)來(lái)取,那我就有了更多的自由時(shí)間。
2.2 code(阻塞性IO的網(wǎng)絡(luò)通信)
現(xiàn)在我們來(lái)演示一下阻塞性IO的網(wǎng)絡(luò)通信
2.2.1 client(阻塞性IO)
這個(gè)代碼大家可以嘗試這刪除sChannel.shutdownOutput(),此時(shí)會(huì)發(fā)現(xiàn)在啟動(dòng)好server,運(yùn)行client程序的時(shí)候,程序也會(huì)阻塞,這是因?yàn)檫@時(shí)服務(wù)端并無(wú)法確定你是否已經(jīng)發(fā)送完成數(shù)據(jù)了,所以client端也產(chǎn)生了阻塞,雙方就一直僵持。
還有一種方法是解阻塞,之后進(jìn)行闡述。
// 1.獲取通道
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("你的IP地址",9898));
// 2.創(chuàng)建文件通道
FileChannel inChannel = FileChannel.open(Paths.get("C:/Users/Administrator/Desktop/testPic.jpg"),StandardOpenOption.READ);
// 3.分配指定大小的緩沖區(qū)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 4.發(fā)送數(shù)據(jù),需要讀取文件
while (inChannel.read(byteBuffer) != -1){byteBuffer.flip();// 將buffer的數(shù)據(jù)寫(xiě)入到通道中sChannel.write(byteBuffer);byteBuffer.clear();
}// 主動(dòng)告訴服務(wù)端,數(shù)據(jù)已經(jīng)發(fā)送完畢
sChannel.shutdownOutput();while (sChannel.read(byteBuffer) != -1){byteBuffer.flip();System.out.println("接收服務(wù)端數(shù)據(jù)成功···");byteBuffer.clear();}// 5.關(guān)閉通道
inChannel.close();
sChannel.close();
2.2.2 server(阻塞性IO)
// 1.獲取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
// 創(chuàng)建一個(gè)輸出通道,將讀取到的數(shù)據(jù)寫(xiě)入到輸出通道中,保存為testPic2
FileChannel outChannel = FileChannel.open(Paths.get("testPic2.jpg"),StandardOpenOption.WRITE,StandardOpenOption.CREATE);
// 2.綁定端口
ssChannel.bind(new InetSocketAddress(9898));
// 3.等待客戶(hù)端連接,連接成功時(shí)會(huì)得到一個(gè)通道
SocketChannel sChannel = ssChannel.accept();
// 4.創(chuàng)建緩沖區(qū)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 5.接收客戶(hù)端的數(shù)據(jù)存儲(chǔ)到本地
while (sChannel.read(byteBuffer) != -1){byteBuffer.flip();outChannel.write(byteBuffer);byteBuffer.clear();
}// 發(fā)送反饋給客戶(hù)端// 向緩沖區(qū)中寫(xiě)入應(yīng)答信息byteBuffer.put("服務(wù)端接收數(shù)據(jù)成功".getBytes());byteBuffer.flip();sChannel.write(byteBuffer);// 關(guān)閉通道
sChannel.close();
outChannel.close();
byteBuffer.clear();
然后再當(dāng)我們的客戶(hù)端運(yùn)行起來(lái),就會(huì)進(jìn)行copy操作
2.3 Selector完成非阻塞IO
使用NIO完成網(wǎng)絡(luò)通信需要三個(gè)核心對(duì)象:
channel:java.nio.channels.Channel接口,SocketChannel,ServerSocketChannel,DatagramChannel
管道相關(guān):Pipe.SinkChannel,Pine.SourceChannel
buffer:負(fù)責(zé)存儲(chǔ)數(shù)據(jù)
Selector:其中Selector是SelectableChannel的多路復(fù)用器,主要是用于監(jiān)控SelectableChannel的IO狀態(tài)
2.3.1 client(非阻塞)
// 1.獲取通道,默認(rèn)是阻塞的
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("192.168.80.1",9898));// 1.1 將阻塞的套接字變成非阻塞
sChannel.configureBlocking(false);// 2.創(chuàng)建指定大小的緩沖區(qū)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 3.發(fā)送數(shù)據(jù)給服務(wù)端,直接將數(shù)據(jù)存儲(chǔ)到緩沖區(qū)
byteBuffer.put(new Date().toString().getBytes());
// 4.將緩沖區(qū)的數(shù)據(jù)寫(xiě)入到sChannel
byteBuffer.flip();
sChannel.write(byteBuffer);
byteBuffer.clear();// 關(guān)閉
sChannel.close();
2.3.2 server(非阻塞)
代碼的注釋中已經(jīng)解釋了整個(gè)過(guò)程的做法,這里就不一一展開(kāi)了。
// 1.獲取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
// 2.將阻塞的套接字設(shè)置為非阻塞的
ssChannel.configureBlocking(false);
// 3.綁定端口號(hào)
ssChannel.bind(new InetSocketAddress(9898));// 4.創(chuàng)建選擇器對(duì)象
Selector selector = Selector.open();// 5.將通道注冊(cè)到選擇器上(這里的第二個(gè)參數(shù)為selectionKey),下面有解釋
// 此時(shí)選擇器就開(kāi)始監(jiān)聽(tīng)這個(gè)通道的接收時(shí)間,此時(shí)接收工作準(zhǔn)備就緒,才開(kāi)始下一步的操作
ssChannel.register(selector,SelectionKey.OP_ACCEPT);// 6.通過(guò)輪詢(xún)的方式獲取選擇器上準(zhǔn)備就緒的事件
// 如果大于0,至少有一個(gè)SelectionKey準(zhǔn)備就緒
while (selector.select() > 0){// 7.獲取當(dāng)前選擇器中所有注冊(cè)的selectionKey(已經(jīng)準(zhǔn)備就緒的監(jiān)聽(tīng)事件)Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();// 迭代獲取已經(jīng)準(zhǔn)備就緒的選擇鍵while (selectionKeyIterator.hasNext()){// 8.獲取已經(jīng)準(zhǔn)備就緒的事件SelectionKey selectionKey = selectionKeyIterator.next();if (selectionKey.isAcceptable()){// 9.調(diào)用accept方法SocketChannel sChannel = ssChannel.accept();// 將sChannel設(shè)置為非阻塞// 再次強(qiáng)調(diào),整個(gè)過(guò)程不能有任何一條阻塞通道sChannel.configureBlocking(false);// 進(jìn)行數(shù)據(jù)接收工作,而且把sChannel也注冊(cè)上選擇器讓選擇器來(lái)監(jiān)聽(tīng)sChannel.register(selector,SelectionKey.OP_READ);}else if (selectionKey.isReadable()){// 如果讀狀態(tài)已經(jīng)準(zhǔn)備就緒,就開(kāi)始讀取數(shù)據(jù)// 10.獲取當(dāng)前選擇器上讀狀態(tài)準(zhǔn)備就緒的通道SocketChannel sChannel = (SocketChannel) selectionKey.channel();// 11.讀取客戶(hù)端發(fā)送的數(shù)據(jù),需要先創(chuàng)建緩沖區(qū)ByteBuffer byteBuffer = ByteBuffer.allocate(1024);// 12.讀取緩沖區(qū)的數(shù)據(jù)while (sChannel.read(byteBuffer) > 0){byteBuffer.flip();// 這里sChannel.read(byteBuffer)就是這個(gè)字節(jié)數(shù)組的長(zhǎng)度System.out.println(new String(byteBuffer.array(),0,sChannel.read(byteBuffer)));// 清空緩沖區(qū)byteBuffer.clear();}}// 當(dāng)selectionKey使用完畢需要移除,否則會(huì)一直優(yōu)先selectionKeyIterator.remove();}}
當(dāng)調(diào)用register方法將通道注冊(cè)到選擇器時(shí),選擇器對(duì)通道的監(jiān)聽(tīng)事件需要通過(guò)第二個(gè)參數(shù)ops決定
讀:SelectionKey.OP_READ(1)
寫(xiě):SelectionKey.OP_WRITE(4)
連接:SelectionKey.OP_CONNECT(8)
接收:SelectionKey.OP_ACCEPT(16)
若注冊(cè)時(shí)不僅僅只有一個(gè)監(jiān)聽(tīng)事件,則需要用位或操作符連接
int selectionKeySet = SelectionKey.OP_READ|SelectionKey.OP_WRITE
而關(guān)于這個(gè)selectionKey,它表示著SelectableChannel和Selectr之間的注冊(cè)關(guān)系。它也有一系列對(duì)應(yīng)的方法
2.3.3 客戶(hù)端的改造
引入Scanner接收輸入信息,不過(guò)請(qǐng)注意,在測(cè)試代碼中輸入IDEA需要進(jìn)行一些設(shè)置,具體做法是在Help-Edit Custom VM Option中加入一行
-Deditable.java.test.console=true
這樣就可以輸入了。
// 1.獲取通道,默認(rèn)是阻塞的
SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("192.168.80.1",9898));// 1.1 將阻塞的套接字變成非阻塞
sChannel.configureBlocking(false);// 2.創(chuàng)建指定大小的緩沖區(qū)
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()){String str = scanner.next();// 3.發(fā)送數(shù)據(jù)給服務(wù)端,直接將數(shù)據(jù)存儲(chǔ)到緩沖區(qū)byteBuffer.put((new Date().toString()+str).getBytes());// 4.將緩沖區(qū)的數(shù)據(jù)寫(xiě)入到sChannelbyteBuffer.flip();sChannel.write(byteBuffer);byteBuffer.clear();
}
// 關(guān)閉
sChannel.close();
這樣就完成了一個(gè)問(wèn)答模式的網(wǎng)絡(luò)通信。
2.4 Pipe管道
Java NIO中的管道是兩個(gè)線(xiàn)程之間的單向數(shù)據(jù)連接,Pipe有一個(gè)source管道和一個(gè)sink管道,數(shù)據(jù)會(huì)被寫(xiě)到sink,從source中獲取
// 1.獲取管道
Pipe pipe = Pipe.open();// 2.創(chuàng)建緩沖區(qū)對(duì)象
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// 3.獲取sink通道
Pipe.SinkChannel sinkChannel = pipe.sink();
byteBuffer.put("通過(guò)單向管道傳輸數(shù)據(jù)".getBytes());// 4.將數(shù)據(jù)寫(xiě)入sinkChannel
byteBuffer.flip();
sinkChannel.write(byteBuffer);
// 5.讀取緩沖區(qū)中的數(shù)據(jù)
Pipe.SourceChannel sourceChannel = pipe.source();
// 6.讀取sourceChannel中的數(shù)據(jù)放入到緩沖區(qū)
byteBuffer.flip();
sourceChannel.read(byteBuffer);
System.out.println(new String(byteBuffer.array(),0,sourceChannel.read(byteBuffer)));sourceChannel.close();
sinkChannel.close();運(yùn)行結(jié)果就是打印了我們的那串字符"通過(guò)單向管道傳輸數(shù)據(jù)"