05-NIO Buffer

NIO Buffer

一、Buffer

  • NIO 中,對於連接數據的讀寫不能直接操作 Channel,而需要操作Buffer,Buffer本質是一塊內存區域,我們用其做數據的讀寫。JDK將這塊內存封裝成 NIO Buffer 對象,並提供了一組API方法,方便我們對該塊內存的讀寫。Buffer 是 java.nio 包下的抽象類,常見實現類繼承關係如下:

在這裏插入圖片描述

  • JDK 中針對每一種基本類型都有Buffer的實現(除了boolean類型),但是仔細看每一種實現都還是抽象類,比如 ByteBuffer 也是一個抽象類,它也有不同的子類,我們針對它來看整個繼承和主要的源碼,其他類型也是基本類似。

二、主要屬性

  • mark 、position 、 limit 、capacity
public abstract class Buffer {

    // Invariants: mark <= position <= limit <= capacity
    private int capacity;
    private int limit;
    private int position = 0;
    private int mark = -1;    

    // Used only by direct buffers
    // NOTE: hoisted here for speed in JNI GetDirectBufferAddress
    long address;

    Buffer(int mark, int pos, int lim, int cap) {       // package-private
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);
        this.capacity = cap;
        limit(lim);
        position(pos);
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("mark > position: ("
                                                   + mark + " > " + pos + ")");
            this.mark = mark;
        }
    }
}
  • 構造方法:用於初始化幾個核心參數
  • capacity:容量;buffer能夠容納元素的最大值,創建時被賦值且不能被修改
  • limit:上限;當前操作的上限,如果是寫模式則limit是capacity,如果是讀模式,limit是buffer中元素的個數,flip()方法切換讀寫模式
  • position:位置;初始化爲0,讀寫操作下,每操作一次之後,position值加一,類似於index,指向下一次操作的下標位置
  • mark:標記;標記上一次讀/寫的位置,通過mark() 方法標記,通過reset()恢復標記,即reset()方法會將position恢復爲上一次mark()標記的地方
  • address:直接內存的時候該字段纔有用,記錄在頂層抽象類而不是子類目的是提高速度,可以理解爲指向直接內存的地址
屬性滿足 : mark <= position <= limit <= capacity

三、主要方法

3.1 創建Buffer

  • 創建Buffer 使用一個靜態方法allocate,這個方法不在 Buffer 中定義,定義在子類中,比如ByteBuffer中:
    public static ByteBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapByteBuffer(capacity, capacity);
    }
  • 這裏實例化的是一個 HeapByteBuffer,從名字容易知道是創建在堆的ByteBuffer實現,另外也可以分配在直接內存,使用直接內存不會受到JVM堆大小的限制了(受物理內存大小限制);

3.2 讀寫Buffer、flip、rewind

  • 寫buffer示例,通過put相關的重載方法往buffer寫數據,注意在 NIO 中當從通道讀取數據的時候,對Buffer就是寫,比如遠端發送了數據來了,從channel讀取數據,此時就是將channel的數據寫入Buffer
public final ByteBuffer put(byte[] src) {
        return put(src, 0, src.length);
    }

//在Channel中,channel的讀就是buffer的寫
SocketChannel#read(java.nio.ByteBuffer)
  • 讀buffer示例, 通過get相關重載方法從buffer中讀取數據到目標數組中 , 當往遠端寫數據,比如寫一個消息給對方,那麼就是將buffer數據寫到channel,此時對於Buffer就是讀,因此這個讀取方法是從Buffer讀到Channel繼而發送,對於Channel是寫,因此方法在Channel裏面
    buf.flip();
    buf.get(dst);


//在Channel中,channel的寫就是buffer的讀
java.nio.channels.SocketChannel#write(java.nio.ByteBuffer)
  • 這裏需要注意的是 Channel 的讀寫方法都是在 SocketChannel 中,ServerSocketChannel 中是沒有的,因爲 ServerSocketChannel 是監聽連接的,真正的讀寫在 SocketChannel 中處理,這也是二者的區別,上一篇 Channel的文章也講過了。

  • flip切換寫模式爲讀模式

    public final Buffer flip() {
        limit = position; //原本寫的當前位置變成了讀模式的limit
        position = 0; //切換到讀模式時,從0開始讀
        mark = -1; //還未標記
        return this;
    }
  • flip() 使用示例
    out.write(buf);     
    buf.rewind();  
    buf.get(array);  
  • rewind 重置 position,重新進行讀寫操作, 一般用於讀模式,重新讀取
  public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }

