Netty 在數據傳輸過程中,會使用緩沖區設計來提高傳輸效率。雖然,Java 在 NIO 編程中已提供 ByteBuffer 類進行使用,但是在使用過程中,其編碼方式相對來說不太友好,也存在一定的不足。所以高性能的 Netty 框架實現了一套更加強大,完善的 ByteBuf,其設計理念也是堪稱一絕。
ByteBuffer 分析
在分析 ByteBuf 之前,先簡單講下 ByteBuffer 類的操作。便於更好理解 ByteBuf 。
ByteBuffer 的讀寫操作共用一個位置指針,讀寫過程通過以下代碼案例分析:
// 分配一個緩沖區,並指定大小
ByteBuffer buffer = ByteBuffer.allocate(100);
// 設置當前最大緩存區大小限制
buffer.limit(15);
System.out.println(String.format("allocate: pos=%s lim=%s cap=%s", buffer.position(), buffer.limit(), buffer.capacity()));
String content = "ytao公眾號";
// 向緩沖區寫入數據
buffer.put(content.getBytes());
System.out.println(String.format("put: pos=%s lim=%s cap=%s", buffer.position(), buffer.limit(), buffer.capacity()));
其中打印了緩沖區三個參數,分別是:
- position 讀寫指針位置
- limit 當前緩存區大小限制
- capacity 緩沖區大小
打印結果:
當我們寫入內容后,讀寫指針值為 13,ytao公眾號
英文字符占 1 個 byte,每個中文占 4 個 byte,剛好 13,小於設置的當前緩沖區大小 15。
接下來,讀取內容里的 ytao 數據:
buffer.flip();
System.out.println(String.format("flip: pos=%s lim=%s cap=%s", buffer.position(), buffer.limit(), buffer.capacity()));
byte[] readBytes = new byte[4];
buffer.get(readBytes);
System.out.println(String.format("get(4): pos=%s lim=%s cap=%s", buffer.position(), buffer.limit(), buffer.capacity()));
String readContent = new String(readBytes);
System.out.println("readContent:"+readContent);
讀取內容需要創建個 byte 數組來接收,並制定接收的數據大小。
在寫入數據后再讀取內容,必須主動調用ByteBuffer#flip
或ByteBuffer#clear
。
ByteBuffer#flip
它會將寫入數據后的指針位置值作為當前緩沖區大小,再將指針位置歸零。會使寫入數據的緩沖區改為待取數據的緩沖區,也就是說,讀取數據會從剛寫入的數據第一個索引作為讀取數據的起始索引。
ByteBuffer#flip
相關源碼:
ByteBuffer#clear
則會重置 limit 為默認值,與 capacity 大小相同。
接下讀取剩余部分內容:
第二次讀取的時候,可使用buffer#remaining
來獲取大於或等於剩下的內容的字節大小,該函數實現為limit - position
,所以當前緩沖區域一定在這個值范圍內。
readBytes = new byte[buffer.remaining()];
buffer.get(readBytes);
System.out.println(String.format("get(remaining): pos=%s lim=%s cap=%s", buffer.position(), buffer.limit(), buffer.capacity()));
打印結果:
以上操作過程中,索引變化如圖:
ByteBuf 讀寫操作
ByteBuf 有讀寫指針是分開的,分別是buf#readerIndex
和buf#writerIndex
,當前緩沖器大小buf#capacity
。
這里緩沖區被兩個指針索引和容量划分為三個區域:
- 0 -> readerIndex 為已讀緩沖區域,已讀區域可重用節約內存,readerIndex 值大於或等於 0
- readerIndex -> writerIndex 為可讀緩沖區域,writerIndex 值大於或等於 readerIndex
- writerIndex -> capacity 為可寫緩沖區域,capacity 值大於或等於 writerIndex
如下圖所示:
分配緩沖區
ByteBuf 分配一個緩沖區,僅僅給定一個初始值就可以。默認是 256。初始值不像 ByteBuffer 一樣是最大值,ByteBuf 的最大值是Integer.MAX_VALUE
ByteBuf buf = Unpooled.buffer(13);
System.out.println(String.format("init: ridx=%s widx=%s cap=%s", buf.readerIndex(), buf.writerIndex(), buf.capacity()));
打印結果:
寫操作
ByteBuf 寫操作和 ByteBuffer 類似,只是寫指針是單獨記錄的,ByteBuf 的寫操作支持多種類型,有以下多個API:
寫入字節數組類型:
String content = "ytao公眾號";
buf.writeBytes(content.getBytes());
System.out.println(String.format("write: ridx=%s widx=%s cap=%s", buf.readerIndex(), buf.writerIndex(), buf.capacity()));
打印結果:
索引示意圖:
讀操作
一樣的,ByteBuf 寫操作和 ByteBuffer 類似,只是寫指針是單獨記錄的,ByteBuf 的讀操作支持多種類型,有以下多個API:
從當前 readerIndex 位置讀取四個字節內容:
byte[] dst = new byte[4];
buf.readBytes(dst);
System.out.println(new String(dst));
System.out.println(String.format("read(4): ridx=%s widx=%s cap=%s", buf.readerIndex(), buf.writerIndex(), buf.capacity()));
打印結果:
索引示意圖:
ByteBuf 動態擴容
通過上面的 ByteBuffer 分配緩沖區例子,向里面添加 [ytao公眾號ytao公眾號] 內容,使寫入的內容大於 limit 的值。
ByteBuffer buffer = ByteBuffer.allocate(100);
buffer.limit(15);
String content = "ytao公眾號ytao公眾號";
buffer.put(content.getBytes());
運行結果異常:
內容字節大小超過了 limit 的值時,緩沖區溢出異常,所以我們每次寫入數據前,得檢查緩區大小是否有足夠空間,這樣對編碼上來說,不是一個好的體驗。
使用 ByteBuf 添加同樣的內容,給定同樣的初始容器大小。
ByteBuf buf = Unpooled.buffer(15);
String content = "ytao公眾號ytao公眾號";
buf.writeBytes(content.getBytes());
System.out.println(String.format("write: ridx=%s widx=%s cap=%s", buf.readerIndex(), buf.writerIndex(), buf.capacity()));
打印運行結果:
通過上面打印信息,可以看到 cap 從設置的 15 變為了 64,當我們容器大小不夠時,就是進行擴容,接下來我們分析擴容過程中是如何做的。
進入 writeBytes 里面:
校驗寫入內容長度:
在可寫區域檢查里:
- 如果寫入內容為空,拋出非法參數異常。
- 如果寫入內容大小小於或等於可寫區域大小,則返回當前緩沖區,當中的
writableBytes()
函數為可寫區域大小capacity - writerIndex
- 如果寫入內容大小大於最大可寫區域大小,則拋出索引越界異常。
- 最后剩下條件的就是寫入內容大小大於可寫區域,小於最大區域大小,則分配一個新的緩沖區域。
在容量不足,重新分配緩沖區的里面,以 4M 為閥門:
- 如果待寫內容剛好為 4M, 那么就分配 4M 的緩沖區。
- 如果待寫內容超過這個閥門且與閥門值之和不大於最大容量值,就分配(閥門值+內容大小值)的緩沖區;如果超過這個閥門且與閥門值之和大於最大容量值,則分配最大容量的緩沖區。
- 如果待寫內容不超過閥門值且大於 64,那么待分配緩沖區大小就以 64 的大小進行倍增,直到相等或大於待寫內容。
- 如果待寫內容不超過閥門值且不大於 64,則返回待分配緩沖區大小為 64。
最后
Netty 實現的緩沖區,八個基本類型中,除了布爾類型,其他7種都有自己對應的 Buffer,但是實際使用過程中, ByteBuf 才是我們嘗試用的,它可兼容任何類型。ByteBuf 在 Netty 體系中是最基礎也是最重要的一員,要想更好掌握和使用 Netty,先理解並掌握 ByteBuf 是必需條件之一。
個人博客: https://ytao.top
關注公眾號 【ytao】,更多原創好文