日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

java之NIO(Channel,Buffer,Selector)

發布時間:2024/7/5 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 java之NIO(Channel,Buffer,Selector) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? java之NIO

1 什么是NIO

Java NIO (New IO,Non-Blocking IO)是從Java 1.4版本開始引入的一套新的IO API。NIO支持面向緩沖區的、基于通道的IO操作。NIO的三大核心部分:通道(Channel),緩沖區(Buffer), 選擇器(Selector),數據總是從通道讀取到緩沖區,或者從緩沖區寫入到通道中,選擇器用于監聽多個通道事件,如連接打開,數據到達等。Java NIO系統的核心在于:通道(Channel)和緩沖區(Buffer),Channel負責傳輸,Buffer負責存儲數據。

BIO與NIO的理解:傳統IO即BIO在進行數據傳輸時必須要建立一個連接才能進行數據的寫入和讀取??梢园蓴祿斫鉃樗?#xff0c;需要有管道,可以認為應用程序和文件之間的連接就是一個管道用來運輸水流。輸入流和輸出流是不同的管道,他們是單向的。NIO在進程應用程序和文件之間數據傳輸時他們的連接不能理解為管道,他有個概念為“通道”,可以理解為鐵軌,還有“緩沖區”可以理解為火車。起到運輸作用,但是本事不能進行運輸數據,數據的運輸需要借助于火車。當我們要讀取磁盤文件的時候數據會先加載到緩沖區,然后傳輸到應用程序。

2 BIO與NIO的區別

(1)BIO是面向流,流是單向的。每次從流中讀取一個或者多個字節,直到讀取完所有字節,沒有被緩存起來,不能前后移動流中的數據,如果想要能前后移動的話需要將他緩存到另外一個緩沖區;NIO是面向緩沖區的,通道可以將數據讀取到緩存區實現雙向傳輸。NIO是將數據讀取到一個稍后處理的緩沖區,并且在需要的時候可以前后移動。

(2)BIO是阻塞式,一個線程調用read()或者write()的時候這個線程被阻塞,直到數據被讀取或者完全寫入,不能再干其他事情;NIO是非阻塞式,一個線程從一個通道發送請求讀取數據,只能獲取到目前可用的,沒數據可用就什么都不會獲取,不保持阻塞,直到數據變得可以讀取之前,這個線程可以做其他事,寫也是這樣。非阻塞IO的線程在空閑時間作用在其他通道上執行IO操作,那么一個線程就可以管理多個輸入輸出通道。

(3)BIO傳輸的是字節流或字符流,NIO是通過塊傳輸。

面向文件IO的區別:BIO是面向流的,NIO是面向緩沖區的。面向網絡IO的區別:BIO是阻塞的,NIO是非阻塞的,并且NIO有選擇器

?

3 緩沖區(Buffer)

3.1?緩沖區相關概念

通道表示IO源到 IO 設備(例如:文件、套接字)的連接。若需要使用 NIO 系統,需要獲取用于連接 IO 設備的通道以及用于容納數據的緩沖區。然后操作緩沖區,對數據進行處理。

緩沖區(Buffer):Buffer主要用于與Channel交互,數據從Channel寫入Buffer,然后再從Buffer寫出到Channel。是一個用于特定基本數據類型(除boolean型外)的容器,底層使用數組存儲,可以保存多個相同類型的數據。所有緩沖區都是java.nio.buffer的子類,常見的子類有ByteBuffer,CharBuffer,IntBuffer,DoubleBuffer,ShortBuffer,LongBuffer,FloatBuffer等,他們管理數據的方法都相似,管理的類型不同而已。

Buffer的實現類都是通過allocate(int,capacity)創建一個容量為capacity的對象。Buffer有以下基本屬性:

