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有很大的提升,采用雙指針的方式,一個讀指針,一個寫指針,結構如下:
擴容規則:
- 如果寫入後的資料大小小于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即可。
切片代碼執行個體:
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