瞭解NIO原理機制

個人博客請訪問 http://www.x0100.top           

NIO和IO到底有什麼區別?有什麼關係?

首先說一下核心區別:

  1. NIO是以塊的方式處理數據,但是IO是以最基礎的字節流的形式去寫入和讀出的。所以在效率上的話,肯定是NIO效率比IO效率會高出很多。

  2. NIO不在是和IO一樣用OutputStream和InputStream 輸入流的形式來進行處理數據的,但是又是基於這種流的形式,而是採用了通道和緩衝區的形式來進行處理數據的。

  3. 還有一點就是NIO的通道是可以雙向的,但是IO中的流只能是單向的。

  4. 還有就是NIO的緩衝區(其實也就是一個字節數組)還可以進行分片,可以建立只讀緩衝區、直接緩衝區和間接緩衝區,只讀緩衝區很明顯就是字面意思,直接緩衝區是爲加快 I/O 速度,而以一種特殊的方式分配其內存的緩衝區。

  5. 補充一點:NIO比傳統的BIO核心區別就是,NIO採用的是多路複用的IO模型,普通的IO用的是阻塞的IO模型,兩個之間的效率肯定是多路複用效率更高

先了解一下什麼是通道,什麼是緩衝區的概念

通道是個什麼意思?

  • 通道是對原 I/O 包中的流的模擬。到任何目的地(或來自任何地方)的所有數據都必須通過一個 Channel 對象(通道)。一個 Buffer 實質上是一個容器對象。發送給一個通道的所有對象都必須首先放到緩衝區中;同樣地,從通道中讀取的任何數據都要讀到緩衝區中。Channel是一個對象,可以通過它讀取和寫入數據。拿 NIO 與原來的 I/O 做個比較,通道就像是流。

  • 正如前面提到的,所有數據都通過 Buffer 對象來處理。您永遠不會將字節直接寫入通道中,相反,您是將數據寫入包含一個或者多個字節的緩衝區。同樣,您不會直接從通道中讀取字節,而是將數據從通道讀入緩衝區,再從緩衝區獲取這個字節。

緩衝區是什麼意思:

  • Buffer 是一個對象, 它包含一些要寫入或者剛讀出的數據。在 NIO 中加入 Buffer 對象,體現了新庫與原 I/O 的一個重要區別。在面向流的 I/O 中,您將數據直接寫入或者將數據直接讀到 Stream 對象中

  • 在 NIO 庫中,所有數據都是用緩衝區處理的。在讀取數據時,它是直接讀到緩衝區中的。在寫入數據時,它是寫入到緩衝區中的。任何時候訪問 NIO 中的數據,您都是將它放到緩衝區中。

  • 緩衝區實質上是一個數組。通常它是一個字節數組,但是也可以使用其他種類的數組。但是一個緩衝區不 僅僅 是一個數組。緩衝區提供了對數據的結構化訪問,而且還可以跟蹤系統的讀/寫進程

緩衝區的類型:

ByteBuffer
CharBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer

NIO的底層工作原理

先來了解一下buffer的工作機制:

  • capacity 緩衝區數組的總長度

  • position 下一個要操作的數據元素的位置

  • limit 緩衝區數組中不可操作的下一個元素的位置,limit<=capacity

  • mark 用於記錄當前 position 的前一個位置或者默認是 0

1.這一步其實是當我們剛開始初始化這個buffer數組的時候,開始默認是這樣的

2、但是當你往buffer數組中開始寫入的時候幾個字節的時候就會變成下面的圖,position會移動你數據的結束的下一個位置,這個時候你需要把buffer中的數據寫到channel管道中,所以此時我們就需要用這個buffer.flip();方法,

3、當你調用完2中的方法時,這個時候就會變成下面的圖了,這樣的話其實就可以知道你剛剛寫到buffer中的數據是在position---->limit之間,然後下一步調用clear();

4、這時底層操作系統就可以從緩衝區中正確讀取這 5 個字節數據發送出去了。在下一次寫數據之前我們在調一下 clear() 方法。緩衝區的索引狀態又回到初始位置。(其實這一步有點像IO中的把轉運字節數組 char[] buf = new char[1024]; 不足1024字節的部分給強制刷新出去的意思)

補充:

1、這裏還要說明一下 mark,當我們調用 mark() 時,它將記錄當前 position 的前一個位置,當我們調用 reset 時,position 將恢復 mark 記錄下來的值

