
本章介紹
ByteBuf
ByteBufHolder
ByteBufAllocator
使用這些接口配置設定緩沖和執行操作
每當你需要傳輸資料時,它必須包含一個緩沖區。Java NIO API自帶的緩沖區類是相當有限的,沒有經過優化,使用JDK的ByteBuffer操作更複雜。緩沖區是一個重要的組建,它是API的一部分。Netty提供了一個強大的緩沖區實作用于表示一個位元組序列,并幫助你操作原始位元組或自定義的POJO。Netty的ByteBuf相當于JDK的ByteBuffer,ByteBuf的作用是在Netty中通過Channel傳輸資料。它被重新設計以解決JDK的ByteBuffer中的一些問題,進而使開發人員開發網絡應用程式顯得更有效率。本章将講述Netty中的緩沖區,并了解它為什麼比JDK自帶的緩沖區實作更優秀,還會深入了解在Netty中使用ByteBuf通路資料以及如何使用它。
Netty的緩沖API有兩個接口:
Netty使用reference-counting(引用計數)的時候知道安全釋放Buf和其他資源,雖然知道Netty有效的使用引用計數,這都是自動完成的。這允許Netty使用池和其他技巧來加快速度和保持記憶體使用率在正常水準,你不需要做任何事情來實作這一點,但是在開發Netty應用程式時,你應該處理資料盡快釋放池資源。
Netty緩沖API提供了幾個優勢:
可以自定義緩沖類型
通過一個内置的複合緩沖類型實作零拷貝
擴充性好,比如StringBuffer
不需要調用flip()來切換讀/寫模式
讀取和寫入索引分開
方法鍊
引用計數
Pooling(池)
當需要與遠端進行互動時,需要以位元組碼發送/接收資料。由于各種原因,一個高效、友善、易用的資料接口是必須的,而Netty的ByteBuf滿足這些需求,ByteBuf是一個很好的經過優化的資料容器,我們可以将位元組資料有效的添加到ByteBuf中或從ByteBuf中擷取資料。ByteBuf有2部分:一個用于讀,一個用于寫。我們可以按順序的讀取資料,并且可以跳到開始重新讀一遍。所有的資料操作,我們隻需要做的是調整讀取資料索引和再次開始讀操作。
寫入資料到ByteBuf後,寫入索引是增加的位元組數量。開始讀位元組後,讀取索引增加。你可以讀取位元組,直到寫入索引和讀取索引處理相同的位置,次數若繼續讀取,則會抛出IndexOutOfBoundsException。調用ByteBuf的任何方法開始讀/寫都會單獨維護讀索引和寫索引。ByteBuf的預設最大容量限制是Integer.MAX_VALUE,寫入時若超出這個值将會導緻一個異常。
ByteBuf類似于一個位元組數組,最大的差別是讀和寫的索引可以用來控制對緩沖區資料的通路。下圖顯示了一個容量為16的ByteBuf:
使用Netty時會遇到3種不同類型的ByteBuf
Heap Buffer(堆緩沖區)
最常用的類型是ByteBuf将資料存儲在JVM的堆空間,這是通過将資料存儲在數組的實作。堆緩沖區可以快速配置設定,當不使用時也可以快速釋放。它還提供了直接通路數組的方法,通過ByteBuf.array()來擷取byte[]資料。
通路非堆緩沖區ByteBuf的數組會導緻UnsupportedOperationException,可以使用ByteBuf.hasArray()來檢查是否支援通路數組。
Direct Buffer(直接緩沖區)
直接緩沖區,在堆之外直接配置設定記憶體。直接緩沖區不會占用堆空間容量,使用時應該考慮到應用程式要使用的最大記憶體容量以及如何限制它。直接緩沖區在使用Socket傳遞資料時性能很好,因為若使用間接緩沖區,JVM會先将資料複制到直接緩沖區再進行傳遞;但是直接緩沖區的缺點是在配置設定記憶體空間和釋放記憶體時比堆緩沖區更複雜,而Netty使用記憶體池來解決這樣的問題,這也是Netty使用記憶體池的原因之一。直接緩沖區不支援數組通路資料,但是我們可以間接的通路資料數組,如下面代碼:
<b>[java]</b> view plain copy
ByteBuf directBuf = Unpooled.directBuffer(16);
if(!directBuf.hasArray()){
int len = directBuf.readableBytes();
byte[] arr = new byte[len];
directBuf.getBytes(0, arr);
}
通路直接緩沖區的資料數組需要更多的編碼和更複雜的操作,建議若需要在數組通路資料使用堆緩沖區會更好。
Composite Buffer(複合緩沖區)
複合緩沖區,我們可以建立多個不同的ByteBuf,然後提供一個這些ByteBuf組合的視圖。複合緩沖區就像一個清單,我們可以動态的添加和删除其中的ByteBuf,JDK的ByteBuffer沒有這樣的功能。Netty提供了CompositeByteBuf類來處理複合緩沖區,CompositeByteBuf隻是一個視圖,CompositeByteBuf.hasArray()總是傳回false,因為它可能包含一些直接或間接的不同類型的ByteBuf。
例如,一條消息由header和body兩部分組成,将header和body組裝成一條消息發送出去,可能body相同,隻是header不同,使用CompositeByteBuf就不用每次都重新配置設定一個新的緩沖區。下圖顯示CompositeByteBuf組成header和body:
若使用JDK的ByteBuffer就不能這樣簡單的實作,隻能建立一個數組或建立一個新的ByteBuffer,再将内容複制到新的ByteBuffer中。下面是使用CompositeByteBuf的例子:
CompositeByteBuf compBuf = Unpooled.compositeBuffer();
ByteBuf heapBuf = Unpooled.buffer(8);
//添加ByteBuf到CompositeByteBuf
compBuf.addComponents(heapBuf,directBuf);
//删除第一個ByteBuf
compBuf.removeComponent(0);
Iterator<ByteBuf> iter = compBuf.iterator();
while(iter.hasNext()){
System.out.println(iter.next().toString());
//使用數組通路資料
if(!compBuf.hasArray()){
int len = compBuf.readableBytes();
compBuf.getBytes(0, arr);
CompositeByteBuf是ByteBuf的子類,我們可以像操作BytBuf一樣操作CompositeByteBuf。并且Netty優化套接字讀寫的操作是盡可能的使用CompositeByteBuf來做的,使用CompositeByteBuf不會操作記憶體洩露問題。
ByteBuf提供了許多操作,允許修改其中的資料内容或隻是讀取資料。ByteBuf和JDK的ByteBuffer很像,但是ByteBuf提供了更好的性能。
ByteBuf使用zero-based-indexing(從0開始的索引),第一個位元組的索引是0,最後一個位元組的索引是ByteBuf的capacity - 1,下面代碼是周遊ByteBuf的所有位元組:
//create a ByteBuf of capacity is 16
ByteBuf buf = Unpooled.buffer(16);
//write data to buf
for(int i=0;i<16;i++){
buf.writeByte(i+1);
//read data from buf
for(int i=0;i<buf.capacity();i++){
System.out.println(buf.getByte(i));
注意通過索引通路時不會推進讀索引和寫索引,我們可以通過ByteBuf的readerIndex()或writerIndex()來分别推進讀索引或寫索引。
ByteBuf提供兩個指針變量支付讀和寫操作,讀操作是使用readerIndex(),寫操作時使用writerIndex()。這和JDK的ByteBuffer不同,ByteBuffer隻有一個方法來設定索引,是以需要使用flip()方法來切換讀和寫模式。
ByteBuf一定符合:0 <= readerIndex <= writerIndex <= capacity。
我們可以調用ByteBuf.discardReadBytes()來回收已經讀取過的位元組,discardReadBytes()将丢棄從索引0到readerIndex之間的位元組。調用discardReadBytes()方法後會變成如下圖:
ByteBuf.discardReadBytes()可以用來清空ByteBuf中已讀取的資料,進而使ByteBuf有多餘的空間容納新的資料,但是discardReadBytes()可能會涉及記憶體複制,因為它需要移動ByteBuf中可讀的位元組到開始位置,這樣的操作會影響性能,一般在需要馬上釋放記憶體的時候使用收益會比較大。
任何讀操作會增加readerIndex,如果讀取操作的參數也是一個ByteBuf而沒有指定目的索引,指定的目的緩沖區的writerIndex會一起增加,沒有足夠的内容時會抛出IndexOutOfBoundException。新配置設定、包裝、複制的緩沖區的readerIndex的預設值都是0。下面代碼顯示了擷取所有可讀資料:
while(buf.isReadable()){
System.out.println(buf.readByte());
(代碼于原書中有出入,原書可能是基于Netty4之前的版本講解的,此處基于Netty4)
任何寫的操作會增加writerIndex。若寫操作的參數也是一個ByteBuf并且沒有指定資料源索引,那麼指定緩沖區的readerIndex也會一起增加。若沒有足夠的可寫位元組會抛出IndexOutOfBoundException。新配置設定的緩沖區writerIndex的預設值是0。下面代碼顯示了随機一個int數字來填充緩沖區,直到緩沖區空間耗盡:
Random random = new Random();
while(buf.writableBytes() >= 4){
buf.writeInt(random.nextInt());
調用ByteBuf.clear()可以設定readerIndex和writerIndex為0,clear()不會清除緩沖區的内容,隻是将兩個索引值設定為0。請注意ByteBuf.clear()與JDK的ByteBuffer.clear()的語義不同。
下圖顯示了ByteBuf調用clear()之前:
下圖顯示了調用clear()之後:
和discardReadBytes()相比,clear()是便宜的,因為clear()不會複制任何記憶體。
各種indexOf()方法幫助你定位一個值的索引是否符合,我們可以用ByteBufProcessor複雜動态順序搜尋實作簡單的靜态單位元組搜尋。如果你想解碼可變長度的資料,如null結尾的字元串,你會發現bytesBefore(byte value)方法有用。例如我們寫一個內建的flash sockets的應用程式,這個應用程式使用NULL結束的内容,使用bytesBefore(byte value)方法可以很容易的檢查資料中的空位元組。沒有ByteBufProcessor的話,我們需要自己做這些事情,使用ByteBufProcessor效率更好。
每個ByteBuf有兩個标注索引,一個存儲readerIndex,一個存儲writerIndex。你可以通過調用一個重置方法重新定位兩個索引之一,它類似于InputStream的标注和重置方法,沒有讀限制。我們可以通過調用readerIndex(int readerIndex)和writerIndex(int writerIndex)移動讀索引和寫索引到指定位置,調用這兩個方法設定指定索引位置時可能抛出IndexOutOfBoundException。
調用duplicate()、slice()、slice(int index, int length)、order(ByteOrder endianness)會建立一個現有緩沖區的視圖。衍生的緩沖區有獨立的readerIndex、writerIndex和标注索引。如果需要現有緩沖區的全新副本,可以使用copy()或copy(int index, int length)獲得。看下面代碼:
// get a Charset of UTF-8
Charset utf8 = Charset.forName("UTF-8");
// get a ByteBuf
ByteBuf buf = Unpooled.copiedBuffer("Netty in Action rocks!", utf8);
// slice
ByteBuf sliced = buf.slice(0, 14);
// copy
ByteBuf copy = buf.copy(0, 14);
// print "Netty in Action rocks!"
System.out.println(buf.toString(utf8));
// print "Netty in Act"
System.out.println(sliced.toString(utf8));
System.out.println(copy.toString(utf8));
有兩種主要類型的讀寫操作:
get/set操作以索引為基礎,在給定的索引設定或擷取位元組
從目前索引開始讀寫,遞增目前的寫索引或讀索引
ByteBuf的各種讀寫方法或其他一些檢查方法可以看ByteBuf的源碼,這裡不贅述了。
ByteBufHolder是一個輔助類,是一個接口,其實作類是DefaultByteBufHolder,還有一些實作了ByteBufHolder接口的其他接口類。ByteBufHolder的作用就是幫助更友善的通路ByteBuf中的資料,當緩沖區沒用了後,可以使用這個輔助類釋放資源。ByteBufHolder很簡單,提供的可供通路的方法也很少。如果你想實作一個“消息對象”有效負載存儲在ByteBuf,使用ByteBufHolder是一個好主意。
盡管Netty提供的各種緩沖區實作類已經很容易使用,但Netty依然提供了一些使用的工具類,使得建立和使用各種緩沖區更加友善。下面會介紹一些Netty中的緩沖區工具類。
Netty支援各種ByteBuf的池實作,來使Netty提供一種稱為ByteBufAllocator成為可能。ByteBufAllocator負責配置設定ByteBuf執行個體,ByteBufAllocator提供了各種配置設定不同ByteBuf的方法,如需要一個堆緩沖區可以使用ByteBufAllocator.heapBuffer(),需要一個直接緩沖區可以使用ByteBufAllocator.directBuffer(),需要一個複合緩沖區可以使用ByteBufAllocator.compositeBuffer()。其他方法的使用可以看ByteBufAllocator源碼及注釋。
擷取ByteBufAllocator對象很容易,可以從Channel的alloc()擷取,也可以從ChannelHandlerContext的alloc()擷取。看下面代碼:
ServerBootstrap b = new ServerBootstrap();
b.group(group).channel(NioServerSocketChannel.class).localAddress(new InetSocketAddress(port))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// get ByteBufAllocator instance by Channel.alloc()
ByteBufAllocator alloc0 = ch.alloc();
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//get ByteBufAllocator instance by ChannelHandlerContext.alloc()
ByteBufAllocator alloc1 = ctx.alloc();
ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);
}
});
}
});
Netty有兩種不同的ByteBufAllocator實作,一個實作ByteBuf執行個體池将配置設定和回收成本以及記憶體使用降到最低;另一種實作是每次使用都建立一個新的ByteBuf執行個體。Netty預設使用PooledByteBufAllocator,我們可以通過ChannelConfig或通過引導設定一個不同的實作來改變。更多細節在後面講述。
Unpooled也是用來建立緩沖區的工具類,Unpooled的使用也很容易。Unpooled提供了很多方法,詳細方法及使用可以看API文檔或Netty源碼。看下面代碼:
//建立複合緩沖區
//建立堆緩沖區
//建立直接緩沖區
ByteBufUtil提供了一些靜态的方法,在操作ByteBuf時非常有用。ByteBufUtil提供了Unpooled之外的一些方法,也許最有價值的是hexDump(ByteBuf buffer)方法,這個方法傳回指定ByteBuf中可讀位元組的十六進制字元串,可以用于調試程式時列印ByteBuf的内容,十六進制字元串相比位元組而言對使用者更友好。
本章主要學習Netty提供的緩沖區類ByteBuf的建立和簡單實用以及一些操作ByteBuf的工具類。
原文位址http://www.bieryun.com/2159.html