netty原始碼解解析(4.0)-19 ChannelHandler: codec--常用編解碼實現
資料包編解碼過程中主要的工作就是:在編碼過程中進行序列化,在解碼過程中從Byte流中分離出資料包然後反序列化。在MessageToByteEncoder中,已經解決了序列化之後的問題,ByteToMessageDecoder中已經部分第解決了從Byte流中分離出資料包的問題。實現具體的資料包編解碼,只需要實現MessageToByteEncoder的encode和ByteToMessageDecoder的decode方法即可。
為了方便開發者使用Netty,在io.netty.handler.codec包中已經實現了一些開箱即用的編解碼ChannelHandler,這些Handler包括:
- FixedLengthFrameDecoder : 固定長度的資料包解碼。
- LengthFieldPrepender, LengthFieldBasedFrameDecoder: 帶有長度欄位的資料包編解碼。
- LineBasedFrameDecoder: 以行字串為一個數據包解碼。
- StringEncoder, StringDecoder: 字符集轉換器。
- Base64Encoder, Base64Decoder: Base64編碼轉換器。
- ProtobufEncoder, ProtobufDecoder: protoBuf序列化格式資料包的編解碼。
- ZlibEncoder,ZlibDecoder: zlib壓縮格式資料包的編解碼。
- SnappyFramedEncoder,SnappyFramedDecoder: Snappy壓縮格式資料包的編解碼。
接下來讓我們來分析幾個關鍵ChannelHandler的實現程式碼。
帶有長度欄位的資料解碼: LengthFieldBasedFrameDecoder
之所以先從LengthFieldBasedFrameDecoder開始,是因為這個類的實現非常典型,它向我們展示瞭解碼二進位制資料包的一些常用方法:
- 從Byte流中反序列化出一個整數。
- 利用整數表示一個數據包的長度。
- 使用偏移量得到資料包的開始位置。
- 使用資料包的開始位置和長度從Byte流中提取資料包。
這個類解碼的資料包可能有三中格式:
| length | content | : 其中length是長度欄位,它表示content的長度。但是這樣格式無法明確地找到資料包的開始位置,需要一個表示開始位置的欄位。
| header | length | conent | : 通過header欄位確定資料包的開始位置。
| header | length | header1 | content |: header2是一個固定長度的欄位,它可能包含一些子欄位。
有一些必要的屬性用來輔助解碼資料包:
byteOrder: 反序列化整數使用的位元組序。
lengthFieldOffset: 長度欄位的偏移量,也是header的長度。
lengthFieldLength: 長度欄位的長度。長度欄位是一個整數,它的長度可能是: 1, 2, 3, 4, 8。
lengthAdjustment: header1的長度。
initialBytesToStrip: 從Byte流中提取資料包時去掉的資料長度。
通過覆蓋decode方法從Byte流中提取資料包:
1 @Override 2 protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { 3 Object decoded = decode(ctx, in); 4 if (decoded != null) { 5 out.add(decoded); 6 } 7 }
程式碼比較簡單,呼叫內部的decode方法完成資料包的提取,乾貨都在這個內部方法中。
1 protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { 2 if (discardingTooLongFrame) { 3 discardingTooLongFrame(in); 4 } 5 6 if (in.readableBytes() < lengthFieldEndOffset) { 7 return null; 8 } 9 10 int actualLengthFieldOffset = in.readerIndex() + lengthFieldOffset; 11 long frameLength = getUnadjustedFrameLength(in, actualLengthFieldOffset, lengthFieldLength, byteOrder); 12 13 if (frameLength < 0) { 14 failOnNegativeLengthField(in, frameLength, lengthFieldEndOffset); 15 } 16 17 frameLength += lengthAdjustment + lengthFieldEndOffset; 18 19 if (frameLength < lengthFieldEndOffset) { 20 failOnFrameLengthLessThanLengthFieldEndOffset(in, frameLength, lengthFieldEndOffset); 21 } 22 23 if (frameLength > maxFrameLength) { 24 exceededFrameLength(in, frameLength); 25 return null; 26 } 27 28 // never overflows because it's less than maxFrameLength 29 int frameLengthInt = (int) frameLength; 30 if (in.readableBytes() < frameLengthInt) { 31 return null; 32 } 33 34 if (initialBytesToStrip > frameLengthInt) { 35 failOnFrameLengthLessThanInitialBytesToStrip(in, frameLength, initialBytesToStrip); 36 } 37 in.skipBytes(initialBytesToStrip); 38 39 // extract frame 40 int readerIndex = in.readerIndex(); 41 int actualFrameLength = frameLengthInt - initialBytesToStrip; 42 ByteBuf frame = extractFrame(ctx, in, readerIndex, actualFrameLength); 43 in.readerIndex(readerIndex + actualFrameLength); 44 return frame; 45 }
呼叫extractFrame(42行)從Byte流中提取資料包之前的關鍵環節有:
- 找到資料包的開始位置: 10行。
- 確定資料包的長度: 11行,呼叫getUnadjustedFrameLength方法反序列化到的length欄位值。此時frameLength值是content的長度,還不是真正的包長度。17行,content長度加上header1的長度(lengthAjustment)再加上header和length的長度(lengthFieldEndOffset),得到的結果才是真正的包長度。 41行,最後減去需要丟掉的那部分資料的長度(initialBytesStrip),得到期望的資料包長度。
- 處理in中資料不足一個數據包的情況: 6-7行,30-31行,返回null。
- 清理掉in中超過最大資料包長度限制的資料: 23-24行如果in中的資料大於frameLength丟掉這個資料包,否則丟掉in中現有的所有資料,記錄下還需要丟棄的資料長度,下次在 2-3行丟掉剩下長度的資料。
- 處理包長度錯誤的情況: 13-14行,19-20行丟掉header和length。34-35行丟掉frameLength長度的資料。
最後得到得到資料包的開始位置和長度之和從in緩衝區中提取出一個完整資料包,並修改in的讀位置, 43-44行。
getUnadjustedFrameLength中反序列環length的值是計算資料包長度的關鍵,這個方法的實現如下:
1 protected long getUnadjustedFrameLength(ByteBuf buf, int offset, int length, ByteOrder order) { 2 buf = buf.order(order); 3 long frameLength; 4 switch (length) { 5 case 1: 6 frameLength = buf.getUnsignedByte(offset); 7 break; 8 case 2: 9 frameLength = buf.getUnsignedShort(offset); 10 break; 11 case 3: 12 frameLength = buf.getUnsignedMedium(offset); 13 break; 14 case 4: 15 frameLength = buf.getUnsignedInt(offset); 16 break; 17 case 8: 18 frameLength = buf.getLong(offset); 19 break; 20 default: 21 throw new DecoderException( 22 "unsupported lengthFieldLength: " + lengthFieldLength + " (expected: 1, 2, 3, 4, or 8)"); 23 } 24 return frameLength; 25 }
2行設定ByteBuf反序列化整數時使用的位元組序,預設BIG_ENDIAN。這個值可以在呼叫構造方法時設定。
4-24行,根據length欄位的不同長度,使用ByteBuf的不同方法反序列化length的值。length欄位的長度只能是: 1,2,3,4,8之一。
以上是LengthFieldBasedFrameDecoder實現的核心程式碼的分析。這個類把一個比較關鍵的問題留給子類實現,就是如何處理header或header1欄位的處理, header1可以沒有,但一定要有header。他們內部可以有比較複雜的結構,而且長度是約定好的,不會變。這個就給子類留下了比較大的擴充套件空間。通過擴充套件這個類,我們可以實現任意格式資料包的提取功能。
帶有長度欄位的資料包編碼: LengthFieldPrepender
這個類實現的功能和LengthFieldBasedFrameDecoder相反,也比它要簡單很多,只是簡單地在已經序列化好的資料前面加上長度欄位。
1 @Override 2 protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) throws Exception { 3 int length = msg.readableBytes() + lengthAdjustment; 4 if (lengthIncludesLengthFieldLength) { 5 length += lengthFieldLength; 6 } 7 8 if (length < 0) { 9 throw new IllegalArgumentException( 10 "Adjusted frame length (" + length + ") is less than zero"); 11 } 12 13 switch (lengthFieldLength) { 14 case 1: 15 if (length >= 256) { 16 throw new IllegalArgumentException( 17 "length does not fit into a byte: " + length); 18 } 19 out.writeByte((byte) length); 20 break; 21 case 2: 22 if (length >= 65536) { 23 throw new IllegalArgumentException( 24 "length does not fit into a short integer: " + length); 25 } 26 out.writeShort((short) length); 27 break; 28 case 3: 29 if (length >= 16777216) { 30 throw new IllegalArgumentException( 31 "length does not fit into a medium integer: " + length); 32 } 33 out.writeMedium(length); 34 break; 35 case 4: 36 out.writeInt(length); 37 break; 38 case 8: 39 out.writeLong(length); 40 break; 41 default: 42 throw new Error("should not reach here"); 43 } 44 45 out.writeBytes(msg, msg.readerIndex(), msg.readableBytes()); 46 }
3-11行,計算並檢查長度欄位的值。
13-43行,在資料前面加上長度欄位。
45行,把資料追加到長度長度欄位之後。
行資料包和固定長度資料包的提取
LineBasedFrameDecoder: 用於從字串流中按行提取資料包。用"\n"或"\r\n"作為前一個數據包的結束標誌和下一個資料包的開始標誌,也是根據這個標誌算出資料包的長度。
FixedLengthFrameDecoder: 用於從Byte流中安固定長度提取資料包。資料包沒有明確的開始標誌或結束標誌,只是簡單地根據約定的長度提取資料包。
這個中資料包提取方式對資料來源可靠性要求較高,實際應用中要多加小心。
字符集轉換
StringEncoder, StringDecoder這兩個類用於把字串從一種字符集轉換成另外一種字符集,不涉及資料包的提取。如:GBK和UTF-8之間的轉換。
Base64編解碼
Base64Encoder, Base64Decoder這個兩個類用於base64的編解碼,不涉及資料包的提取。
ProtoBuf編解碼
ProtobufEncoder, ProtobufDecoder用ProtoBuf格式資料包的編解碼,不涉及資料包的的提取。
壓縮和解壓縮
ZlibEncoder,ZlibDecoder: 對zlib和gzip壓縮和解壓縮演算法的支援,不涉及資料包的提取。
SnappyFramedEncoder,SnappyFramedDecoder: 最Snappy壓縮和解壓演算法的支援,不涉及資料包的提取。
本章小結
本章分析了分析了Netty使用編解碼框架實現特定用途的編解碼工具,其中最重要的是LengthFieldPrepender, LengthFieldBasedFrameDecoder類,這兩個類展示了對任意二進位制資料包打包,和從Byte流中提取二進位制資料包的常用方法,理解了這兩個類的實現,就能針對任意型別資料包實現自己的編解碼Handler。其他的類比較簡單,大部分不涉及資料包的提取,只涉及到一些常用的演算法和編碼格式,Netty在這裡只是提供了一些開箱即用的工具。
前講過LengthFieldBasedFrameDecoder把對header欄位的處理留給子類,這意味著子類可以通過設計自己的header實現更加健壯的,安全的同時兼顧效能的資料包,關於這個話題將在下一章詳細講解。