《netty入門與實戰》筆記-03:數據傳輸載體 ByteBuf 介紹

ByteBuf結構

首先,咱們先來了解一下 ByteBuf 的結構數組

以上就是一個 ByteBuf 的結構圖,從上面這幅圖能夠看到:微信

  1. ByteBuf 是一個字節容器,容器裏面的的數據分爲三個部分,第一個部分是已經丟棄的字節,這部分數據是無效的;第二部分是可讀字節,這部分數據是 ByteBuf 的主體數據, 從 ByteBuf 裏面讀取的數據都來自這一部分;最後一部分的數據是可寫字節,全部寫到 ByteBuf 的數據都會寫到這一段。最後一部分虛線表示的是該 ByteBuf 最多還能擴容多少容量
  2. 以上三段內容是被兩個指針給劃分出來的,從左到右,依次是讀指針(readerIndex)、寫指針(writerIndex),而後還有一個變量 capacity,表示 ByteBuf 底層內存的總容量
  3. 從 ByteBuf 中每讀取一個字節,readerIndex 自增1,ByteBuf 裏面總共有 writerIndex-readerIndex 個字節可讀, 由此能夠推論出當 readerIndex 與 writerIndex 相等的時候,ByteBuf 不可讀
  4. 寫數據是從 writerIndex 指向的部分開始寫,每寫一個字節,writerIndex 自增1,直到增到 capacity,這個時候,表示 ByteBuf 已經不可寫了
  5. ByteBuf 裏面其實還有一個參數 maxCapacity,當向 ByteBuf 寫數據的時候,若是容量不足,那麼這個時候能夠進行擴容,直到 capacity 擴容到 maxCapacity,超過 maxCapacity 就會報錯。

Netty 使用 ByteBuf 這個數據結構能夠有效地區分可讀數據和可寫數據,讀寫之間相互沒有衝突,固然,ByteBuf 只是對二進制數據的抽象,具體底層的實現咱們在下面的小節會講到,在這一小節,咱們 只須要知道 Netty 關於數據讀寫只認 ByteBuf,下面,咱們就來學習一下 ByteBuf 經常使用的 API數據結構

容量 API

capacity()jvm

表示 ByteBuf 底層佔用了多少字節的內存(包括丟棄的字節、可讀字節、可寫字節),不一樣的底層實現機制有不一樣的計算方式,後面咱們講 ByteBuf 的分類的時候會講到函數

maxCapacity()學習

表示 ByteBuf 底層最大可以佔用多少字節的內存,當向 ByteBuf 中寫數據的時候,若是發現容量不足,則進行擴容,直到擴容到 maxCapacity,超過這個數,就拋異常指針

readableBytes() 與 isReadable()code

readableBytes() 表示 ByteBuf 當前可讀的字節數,它的值等於 writerIndex-readerIndex,若是二者相等,則不可讀,isReadable() 方法返回 false對象

writableBytes()、 isWritable() 與 maxWritableBytes()blog

writableBytes() 表示 ByteBuf 當前可寫的字節數,它的值等於 capacity-writerIndex,若是二者相等,則表示不可寫,isWritable() 返回 false,可是這個時候,並不表明不能往 ByteBuf 中寫數據了, 若是發現往 ByteBuf 中寫數據寫不進去的話,Netty 會自動擴容 ByteBuf,直到擴容到底層的內存大小爲 maxCapacity,而 maxWritableBytes() 就表示可寫的最大字節數,它的值等於 maxCapacity-writerIndex

讀寫指針相關的 API

readerIndex() 與 readerIndex(int)

前者表示返回當前的讀指針 readerIndex, 後者表示設置讀指針

writeIndex() 與 writeIndex(int)

前者表示返回當前的寫指針 writerIndex, 後者表示設置寫指針

markReaderIndex() 與 resetReaderIndex()

前者表示把當前的讀指針保存起來,後者表示把當前的讀指針恢復到以前保存的值,下面兩段代碼是等價的

// 代碼片斷1
int readerIndex = buffer.readerIndex();
// .. 其餘操做
buffer.readerIndex(readerIndex);

// 代碼片斷二
buffer.markReaderIndex();
// .. 其餘操做
buffer.resetReaderIndex();

但願你們多多使用代碼片斷二這種方式,不須要本身定義變量,不管 buffer 看成參數傳遞到哪裏,調用 resetReaderIndex() 均可以恢復到以前的狀態,在解析自定義協議的數據包的時候很是常見,推薦你們使用這一對 API

markWriterIndex() 與 resetWriterIndex()

這一對 API 的做用與上述一對 API 相似,這裏再也不 贅述

讀寫 API

本質上,關於 ByteBuf 的讀寫均可以看做從指針開始的地方開始讀寫數據

writeBytes(byte[] src) 與 buffer.readBytes(byte[] dst)

writeBytes() 表示把字節數組 src 裏面的數據所有寫到 ByteBuf,而 readBytes() 指的是把 ByteBuf 裏面的數據所有讀取到 dst,這裏 dst 字節數組的大小一般等於 readableBytes(),而 src 字節數組大小的長度一般小於等於 writableBytes()

writeByte(byte b) 與 buffer.readByte()

writeByte() 表示往 ByteBuf 中寫一個字節,而 buffer.readByte() 表示從 ByteBuf 中讀取一個字節,相似的 API 還有 writeBoolean()、writeChar()、writeShort()、writeInt()、writeLong()、writeFloat()、writeDouble() 與 readBoolean()、readChar()、readShort()、readInt()、readLong()、readFloat()、readDouble() 這裏就不一一贅述了,相信讀者應該很容易理解這些 API

