Java NIO(Netty,Redis,Zookeeper高併發實戰整理)

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高併發實戰》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章