容量(capacity)標識Buffer存儲的最大數據容量,聲明后不能更改,不能為負,通過capacity()獲取
限制(limit)第一個不應該讀取或寫入的數據的索引,也就是limit后的數據不可以讀寫,不能為負,不能大于capacity,通過limit()獲取
位置(position)當前要讀取或者寫入數據的索引,不能為負,不能大于limit,通過position()獲取
標記(mark)標記是一個索引,通過mark()標記后,可以通過調用reset()將position恢復到標記的mark處

上述屬性的范圍大小為:?0 <= mark <= position <= limit <= capacity

3.2?緩沖區的基本操作

緩沖區為所有的子類提供了兩個用于數據操作的方法put和get方法,如ByteBuffer的這兩個方法如下

方法說明
put(byte b)將指定的單個字節寫入緩沖區的當前位置
put(byte[] buf)將buf中的字節寫入緩沖區的當前位置
put(int index,byte b)將指定字節寫入緩沖區的索引位置,不移動position
get()讀取單個字節
get(byte[] buf)批量讀取多個字節到buf中
get(int index)讀取指定索引位置的字節,不移動position

Buffer其他常用方法

方法說明
Buffer flip()將limit設置為當前position,position設置為0,mark設置為-1
Buffer rewind()將position設置為0,mark設置為-1,可以重復讀
Buffer clear()將limit設置為capacity,position設置為0,mark設置為-1,數據沒有清空
Buffer mark()設置緩沖區的mark
Buffer reset()將當前位置的position轉到之前設置的mark的位置
Buffer hasRemaining()判斷緩沖區中是否還有元素
int remaining返回position和limit之間元素的個數
Xxx[] array()返回XxxBuffer底層的Xxx數組
int capacity()返回Buffer的capacity大小
int limit()返回Buffer的limit位置
Buffer limit(int n)將設置緩沖區界限為 n, 并返回一個具有新 limit 的緩沖區對象
int position()返回Buffer的position位置
Buffer position(int n)將設置緩沖區的當前位置為 n , 并返回修改后的 Buffer 對象

說明:①當我們調用ByteBuffer.allocate(10)方法創建了一個10個byte的數組的緩沖區,position的位置為0,capacity和limit默認都是數組長度。②當通過put方法寫入5個字節到緩沖區時,position更新為5。③需要將緩沖區中的5個字節數據寫入Channel的通信信道,調用ByteBuffer.flip()方法,變化為position設回0,并將limit設成之前的position的值④這時底層操作系統就可以從緩沖區中正確讀取這個5個字節數據并發送出去了。在下一次寫數據之前我們再調用clear()方法,緩沖區的索引位置又回到了初始位置。

注意:clear()是把position設回0,limit設置成capacity,換句話說,其實Buffer中的數據沒有清空,只是這些標記告訴我們可以從哪里開始往Buffer里寫數據。如果Buffer中有一些未讀的數據,調用clear()方法,數據將丟棄,那就沒有標記說明哪些數據讀過,哪些還沒有。如果還需要Buffer中未讀的數據,但是還想要先寫些數據,那么使用compact()方法。compact()方法將所有未讀的數據拷貝到Buffer起始處。然后將position設到最后一個未讀元素正后面。limit屬性依然像clear()方法一樣,設置成capacity?,F在Buffer準備好寫數據了,但是不會覆蓋未讀的數據。