與讀寫 API 相似的 API 還有 getBytes、getByte() 與 setBytes()、setByte() 系列,惟一的區別就是 get/set 不會改變讀寫指針,而 read/write 會改變讀寫指針,這點在解析數據的時候千萬要注意

release() 與 retain()

因爲 Netty 使用了堆外內存,而堆外內存是不被 jvm 直接管理的,也就是說申請到的內存沒法被垃圾回收器直接回收,因此須要咱們手動回收。有點相似於c語言裏面,申請到的內存必須手工釋放,不然會形成內存泄漏。

Netty 的 ByteBuf 是經過引用計數的方式管理的,若是一個 ByteBuf 沒有地方被引用到,須要回收底層內存。默認狀況下,當建立完一個 ByteBuf,它的引用爲1,而後每次調用 retain() 方法, 它的引用就加一, release() 方法原理是將引用計數減一,減完以後若是發現引用計數爲0,則直接回收 ByteBuf 底層的內存。

slice()、duplicate()、copy()

這三個方法一般狀況會放到一塊兒比較,這三者的返回值都是一個新的 ByteBuf 對象

  1. slice() 方法從原始 ByteBuf 中截取一段,這段數據是從 readerIndex 到 writeIndex,同時,返回的新的 ByteBuf 的最大容量 maxCapacity 爲原始 ByteBuf 的 readableBytes()
  2. duplicate() 方法把整個 ByteBuf 都截取出來,包括全部的數據,指針信息
  3. slice() 方法與 duplicate() 方法的相同點是:底層內存以及引用計數與原始的 ByteBuf 共享,也就是說通過 slice() 或者 duplicate() 返回的 ByteBuf 調用 write 系列方法都會影響到 原始的 ByteBuf,可是它們都維持着與原始 ByteBuf 相同的內存引用計數和不一樣的讀寫指針
  4. slice() 方法與 duplicate() 不一樣點就是:slice() 只截取從 readerIndex 到 writerIndex 之間的數據,它返回的 ByteBuf 的最大容量被限制到 原始 ByteBuf 的 readableBytes(), 而 duplicate() 是把整個 ByteBuf 都與原始的 ByteBuf 共享
  5. slice() 方法與 duplicate() 方法不會拷貝數據,它們只是經過改變讀寫指針來改變讀寫的行爲,而最後一個方法 copy() 會直接從原始的 ByteBuf 中拷貝全部的信息,包括讀寫指針以及底層對應的數據,所以,往 copy() 返回的 ByteBuf 中寫數據不會影響到原始的 ByteBuf
  6. slice() 和 duplicate() 不會改變 ByteBuf 的引用計數,因此原始的 ByteBuf 調用 release() 以後發現引用計數爲零,就開始釋放內存,調用這兩個方法返回的 ByteBuf 也會被釋放,這個時候若是再對它們進行讀寫,就會報錯。所以,咱們能夠經過調用一次 retain() 方法 來增長引用,表示它們對應的底層的內存多了一次引用,引用計數爲2,在釋放內存的時候,須要調用兩次 release() 方法,將引用計數降到零,纔會釋放內存
  7. 這三個方法均維護着本身的讀寫指針,與原始的 ByteBuf 的讀寫指針無關,相互之間不受影響

retainedSlice() 與 retainedDuplicate()

相信讀者應該已經猜到這兩個 API 的做用了,它們的做用是在截取內存片斷的同時,增長內存的引用計數,分別與下面兩段代碼等價

// retainedSlice 等價於
slice().retain();

// retainedDuplicate() 等價於
duplicate().retain()

使用到 slice 和 duplicate 方法的時候,千萬要理清內存共享,引用計數共享,讀寫指針不共享幾個概念,下面舉兩個常見的易犯錯的例子

1.屢次釋放

Buffer buffer = xxx;
doWith(buffer);
// 一次釋放
buffer.release();


public void doWith(Bytebuf buffer) {
// ...    
    
// 沒有增長引用計數
Buffer slice = buffer.slice();

foo(slice);

}


public void foo(ByteBuf buffer) {
    // read from buffer
    
    // 重複釋放
    buffer.release();
}

這裏的 doWith 有的時候是用戶自定義的方法,有的時候是 Netty 的回調方法,好比 channelRead() 等等

2.不釋放形成內存泄漏

Buffer buffer = xxx;
doWith(buffer);
// 引用計數爲2,調用 release 方法以後,引用計數爲1,沒法釋放內存 
buffer.release();


public void doWith(Bytebuf buffer) {
// ...    
    
// 增長引用計數
Buffer slice = buffer.retainedSlice();

foo(slice);

// 沒有調用 release

}


public void foo(ByteBuf buffer) {
    // read from buffer
}

想要避免以上兩種狀況發生,你們只須要記得一點,在一個函數體裏面,只要增長了引用計數(包括 ByteBuf 的建立和手動調用 retain() 方法),就必須調用 release() 方法。

總結

  1. 本小節,咱們分析了 Netty 對二進制數據的抽象 ByteBuf 的結構,本質上它的原理就是,它引用了一段內存,這段內存能夠是堆內也能夠是堆外的,而後用引用計數來控制這段內存是否須要被釋放,使用讀寫指針來控制對 ByteBuf 的讀寫,能夠理解爲是外觀模式的一種使用
  2. 基於讀寫指針和容量、最大可擴容容量,衍生出一系列的讀寫方法,要注意 read/write 與 get/set 的區別
  3. 多個 ByteBuf 能夠引用同一段內存,經過引用計數來控制內存的釋放,遵循誰 retain() 誰 release() 的原則

以上內容來源於掘金小冊《Netty 入門與實戰:仿寫微信 IM 即時通信系統》,若想得到更多,更詳細的內容,請用微信掃碼訂閱:

相關文章
相關標籤/搜索