Netty In Action中文版 - 第五章:Buffers(緩衝)

本章介紹

  • ByteBuf
  • ByteBufHolder
  • ByteBufAllocator
  • 使用這些接口分配緩衝和執行操作
        每當你需要傳輸數據時,它必須包含一個緩衝區。Java NIO API自帶的緩衝區類是相當有限的,沒有經過優化,使用JDK的ByteBuffer操作更復雜。緩衝區是一個重要的組建,它是API的一部分。Netty提供了一個強大的緩衝區實現用於表示一個字節序列,並幫助你操作原始字節或自定義的POJO。Netty的ByteBuf相當於JDK的ByteBuffer,ByteBuf的作用是在Netty中通過Channel傳輸數據。它被重新設計以解決JDK的ByteBuffer中的一些問題,從而使開發人員開發網絡應用程序顯得更有效率。本章將講述Netty中的緩衝區,並瞭解它爲什麼比JDK自帶的緩衝區實現更優秀,還會深入瞭解在Netty中使用ByteBuf訪問數據以及如何使用它。

5.1 Buffer API

        Netty的緩衝API有兩個接口:
  • ByteBuf
  • ByteBufHolder
Netty使用reference-counting(引用計數)的時候知道安全釋放Buf和其他資源,雖然知道Netty有效的使用引用計數,這都是自動完成的。這允許Netty使用池和其他技巧來加快速度和保持內存利用率在正常水平,你不需要做任何事情來實現這一點,但是在開發Netty應用程序時,你應該處理數據儘快釋放池資源。
        Netty緩衝API提供了幾個優勢:
  • 可以自定義緩衝類型
  • 通過一個內置的複合緩衝類型實現零拷貝
  • 擴展性好,比如StringBuffer
  • 不需要調用flip()來切換讀/寫模式
  • 讀取和寫入索引分開
  • 方法鏈
  • 引用計數
  • Pooling(池)

5.2 ByteBuf - 字節數據容器

        當需要與遠程進行交互時,需要以字節碼發送/接收數據。由於各種原因,一個高效、方便、易用的數據接口是必須的,而Netty的ByteBuf滿足這些需求,ByteBuf是一個很好的經過優化的數據容器,我們可以將字節數據有效的添加到ByteBuf中或從ByteBuf中獲取數據。ByteBuf有2部分:一個用於讀,一個用於寫。我們可以按順序的讀取數據,並且可以跳到開始重新讀一遍。所有的數據操作,我們只需要做的是調整讀取數據索引和再次開始讀操作。

5.2.1 ByteBuf如何在工作?

        寫入數據到ByteBuf後,寫入索引是增加的字節數量。開始讀字節後,讀取索引增加。你可以讀取字節,直到寫入索引和讀取索引處理相同的位置,次數若繼續讀取,則會拋出IndexOutOfBoundsException。調用ByteBuf的任何方法開始讀/寫都會單獨維護讀索引和寫索引。ByteBuf的默認最大容量限制是Integer.MAX_VALUE,寫入時若超出這個值將會導致一個異常。
        ByteBuf類似於一個字節數組,最大的區別是讀和寫的索引可以用來控制對緩衝區數據的訪問。下圖顯示了一個容量爲16的ByteBuf:

5.2.2 不同類型的ByteBuf

        使用Netty時會遇到3種不同類型的ByteBuf
Heap Buffer(堆緩衝區)
        最常用的類型是ByteBuf將數據存儲在JVM的堆空間,這是通過將數據存儲在數組的實現。堆緩衝區可以快速分配,當不使用時也可以快速釋放。它還提供了直接訪問數組的方法,通過ByteBuf.array()來獲取byte[]數據。
        訪問非堆緩衝區ByteBuf的數組會導致UnsupportedOperationException,可以使用ByteBuf.hasArray()來檢查是否支持訪問數組。