3.4 其他方法

3.4.1 mark和reset

  • mark 方法標記當前的位置
    public final Buffer mark() {
        mark = position;
        return this;
    }
  • reset 將當前的position 恢復到之前標記的位置
    public final Buffer reset() {
        int m = mark;
        if (m < 0)
            throw new InvalidMarkException();
        position = m;
        return this;
    }

3.4.2 clear和remaining

  • clear 方法看起來是清空Buffer,其實不是的,它並未真正的清除數據,而是將幾個標誌復位,position置0,limit置爲capacity,丟棄mark(置爲-1),一般在使用前調用一次
    /**
     * Clears this buffer.  The position is set to zero, the limit is set to
     * the capacity, and the mark is discarded.
     * buf.clear();     // Prepare buffer for reading
     * in.read(buf);    // Read data</pre></blockquote>
     */
    public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }
  • clear() 使用示例
      buf.put(magic);    
      in.read(buf);       
      buf.flip();        
      out.write(buf);     
  • remaining返回剩餘可操作的元素數量,比如寫就返回還能寫的空間大小,讀就返回剩餘元素個數,通過limit和position計算得到
    public final int remaining() {
        return limit - position;
    }

四、ByteBuffer

4.1 構造方法

  • Buffer 幾乎所有的子類都有堆分配和直接內存分配兩種策略,ByteBuffer 用於存放字節類型數據
    @Test
    public void typeTest() {

        //分配直接內存
        ByteBuffer direct = ByteBuffer.allocateDirect(1024);
        System.out.println(direct.isDirect());

        ByteBuffer heap = ByteBuffer.allocate(1024);
        System.out.println(heap.isDirect());
    }
    
輸出:
    true
    false
  • 默認返回的是 DirectByteBuffer 對象,DirectByteBuffer是包可訪問權限(默認),下面是構造方法代碼,最關鍵的地方看到是通過 unsafe.allocateMemory(size); 分配內存,這裏涉及到本地方法調用,在直接內存分配
DirectByteBuffer(int cap) {                   // package-private
        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }

4.2 方法示例

  • 下面通過一個例子來演示常用 API,基本涉及到前面所說的 Buffer 接口的方法
    
    static final String content = "helloworld";
    static final int capacity = 128;
    
    @Test
    public void apiTest() {
        //1. 分配一個指定大小的緩衝區
        System.out.println("1. allocate() 創建, 容量爲 " + capacity);
        ByteBuffer buf = ByteBuffer.allocate(capacity);
        printBufDetail(buf);


        //2. 利用 put() 存入數據到緩衝區中
        System.out.println("2. put() 存儲 :存入內容爲:" + content);
        buf.put(content.getBytes());
        printBufDetail(buf);

        //3. 切換讀取數據模式
        System.out.println("3.flip() 切換到讀模式:");
        buf.flip();
        printBufDetail(buf);

        //4. 利用 get() 讀取緩衝區中的數據到dst數組
        System.out.println("4. get() 讀取");

        byte[] dst = new byte[buf.limit()];
        buf.get(dst);
        System.out.println("讀取的內容:" + new String(dst, 0, dst.length));
        printBufDetail(buf);


        //5. rewind() : 重複讀
        System.out.println("5. rewind() 可重複讀");
        buf.rewind();
        printBufDetail(buf);

        //6. clear() : 清空緩衝區. 但是緩衝區中的數據依然存在,標誌狀態位會重置
        System.out.println("6. clear()");
        buf.clear();
        //注意這裏如果get方法帶有index,比如get(1)讀取下標爲1的內容,那麼是不會影響position的,這種是絕對讀取
        System.out.println("clear 後讀取第一個字符: " + (char) buf.get());
        printBufDetail(buf);


        System.out.println("7.重新寫入javahelloworld ");
        buf.put("javahelloworld".getBytes());
        printBufDetail(buf);
        buf.flip();

        System.out.println("8.flip ");
        printBufDetail(buf);

        System.out.println("9.絕對讀取1個,不影響position ");
        byte b = buf.get(3);
        printBufDetail(buf);

        System.out.println("10.讀取批量5個 ");
        byte[] dst1 = new byte[5];
        buf.get(dst1, 0, dst1.length);
        System.out.println("讀取內容:" + new String(dst1));
        printBufDetail(buf);

        System.out.println("11.mark一下,再讀取5個 ");
        buf.mark();
        buf.get(dst1, 0, 5);
        System.out.println("讀取內容:" + new String(dst1));
        printBufDetail(buf);

        System.out.println("12.回到mark處,重新讀取5個,應該和前一次讀取的是一樣的 ");
        buf.reset();
        buf.get(dst1, 0, 5);
        System.out.println("讀取內容:" + new String(dst1));
        printBufDetail(buf);
    }

    private static void printBufDetail(ByteBuffer buf) {
        System.out.println("Buffer 參數:position = " + buf.position() + ", limit= " + buf.limit() + ", capacity = " + buf.capacity() + "\n");
    }
  • 輸出:雙斜槓後面的註釋是後面添加的說明部分
