java之NIO(Channel,Buffer,Selector)
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? java之NIO
1 什么是NIO
Java NIO (New IO,Non-Blocking IO)是從Java 1.4版本開始引入的一套新的IO API。NIO支持面向緩沖區(qū)的、基于通道的IO操作。NIO的三大核心部分:通道(Channel),緩沖區(qū)(Buffer), 選擇器(Selector),數(shù)據(jù)總是從通道讀取到緩沖區(qū),或者從緩沖區(qū)寫入到通道中,選擇器用于監(jiān)聽多個通道事件,如連接打開,數(shù)據(jù)到達等。Java NIO系統(tǒng)的核心在于:通道(Channel)和緩沖區(qū)(Buffer),Channel負責傳輸,Buffer負責存儲數(shù)據(jù)。
BIO與NIO的理解:傳統(tǒng)IO即BIO在進行數(shù)據(jù)傳輸時必須要建立一個連接才能進行數(shù)據(jù)的寫入和讀取??梢园蓴?shù)據(jù)理解為水流,需要有管道,可以認為應用程序和文件之間的連接就是一個管道用來運輸水流。輸入流和輸出流是不同的管道,他們是單向的。NIO在進程應用程序和文件之間數(shù)據(jù)傳輸時他們的連接不能理解為管道,他有個概念為“通道”,可以理解為鐵軌,還有“緩沖區(qū)”可以理解為火車。起到運輸作用,但是本事不能進行運輸數(shù)據(jù),數(shù)據(jù)的運輸需要借助于火車。當我們要讀取磁盤文件的時候數(shù)據(jù)會先加載到緩沖區(qū),然后傳輸?shù)綉贸绦颉?/p>
2 BIO與NIO的區(qū)別
(1)BIO是面向流,流是單向的。每次從流中讀取一個或者多個字節(jié),直到讀取完所有字節(jié),沒有被緩存起來,不能前后移動流中的數(shù)據(jù),如果想要能前后移動的話需要將他緩存到另外一個緩沖區(qū);NIO是面向緩沖區(qū)的,通道可以將數(shù)據(jù)讀取到緩存區(qū)實現(xiàn)雙向傳輸。NIO是將數(shù)據(jù)讀取到一個稍后處理的緩沖區(qū),并且在需要的時候可以前后移動。
(2)BIO是阻塞式,一個線程調(diào)用read()或者write()的時候這個線程被阻塞,直到數(shù)據(jù)被讀取或者完全寫入,不能再干其他事情;NIO是非阻塞式,一個線程從一個通道發(fā)送請求讀取數(shù)據(jù),只能獲取到目前可用的,沒數(shù)據(jù)可用就什么都不會獲取,不保持阻塞,直到數(shù)據(jù)變得可以讀取之前,這個線程可以做其他事,寫也是這樣。非阻塞IO的線程在空閑時間作用在其他通道上執(zhí)行IO操作,那么一個線程就可以管理多個輸入輸出通道。
(3)BIO傳輸?shù)氖亲止?jié)流或字符流,NIO是通過塊傳輸。
面向文件IO的區(qū)別:BIO是面向流的,NIO是面向緩沖區(qū)的。面向網(wǎng)絡IO的區(qū)別:BIO是阻塞的,NIO是非阻塞的,并且NIO有選擇器
?
3 緩沖區(qū)(Buffer)
3.1?緩沖區(qū)相關概念
通道表示IO源到 IO 設備(例如:文件、套接字)的連接。若需要使用 NIO 系統(tǒng),需要獲取用于連接 IO 設備的通道以及用于容納數(shù)據(jù)的緩沖區(qū)。然后操作緩沖區(qū),對數(shù)據(jù)進行處理。
緩沖區(qū)(Buffer):Buffer主要用于與Channel交互,數(shù)據(jù)從Channel寫入Buffer,然后再從Buffer寫出到Channel。是一個用于特定基本數(shù)據(jù)類型(除boolean型外)的容器,底層使用數(shù)組存儲,可以保存多個相同類型的數(shù)據(jù)。所有緩沖區(qū)都是java.nio.buffer的子類,常見的子類有ByteBuffer,CharBuffer,IntBuffer,DoubleBuffer,ShortBuffer,LongBuffer,FloatBuffer等,他們管理數(shù)據(jù)的方法都相似,管理的類型不同而已。
Buffer的實現(xiàn)類都是通過allocate(int,capacity)創(chuàng)建一個容量為capacity的對象。Buffer有以下基本屬性:
| 容量(capacity) | 標識Buffer存儲的最大數(shù)據(jù)容量,聲明后不能更改,不能為負,通過capacity()獲取 |
| 限制(limit) | 第一個不應該讀取或?qū)懭氲臄?shù)據(jù)的索引,也就是limit后的數(shù)據(jù)不可以讀寫,不能為負,不能大于capacity,通過limit()獲取 |
| 位置(position) | 當前要讀取或者寫入數(shù)據(jù)的索引,不能為負,不能大于limit,通過position()獲取 |
| 標記(mark) | 標記是一個索引,通過mark()標記后,可以通過調(diào)用reset()將position恢復到標記的mark處 |
上述屬性的范圍大小為:?0 <= mark <= position <= limit <= capacity
3.2?緩沖區(qū)的基本操作
緩沖區(qū)為所有的子類提供了兩個用于數(shù)據(jù)操作的方法put和get方法,如ByteBuffer的這兩個方法如下
| 方法 | 說明 |
| put(byte b) | 將指定的單個字節(jié)寫入緩沖區(qū)的當前位置 |
| put(byte[] buf) | 將buf中的字節(jié)寫入緩沖區(qū)的當前位置 |
| put(int index,byte b) | 將指定字節(jié)寫入緩沖區(qū)的索引位置,不移動position |
| get() | 讀取單個字節(jié) |
| get(byte[] buf) | 批量讀取多個字節(jié)到buf中 |
| get(int index) | 讀取指定索引位置的字節(jié),不移動position |
Buffer其他常用方法
| 方法 | 說明 |
| Buffer flip() | 將limit設置為當前position,position設置為0,mark設置為-1 |
| Buffer rewind() | 將position設置為0,mark設置為-1,可以重復讀 |
| Buffer clear() | 將limit設置為capacity,position設置為0,mark設置為-1,數(shù)據(jù)沒有清空 |
| Buffer mark() | 設置緩沖區(qū)的mark |
| Buffer reset() | 將當前位置的position轉(zhuǎn)到之前設置的mark的位置 |
| Buffer hasRemaining() | 判斷緩沖區(qū)中是否還有元素 |
| int remaining | 返回position和limit之間元素的個數(shù) |
| Xxx[] array() | 返回XxxBuffer底層的Xxx數(shù)組 |
| int capacity() | 返回Buffer的capacity大小 |
| int limit() | 返回Buffer的limit位置 |
| Buffer limit(int n) | 將設置緩沖區(qū)界限為 n, 并返回一個具有新 limit 的緩沖區(qū)對象 |
| int position() | 返回Buffer的position位置 |
| Buffer position(int n) | 將設置緩沖區(qū)的當前位置為 n , 并返回修改后的 Buffer 對象 |
說明:①當我們調(diào)用ByteBuffer.allocate(10)方法創(chuàng)建了一個10個byte的數(shù)組的緩沖區(qū),position的位置為0,capacity和limit默認都是數(shù)組長度。②當通過put方法寫入5個字節(jié)到緩沖區(qū)時,position更新為5。③需要將緩沖區(qū)中的5個字節(jié)數(shù)據(jù)寫入Channel的通信信道,調(diào)用ByteBuffer.flip()方法,變化為position設回0,并將limit設成之前的position的值④這時底層操作系統(tǒng)就可以從緩沖區(qū)中正確讀取這個5個字節(jié)數(shù)據(jù)并發(fā)送出去了。在下一次寫數(shù)據(jù)之前我們再調(diào)用clear()方法,緩沖區(qū)的索引位置又回到了初始位置。
注意:clear()是把position設回0,limit設置成capacity,換句話說,其實Buffer中的數(shù)據(jù)沒有清空,只是這些標記告訴我們可以從哪里開始往Buffer里寫數(shù)據(jù)。如果Buffer中有一些未讀的數(shù)據(jù),調(diào)用clear()方法,數(shù)據(jù)將丟棄,那就沒有標記說明哪些數(shù)據(jù)讀過,哪些還沒有。如果還需要Buffer中未讀的數(shù)據(jù),但是還想要先寫些數(shù)據(jù),那么使用compact()方法。compact()方法將所有未讀的數(shù)據(jù)拷貝到Buffer起始處。然后將position設到最后一個未讀元素正后面。limit屬性依然像clear()方法一樣,設置成capacity?,F(xiàn)在Buffer準備好寫數(shù)據(jù)了,但是不會覆蓋未讀的數(shù)據(jù)。
public static void main(String[] args) {/*** 通過allocate()獲取緩沖區(qū),緩沖區(qū)主要有2個核心方法:put()將輸入存入緩沖區(qū),get()獲取緩沖區(qū)數(shù)據(jù)*/// 獲取緩沖區(qū)ByteBuffer buffer = ByteBuffer.allocate(1024);System.out.println("capacity:"+ buffer.capacity()+"\t position:"+buffer.position()+"\t limit:"+buffer.limit());// 將數(shù)據(jù)存入緩沖區(qū)String str = "hello";buffer.put(str.getBytes());System.out.println("capacity:"+ buffer.capacity()+"\t position:"+buffer.position()+"\t limit:"+buffer.limit());// 獲取緩沖區(qū)數(shù)據(jù),要獲取緩存區(qū)的數(shù)據(jù)需要flip()切換緩沖區(qū)的模式buffer.flip();System.out.println("capacity:"+ buffer.capacity()+"\t position:"+buffer.position()+"\t limit:"+buffer.limit());// 創(chuàng)建字節(jié)數(shù)據(jù)接收數(shù)據(jù)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:53.3?直接緩沖區(qū)和非直接緩沖區(qū)
緩沖區(qū)分為直接緩沖區(qū)和非直接緩存區(qū):①非直接緩沖區(qū):硬盤-->系統(tǒng)的緩沖區(qū)-->copy-->JVM緩沖區(qū)-->程序②直接緩沖區(qū):需要copy,JVM和緩沖區(qū)實現(xiàn)映射。
直接字節(jié)緩沖區(qū), Java 虛擬機會盡最大努力直接在此緩沖區(qū)上執(zhí)行本機 I/O 操作。也就是說在每次調(diào)用基礎操作系統(tǒng)的一個本機 I/O 操作之前(或之后),虛擬機都會盡量避免將緩沖區(qū)的內(nèi)容復制到中間緩沖區(qū)中(或從中間緩沖區(qū)中復制內(nèi)容)。?
直接字節(jié)緩沖區(qū)可以通過調(diào)用ByteBuffer的 allocateDirect() 工廠方法來創(chuàng)建。此方法返回的緩沖區(qū)進行分配和取消分配所需成本通常高于非直接緩沖區(qū)。直接緩沖區(qū)的內(nèi)容可以駐留在常規(guī)的垃圾回收堆之外,因此,它們對應用程序的內(nèi)存需求量造成的影響可能并不明顯。所以,建議將直接緩沖區(qū)主要分配給那些易受基礎系統(tǒng)的本機 I/O 操作影響的大型、持久的緩沖區(qū)。一般情況下,最好僅在直接緩沖區(qū)能在程序性能方面帶來明顯好處時分配它們。?
直接字節(jié)緩沖區(qū)還可以通過 FileChannel 的 map() 方法 將文件區(qū)域直接映射到內(nèi)存中來創(chuàng)建。該方法返回ByteBuffer的子類:MappedByteBuffer 。Java 平臺的實現(xiàn)有助于通過 JNI 從本機代碼創(chuàng)建直接字節(jié)緩沖區(qū)。如果以上這些緩沖區(qū)中的某個緩沖區(qū)實例指的是不可訪問的內(nèi)存區(qū)域,則試圖訪問該區(qū)域不會更改該緩沖區(qū)的內(nèi)容,并且將會在訪問期間或稍后的某個時間導致拋出不確定的異常。?
?
非直接緩沖區(qū)如上,假設應用程序想要在磁盤中讀取一些數(shù)據(jù)的話。應用程序首先發(fā)起一個請求,要去讀取當前物理磁盤里面的數(shù)據(jù),這個時候需要將物理磁盤的數(shù)據(jù)首先讀到內(nèi)核空間中,然后拷貝一份到用戶空間,然后才能通過read的方法將數(shù)據(jù)讀到應用程序中。同樣的應用程序中有數(shù)據(jù)的話需要先寫入到用戶地址空間,然后復制到內(nèi)核地址空間,再由內(nèi)核空間寫入到物理磁盤。在這個過程中,這兩個復制的操作比較多余,所以他的效率比較低一些,也就是將我們的緩沖區(qū)建立在jvm的內(nèi)存中相對效率更低。
直接字節(jié)緩沖區(qū)如上圖,直接緩沖區(qū)不需要拷貝,是將我們的數(shù)據(jù)直接在物理內(nèi)存中建立一個映射文件,將數(shù)據(jù)寫到這個文件里面去,這個時候我們應用程序要寫一些數(shù)據(jù)的話直接寫到這個映射文件。操作系統(tǒng)就會將這個寫到物理磁盤中。讀磁盤數(shù)據(jù)同理。這個過程就沒有中間的copy就會比較高。
字節(jié)緩沖區(qū)是直接緩沖區(qū)還是非直接緩沖區(qū)可通過調(diào)用其 isDirect() 方法來確定。非直接緩沖區(qū):通過allocate方法分區(qū)緩存區(qū),將緩存區(qū)建立在JVM的內(nèi)存中
直接緩存區(qū):通過allocateDircet方法分區(qū)緩沖區(qū),將緩沖區(qū)建立在物理內(nèi)存中,效率更高。
到allocateDirect和allocate創(chuàng)建的源碼中,發(fā)現(xiàn)allocate創(chuàng)建的是一個HeapByteBuffer,Heap堆其實就是表示用戶空間,在jvm內(nèi)存中創(chuàng)建的一個緩沖區(qū)。
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 緩沖區(qū)使用
Buffer使用一般遵循以下幾個原則:
①分配空間,如ByteBuffer buffer = ByteBuffer.allocate(1024);或者使用allocateDirector
②將數(shù)據(jù)寫入到Buffer中 int readBuffer = inChannel.read(buffer);
③調(diào)用flip()方法,將limit設置為當前position,position設置為0,mark設置為-1
④從Buffer中讀取數(shù)據(jù) readBuffer = inChannel.read(buffer);
⑤調(diào)用clear()(將limit設置為capacity,position設置為0,mark設置為-1,數(shù)據(jù)沒有清空)方法或者compact()方法
?
4 通道(Channel)
Channel表示IO源于目標節(jié)點打開的連接,類似于傳統(tǒng)的流,但是Channel不直接存儲數(shù)據(jù),Channel只能與Buffer進行交互。
以非直接緩沖區(qū)為例,應用程序獲取數(shù)據(jù)需要經(jīng)過用戶空間,然后內(nèi)核空間,再讀取數(shù)據(jù),所有的讀取操作在NIO是直接由CPU負責的。這個流程會存在一些問題,當我們有大量的文件讀取操作的時候cpu他的利用就很低,因為IO操作直接搶占CPU的資源,就不能夠去做其他的事情,所以他的效率就會變低。
計算機CPU和內(nèi)存的交互是最頻繁的,內(nèi)存是我們的高速緩存區(qū),CPU運轉(zhuǎn)速度越來越快,磁盤遠遠跟不上CPU的讀寫速度,才設計了內(nèi)存。這里把CPU的連接干掉了,變成了DMA(Direct Memory Access,直接內(nèi)存存取器),就是直接內(nèi)存存儲。如果要讀取數(shù)據(jù),所以的操作是直接在當前DMA這里直接完成,不再有CPU去進行負責。但是得到DMA還是需要由當前的CPU進行相關的調(diào)度。在這里交給了DMA之后,CPU就能做其他的事,但是如果依然有大量的IO操作的時候又會造成DMA總線的擁堵,因為最終沒有直接和CPU撇開關系。導致在大量的文件讀取請求的時候依然利用率比較低,這個時候就出現(xiàn)了新的數(shù)據(jù)讀寫流程,這個時候就出現(xiàn)了channel通道。
把DMA換成了通道channel。通道channel可以認為他是一個完全獨立的處理器,他就是用來專門負責文件的IO操作的,也就是說以后所有的數(shù)據(jù)直接交給channel去進行負責讀取。這個時候CPU才算是真正的解放了。
java為Channel接口提供的最主要的實現(xiàn)類如下:①FileChannel:用于讀取,寫入、映射和操作文件的通道②SocketChannel:通過TCP讀取網(wǎng)絡中的數(shù)據(jù)③ServerSocketChannel:可以監(jiān)聽新進來的TCP連接,對每個新進來的連接都會創(chuàng)建一個SocketChannel④DatagramChannel:通過UDP讀寫網(wǎng)絡中的數(shù)據(jù)通道
獲取通道的三種方式:①對支持通道的對象調(diào)用getChannel(),支持通道的類有:FileInputStream,FileOutputStream,RandomAccessFile,Socket,ServerSocket,DatagramSocket②通過XxxChannel的靜態(tài)方法open()打開并返回指定的XxxChannel③使用Files工具類的靜態(tài)方法newByteChannel()獲取字節(jié)通道。
FileChannel常用方法
| 方法 | 描述 |
| int read(ByteBuffer dst) | 從Channel中讀取數(shù)據(jù)到ByteBuffer |
| long read(ByteBuffer[] dsts) | 將Channel中的數(shù)據(jù)“分散”到ByteBuffer[] |
| int write(ByteBuffer src) | 將ByteBuffer的數(shù)據(jù)寫入到Channel |
| long write(ByteBuffer[] srcs) | 將ByteBuffer[]的數(shù)據(jù)"聚集"到Channel |
| MappedByteBuffer map(MapMode mode,long position,long size) | 將Channel對應的部分數(shù)據(jù)或者全部數(shù)據(jù)映射到ByteBuffer |
| long position() | 返回次通道的文件位置 |
| FileChannel position(long p) | 設置此通道的文件位置 |
| long size() | 返回此通道的文件大小 |
| FileChannel truncate(long s) | 將此通道的文件截取為給定大小 |
| void force(boolean metadata) | 強制將所有對此通道的文件更新寫入到存儲設備中 |
分散(Scatter)讀取和聚集(Gather)寫入:①分散讀取(Scattering Reads是指從Channel中讀取的數(shù)據(jù)“分散”到多個Buffer中,注意:按照緩沖區(qū)的順序,從 Channel 中讀取的數(shù)據(jù)依次將Buffer填滿。②聚集寫入(Gathering Writes)是指將多個Buffer中的數(shù)據(jù)“聚集”到Channel,注意:按照緩沖區(qū)的順序,寫入position和limit之間的數(shù)據(jù)到Channel。
NIO的強大功能部分來自于Channel的非阻塞特性,套接字的某些操作可能會無限期地阻塞。如對accept()方法的調(diào)用可能會因為等待一個客戶端連接而阻塞;對read()方法的調(diào)用可能會因為沒有數(shù)據(jù)可讀而阻塞,直到連接的另一端傳來新的數(shù)據(jù)??偟膩碚f,創(chuàng)建/接收連接或讀寫數(shù)據(jù)等I/O調(diào)用,都可能無限期地阻塞等待,直到底層的網(wǎng)絡實現(xiàn)發(fā)生了什么。慢速的,有損耗的網(wǎng)絡,或僅僅是簡單的網(wǎng)絡故障都可能導致任意時間的延遲。然而不幸的是,在調(diào)用一個方法之前無法知道其是否阻塞。NIO的channel抽象的一個重要特征就是可以通過配置它的阻塞行為,以實現(xiàn)非阻塞式的信道。 channel.configureBlocking(false)
?在非阻塞式信道上調(diào)用一個方法總是會立即返回。這種調(diào)用的返回值指示了所請求的操作完成的程度。例如,在一個非阻塞式ServerSocketChannel上調(diào)用accept()方法,如果有連接請求來了,則返回客戶端SocketChannel,否則返回null。
對比傳統(tǒng)IO和NIO的代碼
/*** 傳統(tǒng)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();}}}對比直接緩沖區(qū)與內(nèi)存映射文件操作
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);// 創(chuàng)建緩沖區(qū)ByteBuffer buffer = ByteBuffer.allocate(1024);// 將輸入通道的數(shù)據(jù)寫入緩沖區(qū)while (inChannel.read(buffer)!=-1){buffer.flip();// 將緩沖區(qū)數(shù)據(jù)寫入輸出通道outChannel.write(buffer);// 清空緩沖區(qū)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);// 進行內(nèi)存映射文件MappedByteBuffer inMapBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());MappedByteBuffer outMapBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, outChannel.size());// 對緩沖區(qū)進行讀寫操作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:75 選擇器(Selector )
5.1 相關概念
NIO和BIO有一個非常大的區(qū)別是BIO是阻塞的,NIO是非阻塞的。阻塞與非阻塞是相對于網(wǎng)絡通信而言的。網(wǎng)絡通信就會有客戶端的概念??蛻舳艘蚍斩税l(fā)送數(shù)據(jù)的話必須建立連接,在這個過程中會做一些相關的事情,如accpet等待連接,然后客戶端write數(shù)據(jù),服務端read數(shù)據(jù)。這些操作在傳統(tǒng)的套接字socket里面都是阻塞式的。服務端一次只能接待一個客戶端,不能一下多個的客戶端。也就是客戶端請求服務器做些事情的時候,這個客戶端沒有處理完,其他客戶端的請求是進不來的。這種就是阻塞式的,所以服務端如果是這種模型的話,他的效率是非常低的。
要解決這種阻塞就要通過多線程的方式解決,但是線程資源是有限的,那就極大的限制了服務端他的處理效率。這就是經(jīng)典的C10K問題,假如有C10K,就需要創(chuàng)建1W個進程。在NIO中非阻塞的網(wǎng)絡通信模型Selector就能解決這個問題。
系統(tǒng)線程的切換是消耗系統(tǒng)資源的,如果我們每一個連接都用一個線程來管理,資源的開銷會非常大,這個時候就可以用Selector。通過Selector可以實現(xiàn)一個線程管理多個Channel,如果你的應用打開了多個通道,但每個連接的流量都很低,使用Selector就會很方便。例如在一個聊天服務器中。要使用Selector, 得向Selector注冊Channel,然后調(diào)用它的select()方法。這個方法會一直阻塞到某個注冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件(如新的連接進來、數(shù)據(jù)接收等)。Selector 的意義在于只通過一個線程就可以管理成千上萬個 I/O 請求, 相比使用多個線程,避免了線程上下文切換帶來的開銷。
Selector是怎么工作的呢?有了Selector之后,Selector會把每一個客戶端和服務端傳輸數(shù)據(jù)的通道都到Selector上去注冊一下。也就是以后你想向服務端發(fā)送數(shù)據(jù),通道先到Selector選擇器上注冊一下,那么Selector就會監(jiān)控當前channel的IO狀況(讀,寫,連接,接受處理等情況)只有當某個channel上的數(shù)據(jù)完全準備就緒,Selector才會把這樣一個channel里面的任務分配到服務端來進行運行。當我們客戶端要給服務端發(fā)送數(shù)據(jù)的時候,channel需要在Selector上進行注冊,當channel的數(shù)據(jù)完全準備就緒的時候Selector才會將任務分配給服務端的一個線程進行處理。這種非阻塞式的相較于阻塞式的就能非常好的利用cpu的資源,提高cpu的工作效率。
一個Selector實例可以同時檢查一組信道的I/O狀態(tài)。用專業(yè)術語來說,選擇器就是一個多路開關選擇器,因為一個選擇器能夠管理多個信道上的I/O操作。然而如果用傳統(tǒng)的方式來處理這么多客戶端,使用的方法是循環(huán)地一個一個地去檢查所有的客戶端是否有I/O操作,如果當前客戶端有I/O操作,則可能把當前客戶端扔給一個線程池去處理,如果沒有I/O操作則進行下一個輪詢,當所有的客戶端都輪詢過了又接著從頭開始輪詢;這種方法是非常笨而且也非常浪費資源,因為大部分客戶端是沒有I/O操作,我們也要去檢查;而Selector就不一樣了,它在內(nèi)部可以同時管理多個I/O,當一個信道有I/O操作的時候,他會通知Selector,Selector就是記住這個信道有I/O操作,并且知道是何種I/O操作,是讀呢?是寫呢?還是接受新的連接;所以如果使用Selector,它返回的結果只有兩種結果,一種是0,即在你調(diào)用的時刻沒有任何客戶端需要I/O操作,另一種結果是一組需要I/O操作的客戶端,這時你就根本不需要再檢查了,因為它返回給你的肯定是你想要的。這樣一種通知的方式比那種主動輪詢的方式要高效得多。
使用選擇器,首先創(chuàng)建一個Selector實例(使用靜態(tài)工廠方法open())并將其注冊(register)到想要監(jiān)控的信道上(通過channel的方法實現(xiàn),而不是使用selector的方法)。最后,調(diào)用選擇器的select()方法。該方法會阻塞等待,直到有一個或更多的信道準備好了I/O操作或等待超時。select()方法將返回可進行I/O操作的信道數(shù)量?,F(xiàn)在,在一個單獨的線程中,通過調(diào)用select()方法就能檢查多個信道是否準備好進行I/O操作。如果經(jīng)過一段時間后仍然沒有信道準備好,select()方法就會返回0,并允許程序繼續(xù)執(zhí)行其他任務。
Selector 只能與非阻塞模式下的通道一起使用(即需要實現(xiàn) SelectableChannel 接口),否則會拋出 IllegalBlockingModeException 異常
5.2 Selector使用
(1)使用步驟
①創(chuàng)建Selector
②向Selector注冊通道,一個Selector可以注冊多個通道
③通過Selector選擇就緒的通道
// 通過open()方法創(chuàng)建 SelectorSelector selector = Selector.open();// 創(chuàng)建一個通道,以ServerSockeetChannel為例,并且將通道設置為非阻塞模式ServerSocketChannel channel = ServerSocketChannel.open();channel.configureBlocking(false);// 通過register()方法注冊通道channel.register(selector, SelectionKey.OP_ACCEPT);// 通過select()方法從多個通道中以輪詢的方式選擇已經(jīng)準備就緒的通道。根據(jù)之前register()方法中設置的興趣,將可以進行對應操作的通道選擇出來selector.select();// 通過Selector的selectedKeys()方法獲得已選擇鍵集(selected-key set)Set key = selector.selectedKeys();//通過Iterator迭代器依次獲取key中的SelectionKey對象,并通過SelectionKey中的判斷方法執(zhí)行對應的操作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抽象類中定義如上。參數(shù)說明如下
| Selector sel | 通道注冊的選擇器 |
| int ops | interest集合,表示通過Selector監(jiān)聽Channel時對什么事件感興趣 |
| Object att | 這是一個可選參數(shù),在注冊通道時可以附加一個對象,用于之后便于識別某個通道 |
interest集合有下面4種操作
| 操作類型 | 值 | 描述 |
| SelectionKey.OP_ACCEPT | 1<<4 | 接收Socket操作 |
| SelectionKey.OP_READ | 1<<0 | 讀操作 |
| SelectionKey.OP_WRITE | 1<<2 | 寫操作 |
| SelectionKey.OP_CONNECT | 1<<3 | 接收Socket操作 |
注意:通道一般并不會同時支持這四種操作類型,我們可以通過 validOps() 方法獲取通道支持的類型。
(3)select()方法
select有2個重載方法:
①int select():選擇已準備就緒的通道,返回值表示自上一次選擇后有多少新增通道準備就緒;當沒有通道準備就緒時,會一直阻塞下去,直到至少一個通道被選擇、該選擇器的 wakeup() 方法被調(diào)用或當前線程被中斷時。select() 方法實際上調(diào)用了 select(0L) 方法返回
②int select(long timeout):選擇已準備就緒的通道;當沒有通道準備就緒時,會一直阻塞下去,直到至少一個通道被選擇、該選擇器的 wakeup() 方法被調(diào)用、當前線程被中斷或給定時間到期時返回。
除此紫外還可以選擇 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));// 創(chuàng)建選擇器對象Selector selector = Selector.open();// 將通道注冊到選擇器上,那么選擇器就會監(jiān)聽通道的接收時間,如果有接收,并且接收準備就緒才開始進行下一步操作ssChannel.register(selector, SelectionKey.OP_ACCEPT);// 通過輪訓的方式獲取選擇器上準備就緒的事件// selector.select()>0表示至少有個selectionKey準備就緒while (selector.select()>0){// 獲取當前選擇器中所有注冊的選擇鍵Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();// 迭代獲取已經(jīng)準備好的選擇鍵while (iterator.hasNext()){// 獲取已經(jīng)準備就是的事件SelectionKey sk = iterator.next();if(sk.isAcceptable()){// 調(diào)用accpetSocketChannel sChannel = ssChannel.accept();// 將sChannel設置為非阻塞的sChannel.configureBlocking(false);// 將該通道注冊到選擇器上sChannel.register(selector,SelectionKey.OP_READ);}else if(sk.isReadable()){// 如果讀狀態(tài)已經(jīng)準備就是,那么開始讀取數(shù)據(jù)// 獲取當前選擇器上讀狀態(tài)準備就緒的通道SocketChannel sChannel = (SocketChannel)sk.channel();// 創(chuàng)建緩沖區(qū)接收客戶端發(fā)送過來的數(shù)據(jù)ByteBuffer buffer = ByteBuffer.allocate(1024);// 讀取緩沖區(qū)的數(shù)據(jù)int len =0;while ((len=sChannel.read(buffer))>0){buffer.flip();System.out.println(new String(buffer.array(),0,len));buffer.clear();}}// 當selectKey使用完之后要溢出,否則會一直優(yōu)先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);// 創(chuàng)建緩沖區(qū)ByteBuffer buffer = ByteBuffer.allocate(1024);buffer.put("hello".getBytes());// 將緩沖區(qū)數(shù)據(jù)寫入到sChannel中buffer.flip();sChannel.write(buffer);buffer.clear();sChannel.close();} }?
總結
以上是生活随笔為你收集整理的java之NIO(Channel,Buffer,Selector)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: css3案例分析,CSS3动画/动画库以
- 下一篇: Android 利润,惊人利润:Andr