Direct Buffer(直接緩衝區)
        直接緩衝區,在堆之外直接分配內存。直接緩衝區不會佔用堆空間容量,使用時應該考慮到應用程序要使用的最大內存容量以及如何限制它。直接緩衝區在使用Socket傳遞數據時性能很好,因爲若使用間接緩衝區,JVM會先將數據複製到直接緩衝區再進行傳遞;但是直接緩衝區的缺點是在分配內存空間和釋放內存時比堆緩衝區更復雜,而Netty使用內存池來解決這樣的問題,這也是Netty使用內存池的原因之一。直接緩衝區不支持數組訪問數據,但是我們可以間接的訪問數據數組,如下面代碼:
		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 directBuf = Unpooled.directBuffer(16);
		//添加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();
			byte[] arr = new byte[len];
			compBuf.getBytes(0, arr);
		}
CompositeByteBuf是ByteBuf的子類,我們可以像操作BytBuf一樣操作CompositeByteBuf。並且Netty優化套接字讀寫的操作是儘可能的使用CompositeByteBuf來做的,使用CompositeByteBuf不會操作內存泄露問題。

5.3 ByteBuf的字節操作

        ByteBuf提供了許多操作,允許修改其中的數據內容或只是讀取數據。ByteBuf和JDK的ByteBuffer很像,但是ByteBuf提供了更好的性能。

5.3.1 隨機訪問索引

        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()來分別推進讀索引或寫索引。

5.3.2 順序訪問索引

        ByteBuf提供兩個指針變量支付讀和寫操作,讀操作是使用readerIndex(),寫操作時使用writerIndex()。這和JDK的ByteBuffer不同,ByteBuffer只有一個方法來設置索引,所以需要使用flip()方法來切換讀和寫模式。
        ByteBuf一定符合:0 <= readerIndex <= writerIndex <= capacity。

5.3.3 Discardable bytes廢棄字節

        我們可以調用ByteBuf.discardReadBytes()來回收已經讀取過的字節,discardReadBytes()將丟棄從索引0到readerIndex之間的字節。調用discardReadBytes()方法後會變成如下圖:

        ByteBuf.discardReadBytes()可以用來清空ByteBuf中已讀取的數據,從而使ByteBuf有多餘的空間容納新的數據,但是discardReadBytes()可能會涉及內存複製,因爲它需要移動ByteBuf中可讀的字節到開始位置,這樣的操作會影響性能,一般在需要馬上釋放內存的時候使用收益會比較大。

5.3.4 可讀字節(實際內容)

        任何讀操作會增加readerIndex,如果讀取操作的參數也是一個ByteBuf而沒有指定目的索引,指定的目的緩衝區的writerIndex會一起增加,沒有足夠的內容時會拋出IndexOutOfBoundException。新分配、包裝、複製的緩衝區的readerIndex的默認值都是0。下面代碼顯示了獲取所有可讀數據:
		ByteBuf buf = Unpooled.buffer(16);
		while(buf.isReadable()){
			System.out.println(buf.readByte());
		}
(代碼於原書中有出入,原書可能是基於Netty4之前的版本講解的,此處基於Netty4)

5.3.5 可寫字節Writable bytes

        任何寫的操作會增加writerIndex。若寫操作的參數也是一個ByteBuf並且沒有指定數據源索引,那麼指定緩衝區的readerIndex也會一起增加。若沒有足夠的可寫字節會拋出IndexOutOfBoundException。新分配的緩衝區writerIndex的默認值是0。下面代碼顯示了隨機一個int數字來填充緩衝區,直到緩衝區空間耗盡:
		Random random = new Random();
		ByteBuf buf = Unpooled.buffer(16);
		while(buf.writableBytes() >= 4){
			buf.writeInt(random.nextInt());
		}

