Java NIO
NIO與OIO的對比
1.OIO事面向流的,NIO是面向緩衝區的。OIO是面向字節流或字符流的,在一般的OIO操作中,一流式的方法順序地從一個流中讀取一個或多個字節,因此,不能隨意地改變讀取指針的位置。NIO中引入了Channel(通道)和Buffer(緩衝區)的概念。讀取和寫入,只需要從通道中讀取數據到緩衝區中,或將數據從緩衝區中寫入到Channel中。可以隨意地讀取Buffer中任意位置的數據。
2.OIO的操作是阻塞的,而NIO的操作是非阻塞的。
3.OIO沒有選擇器的概念,而NIO有選擇器的概念。
Buffer緩衝區
應用程序與Channel主要的交互操作,就是進行數據的read讀取和write寫入。通道的讀取就是將數據從通道讀取到緩衝區;通道的寫入就是將數據從緩衝區寫入到通道中。
NIO的Buffer(緩衝區)本質是一個內存塊,既可以寫入數據,也可以從中讀取數據。NIO的Buffer類,是一個抽象類,位於java.nio包中,其內部類是一個內存塊(數組)。
Buffer類是一個非線程安全類。
Buffer類是一個抽象類,對應與java的主要數據類型,在NIO中有8種緩衝區類型。分別是:ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer,MappedByteBuffer.
前7種Buffer類型,覆蓋了能在IO中傳輸的所有java基本數據類型。第8種類型MappedByteBuffer是專門用於內存映射的一種ByteBuffer.
Buffer類屬性
Buffer類在其內部有一個byte[]數組內存塊,作爲內存緩衝區。爲了記錄讀寫的狀態和位置,提供了四個成員屬性:capacity(容量),position(讀寫位置),mark(標記)
capacity屬性
Buffer類的capacity屬性,表示內部容量的大小。一旦寫入的對象數量超過了capacity,緩衝區就滿了,不能在寫入了。
capacity容量一旦初始化,就不能再改變。capacity不是指內存塊byte[]數組的字節數量,是指寫入數據對象的數量。
position屬性
Buffer類的position屬性,表示當前的位置。position屬性與緩衝區的讀寫模式有關。
在寫入模式下,position的值變化規則如下:
1.當緩衝區剛開始進入到讀模式時,position會被重置爲0.
2.當從緩衝區讀取時,也是從position位置開始讀。讀取數據後,position向前移動到下一個可讀的位置。
3.position的最大值爲最大可讀上限limit,當position到達limit時,表明緩衝區就沒有空間可以寫了。
在讀模式下,position的值變化規則如下:
1.當緩衝區剋是進入到讀模式時,position會被重置爲0.
2.當從緩衝區讀取時,也是從position位置開始讀。讀取數據後,position先前移動到下一個可讀的位置。
3.position最大的值爲最大可讀上限limit,當position達到limit時,表明緩衝區已經無數據可讀。
limit屬性
Buffer類的limit屬性,表示讀寫的最大上限。limit屬性,也與緩衝區的讀寫模式有關。在不同的模式下,limit的值的含義時不同的。
在寫模式下,limit屬性值的含義爲可以寫入的數量最大上限。在剛進入到寫模式時,limit的值會被設置成緩衝區的capacity容量值,表示可以一直將緩衝區的容量寫滿。
在讀模式下,limit的值含義爲最多能從緩衝區中讀取到多少數據。
Buffer 類的重要方法
allocate()創建緩衝區
在使用Buffer(緩衝區)之前,首先需要獲取Buffer子類的實例對象,並且分配內存空間。爲了獲取一個Buffer實例對象,不能使用子類構造器new創建一個實例對象,而時調用子類的allocate()方法。
put()寫入到緩衝區
在調用allocate方法分配內存,返回了實例對象後,緩衝區實例對象處於寫模式,可以寫入對象。要寫入緩衝區,需要調用put()方法。put方法只有一個參數,即爲所需要的對象。寫入的數據類型要求與緩衝區的類型一致。
filp()翻轉
向緩衝區寫入數據後,不能直接從緩衝區中讀取數據,需要調用filp()方法將寫入模式轉成讀取模式。
get()從緩衝區讀取
調用flip方法,將緩衝區切換成讀取模式。在調用get方法就可以,每次從position的位置讀取一個數據,並且進行相應的緩衝區屬性的調整。
rewind()倒帶
已經讀完的數據,如果需要在讀一遍,可以調用rewind()方法。rewind()方法,主要是調整了緩衝區的position屬性,規則如下:
1.position重置爲0,所有可以重讀緩衝區中的所有數據。
2.limit保持不變,數據量還是一樣的。
3.nark標記被清理,表示之前的臨時位置不能在用了。
mark()和reset()設置position位置
Buffer.mark()方法的作用是將當前position的值保存器來,放在mark屬性中,讓mark屬性記住這個臨時位置。Buffer.reset()方法將mark的值恢復到position中。
clear()清空緩衝區
在讀取模式下,調用clear()方法將緩衝區切換成寫入模式。此方法會將position清零,limit設置爲capaity最大容量值,可以一直寫下去,知道緩衝區寫滿。
通道(Channel)
在OIO中,同一個網絡連接會關聯到兩個流:一個輸入流,另一個是輸出流。通過這兩個流。不斷地進行輸入和輸出操作。
在NIO中,同一個網絡連接使用一個通道表示,所有的NIO的IO操作tong都是從通道開始的。一個通道類似與OIO中兩個流的結合體,既可以從通道讀取,也可以向通道寫入。
常用的Channel有四種具體實現:FileChannel,SocketChannel,ServerSocketChannel,DataGramChannel.
FileChannel:文件通道,用於文件的數據讀寫。
SocketChannel:套接字通道,用於Socket套接字TCP連接的數據讀寫
ServerSocketChannel:服務器嵌套接字通道,允許監聽TCP連接請求,爲每個監聽到的請求,創建一個SocketChannel
DatagramChannel:數據報通道,用於UDP協議的數據讀寫。
FileChannel 文件通道
FileChannel是專門操作文件的通道。通過FileChannel,可以從一個文件讀取數據,也可以將數據寫入到文件中。FileChannel是阻塞模式,不能設置爲非阻塞模式。
獲取FileChannel通道
可以通過文件的輸入流,輸出流獲取FileChannel文件通道。也可以通過RandomAccessFile文件隨機訪問類,獲取FileChannel文件通道。
讀取FileChannel通道
從通道讀取數據都會調用通道的int read(ByteBuffer buf) 方法,從通道讀取到數據寫入到ByteBuffer緩衝區,並且返回讀取到的數據量
注意:對於Channel來說是讀取數據,但是對於ByteBuffer緩衝區來說是寫入數據,這時候ByteBuffer緩衝區處於寫入模式。
寫入FileChannel
寫入數據到通道,都會調用通道的int write(ByteBuffer buf) write方法的作用是,從ByteBuffer緩衝區中讀取數據,然後寫入到通道自身,而返回值是寫入成功的字節數。
注意:此時的ByteBuffer緩衝區要求的是可讀的,處於讀模式下。
關閉通道
當通道使用完成後,必須將器關閉。調用close方法即可。
強制刷新到磁盤
將緩衝區寫入通道是,由於性能原因,操作系統不可能每次都實時將數據寫入磁盤。如果需要保證寫入通道的緩衝數據,最終都真正的寫入磁盤。可以調用FileChannel的force()方法。
SocketChannel和ServerSocketChannel套接字通道
在NIO中,涉及網絡連接的通道有兩個,一個是SocketChannel複製連接傳輸,另一個是ServerSocketChannel負責連接的監聽。
ServerSocketSocket應用與服務器端,而SocketChannel同時處於服務器端和客戶端。
無論是ServerSocketChannel,還是SocketChannel,都支持阻塞和非阻塞兩種模式。設置方法如下:
1.socketChannel.configureBlocking(false)//設置爲非阻塞模式
2.socketChannel.configureBlocking(true)
阻塞模式下,SocketChannel通道的connect連接,read,write都是同步和阻塞時的,與OIO相同。所有下面的以非阻塞的特點。
獲取SocketChannel傳輸通道
客戶端
通過SocketChannel靜態方法open()獲得一個套接字傳輸通道;然後將socket設置成爲非阻塞的;最後通過connect()實例化方法,對服務器IP和端口發起連接。
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1",80));
while(! socketChannel.finishConnect()){
//不斷自旋 等待。或者做一些其他的事情
}
服務端‘
當新連接事件到來時,在服務器端的ServerSocketChannel能夠成功查詢出一個新連接事件,並且通過調用服務器端ServerSocketChannel監聽套接字accpet()方法,來獲取連接套接字通道。
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
SocketChannel accept = serverSocketChannel.accept();
accept.configureBlocking(false);
讀取SocketChannel傳輸通道
當SocketChannel可讀時,可以從SocketChannel讀取數據,調用read方法將數據讀入緩衝區
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = accept.read(buffer);
非阻塞的讀取數據請開Selector章節
寫入SocketChannel傳輸通道
調用讀方法
buffer.flip();//將緩衝區變成讀取模式
socketChannel.write(buffer)
關閉SocketChannel傳輸通道
在關閉SockerChannel傳輸通道籤,如果傳輸通道用來寫入數據,則建議一次shutdownOutput()種植輸出方法,向對方發送一個輸出結束標誌(-1).然後調用socketChannel.close()方法,關閉套接字連接。
DatagramChannel數據報通道
DatagramChannel是UDP傳輸的。只需要知道對方的IP和端口就可傳輸數據
獲取DatagramChannel
DatagramChannel channel = DatagramChannel.open();
channel.configureBlocking(false);
channel.socket().bind(new InetSocketAddress(15555));
讀取DatagramChannel
阻塞狀態:需要調用SocketAddress receive(ByteBuffer buf)
ByteBuffer buffer = ByteBuffer.allocate(1222);
SocketAddress socketAddress = channel.receive(buffer);
非阻塞狀態見Select選擇器
寫入DatagramChannel
調用send(方法)
buffer.flip();
channel.send(buffer,socketAddress);
buffer.clear();
Selector選擇器
爲了實現IO多路複用,首先把Channel註冊到Selector選擇器中,然後通過選擇器內部機制,可以查詢select這些註冊的通道是否有已經就緒的IO事件。
與OIO相比,使用選擇器的最大優勢: 系統開銷小,系統不必爲每一個網絡連接(文件描述符)創建進程/線程,從而大大減小了系統的開銷。
一個單線程處理一個Selector選擇器,一個選擇器可以監控很多Channel。通過Selector,一個線程可以處理數百,數千,數萬,甚至更多的通道。
Channel和Selecotr之間的關係,通過register的方法完成。調用Channel的register(Selector sel,int ops)方法
可以選擇Selelctor的Channel的IO事件類型,包括以下四種:
1.可讀:SelectionKey.OP_READ
2.可寫:SelectionKey.OP_WRITE
3.連接:SelectionKey.OP_CONNECT
4.接收:SelectionKey.OP_ACCEPT
事件類型定義在SlectionKey類中。如果選擇器要監控Channel的多種事件,可以用“|”來實現
int key = SelectionKey.OP_READ | SelectorKey.OP_WRITE
SelectableChannel 可選通道
不是所有的Channel都能被Selelctor選擇器監控的。只有繼承了SelectableChannel,纔可以被選擇。
SelectionKey選擇鍵
Channel和Selector的監控關係註冊後,就可以選擇就緒事件。具體的選擇工作,和調用選擇器Selector的select()方法來完成。通過select方法,選擇器可以不斷地選擇Channel中所發生操作的就緒狀態,返回註冊過的感興趣的那些IO事件。
SelectionKey是那些被Selector選中的IO事件。一個IO事件發生後,如果之前在Selector中註冊過 ,就會被Selector選中,並放入SelelctorKey集合中;如果沒有註冊過,即使發生了IO事件,也不會被Selector選中。
Selector使用流程
步驟如下:
1. 獲取選擇器實例
2.將通道註冊到選擇器中;
3.輪詢註冊的IO就緒事件
Selector 的類方法open()的內部,是向選擇器SPI(SelecotrProvider)發出請求,通過默認的SelectorProvide對象,獲取一個新的Selector實例。Java中SPI全稱爲(Service Provider Interface,服務提供者接口),是jdk的一種可以擴展的服務提供和發現機制。
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(99999));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(selector.select()>0){
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()){
//io事件 ServerSocketChannel服務器監聽通道有新連接
}else if (selectionKey.isConnectable()){
//IO事件 傳輸通道連接成功
}else if (selectionKey.isReadable()){
//IO事件 傳輸通道可讀
}else if (selectionKey.isWritable()){
//IO事件 傳輸通道可讀
}
iterator.remove();
}
}
參考書籍
《Netty,Redis,Zookeeper高併發實戰》