1. allocate() 創建, 容量爲 128
Buffer 參數:position = 0, limit= 128, capacity = 128    //新創建的Buffer

2. put() 存儲 :存入內容爲:helloworld
Buffer 參數:position = 10, limit= 128, capacity = 128   //存入了10字節的helloworld後,寫模式下position是10

3.flip() 切換到讀模式:
Buffer 參數:position = 0, limit= 10, capacity = 128    //切換讀模式,position是0,limit是可讀限制 10

4. get() 讀取
讀取的內容:helloworld
Buffer 參數:position = 10, limit= 10, capacity = 128   //讀取全部內容後,還是讀模式,position還在10

5. rewind() 可重複讀
Buffer 參數:position = 0, limit= 10, capacity = 128    //rewind將position置爲0,還在讀模式,重新讀取,limit不變,

6. clear()
clear 後讀取第一個字符: h
Buffer 參數:position = 1, limit= 128, capacity = 128   //clear沒有真正清除數據,但是重置了參數,讀取一個元素之後position是1

7.重新寫入 javahelloworld 
Buffer 參數:position = 15, limit= 128, capacity = 128  
//高亮1:這裏尤其注意,在讀模式下position在1,因此直接寫入javahelloworld會從1開始寫入,寫入之後buf中內容是hjavahelloworld,後面的覆蓋了,第一個h保留了

8.flip 
Buffer 參數:position = 0, limit= 15, capacity = 128    //flip切換之後limit是15,實際上javahelloworld長度是14,因爲寫入的時候從1位置開始寫導致總長度是15

9.絕對讀取1個,不影響position 
Buffer 參數:position = 0, limit= 15, capacity = 128   //高亮2:絕對讀取不影響position,絕對讀取就是get(index)這種方式,讀取之後position還是0

10.讀取批量5個 
讀取內容:hjava
Buffer 參數:position = 5, limit= 15, capacity = 128   //批量讀取5個,前面說過內容是hjavahelloworld,因此讀取到的是 hjava

11.mark一下,再讀取5個 
讀取內容:hello
Buffer 參數:position = 10, limit= 15, capacity = 128  //在前一次讀取的時候mark,但是mark不影響後一次讀取,所以再讀5個,讀取到的是 hello

12.回到mark處,重新讀取5個,應該和前一次讀取的是一樣的  //回到mark處,也就是第一次讀取完畢的時候mark的地方,因此再讀,就和mark後的一次讀取內容一樣,也是 hello
讀取內容:hello
Buffer 參數:position = 10, limit= 15, capacity = 128

  • 通過這些例子其實發現 ByteBuffer 本身完全可以理解爲一個可複用的定長字節數組的封裝,通過給定的API來操作,在讀寫的時候相關的幾個參數會變化(pisition,limit),其他類型的Buffer也可以同樣類比理解;
  • 需要注意的一個是絕對讀取不影響position的,另外讀取到一定位置再寫入,則會從position位置開始寫,有時候會產生奇怪的現象,需要注意,從下面的方法也能看出來;
//ByteBuffer底層寫入方法
public ByteBuffer put(byte[] src, int offset, int length) {

        checkBounds(offset, length, src.length);
        if (length > remaining())
            throw new BufferOverflowException();
        //從position後面開始寫入
        System.arraycopy(src, offset, hb, ix(position()), length);
        position(position() + length);
        return this;
    }

五、Buffer到Netty ByteBuf

  • Netty 並未直接使用 JDK 的 Buffer,而是自定義了ByteBuf,在ByteBuf中定義了比JDK Buffer更加友好的API和使用方式,比如ByteBuf中包括readerIndex 和 writerIndex 兩個屬性來讀寫,避免出現讀模式和寫模式的切換等。(0 <= readerIndex <= writerIndex <= capacity)

  • 關於ByteBuf的更多內容在後面文章中學習分析

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