5.3.6 清除緩衝區索引Clearing the buffer indexs

        調用ByteBuf.clear()可以設置readerIndex和writerIndex爲0,clear()不會清除緩衝區的內容,只是將兩個索引值設置爲0。請注意ByteBuf.clear()與JDK的ByteBuffer.clear()的語義不同。
        下圖顯示了ByteBuf調用clear()之前:

        下圖顯示了調用clear()之後:

        和discardReadBytes()相比,clear()是便宜的,因爲clear()不會複製任何內存。

5.3.7 搜索操作Search operations

        各種indexOf()方法幫助你定位一個值的索引是否符合,我們可以用ByteBufProcessor複雜動態順序搜索實現簡單的靜態單字節搜索。如果你想解碼可變長度的數據,如null結尾的字符串,你會發現bytesBefore(byte value)方法有用。例如我們寫一個集成的flash sockets的應用程序,這個應用程序使用NULL結束的內容,使用bytesBefore(byte value)方法可以很容易的檢查數據中的空字節。沒有ByteBufProcessor的話,我們需要自己做這些事情,使用ByteBufProcessor效率更好。

5.3.8 標準和重置Mark and reset

        每個ByteBuf有兩個標註索引,一個存儲readerIndex,一個存儲writerIndex。你可以通過調用一個重置方法重新定位兩個索引之一,它類似於InputStream的標註和重置方法,沒有讀限制。我們可以通過調用readerIndex(int readerIndex)和writerIndex(int writerIndex)移動讀索引和寫索引到指定位置,調用這兩個方法設置指定索引位置時可能拋出IndexOutOfBoundException。

5.3.9 衍生的緩衝區Derived buffers

        調用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));
		// print "Netty in Act"
		System.out.println(copy.toString(utf8));

5.3.10 讀/寫操作以及其他一些操作

        有兩種主要類型的讀寫操作:
  • get/set操作以索引爲基礎,在給定的索引設置或獲取字節
  • 從當前索引開始讀寫,遞增當前的寫索引或讀索引
        ByteBuf的各種讀寫方法或其他一些檢查方法可以看ByteBuf的源碼,這裏不贅述了。

5.4 ByteBufHolder

        ByteBufHolder是一個輔助類,是一個接口,其實現類是DefaultByteBufHolder,還有一些實現了ByteBufHolder接口的其他接口類。ByteBufHolder的作用就是幫助更方便的訪問ByteBuf中的數據,當緩衝區沒用了後,可以使用這個輔助類釋放資源。ByteBufHolder很簡單,提供的可供訪問的方法也很少。如果你想實現一個“消息對象”有效負載存儲在ByteBuf,使用ByteBufHolder是一個好主意。
        儘管Netty提供的各種緩衝區實現類已經很容易使用,但Netty依然提供了一些使用的工具類,使得創建和使用各種緩衝區更加方便。下面會介紹一些Netty中的緩衝區工具類。

5.4.1 ByteBufAllocator

        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或通過引導設置一個不同的實現來改變。更多細節在後面講述。

5.4.2 Unpooled

        Unpooled也是用來創建緩衝區的工具類,Unpooled的使用也很容易。Unpooled提供了很多方法,詳細方法及使用可以看API文檔或Netty源碼。看下面代碼:
		//創建複合緩衝區
		CompositeByteBuf compBuf = Unpooled.compositeBuffer();
		//創建堆緩衝區
		ByteBuf heapBuf = Unpooled.buffer(8);
		//創建直接緩衝區
		ByteBuf directBuf = Unpooled.directBuffer(16);

5.4.3 ByteBufUtil

        ByteBufUtil提供了一些靜態的方法,在操作ByteBuf時非常有用。ByteBufUtil提供了Unpooled之外的一些方法,也許最有價值的是hexDump(ByteBuf buffer)方法,這個方法返回指定ByteBuf中可讀字節的十六進制字符串,可以用於調試程序時打印ByteBuf的內容,十六進制字符串相比字節而言對用戶更友好。

5.5 Summary

        本章主要學習Netty提供的緩衝區類ByteBuf的創建和簡單實用以及一些操作ByteBuf的工具類。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章