本文是Netty文集中“Netty in action”系列的文章颖低。主要是對Norman Maurer and Marvin Allen Wolfthal 的 《Netty in action》一書簡要翻譯动知,同時對重要點加上一些自己補充和擴展暑刃。
本章含蓋
- ByteBuf —— Netty 數(shù)據(jù)容器
- API細節(jié)
- 使用場景
- 內(nèi)容分配
如我們之前提到的,網(wǎng)絡(luò)數(shù)據(jù)的基本單位是字節(jié)转砖。JAVA NIO 提供了 ByteBuffer 作為字節(jié)的容器舞虱,但是這個類使用過于復(fù)雜并且在一些情況下使用過于笨重。
Netty使用ByteBuf 替換 ByteBuffer当窗,一個強大的實現(xiàn)解決了JDK API 地址的局限性,并且提供了更好的API給網(wǎng)絡(luò)應(yīng)用開發(fā)者
Q:JDK API 的局限性指什么寸宵?
A:JDK API 的局限性有如下幾點:
① 長度固定崖面。一旦buffer分配完成,它的容量不能動態(tài)擴展或者收縮邓馒,當(dāng)需要編碼的POJO對象大于ByteBuffer容量時嘶朱,會發(fā)生索引越界異常蛾坯。
② 只有一個標(biāo)識位置的指針position光酣。讀寫是需要手動flip和rewind等,需要十分小心使用這些API脉课,否則很容易導(dǎo)致異常救军。
③ API功能有限财异。不支持一些高級使用的特性,需要用戶自己實現(xiàn)唱遭。
The ByteBuf API
Netty數(shù)據(jù)處理API通過兩個組件暴露 —— ByteBuf抽象類 和 ByteBufHolder接口
下面是ByteBuf API 的一些優(yōu)點:
- 它是可擴展的用戶自定義緩沖器類型
- 通過構(gòu)建一個復(fù)合緩沖器類型來實現(xiàn)傳輸?shù)牧憧截?/li>
- 容量根據(jù)需求可擴展( 如同JDK的StringBuilder )
- 讀模式和寫模式的轉(zhuǎn)換不需要調(diào)用ByteBuffer的flip方法
- 讀和寫使用不同的索引戳寸,即有兩個索引,一個讀索引和一個寫索引拷泽。
- 支持方法鏈疫鹊,即鏈?zhǔn)秸{(diào)用方法
- 支持引用計數(shù),即一個ByteBuf的引用次數(shù)
- 支持池
ByteBuf 類 —— Netty 數(shù)據(jù)容器
ByteBuf是如何工作的
ByteBuf包含了兩個不同的索引:一個用于讀司致,一個用于寫拆吆。當(dāng)你從ByteBuf中讀數(shù)據(jù)時里伯,readerIndex將增加所讀字節(jié)數(shù)量鹊汛。類似的橙垢,當(dāng)你寫數(shù)據(jù)到ByteBuf時忌堂,writeIndex將增加强窖。
如果你嘗試讀取大于writeIndex位置的數(shù)據(jù)容燕,將觸發(fā)IndexOutOfBoundsException桥狡。
ByteBuf類以 ‘read’ 和 ‘write’ 打頭的方法將增加相應(yīng)的索引拄轻,然后以’set’和‘get’打頭的方法并不會增加索引的值颅围。后一種方法,對相對索引的操作恨搓,會將索引作為參數(shù)傳遞給方法谷浅。
比如:
能夠指定ByteBuf的最大容量,當(dāng)嘗試移動寫索引超過最大容量時將觸發(fā)異常奶卓。( 默認限制 Integer.MAX_VALUE )
ByteBuf 使用模式
當(dāng)我們通過Netty工作時一疯,你將遇到幾種圍繞ByteBuf構(gòu)建的常見使用模式。
ByteBuf主要3種使用模式:①Heap Buffers —— 堆緩沖區(qū)夺姑;②Direct Buffers —— 直接緩沖區(qū)墩邀;③Composite Buffers —— 復(fù)合緩沖區(qū)
Heap Buffers
Heap Buffers :最經(jīng)常使用的ByteBuf模式,存儲數(shù)據(jù)到JVM的堆空間盏浙∶级茫看做一個后臺數(shù)組,這種模式支持快速分配和釋放在不是用池的情況下废膘。
適用于處理遺留數(shù)據(jù)的場景
注意:嘗試去訪問一個后臺數(shù)組當(dāng)hasArray()返回false竹海,這將觸發(fā)一個UnsupportedOperationException異常。這種模式類似與JDK ByteBuffer的使用丐黄。
Direct Buffers
Direct Buffer是另外一種ByteBuf模式斋配。我們希望總是從堆中給創(chuàng)建的對象分配內(nèi)存,但是這不是必須的 —— JDK1.4引進的用于NIO的ByteBuffer允許JVM通過本地調(diào)用實現(xiàn)一個內(nèi)存分配。這個做的目的是為了避免拷貝緩沖區(qū)內(nèi)容到( or from )一個中間緩沖區(qū)在每次本地I/O操作調(diào)用前( or after )艰争。
Javadoc 對于ByteBuffer 明確聲明坏瞄,“直接緩沖區(qū)的內(nèi)容將屬于標(biāo)準(zhǔn)垃圾回收的堆范圍外”。這就解釋了為什么直接緩沖區(qū)是網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)睦硐脒x擇甩卓。如果你的數(shù)據(jù)被包含在一個堆分配的緩沖區(qū)中鸠匀,則JVM實際上就是復(fù)制你的緩沖區(qū)數(shù)據(jù)到直接緩沖區(qū),然后在通過socket發(fā)送逾柿。
直接緩沖區(qū)的主要缺點就是:分配和釋放比基于堆的緩沖區(qū)開銷更高些缀棍。如果你工作在一個遺留代碼上,你可能還會遇到另外一個缺點:因為數(shù)據(jù)不在堆上机错,所以你需要將數(shù)據(jù)拷貝到堆上睦柴。如下:
Composite Buffers
最后一個模式是復(fù)合緩沖區(qū)狱窘,該復(fù)合緩沖區(qū)表示一個多ByteBuf的聚合視圖。你能夠根據(jù)需要添加或刪除ByteBuf實例财搁,這是JDK ByteBuffer現(xiàn)實完全不具有的特性蘸炸。
Netty通過ByteBuf 的子類 CompositeByteBuf來實現(xiàn)這個模式,該模式提供將多個緩沖區(qū)合并為一個緩沖區(qū)的虛擬表示尖奔。
例子:一個包含了兩個部分的消息茴扁,消息頭和消息體铃岔,通過HTTP傳輸。這兩個部分通過不同的應(yīng)用模式生成和裝配當(dāng)消息被發(fā)送的時候峭火。應(yīng)用可選擇復(fù)用消息體對于多個不同的消息毁习。當(dāng)這發(fā)生時,每個消息都會創(chuàng)建一個新的消息頭卖丸。
因為我們不想重新分配兩個緩沖區(qū)給每個消息纺且,CompositeByteBuf完美適用該情況;它消除了不必要的拷貝通過暴露通用的ByteBuf API稍浆。
??直接當(dāng)做整個緩沖區(qū)模式的訪問
注意裳仆,Netty使用CompositeByteBuf優(yōu)化socket I/O 的操作,盡可能的消除JDK的buffer實現(xiàn)造成的性能和內(nèi)存使用量的問題孤钦。這個優(yōu)化實現(xiàn)在了Netty的核心代碼中歧斟,也就是說它不會被暴露,但是我們需要意識到這個造成的影響偏形。
所說的影響可能是:如果你將ByteBuf1静袖、ByteBuf2復(fù)合成一個CompositeByteBuf,那么你對ByteBuf1俊扭、ByteBuf2的修改都會影響到CompositeByteBuf队橙,因為CompositeByteBuf并不會將ByteBuf1和ByteBuf2中的數(shù)據(jù)拷貝一份過來,而是共享了ByteBuf1萨惑、ByteBuf2數(shù)據(jù)捐康。而JDK Bytebuffer的話,不存在該問題庸蔼,因為ByteBuffer的復(fù)合使用只能夠直接拷貝數(shù)據(jù)過來解总,這樣多個ByteBuffer和復(fù)合ByteBuffer之間就不存在數(shù)據(jù)共享的情況了。
字節(jié)操作
ByteBuf提供了大量的讀和寫操作用于修改它的數(shù)據(jù)姐仅。
隨機訪問索引
就像一個普通的java數(shù)組花枫,ByteBuf索引從0開始,最后一個索引值為capacity()-1.
順序訪問索引
ByteBuf有兩個索引馒疹,一個讀索引磕道,一個寫索引。而JDK的ByteBuffer只有個一個索引行冰,這就是為什么在從寫模式轉(zhuǎn)換到讀模式時需要調(diào)用flip()方法溺蕉。
廢棄的字節(jié)
廢棄的字節(jié):已經(jīng)被讀取過的字節(jié)。
已經(jīng)讀取過的字節(jié)能被丟棄并通過調(diào)用discardReadBytes()回收空間悼做。并初始化readerIndex大小為0疯特。
下圖顯示了在圖5.3所示的基礎(chǔ)上調(diào)用discardReadBytes()后的結(jié)果。你能看到廢棄字節(jié)段的空間被轉(zhuǎn)換成了可寫入空間肛走。
你可能嘗試通過頻繁調(diào)用discardReadBytes()為了獲得最大的可寫段漓雅,請留意這將很可能造成內(nèi)存拷貝,因為可讀字節(jié)必須被移動到緩沖區(qū)頭。除非真的需要我們應(yīng)該避免這樣的操作邻吞。
可讀字節(jié)
ByteBuf的可讀字節(jié)段存儲了真實的數(shù)據(jù)组题。一個新分配、封裝抱冷、或復(fù)制的緩沖區(qū)默認的readerIndex為0崔列。任何以’read’打頭的方法或skip方法將檢索或跳過數(shù)據(jù),從當(dāng)前的readerIndex起并通過讀入字節(jié)的數(shù)增加readerIndex旺遮。
如果方法調(diào)用傳入ByteBuf參數(shù)作為一個寫目標(biāo)對象并且沒有一個目標(biāo)index參數(shù)赵讯,那么這個ByteBuf [ 作為參數(shù)傳入的ByteBuf ]的writerIndex將會增加,比如:
readBytes(ByteBuf dest);
如果嘗試從一個可讀字節(jié)已經(jīng)耗盡的緩沖區(qū)里進行讀操作耿眉,那么將引發(fā)IndexOutOfBoundsException異常边翼。
下面展示了如何讀取所有可讀的字節(jié)
可寫字節(jié)
可寫字節(jié)段是一個未定義內(nèi)容的內(nèi)存區(qū)域,并為寫入作好準(zhǔn)備鸣剪。一個新分配的緩沖區(qū)writerIndex的默認值是0组底。任何一個以‘write’打頭的方法操作都會從writerIndex索引開始,增加相應(yīng)的寫入的字節(jié)數(shù)筐骇。如果寫操作的目標(biāo)是一個ByteBuf [ 如斤寇,為下面例子中dest ]并且沒有指定源索引,那么這個被操作的ByteBuf [ 如拥褂,為下面例子中dest ]的readerIndex將增加相應(yīng)的數(shù)量娘锁,比如:
writeBytes(ByteBuf dest);
如果試圖在超過目標(biāo)容量的索引下進行寫入操作,這將引發(fā)一個IndexOutOfBoundException異常饺鹃。
Q:這里說的目標(biāo)容量是不是指最大容量莫秆,因為前面的內(nèi)容說,如果寫索引超過了容量會自動進行擴容悔详,只有寫索引超過了最大容量時镊屎,才會引發(fā)一個異常。
A:是超過最大容量才會引發(fā)異常的
ByteBuf buf = Unpooled.buffer(20, 32);
for(int i = 1; i <= 6; i++ ) {
buf.writeInt(i);
}
for(int i = 7; i <= 9; i++) {
buf.writeInt(i);
}
// 異常
Exception in thread "main" java.lang.IndexOutOfBoundsException: writerIndex(32) + minWritableBytes(4) exceeds maxCapacity(32): UnpooledByteBufAllocator$InstrumentedUnpooledUnsafeHeapByteBuf(ridx: 0, widx: 32, cap: 32/32)
at io.netty.buffer.AbstractByteBuf.ensureWritable0(AbstractByteBuf.java:275)
at io.netty.buffer.AbstractByteBuf.writeInt(AbstractByteBuf.java:982)
at com.bayern.netty.buffer.BufferDemo.main(BufferDemo.java:26)
A:參照的是初始化長度缝驳。
但是請注意,當(dāng)寫入的數(shù)據(jù)超過了初始容量大小归苍,但是小于最大容量大小時用狱,ByteBuf會根據(jù)一定的邏輯進行擴容操作,并更新capacity為新的容量大小值拼弃。新的capacity范圍在minNewCapacity到maxCapacity作為一個上界夏伊。
索引管理
你能夠設(shè)置和重定位ByteBuf的readerIndex和writerIndex通過調(diào)用markReaderIndex(),markWriterIndex()吻氧,resetReaderIndex()溺忧,和resetWriterIndex()咏连。這些操作類似于JDK InputStream的mark(int readlimit)和reset()方法,除了這里沒有指定readlimit參數(shù)來指定讀入多少字節(jié)后mark變成無效鲁森。
你能夠移動索引到指定位置通過調(diào)用readerIndex(int)或writerIndex(int)祟滴。設(shè)置任何一個索引到一個無效的位置將會引發(fā)IndexOutOfBoundsException異常。
通過調(diào)用clear()可以將readerIndex和writerIndex同時置為0歌溉。注意垄懂,這并沒有清除內(nèi)存中的內(nèi)容。如下圖所示:
調(diào)用clear()的開銷遠遠小于discardReadBytes()研底,因為clear()重置了索引的位置埠偿,當(dāng)沒有進行內(nèi)存拷貝透罢。
查詢操作
這有幾種方式去確定ByteBuf中一個指定值的索引榜晦。
① indexOf()
② 更復(fù)雜的查詢可以執(zhí)行一個帶ByteBufProcessor參數(shù)的方法。
派生的緩沖區(qū)
一個派生緩沖區(qū)提供專門的方式表示其內(nèi)容的ByteBuf的視圖羽圃。
視圖的創(chuàng)建方法:
- duplicate()
- slice()
- slice(int, int)
- Unpooled.unmodifiableBuffer(...)
- orderSlice(int)
每個方法都會返回一個新的ByteBuf實例乾胶,每個實例都有他們自己的reader、writer朽寞、marker索引识窿。
同JDK ByteBuffer一樣,其內(nèi)部存儲是共享的脑融。這使得能夠廉價的創(chuàng)建一個被派生的緩沖區(qū)喻频,但這也意味著,如果你修改了派生緩沖區(qū)的內(nèi)容肘迎,那么源實例的內(nèi)容也會被修改甥温。
JDK ByteBuffer的slice()派生的緩沖器也是內(nèi)容共享的:
ByteBuf copying
如果你需要對一個已經(jīng)存在的緩沖區(qū)完全拷貝,可以使用copy()或copy(int, int)妓布。不同于一個派生的緩沖區(qū)姻蚓,該方法返回的ByteBuf是數(shù)據(jù)的獨立副本。
讀/寫 操作
讀/寫的兩種分類:
- get() 和 set() 操作匣沼。從一個給定的索引開始操作狰挡,操作完索引值不會改變
- read() 和 write() 操作。從一個給定索引開始操作释涛,操作完畢會根據(jù)訪問的字節(jié)數(shù)量對索引值做調(diào)整加叁。
更多操作
ByteBuf提供了額外的實用性操作
ByteBufHolder 接口
我們經(jīng)常發(fā)現(xiàn),除了實際數(shù)據(jù)的有效負載外唇撬,我們還需要存儲各種屬性值殉农。HTTP的返回是一個很好的例子;除了字節(jié)代表的內(nèi)容外局荚,還有狀態(tài)碼超凳、cookies等其他屬性愈污。
Netty提供了ByteBufHolder來處理這個常見的情況。ByteBufHolder還提供了Netty高級功能的支持轮傍,比如:緩沖池暂雹。一個ByteBuf能從一個池中獲取,并在不需要的時候自動釋放( 釋放的確切含義能被實現(xiàn)特定 )创夜。
ByteBufHolder只有幾個少數(shù)的方法用于訪問底層數(shù)據(jù)和引用計數(shù)( reference counting )杭跪。
ByteBufHolder是一個很好的選擇,如果你想要實現(xiàn)將一個消息對象的負載存儲在一個ByteBuf中驰吓。
ByteBuf 分配
該章節(jié)我們將介紹幾種管理ByteBuf實例的方法
ByteBufAllocator 接口
為了減少分配和釋放內(nèi)存的消耗涧尿,Netty使用ByteBufAllocator接口實現(xiàn)了池,該實現(xiàn)能夠分配我們所描述的任何種類的ByteBuf實例檬贰。池的使用是特定于應(yīng)用程序的決定姑廉,這決定不會對ByteBuf API做任何改變。
你能夠從一個Channel或通過ChannelHandlerContext得到一個ByteBufAllocator的引用翁涤。
Netty為ByteBufAllocator提供了兩個實現(xiàn):PooledByteBufAllocator和UnpooledByteBufAllocator桥言。
前一種( PooledByteBufAllocator )實現(xiàn)池的ByteBuf,用于提高性能和減小內(nèi)存碎片葵礼。該實現(xiàn)使用了jemalloc的內(nèi)存分配的有效方法号阿,jemalloc已經(jīng)被大部分現(xiàn)代操作系統(tǒng)所采用。
后一種( UnpooledByteBufAllocator )實現(xiàn)非池的ByteBuf實例鸳粉,當(dāng)調(diào)用時總是返回一個新的對象扔涧。
盡管Netty默認使用PooledByteBufAllocator,但這能被輕易的改變届谈,通過ChannelConfig API 或在啟動你的應(yīng)用時指定一個不同的分配器枯夜。
Unpooled buffers
當(dāng)你沒有一個ByteBufAllocator引用時,Netty提供了一個可利用的類叫Unpooled疼约,Unpooled提供了靜態(tài)的幫助方法去創(chuàng)建一個非池的ByteBuf實例卤档。
Unpooled類使ByteBuf能在在非網(wǎng)絡(luò)項目中有效使用,這使得項目能從高性能可擴展的buffer API中獲益并且不需要其他的Netty組件程剥。
ByteBufUtil 類
ByteBufUtil 提供靜態(tài)的幫助方法用于管理一個ByteBuf劝枣。因為ByteBufUtil的API 非常通用且與池?zé)o關(guān),所以它的方法實現(xiàn)都在分配類外面织鲸。
ByteBufUtil最重要的方法大概就是hexdump()舔腾,hexdump()打印一個ByteBuf內(nèi)容的十六進制的表示。這在多種情況下是非常有用的搂擦,比如打印內(nèi)容用于debug稳诚。一個十六進制的表示通常比直接使用二進制的表示提供更有用的日志條目。而且瀑踢,十六進制版本能夠更簡單的被轉(zhuǎn)換回實際的字節(jié)的表示扳还。
另一個有用的方法是boolean equals(ByteBuf , ByteBuf)才避,該方法決定了兩個ByteBuf實例是否相同。
reference counting —— 引用計數(shù)
引用計數(shù)是一個用于優(yōu)化內(nèi)存使用和性能的技術(shù)氨距,該技術(shù)通過釋放對象持有的資源來實現(xiàn)優(yōu)化內(nèi)存和性能桑逝,當(dāng)對象不再被任何一個對象引用時該對象就會被釋放。
Netty 4 對ByteBuf 和 ByteBufHolder 引入了引用計數(shù)俏让,ByteBuf 和 ByteBufHolder都實現(xiàn)了ReferenceCounted接口楞遏。
引用計數(shù)的思路并不復(fù)雜;通常它包含追蹤活躍引用的數(shù)量到一個指定的對象首昔。一個ReferenceCounted實現(xiàn)實例通常以活躍引用值1開始( 也就是當(dāng)ReferenceCounted實現(xiàn)實例被創(chuàng)建的時候寡喝,其引用計數(shù)值就為1了 )。當(dāng)引用計數(shù)值大于0時勒奇,該對象保證不會被釋放预鬓。當(dāng)引用計數(shù)指減小到0時,該實例將被釋放撬陵。注意珊皿,釋放的確切含義能被實現(xiàn)特定网缝,但是已經(jīng)被釋放的對象不應(yīng)該再被使用巨税。
引用計數(shù)對于池的實現(xiàn)是不可以缺少的,比如像PooledByteBufAllocator粉臊,它減少了內(nèi)存分配的開銷草添。
嘗試去訪問一個已經(jīng)釋放的引用計數(shù)對象,將返回一個IllegalReferenceCountException異常扼仲。
注意远寸,一個指定的類可以定義它自己的釋放計數(shù)契約以它們特有的方式。舉個例子屠凶,我們能想象一個類驰后,它實現(xiàn)了release()方法,總是設(shè)置引用值為0無論當(dāng)前的值是什么矗愧,這將使所有的有效引用同時變得無效灶芝。
后記
本文主要對Netty的ByteBuf進行了詳細的介紹。Bytebuf是Netty中存儲數(shù)據(jù)的容器唉韭,相比于JDK 的ByteBuffer又進行了進一步的優(yōu)化和加強夜涕。后期我們會通過源碼解析的方式進一步的了解ByteBuf在Netty中的使用。
若文章有任何錯誤属愤,望大家不吝指教:)