天天看點

Netty元件-ByteBuf

ByteBuf是netty對nio中ByteBuffer的更新和優化,是的資料流更加的友善操作和更叫的高效。

一、建立

package com.test.netty.c5;

import com.test.utils.ByteBufUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TestByteBuf {
    public static void main(String[] args) {
        //可以動态擴容
        //ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.heapBuffer();
        System.out.println(byteBuf.getClass());
        ByteBufUtils.log(byteBuf);
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 32; i++) {
            sb.append("a");
        }
        byteBuf.writeBytes(sb.toString().getBytes());
        ByteBufUtils.log(byteBuf);
    }
}
           

ByteBuf可以通過使用ByteBufAllocator工具類進行建立,預設使用的就是直接記憶體,選擇使用堆記憶體進行建立,同時預設大小是256。當資料的大小大于ByteBuf的指定大小,就會進行自動擴容。但是在handler中使用ByteBuf的時候,盡量使用channelHandlerContext的alloc.buffer()方法進行建立。

二、直接記憶體和堆記憶體

同ByteBuffer一樣,ByteBuf也可以使用直接記憶體或者是堆記憶體,一下是建立方法和使用記憶體情況:

  • ByteBufAllocator.DEFAULT.buffer(16); = 池化直接記憶體
  • ByteBufAllocator.DEFAULT.heapBuffer(16); = 池化堆記憶體
  • ByteBufAllocator.DEFAULT.directBuffer(16); = 池化直接記憶體

其實跟ByteBuffer的使用記憶體的優缺點一樣:

  • 直接記憶體,配置設定效率低,但是使用效率高,不受GC回收的影響,需要注意手動釋放(更好的配合池化)
  • 堆記憶體,配置設定效率高,但是使用效率相對低,受GC的影響,可以選擇不手動釋放

三、池化和非池化

其實池化的技術在開發中應用的非常廣泛,線程池、資料庫連接配接池等等,ByteBuf的池化技術也是一個意思,就是針對ByteBuf的重用,有點如下:

  • 不使用池化技術,每次都要重新配置設定存儲,增加GC壓力
  • 有了池化技術,可以重用ByteBuf,采用了jemalloc 類似的配置設定算法提升配置設定效率,并發高的時候,池化更加節約記憶體,減少記憶體溢出的可能性

是否開啟池化技術,系統環境變量:-Dio.netty.allocator.type={unpooled|pooled}

注意:

  • 4.1之後,android平台不開啟池化,其他平台預設開啟
  • 4.1之前,池化不成熟,預設不開啟

四、ByteBuf的組成

建立ByteBuf的時候,可以傳遞2個參數,第一個是初始容量,第二個是最大容量,最大容量預設是Integer.MAX_VALUE,當容量不夠的時候,ByteBuf就會自動擴容,當擴容到最大容量的時候,就會抛出異常。

ByteBuf讀寫相對ByteBuffer有很大的提升,采用雙指針的方式,一個讀指針,一個寫指針,結構如下:

Netty元件-ByteBuf

擴容規則: 

  • 如果寫入後的資料大小小于512位元組,下一次擴容就是16的整數倍
  • 如果寫入後的資料大小大于512位元組,下一次擴容就是2的N次方

五、寫入和讀取方法

寫入方法:

方法簽名 含義 備注
writeBoolean(boolean value) 寫入 boolean 值 用一位元組 01|00 代表 true|false
writeByte(int value) 寫入 byte 值
writeShort(int value) 寫入 short 值
writeInt(int value) 寫入 int 值 Big Endian(大端寫入),即 0x250,寫入後 00 00 02 50
writeIntLE(int value) 寫入 int 值 Little Endian(小端寫入),即 0x250,寫入後 50 02 00 00
writeLong(long value) 寫入 long 值
writeChar(int value) 寫入 char 值
writeFloat(float value) 寫入 float 值
writeDouble(double value) 寫入 double 值
writeBytes(ByteBuf src) 寫入 netty 的 ByteBuf
writeBytes(byte[] src) 寫入 byte[]
writeBytes(ByteBuffer src) 寫入 nio 的 ByteBuffer
int writeCharSequence(CharSequence sequence, Charset charset) 寫入字元串 CharSequence為字元串類的父類,第二個參數為對應的字元集
  • 所有方法傳回的都是ByteBuf,是以可以使用鍊式調用
  • 注意大端寫入和小端寫入,網絡程式設計中習慣用的是大段寫入
  • 也可以使用相關set方法進行寫入,但是不會改變寫指針的位置