public static void main(String[] args) {/*** 通過allocate()獲取緩沖區,緩沖區主要有2個核心方法:put()將輸入存入緩沖區,get()獲取緩沖區數據*/// 獲取緩沖區ByteBuffer buffer = ByteBuffer.allocate(1024);System.out.println("capacity:"+ buffer.capacity()+"\t position:"+buffer.position()+"\t limit:"+buffer.limit());// 將數據存入緩沖區String str = "hello";buffer.put(str.getBytes());System.out.println("capacity:"+ buffer.capacity()+"\t position:"+buffer.position()+"\t limit:"+buffer.limit());// 獲取緩沖區數據,要獲取緩存區的數據需要flip()切換緩沖區的模式buffer.flip();System.out.println("capacity:"+ buffer.capacity()+"\t position:"+buffer.position()+"\t limit:"+buffer.limit());// 創建字節數據接收數據byte[] b = new byte[buffer.limit()];buffer.get(b);System.out.println(new String(b,0,buffer.limit()));System.out.println("capacity:"+ buffer.capacity()+"\t position:"+buffer.position()+"\t limit:"+buffer.limit());// rewind()可重復讀buffer.rewind();System.out.println("capacity:"+ buffer.capacity()+"\t position:"+buffer.position()+"\t limit:"+buffer.limit());} ----------------------------------- 輸出結果: capacity:1024 position:0 limit:1024 capacity:1024 position:5 limit:1024 capacity:1024 position:0 limit:5 hello capacity:1024 position:5 limit:5 capacity:1024 position:0 limit:5

3.3?直接緩沖區和非直接緩沖區

緩沖區分為直接緩沖區和非直接緩存區:①非直接緩沖區:硬盤-->系統的緩沖區-->copy-->JVM緩沖區-->程序②直接緩沖區:需要copy,JVM和緩沖區實現映射。

直接字節緩沖區, Java 虛擬機會盡最大努力直接在此緩沖區上執行本機 I/O 操作。也就是說在每次調用基礎操作系統的一個本機 I/O 操作之前(或之后),虛擬機都會盡量避免將緩沖區的內容復制到中間緩沖區中(或從中間緩沖區中復制內容)。?

直接字節緩沖區可以通過調用ByteBuffer的 allocateDirect() 工廠方法來創建。此方法返回的緩沖區進行分配和取消分配所需成本通常高于非直接緩沖區。直接緩沖區的內容可以駐留在常規的垃圾回收堆之外,因此,它們對應用程序的內存需求量造成的影響可能并不明顯。所以,建議將直接緩沖區主要分配給那些易受基礎系統的本機 I/O 操作影響的大型、持久的緩沖區。一般情況下,最好僅在直接緩沖區能在程序性能方面帶來明顯好處時分配它們。?

直接字節緩沖區還可以通過 FileChannel 的 map() 方法 將文件區域直接映射到內存中來創建。該方法返回ByteBuffer的子類:MappedByteBuffer 。Java 平臺的實現有助于通過 JNI 從本機代碼創建直接字節緩沖區。如果以上這些緩沖區中的某個緩沖區實例指的是不可訪問的內存區域,則試圖訪問該區域不會更改該緩沖區的內容,并且將會在訪問期間或稍后的某個時間導致拋出不確定的異常。?

?

非直接緩沖區如上,假設應用程序想要在磁盤中讀取一些數據的話。應用程序首先發起一個請求,要去讀取當前物理磁盤里面的數據,這個時候需要將物理磁盤的數據首先讀到內核空間中,然后拷貝一份到用戶空間,然后才能通過read的方法將數據讀到應用程序中。同樣的應用程序中有數據的話需要先寫入到用戶地址空間,然后復制到內核地址空間,再由內核空間寫入到物理磁盤。在這個過程中,這兩個復制的操作比較多余,所以他的效率比較低一些,也就是將我們的緩沖區建立在jvm的內存中相對效率更低。

直接字節緩沖區如上圖,直接緩沖區不需要拷貝,是將我們的數據直接在物理內存中建立一個映射文件,將數據寫到這個文件里面去,這個時候我們應用程序要寫一些數據的話直接寫到這個映射文件。操作系統就會將這個寫到物理磁盤中。讀磁盤數據同理。這個過程就沒有中間的copy就會比較高。

字節緩沖區是直接緩沖區還是非直接緩沖區可通過調用其 isDirect() 方法來確定。非直接緩沖區:通過allocate方法分區緩存區,將緩存區建立在JVM的內存中
直接緩存區:通過allocateDircet方法分區緩沖區,將緩沖區建立在物理內存中,效率更高。

到allocateDirect和allocate創建的源碼中,發現allocate創建的是一個HeapByteBuffer,Heap堆其實就是表示用戶空間,在jvm內存中創建的一個緩沖區。

public static ByteBuffer allocate(int capacity) {if (capacity < 0)throw new IllegalArgumentException();return new HeapByteBuffer(capacity, capacity);}public static ByteBuffer allocateDirect(int capacity) {return new DirectByteBuffer(capacity);}

3.4 緩沖區使用

Buffer使用一般遵循以下幾個原則:

①分配空間,如ByteBuffer buffer = ByteBuffer.allocate(1024);或者使用allocateDirector

②將數據寫入到Buffer中 int readBuffer = inChannel.read(buffer);

③調用flip()方法,將limit設置為當前position,position設置為0,mark設置為-1

④從Buffer中讀取數據 readBuffer = inChannel.read(buffer);

⑤調用clear()(將limit設置為capacity,position設置為0,mark設置為-1,數據沒有清空)方法或者compact()方法

?

4 通道(Channel)

Channel表示IO源于目標節點打開的連接,類似于傳統的流,但是Channel不直接存儲數據,Channel只能與Buffer進行交互。

以非直接緩沖區為例,應用程序獲取數據需要經過用戶空間,然后內核空間,再讀取數據,所有的讀取操作在NIO是直接由CPU負責的。這個流程會存在一些問題,當我們有大量的文件讀取操作的時候cpu他的利用就很低,因為IO操作直接搶占CPU的資源,就不能夠去做其他的事情,所以他的效率就會變低。

計算機CPU和內存的交互是最頻繁的,內存是我們的高速緩存區,CPU運轉速度越來越快,磁盤遠遠跟不上CPU的讀寫速度,才設計了內存。這里把CPU的連接干掉了,變成了DMA(Direct Memory Access,直接內存存取器),就是直接內存存儲。如果要讀取數據,所以的操作是直接在當前DMA這里直接完成,不再有CPU去進行負責。但是得到DMA還是需要由當前的CPU進行相關的調度。在這里交給了DMA之后,CPU就能做其他的事,但是如果依然有大量的IO操作的時候又會造成DMA總線的擁堵,因為最終沒有直接和CPU撇開關系。導致在大量的文件讀取請求的時候依然利用率比較低,這個時候就出現了新的數據讀寫流程,這個時候就出現了channel通道。

把DMA換成了通道channel。通道channel可以認為他是一個完全獨立的處理器,他就是用來專門負責文件的IO操作的,也就是說以后所有的數據直接交給channel去進行負責讀取。這個時候CPU才算是真正的解放了。

java為Channel接口提供的最主要的實現類如下:①FileChannel:用于讀取,寫入、映射和操作文件的通道②SocketChannel:通過TCP讀取網絡中的數據③ServerSocketChannel:可以監聽新進來的TCP連接,對每個新進來的連接都會創建一個SocketChannel④DatagramChannel:通過UDP讀寫網絡中的數據通道

獲取通道的三種方式:①對支持通道的對象調用getChannel(),支持通道的類有:FileInputStream,FileOutputStream,RandomAccessFile,Socket,ServerSocket,DatagramSocket②通過XxxChannel的靜態方法open()打開并返回指定的XxxChannel③使用Files工具類的靜態方法newByteChannel()獲取字節通道。

FileChannel常用方法

方法描述
int read(ByteBuffer dst)從Channel中讀取數據到ByteBuffer
long read(ByteBuffer[] dsts)將Channel中的數據“分散”到ByteBuffer[]
int write(ByteBuffer src)將ByteBuffer的數據寫入到Channel
long write(ByteBuffer[] srcs)將ByteBuffer[]的數據"聚集"到Channel
MappedByteBuffer map(MapMode mode,long position,long size)將Channel對應的部分數據或者全部數據映射到ByteBuffer
long position()返回次通道的文件位置
FileChannel position(long p)設置此通道的文件位置
long size()返回此通道的文件大小
FileChannel truncate(long s)將此通道的文件截取為給定大小
void force(boolean metadata)強制將所有對此通道的文件更新寫入到存儲設備中

分散(Scatter)讀取和聚集(Gather)寫入:①分散讀取(Scattering Reads是指從Channel中讀取的數據“分散”到多個Buffer中,注意:按照緩沖區的順序,從 Channel 中讀取的數據依次將Buffer填滿。②聚集寫入(Gathering Writes)是指將多個Buffer中的數據“聚集”到Channel,注意:按照緩沖區的順序,寫入position和limit之間的數據到Channel。

NIO的強大功能部分來自于Channel的非阻塞特性,套接字的某些操作可能會無限期地阻塞。如對accept()方法的調用可能會因為等待一個客戶端連接而阻塞;對read()方法的調用可能會因為沒有數據可讀而阻塞,直到連接的另一端傳來新的數據??偟膩碚f,創建/接收連接或讀寫數據等I/O調用,都可能無限期地阻塞等待,直到底層的網絡實現發生了什么。慢速的,有損耗的網絡,或僅僅是簡單的網絡故障都可能導致任意時間的延遲。然而不幸的是,在調用一個方法之前無法知道其是否阻塞。NIO的channel抽象的一個重要特征就是可以通過配置它的阻塞行為,以實現非阻塞式的信道。 channel.configureBlocking(false)

?在非阻塞式信道上調用一個方法總是會立即返回。這種調用的返回值指示了所請求的操作完成的程度。例如,在一個非阻塞式ServerSocketChannel上調用accept()方法,如果有連接請求來了,則返回客戶端SocketChannel,否則返回null。

對比傳統IO和NIO的代碼

/*** 傳統IO*/public static void IO_FileInputStream(){BufferedInputStream bis = null;BufferedOutputStream bos = null;try {bis = new BufferedInputStream(new FileInputStream(new File("a.txt")));bos = new BufferedOutputStream(new FileOutputStream(new File("b.txt")));byte[] buffer = new byte[1024];int len;while ((len=bis.read(buffer))!=-1){bos.write(buffer,0,len);bos.flush();}} catch (IOException e) {e.printStackTrace();}finally {try {if(bis != null){bis.close();}if(bos != null){bos.close();}} catch (IOException e) {e.printStackTrace();}}}/*** NIO*/public static void NIO_FileInputStream(){FileInputStream fis = null;FileOutputStream fos = null;try {fis = new FileInputStream(new File("a.txt"));fos = new FileOutputStream(new File("b.txt"));FileChannel inChannel = fis.getChannel();FileChannel outChannel = fos.getChannel();ByteBuffer buffer = ByteBuffer.allocate(1024);int readBuffer = inChannel.read(buffer);while (readBuffer!=-1){buffer.flip();while(buffer.hasRemaining()){outChannel.write(buffer);}buffer.compact();readBuffer = inChannel.read(buffer);}} catch (IOException e) {e.printStackTrace();}finally{try{if(fis != null){fis.close();}if(fos != null){fos.close();}}catch (IOException e){e.printStackTrace();}}}

對比直接緩沖區與內存映射文件操作

public class NioTest {public static void main(String[] args) {nioBuffer();nioDirectBuffer();}private static void nioBuffer() {long start = System.currentTimeMillis();FileChannel inChannel = null;FileChannel outChannel = null;try {// 獲取通道inChannel = FileChannel.open(Paths.get("D:\\test\\doneFile0comlog_20201117_01.log.gz"), StandardOpenOption.READ);outChannel = FileChannel.open(Paths.get("D:\\test\\doneFile0comlog_20201117_01.log.gz.bak"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);// 創建緩沖區ByteBuffer buffer = ByteBuffer.allocate(1024);// 將輸入通道的數據寫入緩沖區while (inChannel.read(buffer)!=-1){buffer.flip();// 將緩沖區數據寫入輸出通道outChannel.write(buffer);// 清空緩沖區buffer.clear();}} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}finally {if (outChannel!=null){try {outChannel.close();} catch (IOException e) {e.printStackTrace();}}if (inChannel!=null){try {inChannel.close();} catch (IOException e) {e.printStackTrace();}}}long end = System.currentTimeMillis();System.out.println("nioBuffer:"+(end-start));}private static void nioDirectBuffer() {long start = System.currentTimeMillis();FileChannel inChannel = null;FileChannel outChannel = null;try {// 獲取通道inChannel = FileChannel.open(Paths.get("D:\\test\\doneFile0comlog_20201117_01.log.gz"), StandardOpenOption.READ);outChannel = FileChannel.open(Paths.get("D:\\test\\doneFile0comlog_20201117_01.log.gz.bak"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);// 進行內存映射文件MappedByteBuffer inMapBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());MappedByteBuffer outMapBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, outChannel.size());// 對緩沖區進行讀寫操作byte[] b = new byte[inMapBuffer.limit()];inMapBuffer.get(b);outMapBuffer.put(b);} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (outChannel != null) {try {outChannel.close();} catch (IOException e) {e.printStackTrace();}}if (inChannel != null) {try {inChannel.close();} catch (IOException e) {e.printStackTrace();}}}long end = System.currentTimeMillis();System.out.println("nioDirectBuffer:" + (end - start));}}---------------------------------- 結果為: nioBuffer:94 nioDirectBuffer:7

5 選擇器(Selector

5.1 相關概念

NIO和BIO有一個非常大的區別是BIO是阻塞的,NIO是非阻塞的。阻塞與非阻塞是相對于網絡通信而言的。網絡通信就會有客戶端的概念??蛻舳艘蚍斩税l送數據的話必須建立連接,在這個過程中會做一些相關的事情,如accpet等待連接,然后客戶端write數據,服務端read數據。這些操作在傳統的套接字socket里面都是阻塞式的。服務端一次只能接待一個客戶端,不能一下多個的客戶端。也就是客戶端請求服務器做些事情的時候,這個客戶端沒有處理完,其他客戶端的請求是進不來的。這種就是阻塞式的,所以服務端如果是這種模型的話,他的效率是非常低的。

要解決這種阻塞就要通過多線程的方式解決,但是線程資源是有限的,那就極大的限制了服務端他的處理效率。這就是經典的C10K問題,假如有C10K,就需要創建1W個進程。在NIO中非阻塞的網絡通信模型Selector就能解決這個問題。

系統線程的切換是消耗系統資源的,如果我們每一個連接都用一個線程來管理,資源的開銷會非常大,這個時候就可以用Selector。通過Selector可以實現一個線程管理多個Channel,如果你的應用打開了多個通道,但每個連接的流量都很低,使用Selector就會很方便。例如在一個聊天服務器中。要使用Selector, 得向Selector注冊Channel,然后調用它的select()方法。這個方法會一直阻塞到某個注冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件(如新的連接進來、數據接收等)。Selector 的意義在于只通過一個線程就可以管理成千上萬個 I/O 請求, 相比使用多個線程,避免了線程上下文切換帶來的開銷。

Selector是怎么工作的呢?有了Selector之后,Selector會把每一個客戶端和服務端傳輸數據的通道都到Selector上去注冊一下。也就是以后你想向服務端發送數據,通道先到Selector選擇器上注冊一下,那么Selector就會監控當前channel的IO狀況(讀,寫,連接,接受處理等情況)只有當某個channel上的數據完全準備就緒,Selector才會把這樣一個channel里面的任務分配到服務端來進行運行。當我們客戶端要給服務端發送數據的時候,channel需要在Selector上進行注冊,當channel的數據完全準備就緒的時候Selector才會將任務分配給服務端的一個線程進行處理。這種非阻塞式的相較于阻塞式的就能非常好的利用cpu的資源,提高cpu的工作效率。

一個Selector實例可以同時檢查一組信道的I/O狀態。用專業術語來說,選擇器就是一個多路開關選擇器,因為一個選擇器能夠管理多個信道上的I/O操作。然而如果用傳統的方式來處理這么多客戶端,使用的方法是循環地一個一個地去檢查所有的客戶端是否有I/O操作,如果當前客戶端有I/O操作,則可能把當前客戶端扔給一個線程池去處理,如果沒有I/O操作則進行下一個輪詢,當所有的客戶端都輪詢過了又接著從頭開始輪詢;這種方法是非常笨而且也非常浪費資源,因為大部分客戶端是沒有I/O操作,我們也要去檢查;而Selector就不一樣了,它在內部可以同時管理多個I/O,當一個信道有I/O操作的時候,他會通知Selector,Selector就是記住這個信道有I/O操作,并且知道是何種I/O操作,是讀呢?是寫呢?還是接受新的連接;所以如果使用Selector,它返回的結果只有兩種結果,一種是0,即在你調用的時刻沒有任何客戶端需要I/O操作,另一種結果是一組需要I/O操作的客戶端,這時你就根本不需要再檢查了,因為它返回給你的肯定是你想要的。這樣一種通知的方式比那種主動輪詢的方式要高效得多。

使用選擇器,首先創建一個Selector實例(使用靜態工廠方法open())并將其注冊(register)到想要監控的信道上(通過channel的方法實現,而不是使用selector的方法)。最后,調用選擇器的select()方法。該方法會阻塞等待,直到有一個或更多的信道準備好了I/O操作或等待超時。select()方法將返回可進行I/O操作的信道數量?,F在,在一個單獨的線程中,通過調用select()方法就能檢查多個信道是否準備好進行I/O操作。如果經過一段時間后仍然沒有信道準備好,select()方法就會返回0,并允許程序繼續執行其他任務。

Selector 只能與非阻塞模式下的通道一起使用(即需要實現 SelectableChannel 接口),否則會拋出 IllegalBlockingModeException 異常

5.2 Selector使用

(1)使用步驟

①創建Selector

②向Selector注冊通道,一個Selector可以注冊多個通道

③通過Selector選擇就緒的通道

// 通過open()方法創建 SelectorSelector selector = Selector.open();// 創建一個通道,以ServerSockeetChannel為例,并且將通道設置為非阻塞模式ServerSocketChannel channel = ServerSocketChannel.open();channel.configureBlocking(false);// 通過register()方法注冊通道channel.register(selector, SelectionKey.OP_ACCEPT);// 通過select()方法從多個通道中以輪詢的方式選擇已經準備就緒的通道。根據之前register()方法中設置的興趣,將可以進行對應操作的通道選擇出來selector.select();// 通過Selector的selectedKeys()方法獲得已選擇鍵集(selected-key set)Set key = selector.selectedKeys();//通過Iterator迭代器依次獲取key中的SelectionKey對象,并通過SelectionKey中的判斷方法執行對應的操作Iterator<SelectionKey> iterator = key.iterator();while (iterator.hasNext()) {SelectionKey selectionKey = iterator.next();if (selectionKey.isAcceptable()) {//TODO}if (selectionKey.isReadable()){//TODO}if(selectionKey.isWritable()&&key.isValid()){//TODO}if (selectionKey.isConnectable()){//TODO}iterator.remove();}

(2)register()方法

public abstract SelectionKey register(Selector sel, int ops, Object att)throws ClosedChannelException;

register() 方法返回SelectionKey對象,在SelectableChannel抽象類中定義如上。參數說明如下

Selector sel

通道注冊的選擇器
int opsinterest集合,表示通過Selector監聽Channel時對什么事件感興趣
Object att這是一個可選參數,在注冊通道時可以附加一個對象,用于之后便于識別某個通道

interest集合有下面4種操作

操作類型描述
SelectionKey.OP_ACCEPT1<<4接收Socket操作
SelectionKey.OP_READ1<<0讀操作
SelectionKey.OP_WRITE1<<2寫操作
SelectionKey.OP_CONNECT1<<3接收Socket操作

注意:通道一般并不會同時支持這四種操作類型,我們可以通過 validOps() 方法獲取通道支持的類型。

(3)select()方法

select有2個重載方法:

①int select():選擇已準備就緒的通道,返回值表示自上一次選擇后有多少新增通道準備就緒;當沒有通道準備就緒時,會一直阻塞下去,直到至少一個通道被選擇、該選擇器的 wakeup() 方法被調用或當前線程被中斷時。select() 方法實際上調用了 select(0L) 方法返回

②int select(long timeout):選擇已準備就緒的通道;當沒有通道準備就緒時,會一直阻塞下去,直到至少一個通道被選擇、該選擇器的 wakeup() 方法被調用、當前線程被中斷或給定時間到期時返回。

除此紫外還可以選擇 selectNow() 方法,該方法為非阻塞方法,無論有無通道就緒都會立即返回。如果自前一次 select 操作后沒有新的通道準備就緒,則會立即返回 0。

(4)SelectionKey

SelectionKey中有下面幾種判斷方法,與操作類型相對應:

boolean isReadable()是否可讀,是返回 true
boolean isWritable()是否可寫,是返回 true
boolean isConnectable()是否可連接,是返回 true
boolean isAcceptable()

是否可接收,是返回 true

selectedKeys() 獲得的是已就緒的通道對應的 SelectionKey。如果想獲得該選擇器上所有通道對應的 SelectionKey,可以通過 keys() 方法獲取。

(5)使用例子

public class NIOServer {public static void main(String[] args) throws IOException {// 獲取通道,并設置為非阻塞ServerSocketChannel ssChannel = ServerSocketChannel.open();ssChannel.configureBlocking(false);// 綁定端口號ssChannel.bind(new InetSocketAddress(9999));// 創建選擇器對象Selector selector = Selector.open();// 將通道注冊到選擇器上,那么選擇器就會監聽通道的接收時間,如果有接收,并且接收準備就緒才開始進行下一步操作ssChannel.register(selector, SelectionKey.OP_ACCEPT);// 通過輪訓的方式獲取選擇器上準備就緒的事件// selector.select()>0表示至少有個selectionKey準備就緒while (selector.select()>0){// 獲取當前選擇器中所有注冊的選擇鍵Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();// 迭代獲取已經準備好的選擇鍵while (iterator.hasNext()){// 獲取已經準備就是的事件SelectionKey sk = iterator.next();if(sk.isAcceptable()){// 調用accpetSocketChannel sChannel = ssChannel.accept();// 將sChannel設置為非阻塞的sChannel.configureBlocking(false);// 將該通道注冊到選擇器上sChannel.register(selector,SelectionKey.OP_READ);}else if(sk.isReadable()){// 如果讀狀態已經準備就是,那么開始讀取數據// 獲取當前選擇器上讀狀態準備就緒的通道SocketChannel sChannel = (SocketChannel)sk.channel();// 創建緩沖區接收客戶端發送過來的數據ByteBuffer buffer = ByteBuffer.allocate(1024);// 讀取緩沖區的數據int len =0;while ((len=sChannel.read(buffer))>0){buffer.flip();System.out.println(new String(buffer.array(),0,len));buffer.clear();}}// 當selectKey使用完之后要溢出,否則會一直優先iterator.remove();}}} }public class NIOClient {public static void main(String[] args) throws IOException {// 獲取通道,默認是阻塞的SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));// 設置通道為非阻塞的sChannel.configureBlocking(false);// 創建緩沖區ByteBuffer buffer = ByteBuffer.allocate(1024);buffer.put("hello".getBytes());// 將緩沖區數據寫入到sChannel中buffer.flip();sChannel.write(buffer);buffer.clear();sChannel.close();} }

?

總結

以上是生活随笔為你收集整理的java之NIO(Channel,Buffer,Selector)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。