前言
每個網絡應用程序都必須定義如何解析在兩個節點之間來回傳輸的原始字節,以及如何將其和 目標應用程序的數據格式做相互轉換。這種轉換邏輯由編解碼器處理,編解碼器由編碼器和解碼 器組成,它們每種都可以將字節流從一種格式轉換爲另一種格式。那麼它們的區別是什麼呢?
如果將消息看作是對於特定的應用程序具有具體含義的結構化的字節序列— 它的數據。那麼編碼器是將消息轉換爲適合於傳輸的格式(最有可能的就是字節流);而對應的解碼器則是將 網絡字節流轉換回應用程序的消息格式。因此,編碼器操作出站數據,而解碼器處理入站數據。
一、Netty中的解碼器基類ByteToMessageDecoder
由於業務層僅僅關係業務數據,而網絡層傳輸的又是字節數據,所以需要有一個解碼器將字節數據轉化成業務數據類型,比如JSON格式、字符串格式、對象格式等等。Netty中提供瞭解碼器基類用於將接收到的緩衝字節格式數據轉化成業務層需要的對象格式。
Netty中的解碼器基類爲ByteToMessageDecoder,該類繼承之ChannelInboundHandlerApapter,既然是解碼字節數據,所以必然會實現channelRead方法。源碼如下:
1 /** 累積緩存數據 */ 2 ByteBuf cumulation; 3 /** 累積緩存合併工具 */ 4 private Cumulator cumulator = MERGE_CUMULATOR; 5 /** 是否第一次接收 */ 6 private boolean first; 7 8 @Override 9 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 10 if (msg instanceof ByteBuf) { 11 /** out存儲已解析的對象集合 */ 12 CodecOutputList out = CodecOutputList.newInstance(); 13 try { 14 ByteBuf data = (ByteBuf) msg; 15 /** 16 * ByteBuf cumulation表示累積的緩衝字節 17 * 如果cumluation爲空,表示當前沒有累積字節 18 * */ 19 first = cumulation == null; 20 if (first) { 21 /** 如果是第一次,則直接將接收的數據賦值給cumlation*/ 22 cumulation = data; 23 } else { 24 /** 如果有累積數據,則將累積數據和接收的新數據進行合併*/ 25 cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data); 26 } 27 /** 調用具體的解碼器進行解碼,解析到對象成功之後存放到out中 */ 28 callDecode(ctx, cumulation, out); 29 } catch (DecoderException e) { 30 throw e; 31 } catch (Exception e) { 32 throw new DecoderException(e); 33 } finally { 34 /** 如果累積字節不爲空並且ByteBuf不可讀,那麼將累積的ByteBuf進行釋放*/ 35 if (cumulation != null && !cumulation.isReadable()) { 36 numReads = 0; 37 cumulation.release(); 38 cumulation = null; 39 } else if (++ numReads >= discardAfterReads) { 40 /** numReads表示讀取次數,達到一定次數之後進行可讀區域的壓縮*/ 41 numReads = 0; 42 /** 調用ByteBuf的discard方法進行空間壓縮*/ 43 discardSomeReadBytes(); 44 } 45 int size = out.size(); 46 firedChannelRead |= out.insertSinceRecycled(); 47 fireChannelRead(ctx, out, size); 48 out.recycle(); 49 } 50 } else { 51 //如果msg不是ByteBuf類型直接不處理交給下一個ChannelHandlerContext處理 52 ctx.fireChannelRead(msg); 53 } 54 }
這裏代碼比較多,所以需要捋下邏輯,整體邏輯不復雜,主要分成以下幾個步驟:
1、首先判斷接收到的數據是否是ByteBuf類型,如果不是則直接交給下一個ChannelHandlerContext處理,如果是ByteBuf類型才嘗試進行解析
2、由於網絡傳輸可能存在粘包或拆包的情況,所以每次接收到一個ByteBuf並不一定就是一個完整的業務數據,比如客戶端發送了一個字符串過來,但是字符串比較大被分成了兩個ByteBuf傳遞過來,也有可能粘包將兩個字符串合併成功一個ByteBuf傳遞過來
所以需要對ByteBuf進行解析,解析之後可能會剩餘部分字節,或者一次性解析不成功,那麼就需要暫時將解析不了的ByteBuf緩存起來。該類內部有一個屬性ByteBuf類型的cumulation就是用來存儲累積的緩存數據。
3、判斷cumulation是否爲空,如果爲空表示沒有累積數據,那麼當前就是第一次接收數據,則直接將接收的數據存到cumulation中;如果已經存在累積數據,那麼就需要對累積數據和接收的新數據進行合併處理
4、將接收到的數據或者是合併累積數據之後的數據調用callDecode方法進行解析,CodecOutputList是用來存儲解析成功的對象列表,比如解析成功了兩個字符串,那麼就將這兩個字符串存放到這個List中
5、最後調用fireChannelRead方法將解析到的業務數據列表傳遞給其他的ChannlHandler進行處理
整體流程捋清楚之後再逐步進行分析,
一、首先看下callDecode方法的實現,看看是如何進行業務數據的解析的。源碼如下:
1 protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { 2 try { 3 /** 當ByteBuf可讀 */ 4 while (in.isReadable()) { 5 /** 如果outSize有值,表示至少有一個業務包被完全解碼成功*/ 6 int outSize = out.size(); 7 8 if (outSize > 0) { 9 /** 將業務包傳遞給下一個ChannelHandlerContext處理*/ 10 fireChannelRead(ctx, out, outSize); 11 /** 清理緩存*/ 12 out.clear(); 13 14 if (ctx.isRemoved()) { 15 break; 16 } 17 outSize = 0; 18 } 19 20 /** 獲取ByteBuf可讀字節數*/ 21 int oldInputLength = in.readableBytes(); 22 /** 具體解碼處理 */ 23 decodeRemovalReentryProtection(ctx, in, out); 24 if (ctx.isRemoved()) { 25 break; 26 } 27 /** 如果解析的對象數量不變,也就是子類沒有解析成功*/ 28 if (outSize == out.size()) { 29 /*** 如果可讀字節數沒有變化,跳出循環*/ 30 if (oldInputLength == in.readableBytes()) { 31 break; 32 } else { 33 /** 如果可讀字節數發生變化,那麼繼續進行解析*/ 34 continue; 35 } 36 } 37 38 /** 執行到此處說明out數量發生改變了,也就是成功解析到了對象 39 * 此時如果可讀字節沒有變化,那麼意思就是沒有讀任何字節但是解析到了一個對象,很顯然是代碼出問題了,拋異常 40 * */ 41 if (oldInputLength == in.readableBytes()) { 42 throw new DecoderException( 43 StringUtil.simpleClassName(getClass()) + 44 ".decode() did not read anything but decoded a message."); 45 } 46 if (isSingleDecode()) { 47 break; 48 } 49 } 50 } catch (DecoderException e) { 51 throw e; 52 } catch (Exception cause) { 53 throw new DecoderException(cause); 54 } 55 }
1 final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) 2 throws Exception { 3 decodeState = STATE_CALLING_CHILD_DECODE; 4 try { 5 /** 調用子類進行具體的解碼邏輯,解析對象成功之後會存到out中 */ 6 decode(ctx, in, out); 7 } finally { 8 boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING; 9 decodeState = STATE_INIT; 10 if (removePending) { 11 /** 如果ContextHandlerContext被刪除了,就將解析的數據傳播出去*/ 12 fireChannelRead(ctx, out, out.size()); 13 out.clear(); 14 handlerRemoved(ctx); 15 } 16 } 17 }
還是捋清楚步驟爲主,首先要清楚該方法的作用是什麼,callDecode方法的作用是將參數ByteBuf in中的數據進行讀取並解析,解析到業務數據之後存放到List<Object> out中,並將業務數據傳播給下一個ChannelHandlerContext ctx
主要步驟如下:
1、while循環中判斷in是否可讀,可讀的情況下才嘗試解析
2、判斷當前的out是否爲空,如果不爲空則先將解析成功的業務數據調用fireChannelRead方法將業務數據傳播出去,並調用out.clear()清空
3、獲取in的可讀字節數,並調用decodeRemovalReentryProtection(ctx, in ,out)方法進行解析,該方法的工作就是實際的解析過程,調用子類的decode方法進行解析。具體的解析規則需要具體的子類去實現
4、解析完成之後再判斷一次out是否爲空,如果out數量不變,就表示沒有解析業務數據成功,此時再判斷可讀字節數有沒有發生改變,如果沒有發生變化說明當前的緩存in還不足以解析成一個業務數據,所以直接跳出循環,等着再接收一下數據來了再解析
5、如果可讀字節數發生了變化,那麼可能部分數據已經被解析成功了,所以執行continue繼續進解析
二、分析完了解析的流程,再看下fireChannelRead傳播的過程,源碼如下:
/** * @param ctx:下一個ChannelHandlerContext * @param msgs:解析成功的業務數據列表 * @param numElements:業務數據個數 * */ static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, int numElements) { for (int i = 0; i < numElements; i ++) { ctx.fireChannelRead(msgs.getUnsafe(i)); } }
邏輯比較簡單,就是遍歷解析成功的業務數據列表,調用下一個ChannelHandlerContext的fireChannelRead方法進行傳播
三、最後再分析下累積數據合併的邏輯
ByteBuf合併的任務由一個合併工具接口Cumlator,實現類源碼如下:
1 /** 2 * 將累積未讀的緩存數據cumulation和新接收到的字節數據in進行合併 3 * */ 4 public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) { 5 final ByteBuf buffer; 6 /** 7 * cumulation.writeIndex:累積緩存的寫索引 8 * cumulation.maxCapacity:累積緩存的最大容量 9 * in.readableBytes:新接收緩存的可讀字節數 10 * 11 * 如果累積緩存區容量不足 或者 累積緩衝區引用數大於1 或者 累積緩衝區是隻讀的 12 * 則不可以直接將新接收的數據in寫入到cumulation對象中,就需要進行重新分配緩衝區 13 * */ 14 if (cumulation.writerIndex() > cumulation.maxCapacity() - in.readableBytes() 15 || cumulation.refCnt() > 1 || cumulation.isReadOnly()) { 16 17 /** 將累積緩衝區進行擴容,重新分配內存給buffer對象,並將累積緩存數據寫入新buffer中 */ 18 buffer = expandCumulation(alloc, cumulation, in.readableBytes()); 19 } else { 20 /** 將累積緩衝區賦值給buffer */ 21 buffer = cumulation; 22 } 23 /** 將新接收的數據寫入到buffer中 */ 24 buffer.writeBytes(in); 25 /** 釋放in對象*/ 26 in.release(); 27 return buffer; 28 } 29 30 /** 31 * @param alloc:內存分配器 32 * @param cumulation:累積緩衝區 33 * @param readable:可讀容量 34 * */ 35 static ByteBuf expandCumulation(ByteBufAllocator alloc, ByteBuf cumulation, int readable) { 36 ByteBuf oldCumulation = cumulation; 37 /** 重新分配緩衝區,容量大小爲緩衝區容量+新接收的可讀容量 */ 38 cumulation = alloc.buffer(oldCumulation.readableBytes() + readable); 39 /** 將累積緩衝寫入到擴容之後的緩衝區對象中 */ 40 cumulation.writeBytes(oldCumulation); 41 /** 釋放舊的累積緩衝區對象 */ 42 oldCumulation.release(); 43 return cumulation; 44 }
這裏邏輯比較清晰,合併緩存實際就是將新接收的ByteBuf寫入到累積的ByteBuf中,如果累積的ByteBuf容量不足,就先進行擴容操作,然後再將新接收的ByteBuf寫入進行達到合併的效果。
二、解碼器之LengthFieldBasedFrameDecoder
ByteToMessageDecoder的作用實際上是使用了模版方法設計模式,將具體的解碼業務留給子類實現,而除了解碼工作之外的工作都已經實現了。比如將不能解析的數據進行累積緩存、合併緩存、將業務數據繼續傳播等。
LengthFieldBasedFrameDecoder是ByteToMessageDecoder的子類,作用是將數據包分成包頭和消息體,包頭中包含了消息體的長度,先讀取包頭讀取長度,然後再讀取對應長度的消息。
通過上一節可知,ByteToMessageDecoder將具體的解碼工作是執行decode方法,這個方法是個抽象方法,需要子類去實現,所以LengthFieldBasedFrameDecoder的核心也就是實現decode方法。
LengthFieldBasedFrameDecoder的構造函數源碼如下:
1 /** 構造函數 2 * @param byteOrder:數據存儲採用大端模式或小端模式 3 * @param maxFrameLength:發生的數據幀最大長度 4 * @param lengthFieldOffset:數據長度值位於字節數組的位置 5 * @param lengthFieldLength:數據長度值佔用字節數組的長度 6 * @param initialBytesToStrip:跳過數據包前多少位數 7 * @param failFast:值爲true表示如果讀取到長度值超過maxFrameLength直接報錯 8 * 9 * 如lengthFieldOffset=0,lengthFieldLength=4,那麼接收到ByteBuf之後,會從ByteBuf的數組第0位字節開始讀取4個字節,得到數據包的長度 10 * 然後再從數組第4位字節開始讀取指定長度的數據進行解析 11 * */ 12 public LengthFieldBasedFrameDecoder( 13 ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, 14 int lengthAdjustment, int initialBytesToStrip, boolean failFast) { 15 /** 參數校驗*/ 16 if (byteOrder == null) { 17 throw new NullPointerException("byteOrder"); 18 } 19 20 if (maxFrameLength <= 0) { 21 throw new IllegalArgumentException( 22 "maxFrameLength must be a positive integer: " + 23 maxFrameLength); 24 } 25 26 if (lengthFieldOffset < 0) { 27 throw new IllegalArgumentException( 28 "lengthFieldOffset must be a non-negative integer: " + 29 lengthFieldOffset); 30 } 31 32 if (initialBytesToStrip < 0) { 33 throw new IllegalArgumentException( 34 "initialBytesToStrip must be a non-negative integer: " + 35 initialBytesToStrip); 36 } 37 /** 長度值位數不可超過最大長度 */ 38 if (lengthFieldOffset > maxFrameLength - lengthFieldLength) { 39 throw new IllegalArgumentException( 40 "maxFrameLength (" + maxFrameLength + ") " + 41 "must be equal to or greater than " + 42 "lengthFieldOffset (" + lengthFieldOffset + ") + " + 43 "lengthFieldLength (" + lengthFieldLength + ")."); 44 } 45 /** 屬性賦值 */ 46 this.byteOrder = byteOrder; 47 this.maxFrameLength = maxFrameLength; 48 this.lengthFieldOffset = lengthFieldOffset; 49 this.lengthFieldLength = lengthFieldLength; 50 this.lengthAdjustment = lengthAdjustment; 51 lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength;//表示長度值的結束下標值 52 this.initialBytesToStrip = initialBytesToStrip; 53 this.failFast = failFast; 54 }
LengthFieldBasedFrameDecoder核心屬性lengthFieldOffset表示數據包長度的開始位,lengthFiledEndOffset表示數據包長度的結束位,也就表示數組[lengthFieldOffset] ~ 數組[lengthFieldEndOffset] 的值表示數據包長度
再看下LengthFieldBasedFrameDecoder的具體實現,源碼如下:
1 protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { 2 if (discardingTooLongFrame) { 3 discardingTooLongFrame(in); 4 } 5 6 /** 如果ByteBuf的長度小於數據包長度的偏移量,那麼表示數據包長度都無法解析,直接返回null*/ 7 if (in.readableBytes() < lengthFieldEndOffset) { 8 return null; 9 } 10 11 /** 長度偏移量 + ByteBuf的讀索引,表示需要讀取的數據包長度區域偏移量*/ 12 int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset; 13 /** 獲取包長度*/ 14 long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder); 15 16 /** 當frameLength爲負數,直接拋異常*/ 17 if (frameLength < 0) { 18 failOnNegativeLengthField(in, frameLength, lengthFieldEndOffset); 19 } 20 21 frameLength += lengthAdjustment + lengthFieldEndOffset; 22 23 /** 當包長度小於長度區位直接拋異常*/ 24 if (frameLength < lengthFieldEndOffset) { 25 failOnFrameLengthLessThanLengthFieldEndOffset(in, frameLength, lengthFieldEndOffset); 26 } 27 28 /** 超過最大長度表示出現大包情況,則直接丟棄數據*/ 29 if (frameLength > maxFrameLength) { 30 exceededFrameLength(in, frameLength); 31 return null; 32 } 33 34 int frameLengthInt = (int) frameLength; 35 /** 如果ByteBuf可讀字節數小於數據包長度,那麼表示無法解析成一個完成的數據包,直接返回null*/ 36 if (in.readableBytes() < frameLengthInt) { 37 return null; 38 } 39 /** 如果需要跳過的字節數大於數據包長度,那麼直接拋異常*/ 40 if (initialBytesToStrip > frameLengthInt) { 41 failOnFrameLengthLessThanInitialBytesToStrip(in, frameLength, initialBytesToStrip); 42 } 43 /** 跳過指定長度的字節 */ 44 in.skipBytes(initialBytesToStrip); 45 46 /** 進入到這裏說明參數校驗都已經通過,並且可以解析成一個完整的業務數據包了 */ 47 48 //獲取讀索引 49 int readerIndex = in.readerIndex(); 50 //獲取數據包總長度-跳過的字節數 = 實際需要讀的字節數 51 int actualFrameLength = frameLengthInt - initialBytesToStrip; 52 /** 從ByteBuf中抽取完整的業務數據包, 從readerIndex位置開始讀到actualFrameLength位置 */ 53 ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength); 54 /** 修改ByteBuf的讀索引值 */ 55 in.readerIndex(readerIndex + actualFrameLength); 56 /** 返回讀取到的完整數據包 */ 57 return frame; 58 }
這裏代碼比較多,但是總體的邏輯並不複雜,主要是對於數據長度的校驗工作,核心步驟如下:
1、調用getUnadjustedFrameLength方法讀取數據包的長度
2、判斷數據包長度和緩衝區的可讀數據長度,如果數據包長度大於緩衝區可讀長度表示發生拆包現象,直接返回null;如果數據包長度小於等於緩衝區可讀長度,那麼表示可以讀取到一個完整的數據包
3、根據設置的跳過字節長度,跳過指定位數的數據
4、調用extractFrame方法從緩衝區讀取一個完整的數據包
5、更新緩衝區的讀索引值,並返回完整的數據包
接下里對核心方法進行分析
1、getUnadjustedFrameLength方法源碼如下:
1 /** 2 * 獲取包的長度 3 * 從buf的字節數組中offset位置開始讀取,讀取位數爲length 4 * */ 5 protected long getUnadjustedFrameLength(ByteBuf buf, int offset, int length, ByteOrder order) { 6 buf = buf.order(order); 7 long frameLength; 8 switch (length) { 9 case 1://讀取1個字節,表示長度爲byte類型 10 frameLength = buf.getUnsignedByte(offset); 11 break; 12 case 2://讀取2個字節,表示長度爲short類型 13 frameLength = buf.getUnsignedShort(offset); 14 break; 15 case 3://讀取3個字節,表示長度爲Medium類型 16 frameLength = buf.getUnsignedMedium(offset); 17 break; 18 case 4://讀取4個字節,表示長度爲int類型 19 frameLength = buf.getUnsignedInt(offset); 20 break; 21 case 8://讀取8個字節,表示長度爲long類型 22 frameLength = buf.getLong(offset); 23 break; 24 default: 25 throw new DecoderException( 26 "unsupported lengthFieldLength: " + lengthFieldLength + " (expected: 1, 2, 3, 4, or 8)"); 27 } 28 return frameLength; 29 }
主要是判斷數據包長度的佔字節數,根據字節長度從緩衝區ByteBuf中讀取指定的值
2、extractFrame方法源碼如下:
1 protected ByteBuf extractFrame(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) { 2 return buffer.retainedSlice(index, length); 3 }
代碼比較簡潔實際就是調用ByteBuuuf的retainedSlice方法,該方法的作用是從ByteBuf從截取一部分數據,從index位置開始截取長度爲length的數據。而retain的作用是不修改原緩衝區的讀寫索引,相當於複製一部分數據生成新的ByteBuf對象。
總結LengthFieldBasedFrameDecoder的實現原理:
1、首先從ByteBuf的指定位置讀取指定長度的字節數據,得到數據包的長度,並進行長度校驗,如果長度校驗不通過則直接返回null,表示解析業務數據包失敗。
2、從ByteBuf中指定位置開始複製指定長度的數據,得到一個完整的數據包
3、修改ByteBuf的讀索引,並返回完整的數據包