學習Netty也有一段時間了,Netty做爲一個高性能的異步框架,不少RPC框架也運用到了Netty中的知識,在rpc框架中豐富的數據協議及編解碼可讓使用者更加青睞;
Netty支持豐富的編解碼框架,其自己內部提供的編解碼也能夠應對各類業務場景;
今天主要就是學習下Netty中提供的編、解碼類,以前只是簡單的使用了下Netty提供的解碼類,今天更加深刻的研究下Netty中編、解碼的源碼及部分使用。java
編碼(Encoder)git
編碼就是將咱們發送的數據編碼成字節數組方便在網絡中進行傳輸,相似Java中的序列化,將對象序列化成字節傳輸
解碼(Decoder)github
解碼和編碼相反,將傳輸過來的字節數組轉化爲各類對象來進行展現等,相似Java中的反序列化 如: // 將字節數組轉化爲字符串 new String(byte bytes[], Charset charset)
ByteToMessageDecoder: 解碼超類,將字節轉換成消息數組
解碼解碼通常用於將獲取到的消息解碼成系統可識別且本身須要的數據結構;所以ByteToMessageDecoder須要繼承ChannelInboundHandlerAdapter入站適配器來獲取到入站的數據,在handler使用以前經過channelRead獲取入站數據進行一波解碼;
經過channelRead獲取入站數據,將數據緩存至cumulation數據緩衝區,最後在傳給decode進行解碼,在read完成以後清空緩存的數據promise
1. 獲取入站數據緩存
/** * 經過重寫channelRead方法來獲取入站數據 */ @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { // 檢測是不是byteBuf對象格式數據 if (msg instanceof ByteBuf) { // 實例化字節解碼成功輸出集合 即List<Object> out CodecOutputList out = CodecOutputList.newInstance(); try { // 獲取到的請求的數據 ByteBuf data = (ByteBuf) msg; // 若是緩衝數據區爲空則表明是首次觸發read方法 first = cumulation == null; if (first) { // 若是是第一次read則當前msg數據爲緩衝數據 cumulation = data; } else { // 若是不是則觸發累加,將緩衝區的舊數據和新獲取到的數據經過 expandCumulation 方法累加在一塊兒存入緩衝區cumulation // cumulator 累加類,將緩衝池中數據和新數據進行組合在一塊兒 // private Cumulator cumulator = MERGE_CUMULATOR; cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data); } // 將緩衝區數據cumulation進行解碼 callDecode(ctx, cumulation, out); } catch (DecoderException e) { throw e; } catch (Throwable t) { throw new DecoderException(t); } finally { // 在解碼完畢後釋放引用和清空全局字節緩衝區 if (cumulation != null && !cumulation.isReadable()) { numReads = 0; cumulation.release(); cumulation = null; // discardAfterReads爲netty中設置的讀取多少次後開始丟棄字節 默認值16 // 可經過setDiscardAfterReads(int n)來設置值不設置默認16次 } else if (++ numReads >= discardAfterReads) { // We did enough reads already try to discard some bytes so we not risk to see a OOME. // 在咱們讀取了足夠的數據能夠嘗試丟棄一些字節已保證不出現內存溢出的異常 // // See https://github.com/netty/netty/issues/4275 // 讀取次數重置爲0 numReads = 0; // 重置讀寫指針或丟棄部分已讀取的字節 discardSomeReadBytes(); } // out爲解碼成功的傳遞給下一個handler int size = out.size(); decodeWasNull = !out.insertSinceRecycled(); // 結束當前read傳遞到下個ChannelHandler fireChannelRead(ctx, out, size); // 回收響應集合 將insertSinceRecycled設置爲false; // insertSinceRecycled用於channelReadComplete判斷使用 out.recycle(); } } else { // 不是的話直接fire傳遞給下一個handler ctx.fireChannelRead(msg); } }
public static final Cumulator MERGE_CUMULATOR = new Cumulator() { /** * alloc ChannelHandlerContext分配的字節緩衝區 * cumulation 當前ByteToMessageDecoder類全局的字節緩衝區 * in 入站的字節緩衝區 **/ @Override public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) { final ByteBuf buffer; // 若是全局ByteBuf寫入的字節+當前入站的字節數據大於全局緩衝區最大的容量或者全局緩衝區的引用數大於1個或全局緩衝區只讀 if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes() || cumulation.refCnt() > 1 || cumulation.isReadOnly()) { // Expand cumulation (by replace it) when either there is not more room in the buffer // or if the refCnt is greater then 1 which may happen when the user use slice().retain() or // duplicate().retain() or if its read-only. // // See: // - https://github.com/netty/netty/issues/2327 // - https://github.com/netty/netty/issues/1764 // 進行擴展全局字節緩衝區(容量大小 = 新數據追加到舊數據末尾組成新的全局字節緩衝區) buffer = expandCumulation(alloc, cumulation, in.readableBytes()); } else { buffer = cumulation; } // 將新數據寫入緩衝區 buffer.writeBytes(in); // 釋放當前的字節緩衝區的引用 in.release(); return buffer; } }; /** * alloc 字節緩衝區操做類 * cumulation 全局累加字節緩衝區 * readable 讀取到的字節數長度 */ // 字節緩衝區擴容方法 static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) { // 舊數據 ByteBuf oldCumulation = cumulation; // 經過ByteBufAllocator將緩衝區擴大到oldCumulation + readable大小 cumulation = alloc.buffer(oldCumulation.readableBytes() + readable); // 將舊數據從新寫入到新的字節緩衝區 cumulation.writeBytes(oldCumulation); // 舊字節緩衝區引用-1 oldCumulation.release(); return cumulation; }
@Override public boolean release() { return release0(1); } @Override public boolean release(int decrement) { return release0(checkPositive(decrement, "decrement")); } /** * decrement 減量 */ private boolean release0(int decrement) { for (;;) { int refCnt = this.refCnt; // 當前引用小於減量 if (refCnt < decrement) { throw new IllegalReferenceCountException(refCnt, -decrement); } // 這裏就利用裏線程併發中的知識CAS,線程安全的設置refCnt的值 if (refCntUpdater.compareAndSet(this, refCnt, refCnt - decrement)) { // 若是減量和引用量相等 if (refCnt == decrement) { // 所有釋放 deallocate(); return true; } return false; } } }
4. 將全局字節緩衝區進行解碼安全
/** * ctx ChannelHandler的上下文,用於傳輸數據與下一個handler來交互 * in 入站數據 * out 解析以後的出站集合 (此出站不是返回給客戶端的而是傳遞給下個handler的) */ protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { try { // 若是入站數據還有沒解析的 while (in.isReadable()) { // 解析成功的出站集合長度 int outSize = out.size(); // 若是大於0則說明解析成功的數據還沒被消費完,直接fire掉給通道中的後續handler繼續 消費 if (outSize > 0) { fireChannelRead(ctx, out, outSize); out.clear(); // Check if this handler was removed before continuing with decoding. // 在這個handler刪除以前檢查是否還在繼續解碼 // If it was removed, it is not safe to continue to operate on the buffer. // 若是移除了,它繼續操做緩衝區是不安全的 // // See: // - https://github.com/netty/netty/issues/4635 if (ctx.isRemoved()) { break; } outSize = 0; } // 入站數據字節長度 int oldInputLength = in.readableBytes(); // 開始解碼數據 decodeRemovalReentryProtection(ctx, in, out); // Check if this handler was removed before continuing the loop. // // If it was removed, it is not safe to continue to operate on the buffer. // // See https://github.com/netty/netty/issues/1664 if (ctx.isRemoved()) { break; } // 解析完畢跳出循環 if (outSize == out.size()) { if (oldInputLength == in.readableBytes()) { break; } else { continue; } } if (oldInputLength == in.readableBytes()) { throw new DecoderException( StringUtil.simpleClassName(getClass()) + ".decode() did not read anything but decoded a message."); } if (isSingleDecode()) { break; } } } catch (DecoderException e) { throw e; } catch (Throwable cause) { throw new DecoderException(cause); } } final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { // 設置解碼狀態爲正在解碼 STATE_INIT = 0; STATE_CALLING_CHILD_DECODE = 1; STATE_HANDLER_REMOVED_PENDING = 2; 分別爲初始化; 解碼; 解碼完畢移除 decodeState = STATE_CALLING_CHILD_DECODE; try { // 具體的解碼邏輯(netty提供的解碼器或自定義解碼器中重寫的decode方法) decode(ctx, in, out); } finally { // 此時decodeState爲正在解碼中 值爲1,返回false boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING; // 在設置爲初始化等待解碼 decodeState = STATE_INIT; // 解碼完成移除當前ChannelHandler標記爲不處理 // 能夠看看handlerRemoved源碼。若是緩衝區還有數據直接傳遞給下一個handler if (removePending) { handlerRemoved(ctx); } } }
@Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { // 讀取次數重置 numReads = 0; // 重置讀寫index discardSomeReadBytes(); // 在channelRead meth中定義賦值 decodeWasNull = !out.insertSinceRecycled(); // out指的是解碼集合List<Object> out; 我們能夠點進 if (decodeWasNull) { decodeWasNull = false; if (!ctx.channel().config().isAutoRead()) { ctx.read(); } } // fire掉readComplete傳遞到下一個handler的readComplete ctx.fireChannelReadComplete(); } /** * 而後咱們能夠搜索下insertSinceRecucled在什麼地方被賦值了 * Returns {@code true} if any elements where added or set. This will be reset once {@link #recycle()} was called. */ boolean insertSinceRecycled() { return insertSinceRecycled; } // 搜索下insert的調用咱們能夠看到是CodecOutputList類即爲channelRead中的out集合,衆所周知在 decode完以後,解碼數據就會被調用add方法,此時insertSinceRecycled被設置爲true private void insert(int index, Object element) { array[index] = element; insertSinceRecycled = true; } /** * 清空回收數組內部的全部元素和存儲空間 * Recycle the array which will clear it and null out all entries in the internal storage. */ // 搜索recycle的調用我麼能夠知道在channelRead的finally邏輯中 調用了out.recycle();此時 insertSinceRecycled被設置爲false void recycle() { for (int i = 0 ; i < size; i ++) { array[i] = null; } clear(); insertSinceRecycled = false; handle.recycle(this); }
至此ByteToMessageDecoder解碼類應該差很少比較清晰了!!!網絡
MessageToByteEncoder: 編碼超類,將消息轉成字節進行編碼發出數據結構
何謂編碼,就是將發送數據轉化爲客戶端和服務端約束好的數據結構和格式進行傳輸,咱們能夠在編碼過程當中將消息體body的長度和一些頭部信息有序的設置到ByteBuf字節緩衝區中;方便解碼方靈活的運用來判斷(是否完整的包等)和處理業務;解碼是繼承入站數據,反之編碼應該繼承出站的數據;接下來咱們看看編碼類是怎麼進行編碼的;
既然是繼承出站類,咱們直接看看write方法是怎麼樣的併發
/** * 經過write方法獲取到出站的數據即要發送出去的數據 * ctx channelHandler上下文 * msg 發送的數據 Object能夠經過繼承類指定的泛型來指定 * promise channelPromise異步監聽,相似ChannelFuture,只不過promise能夠設置監聽的結果,future只能經過獲取監聽的成功失敗結果;能夠去了解下promise和future的區別 */ @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { ByteBuf buf = null; try { // 檢測發送數據的類型 經過TypeParameterMatcher類型匹配器 if (acceptOutboundMessage(msg)) { @SuppressWarnings("unchecked") I cast = (I) msg; // 分配字節緩衝區 preferDirect默認爲true buf = allocateBuffer(ctx, cast, preferDirect); try { // 進行編碼 encode(ctx, cast, buf); } finally { // 完成編碼後釋放對象的引用 ReferenceCountUtil.release(cast); } // 若是緩衝區有數據則經過ctx發送出去,promise能夠監聽數據傳輸並設置是否完成 if (buf.isReadable()) { ctx.write(buf, promise); } else { // 若是沒有數據則釋放字節緩衝區的引用併發送一個empty的空包 buf.release(); ctx.write(Unpooled.EMPTY_BUFFER, promise); } buf = null; } else { // 非TypeParameterMatcher類型匹配器匹配的類型直接發送出去 ctx.write(msg, promise); } } catch (EncoderException e) { throw e; } catch (Throwable e) { throw new EncoderException(e); } finally { if (buf != null) { buf.release(); } } } // 初始化設置preferDirect爲true protected MessageToByteEncoder() { this(true); } protected MessageToByteEncoder(boolean preferDirect) { matcher = TypeParameterMatcher.find(this, MessageToByteEncoder.class, "I"); this.preferDirect = preferDirect; }
編碼: 重寫encode方法,根據實際業務來進行數據編碼
// 此處就是咱們須要重寫的編碼方法了,咱們和根據約束好的或者本身定義好想要的數據格式發送給對方 // 下面是我本身寫的demo的編碼方法;頭部設置好body的長度,服務端能夠根據長度來判斷是不是完整的包,僅僅自學寫的簡單的demo非正常線上運營項目的邏輯 public class MyClientEncode extends MessageToByteEncoder<String> { @Override protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception { if (null != msg) { byte[] request = msg.getBytes(Charset.forName("UTF-8")); out.writeInt(request.length); out.writeBytes(request); } } }
編碼類相對要簡單不少,由於只須要將發送的數據序列化,按照必定的格式進行發送數據!!!
項目實戰
項目主要簡單的實現下自定義編解碼器的運用及LengthFieldBasedFrameDecoder的使用
項目結構以下
│ hetangyuese-netty-06.iml │ pom.xml │ ├─src │ ├─main │ │ ├─java │ │ │ └─com │ │ │ └─hetangyuese │ │ │ └─netty │ │ │ ├─client │ │ │ │ MyClient06.java │ │ │ │ MyClientChannelInitializer.java │ │ │ │ MyClientDecoder.java │ │ │ │ MyClientEncode.java │ │ │ │ MyClientHandler.java │ │ │ │ MyMessage.java │ │ │ │ │ │ │ └─server │ │ │ MyChannelInitializer.java │ │ │ MyServer06.java │ │ │ MyServerDecoder.java │ │ │ MyServerDecoderLength.java │ │ │ MyServerEncoder.java │ │ │ MyServerHandler.java │ │ │ │ │ └─resources │ └─test │ └─java
服務端
Serverhandler: 只是簡單的將解碼的內容輸出
public class MyServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("客戶端鏈接成功 time: " + new Date().toLocaleString()); } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { System.out.println("客戶端斷開鏈接 time: " + new Date().toLocaleString()); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { String body = (String) msg; System.out.println("content:" + body); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { // 出現異常關閉通道 cause.printStackTrace(); ctx.close(); } }
解碼器
public class MyServerDecoder extends ByteToMessageDecoder { // 此處我頭部只塞了長度字段佔4個字節,別問爲啥我知道,這是要客戶端和服務端約束好的 private static int min_head_length = 4; @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { // 解碼的字節長度 int size = in.readableBytes(); if(size < min_head_length) { System.out.println("解析的數據長度小於頭部長度字段的長度"); return ; } // 讀取的時候指針已經移位到長度字段的尾端 int length = in.readInt(); if (size < length) { System.out.println("解析的數據長度與長度不符合"); return ; } // 上面已經讀取到了長度字段,後面的長度就是body ByteBuf decoderArr = in.readBytes(length); byte[] request = new byte[decoderArr.readableBytes()]; // 將數據寫入空數組 decoderArr.readBytes(request); String body = new String(request, Charset.forName("UTF-8")); out.add(body); } }
將解碼器加入到channelHandler中:記得加到業務handler的前面不然無效
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline() // .addLast(new MyServerDecoderLength(10240, 0, 4, 0, 0)) // .addLast(new LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0)) .addLast(new MyServerDecoder()) .addLast(new MyServerHandler()) ; } }
客戶端
ClientHandler
public class MyClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("與服務端鏈接成功"); for (int i = 0; i<10; i++) { ctx.writeAndFlush("hhhhh" + i); } } @Override public void channelInactive(ChannelHandlerContext ctx) throws Exception { System.out.println("與服務端斷開鏈接"); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("收到服務端消息:" +msg+ " time: " + new Date().toLocaleString()); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { cause.printStackTrace(); ctx.close(); } }
編碼器
public class MyClientEncode extends MessageToByteEncoder<String> { @Override protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) throws Exception { if (null != msg) { byte[] request = msg.getBytes(Charset.forName("UTF-8")); out.writeInt(request.length); out.writeBytes(request); } } }
將編碼器加到ClientHandler的前面
public class MyClientChannelInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline() .addLast(new MyClientDecoder()) .addLast(new MyClientEncode()) .addLast(new MyClientHandler()) ; } }
服務端運行結果
MyServer06 is start ................... 客戶端鏈接成功 time: 2019-11-19 16:35:47 content:hhhhh0 content:hhhhh1 content:hhhhh2 content:hhhhh3 content:hhhhh4 content:hhhhh5 content:hhhhh6 content:hhhhh7 content:hhhhh8 content:hhhhh9
若是不用自定義的解碼器怎麼獲取到body內容呢
將自定義編碼器換成LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0)
public class MyChannelInitializer extends ChannelInitializer<SocketChannel> { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline() // .addLast(new MyServerDecoderLength(10240, 0, 4, 0, 0)) .addLast(new LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0)) // .addLast(new MyServerDecoder()) .addLast(new MyServerHandler()) ; } } // 怕忘記的各個參數的含義在這在說明一次,本身不斷的修改每一個值觀察結果就能夠更加深入的理解 /** * maxFrameLength:消息體的最大長度,好像默認最大值爲1024*1024 * lengthFieldOffset 長度字段所在字節數組的下標 (我這是第一個write的因此下標是0) * lengthFieldLength 長度字段的字節長度(int類型佔4個字節) * lengthAdjustment 長度字段補償的數值 (lengthAdjustment = 數據包長度 - lengthFieldOffset - lengthFieldLength - 長度域的值),解析須要減去對應的數值 * initialBytesToStrip 是否去掉長度字段(0不去除,對應長度域字節長度) */ public LengthFieldBasedFrameDecoder( int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip)
MyServer06 is start ................... 客戶端鏈接成功 time: 2019-11-19 17:53:42 收到客戶端發來的消息: hhhhh0, time: 2019-11-19 17:53:42 收到客戶端發來的消息: hhhhh1, time: 2019-11-19 17:53:42 收到客戶端發來的消息: hhhhh2, time: 2019-11-19 17:53:42 收到客戶端發來的消息: hhhhh3, time: 2019-11-19 17:53:42 收到客戶端發來的消息: hhhhh4, time: 2019-11-19 17:53:42 收到客戶端發來的消息: hhhhh5, time: 2019-11-19 17:53:42 收到客戶端發來的消息: hhhhh6, time: 2019-11-19 17:53:42 收到客戶端發來的消息: hhhhh7, time: 2019-11-19 17:53:42 收到客戶端發來的消息: hhhhh8, time: 2019-11-19 17:53:42 收到客戶端發來的消息: hhhhh9, time: 2019-11-19 17:53:42
若是咱們在客戶端的長度域中作手腳 LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 0)
舊: out.writeInt(request.length); 新: out.writeInt(request.length + 1);
// 看結果就不正常,0後面多了一個0;可是不知道爲啥只解碼了一次??? 求解答 MyServer06 is start ................... 客戶端鏈接成功 time: 2019-11-19 17:56:55 收到客戶端發來的消息: hhhhh0 , time: 2019-11-19 17:56:55 // 正確修改成 LengthFieldBasedFrameDecoder(10240, 0, 4, -1, 0) // 結果: MyServer06 is start ................... 客戶端鏈接成功 time: 2019-11-19 18:02:18 收到客戶端發來的消息: hhhhh0, time: 2019-11-19 18:02:18 收到客戶端發來的消息: hhhhh1, time: 2019-11-19 18:02:18 收到客戶端發來的消息: hhhhh2, time: 2019-11-19 18:02:18 收到客戶端發來的消息: hhhhh3, time: 2019-11-19 18:02:18 收到客戶端發來的消息: hhhhh4, time: 2019-11-19 18:02:18 收到客戶端發來的消息: hhhhh5, time: 2019-11-19 18:02:18 收到客戶端發來的消息: hhhhh6, time: 2019-11-19 18:02:18 收到客戶端發來的消息: hhhhh7, time: 2019-11-19 18:02:18 收到客戶端發來的消息: hhhhh8, time: 2019-11-19 18:02:18 收到客戶端發來的消息: hhhhh9, time: 2019-11-19 18:02:18
捨棄長度域 :LengthFieldBasedFrameDecoder(10240, 0, 4, 0, 4)
// 結果 MyServer06 is start ................... 客戶端鏈接成功 time: 2019-11-19 18:03:44 收到客戶端發來的消息:hhhhh0, time: 2019-11-19 18:03:44 收到客戶端發來的消息:hhhhh1, time: 2019-11-19 18:03:44 收到客戶端發來的消息:hhhhh2, time: 2019-11-19 18:03:44 收到客戶端發來的消息:hhhhh3, time: 2019-11-19 18:03:44 收到客戶端發來的消息:hhhhh4, time: 2019-11-19 18:03:44 收到客戶端發來的消息:hhhhh5, time: 2019-11-19 18:03:44 收到客戶端發來的消息:hhhhh6, time: 2019-11-19 18:03:44 收到客戶端發來的消息:hhhhh7, time: 2019-11-19 18:03:44 收到客戶端發來的消息:hhhhh8, time: 2019-11-19 18:03:44 收到客戶端發來的消息:hhhhh9, time: 2019-11-19 18:03:44
源碼中的示例
* <pre> * lengthFieldOffset = 0 * lengthFieldLength = 2 * <b>lengthAdjustment</b> = <b>-2</b> (= the length of the Length field) * initialBytesToStrip = 0 * * BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes) * +--------+----------------+ +--------+----------------+ * | Length | Actual Content |----->| Length | Actual Content | * | 0x000E | "HELLO, WORLD" | | 0x000E | "HELLO, WORLD" | * +--------+----------------+ +--------+----------------+ * </pre>
長度域中0x000E爲16進制,轉換成10進制是14,說明消息體長度爲14;根據公式:14-0-2-14 = -2
* <pre> * lengthFieldOffset = 0 * lengthFieldLength = 3 * <b>lengthAdjustment</b> = <b>2</b> (= the length of Header 1) * initialBytesToStrip = 0 * * BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes) * +----------+----------+----------------+ +----------+----------+----------------+ * | Length | Header 1 | Actual Content |----->| Length | Header 1 | Actual Content | * | 0x00000C | 0xCAFE | "HELLO, WORLD" | | 0x00000C | 0xCAFE | "HELLO, WORLD" | * +----------+----------+----------------+ +----------+----------+----------------+ * </pre>
從上的例子能夠知道;lengthAdjustment(2) = 17- 12(00000C)-lengthFieldOffset(0) - lengthFieldLength(3);
.......等等