2.clear()方法會:清空整個緩衝區。position將被設回0,limit被設置成 capacity的值(這個個人的理解就是當你在flip()方法的基礎上已經記住你寫入了多少字節數據,直接把position到limit之間的也就是你寫入已經記住的數據給“複製”到管道中)

3.當你把緩衝區的數局寫入到管道中的時候,你需要調用flip()方法將Buffer從寫模式切換到讀模式,調用flip()方法會將position設回0,並將limit設置成之前position的值。buf.flip();(其實我個人理解的就相當於先記住緩衝區緩衝了多少數據)

NIO 工作代碼示例

public void selector() throws IOException {
//先給緩衝區申請內存空間
        ByteBuffer buffer = ByteBuffer.allocate(1024);
     //打開Selector爲了它可以輪詢每個 Channel 的狀態
        Selector selector = Selector.open();
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false);//設置爲非阻塞方式
        ssc.socket().bind(new InetSocketAddress(8080));
        ssc.register(selector, SelectionKey.OP_ACCEPT);//註冊監聽的事件
        while (true) {
            Set selectedKeys = selector.selectedKeys();//取得所有key集合
            Iterator it = selectedKeys.iterator();
            while (it.hasNext()) {
                SelectionKey key = (SelectionKey) it.next();
                if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) {
                    ServerSocketChannel ssChannel = (ServerSocketChannel) key.channel();
                 SocketChannel sc = ssChannel.accept();//接受到服務端的請求
                    sc.configureBlocking(false);
                    sc.register(selector, SelectionKey.OP_READ);
                    it.remove();
                } else if 
                ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) {
                    SocketChannel sc = (SocketChannel) key.channel();
                    while (true) {
                        buffer.clear();
                        int n = sc.read(buffer);//讀取數據
                        if (n <= 0) {
                            break;
                        }
                        buffer.flip();
                    }
                    it.remove();
                }
            }
        }
}

最後給大家看一下整體的NIO的示意圖

NIO和Netty的工作模型對比?

(1)NIO的工作流程步驟:

  1. 首先是先創建ServerSocketChannel 對象,和真正處理業務的線程池

  2. 然後給剛剛創建的ServerSocketChannel 對象進行綁定一個對應的端口,然後設置爲非阻塞

  3. 然後創建Selector對象並打開,然後把這Selector對象註冊到ServerSocketChannel 中,並設置好監聽的事件,監聽 SelectionKey.OP_ACCEPT

  4. 接着就是Selector對象進行死循環監聽每一個Channel通道的事件,循環執行 Selector.select() 方法,輪詢就緒的 Channel

  5. 從Selector中獲取所有的SelectorKey(這個就可以看成是不同的事件),如果SelectorKey是處於 OP_ACCEPT 狀態,說明是新的客戶端接入,調用 ServerSocketChannel.accept 接收新的客戶端。

  6. 然後對這個把這個接受的新客戶端的Channel通道註冊到ServerSocketChannel上,並且把之前的OP_ACCEPT 狀態改爲SelectionKey.OP_READ讀取事件狀態,並且設置爲非阻塞的,然後把當前的這個SelectorKey給移除掉,說明這個事件完成了

  7. 如果第5步的時候過來的事件不是OP_ACCEPT 狀態,那就是OP_READ讀取數據的事件狀態,然後調用本文章的上面的那個讀取數據的機制就可以了

(2)Netty的工作流程步驟:

  1. 創建 NIO 線程組 EventLoopGroup 和 ServerBootstrap。

  2. 設置 ServerBootstrap 的屬性:線程組、SO_BACKLOG 選項,設置 NioServerSocketChannel 爲 Channel,設置業務處理 Handler

  3. 綁定端口,啓動服務器程序。

  4. 在業務處理 TimeServerHandler 中,讀取客戶端發送的數據,並給出響應

(3)兩者之間的區別:

  1. OP_ACCEPT 的處理被簡化,因爲對於 accept 操作的處理在不同業務上都是一致的。

  2. 在 NIO 中需要自己構建 ByteBuffer 從 Channel 中讀取數據,而 Netty 中數據是直接讀取完成存放在 ByteBuf 中的。相當於省略了用戶進程從內核中複製數據的過程。

  3. 在 Netty 中,我們看到有使用一個解碼器 FixedLengthFrameDecoder,可以用於處理定長消息的問題,能夠解決 TCP 粘包讀半包問題,十分方便。

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