讀出方法:

  • 以read開頭的方法,讀取後會改變讀指針的位置,以get開頭的方法正好相反
  • 如果期望重複讀取,可以先使用 buffer.markReaderIndex() 進行标記,然後再使用 buffer.resetReaderIndex() 恢複标記位置

六、記憶體釋放

因為ByteBuf可以使用直接記憶體,是以直接使用之後都需要進行手動的記憶體釋放。Netty中提供了ReferenceCounted接口來進行記憶體的釋放,并且每個ByteBuf都實作了改接口,釋放算法:

  • ByteBuf初始對象的計數為1
  • 調用 release 方法計數-1,當計數為0的時候,記憶體釋放
  • 調用 retain 方法計數+1,表示有地方在使用這個ByteBuf,保證不會因為其它地方調用 release方法而導緻誤回收 

因為pipelin的存在,資料是在整個handler鍊中進行流轉的,是以ByteBuf在哪裡釋放就顯得很重要,基本原則是哪個handler使用,就在哪個handler釋放,雖然head和tail都有釋放的功能,但是因為中間的handler可能對ByteBuff進行加工,傳遞到head和tail就已經不是ByteBuf對象了,是以還是要遵循基本原則:誰最後使用,誰負責release

  • 當handler使用了ByteBuf,并且不向下傳遞了,就調用release
  • 當到達最後一個handler了,不需要向下傳遞了,也需要調用release
  • 異常無法成功傳遞到下一個handler,也需要調用release
  • 出棧一般情況下,因為是最後轉換成ByteBuf,就會由head進行釋放

七、切片和合并

ByteBuf中有許多零拷貝的展現,切片和合并就是,不論切片還是合并,其實都是使用的原ByteBuf,但是對讀寫指針是獨立的維護。是以在使用新生成的ByteBuf的時候,就要注意,如果原ByteBuf被記憶體釋放了,那麼新生成的ByteBuf也會無法使用,是以需要在使用的時候調用retain方法,讓計數+1即可。

Netty元件-ByteBuf

切片代碼執行個體:

package com.test.netty.c5;


import com.test.utils.ByteBufUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class TestSlice {

    public static void main(String[] args) {
        ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer(10);
        byteBuf.writeBytes(new byte[]{'a','b','c','d','e','f','g','h','i','j'});
        ByteBufUtils.log(byteBuf);
        //在切片過程中,沒有發生資料的複制
        ByteBuf f1 = byteBuf.slice(0, 5);
        ByteBuf f2 = byteBuf.slice(5, 5);
        ByteBufUtils.log(f1);
        ByteBufUtils.log(f2);
        //切片後的ByteBuf是無法寫入的
        //原有的ByteBuf釋放記憶體後,切片後的也會受影響
        //上面兩個原因都是因為切片後的ByteBuf是原始ByteBuf的映射

        f1.setByte(0, 'b');
        ByteBufUtils.log(f1);
        ByteBufUtils.log(byteBuf);
    }
}
           

 合并代碼執行個體:

package com.test.netty.c5;

import com.test.utils.ByteBufUtils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.CompositeByteBuf;

public class TestCompositeByteBuf {

    public static void main(String[] args) {
        ByteBuf b1 = ByteBufAllocator.DEFAULT.buffer(10);
        b1.writeBytes(new byte[]{'a','b','c','d','e'});
        ByteBuf b2 = ByteBufAllocator.DEFAULT.buffer(10);
        b2.writeBytes(new byte[]{'f','g','h','i','j'});

        CompositeByteBuf byteBufs = ByteBufAllocator.DEFAULT.compositeBuffer();
        byteBufs.addComponents(true, b1, b2);
        ByteBufUtils.log(byteBufs);
    }

}
           

八、優勢

  • 池化思想,提升使用效率
  • 讀寫指針,友善操作
  • 自動擴容
  • 方法鍊式調用,閱讀和書寫更加友善
  • 零拷貝思想展現多,如  slice、duplicate、CompositeByteBuf