Netty 系列三(ByteBuf).


一、概述和原理

    網絡數據傳輸的基本單位總是字節,Netty 提供了 ByteBuf 作為它的字節容器,既解決了 JDK API 的局限性,又為網絡應用程序提供了更好的 API,ByteBuf 的優點:

1、可以被用戶自定義的緩沖區類型擴展
2、通過內置的復合緩沖區類型實現了透明的零拷貝
3、容量可以按需增長
4、在讀和寫這兩種模式之間切換不需要調用 ByteBuffer 的 flip()方法
5、讀和寫使用了不同的索引
6、支持方法的鏈式調用
7、支持引用計數
8、支持池化

    ByteBuf通過兩個索引(readerIndex、writerIndex)划分為三個區域:

1、任何名稱以 read 或者 skip 開頭的操作都將檢索或者跳過位於當前readerIndex 的數據,並且將它增加已讀字節數;任何名稱以 write 開頭的操作都將從當前的 writerIndex 處開始寫數據,並將它增加已經寫入的字節數。readerIndex 不能超過 writerIndex。

2、如果被調用的方法需要一個 ByteBuf 參數作為讀取的目標,並且沒有指定目標索引參數,那么該目標緩沖區的 writerIndex 也將被增加;如果寫操作的目標也是 ByteBuf,並且沒有指定源索引的值,則源緩沖區的 readerIndex 也同樣會被增加相同的大小。

3、如果嘗試在緩沖區的可讀字節數已經耗盡時從中讀取數據, 那么將會引發一個IndexOutOfBoundsException;如果嘗試往目標寫入超過目標容量的數據,將檢查當前的寫索引以及最大容量是否可以在擴展后容納該數據,可以的話就會分配並調整容量,否則將會引發一個IndexOutOfBoundException。

4、通過調用 discardReadBytes()方法, 可以丟棄已讀字節並回收空間。但不建議頻繁的調用discardReadBytes(),因為將可能導致內存復制:

5、通過調用 clear()方法來將 readerIndex writerIndex 都設置為 0。 注意,調用 clear()比調用 discardReadBytes()輕量得多, 因為它將只是重置索引而不會復制任何的內存。 

二、ByteBuf 分配

    ByteBuf 是一個抽象類,在程序中直接new ByteBuf() 是不現實的啦!畢竟還要實現一堆的方法,並且缺乏池化的管理。那么,在 Netty 中要怎么得到 ByteBuf 呢?

    有兩種方法可以得到 ByteBuf 實例,一種是 ByteBufAllocator (實現了池化,有效的降低了分配和釋放內存的開銷),另一種是 Unpooled (Netty 提供的工具類來創建未池化的ByteBuf 實例)。
    ByteBufAllocator:Netty 提供了兩種 ByteBufAllocator 的實現,PooledByteBufAllocator(默認使用)和UnpooledByteBufAllocator。前者池化了ByteBuf的實例以提高性能並最大限度地減少內存碎片;后者的實現不池化ByteBuf實例, 並且在每次它被調用時都會返回一個新的實例。

    創建 ButeBuf 實例的代碼如下:

