簡介
netty中的資料是通過ByteBuf來進行傳輸的,一個ByteBuf中可能包含多個有意義的資料,這些資料可以被稱作frame,也就是說一個ByteBuf中可以包含多個Frame。
對於訊息的接收方來說,接收到了ByteBuf,還需要從ByteBuf中解析出有用而資料,那就需要將ByteBuf中的frame進行拆分和解析。
一般來說不同的frame之間會有有些特定的分隔符,我們可以通過這些分隔符來區分frame,從而實現對資料的解析。
netty為我們提供了一些合適的frame解碼器,通過使用這些frame解碼器可以有效的簡化我們的工作。下圖是netty中常見的幾個frame解碼器:
<img src="https://img-blog.csdnimg.cn/6f394018c43a40a6a53a5260fc577575.png" style="zoom:67%;" />
接下來我們來詳細介紹一下上面幾個frame解碼器的使用。
LineBasedFrameDecoder
LineBasedFrameDecoder從名字上看就是按行來進行frame的區分。根據作業系統的不同,換行可以有兩種換行符,分別是 "\n" 和 "\r\n" 。
LineBasedFrameDecoder的基本原理就是從ByteBuf中讀取對應的字元來和"\n" 跟 "\r\n",可以了可以準確的進行字元的比較,這些frameDecoder對字元的編碼也會有一定的要求,一般來說是需要UTF-8編碼。因為在這樣的編碼中,"\n"和"\r"是以一個byte出現的,並且不會用在其他的組合編碼中,所以用"\n"和"\r"來進行判斷是非常安全的。
LineBasedFrameDecoder中有幾個比較重要的屬性,一個是maxLength的屬性,用來檢測接收到的訊息長度,如果超出了長度限制,則會丟擲TooLongFrameException異常。
還有一個stripDelimiter屬性,用來判斷是否需要將delimiter過濾掉。
還有一個是failFast,如果該值為true,那麼不管frame是否讀取完成,只要frame的長度超出了maxFrameLength,就會丟擲TooLongFrameException。如果該值為false,那麼TooLongFrameException會在整個frame完全讀取之後再丟擲。
LineBasedFrameDecoder的核心邏輯是先找到行的分隔符的位置,然後根據這個位置讀取到對應的frame資訊,這裡來看一下找到行分隔符的findEndOfLine方法:
private int findEndOfLine(final ByteBuf buffer) {
int totalLength = buffer.readableBytes();
int i = buffer.forEachByte(buffer.readerIndex() + offset, totalLength - offset, ByteProcessor.FIND_LF);
if (i >= 0) {
offset = 0;
if (i > 0 && buffer.getByte(i - 1) == '\r') {
i--;
}
} else {
offset = totalLength;
}
return i;
}
這裡使用了一個ByteBuf的forEachByte對ByteBuf進行遍歷。我們要找的字元是:ByteProcessor.FIND_LF。
最後LineBasedFrameDecoder解碼之後的物件還是一個ByteBuf。
DelimiterBasedFrameDecoder
上面講的LineBasedFrameDecoder只對行分隔符有效,如果我們的frame是以其他的分隔符來分割的話LineBasedFrameDecoder就用不了了,所以netty提供了一個更加通用的DelimiterBasedFrameDecoder,這個frameDecoder可以自定義delimiter:
public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {
public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf delimiter) {
this(maxFrameLength, true, delimiter);
}
傳入的delimiter是一個ByteBuf,所以delimiter可能不止一個字元。
為了解決這個問題在DelimiterBasedFrameDecoder中定義了一個ByteBuf的陣列:
private final ByteBuf[] delimiters;
delimiters= delimiter.readableBytes();
這個delimiters是通過呼叫delimiter的readableBytes得到的。
DelimiterBasedFrameDecoder的邏輯和LineBasedFrameDecoder差不多,都是通過對比bufer中的字元來對bufer中的資料進行擷取,但是DelimiterBasedFrameDecoder可以接受多個delimiters,所以它的用處會根據廣泛。
FixedLengthFrameDecoder
除了進行ByteBuf中字元比較來進行frame拆分之外,還有一些其他常見的frame拆分的方法,比如根據特定的長度來區分,netty提供了一種這樣的decoder叫做FixedLengthFrameDecoder。
public class FixedLengthFrameDecoder extends ByteToMessageDecoder
FixedLengthFrameDecoder也是繼承自ByteToMessageDecoder,它的定義很簡單,可以傳入一個frame的長度:
public FixedLengthFrameDecoder(int frameLength) {
checkPositive(frameLength, "frameLength");
this.frameLength = frameLength;
}
然後呼叫ByteBuf的readRetainedSlice方法來讀取固定長度的資料:
in.readRetainedSlice(frameLength)
最後將讀取到的資料返回。
LengthFieldBasedFrameDecoder
還有一些frame中包含了特定的長度欄位,這個長度欄位表示ByteBuf中有多少可讀的資料,這樣的frame叫做LengthFieldBasedFrame。
netty中也提供了一個對應的處理decoder:
public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder
讀取的邏輯很簡單,首先讀取長度,然後再根據長度再讀取資料。為了實現這個邏輯,LengthFieldBasedFrameDecoder提供了4個欄位,分別是 lengthFieldOffset,lengthFieldLength,lengthAdjustment和initialBytesToStrip。
lengthFieldOffset指定了長度欄位的開始位置,lengthFieldLength定義的是長度欄位的長度,lengthAdjustment是對lengthFieldLength進行調整,initialBytesToStrip表示是否需要去掉長度欄位。
聽起來好像不太好理解,我們舉幾個例子,首先是最簡單的:
BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
要編碼的訊息有個長度欄位,長度欄位後面就是真實的資料,0x000C是一個十六進位制,表示的資料是12,也就是"HELLO, WORLD" 中字串的長度。
這裡4個屬性的值是:
lengthFieldOffset = 0
lengthFieldLength = 2
lengthAdjustment = 0
initialBytesToStrip = 0
表示的是長度欄位從0開始,並且長度欄位佔有兩個位元組,長度不需要調整,也不需要對欄位進行調整。
再來看一個比較複雜的例子,在這個例子中4個屬性值如下:
lengthFieldOffset = 1
lengthFieldLength = 2
lengthAdjustment = 1
initialBytesToStrip = 3
對應的編碼資料如下所示:
BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes)
+------+--------+------+----------------+ +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x000C | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+
上面的例子中長度欄位是從第1個位元組開始的(第0個位元組是HDR1),長度欄位佔有2個位元組,長度再調整一個位元組,最終資料的開始位置就是1+2+1=4,然後再擷取前3個位元組的資料,得到了最後的結果。
總結
netty提供的這幾個基於字符集的frame decoder基本上能夠滿足我們日常的工作需求了。當然,如果你傳輸的是一些更加複雜的物件,那麼可以考慮自定義編碼和解碼器。自定義的邏輯步驟和上面我們講解的保持一致就行了。
本文已收錄於 http://www.flydean.com/14-5-netty-frame-decoder/
最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!