public void createBuf() {
    //獲得ByteBufAllocator 的兩種方式
    //1、
    Channel channel = null;
    ByteBufAllocator alloc = channel.alloc();
    //2、
    ChannelHandlerContext channelHandlerContext = null;
    ByteBufAllocator alloc1 = channelHandlerContext.alloc();
    
//ByteBufAllocator 創建 ByteBuf 實例 //1、返回一個基於堆或者直接內存存儲的ByteBuf ByteBuf byteBuf = alloc.buffer(256, Integer.MAX_VALUE); //2、返回一個基於堆內存存儲的 ByteBuf 實例 ByteBuf byteBuf1 = alloc.heapBuffer(256);
byteBuf1.refCnt();
//檢查該ByteBuf的引用計數 byteBuf1.release();//將ByteBuf的引用計數設為0並釋放
//3、返回一個基於直接內存存儲的 ByteBuf ByteBuf byteBuf2 = alloc.directBuffer(); //4、返回一個可以通過添加最大到指定數目的基於堆的或者直接內存存儲的緩沖區來擴展的 CompositeByteBuf CompositeByteBuf compositeByteBuf = alloc.compositeBuffer(); CompositeByteBuf compositeByteBuf1 = alloc.compositeHeapBuffer(16); CompositeByteBuf compositeByteBuf2 = alloc.compositeDirectBuffer(16); //5、返回一個用於套接字的 I/O 操作的ByteBuf ByteBuf byteBuf3 = alloc.ioBuffer();

//Unpooled 創建 ByteBuf 實例 //1、創建一個未池化的基於堆內存存儲的 ByteBuf 實例 ByteBuf buf = Unpooled.buffer(); //2、創建一個未池化的基於內存存儲的ByteBuf ByteBuf buf1 = Unpooled.directBuffer(256, Integer.MAX_VALUE); //3、返回一個包裝了給定數據的 ByteBuf Unpooled.wrappedBuffer("Hello Netty".getBytes()); //4、返回一個復制了給定數據的 ByteBuf Unpooled.copiedBuffer("Hello Netty",CharsetUtil.UTF_8); }

三、ByteBuf 操作

    涉及到 ByteBuf 的操作主要是兩個方面,一個是 ByteBuf 的讀/寫,另一個是 ByteBuf 的復制分片操作。

    ByteBuf 的讀/寫操作操作有兩種類別:get() 和 set() 操作,從給定的索引開始,並且保持索引不變,也就是說get() 和 set() 操作並不會改變 readerIndex 和 writerIndex 的值;read()和 write()操作, 從給定的索引開始,並且會根據已經訪問過的字節數對索引進行調整,比如 read() 操作 readerIndex 會根據讀取的數據類型(byte 1個字節,short 2個字節,int 4個字節,long 8個字節)增加對應的索引數。

    ByteBuf 提供了專門的方式來實現復制分片操作:

duplicate();
slice();
slice(int, int);
Unpooled.unmodifiableBuffer(…);
order(ByteOrder);
readSlice(int)。

    這些方法都將返回一個新的 ByteBuf 實例,它具有自己的讀索引、寫索引和標記索引。其內部存儲和 JDK 的 ByteBuffer 一樣也是共享的。這意味着,如果你修改了它的內容,也同時修改了其對應ByteBuf的源實例。

    如果需要一個現有緩沖區的真實副本,請使用 copy()或者 copy(int, int)方法。不同於派生緩沖區,由這個調用所返回的 ByteBuf 擁有獨立的數據副本。

    現在我們具體來看看 ByteBuf 的操作:

    1、數據遍歷

for (int i = 0; i < byteBuf.capacity(); i++) {
    byte aByte = byteBuf.getByte(i);
    System.out.print((char) aByte);
}

while (byteBuf.isReadable()){
    System.out.print((char) byteBuf.readByte());
}

    2、寫入數據

while (byteBuf.writableBytes() >= 4){
    byteBuf.writeByte(65);
}

    3、索引標記管理

ByteBuf buf = byteBuf.readerIndex(0);//將 readerIndex 移動到指定的位置
buf.markReaderIndex();//標記當前的 readerIndex
while (buf.isReadable()){
    System.out.print((char) buf.readByte());
}
buf.resetReaderIndex();//回退到之前標記的 readerIndex
while (buf.isReadable()){
    System.out.print((char) buf.readByte());
}

int index = byteBuf.indexOf(0, byteBuf.capacity() - 1, (byte) 65);//在某個范圍內查找某個字節的索引

    4、分片操作

ByteBuf slice = byteBuf.slice(0, 15);
System.out.print(slice.toString(CharsetUtil.UTF_8));
//更新索引0處的字節
slice.setByte(0, (byte) 'J');
byte aByte = byteBuf.getByte(0);
System.out.print("\r\n" + (char)aByte);

    5、復制操作

ByteBuf copy = buf.copy(0, 15);
System.out.println(copy.toString(CharsetUtil.UTF_8));
copy.setByte(0, (byte) 'A');
System.out.println((char) byteBuf.getByte(0));

    6、其他操作

System.out.println("如果至少有一個字節可讀取:" + byteBuf.isReadable());
System.out.println("如果至少有一個字節可寫入:" + byteBuf.isWritable());
System.out.println("返回可被讀取的字節數:" + byteBuf.readableBytes());
System.out.println("返回可被寫入的字節數:" + byteBuf.writableBytes());
System.out.println("可容納的字節數:" + byteBuf.capacity() + ",可擴展最大的字節數:" + byteBuf.maxCapacity());
System.out.println("是否由一個字節數組支撐:" + byteBuf.hasArray());
System.out.println("由一個字節數組支撐則返回該數組:" + byteBuf.array().length);
System.out.println("計算第一個字節的偏移量:" + byteBuf.arrayOffset());
System.out.println("返回Bytebuf的十六進制:" + ByteBufUtil.hexDump(byteBuf.array()));

四、補充

ByteBuf 的使用模式 :

    1、堆緩沖區:最常用的 ByteBuf 模式是將數據存儲在 JVM 的堆空間中。 這種模式被稱為支撐數組(backing array), 它能在沒有使用池化的情況下提供快速的分配和釋放。

    2、直接緩沖區:將數據駐留在會被垃圾回收的堆之外,直接緩沖區對於網絡數據傳輸是最理想的選擇,不過,相對於基於堆的緩沖區,它們的分配和釋放都較為昂貴。另外,如果你的數據包含在一個在堆上分配的緩沖區中, 那么事實上,在通過套接字發送它之前, JVM將會在內部把你的緩沖區復制到一個直接緩沖區中。經驗表明,Bytebuf的最佳實踐是在IO通信線程的讀寫緩沖區使用DirectByteBuf,后端業務使用HeapByteBuf。

    3、復合緩沖區:為多個 ByteBuf 提供一個聚合視圖。 在這里你可以根據需要添加或者刪除 ByteBuf 實例。

                               Netty 通過一個 ByteBuf 子類——CompositeByteBuf——實現了這個模式, 它提供了一個將多個緩沖區表示為單個合並緩沖區的虛擬表示。

                               使用 CompositeByteBuf 的復合緩沖區模式:

public void CompositeBuffer() {
    CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
    ByteBuf headBuf = Unpooled.copiedBuffer("Hello,", CharsetUtil.UTF_8);
    ByteBuf bodyBuf = Unpooled.copiedBuffer("Netty!", CharsetUtil.UTF_8);
    //將 ByteBuf 實例追加到 CompositeByteBuf
    messageBuf.addComponents(headBuf, bodyBuf);
    Iterator<ByteBuf> it = messageBuf.iterator();
    //訪問CompositeByteBuf數據
    while(it.hasNext()){
        ByteBuf buf = it.next();
        while (buf.isReadable()){
            System.out.print((char) buf.readByte());
        }
    }
    //使用數組訪問數據
    if(!messageBuf.hasArray()){
        int len = messageBuf.readableBytes();
        byte[] arr = new byte[len];
        messageBuf.getBytes(0, arr);
        for (byte b : arr){
            System.out.print((char)b);
        }
    }
    messageBuf.removeComponent(0); //刪除位於索引位置為 0(第一個組件)的 ByteBuf
}

 

 

參考資料:《Netty IN ACTION》

演示源代碼:https://github.com/JMCuixy/NettyDemo/blob/master/src/main/java/org/netty/demo/bytebuf/ByteBufOperation.java


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM