瘋狂創客圈,一個Java 高並發研習社群 【博客園 總入口 】
瘋狂創客圈,傾力推出: 《Netty Zookeeper Redis 高並發實戰》一書, 面試必備 + 面試必備 + 面試必備
寫在前面
大家好,我是作者尼恩。目前和幾個小伙伴一起,組織了一個高並發的實戰社群【瘋狂創客圈】。正在開始高並發、億級流程的 IM 聊天程序 學習和實戰
有的小伙伴對幀解碼器FrameDecoder ,尤其是LengthFieldBasedFrameDecoder(自定義長度幀解碼器) 不是太了解,尤其是覺得LengthFieldBasedFrameDecoder 參數多,不理解。
這里單獨撰文,對LengthFieldBasedFrameDecoder 的參數,進行重點介紹。看完之后,就會徹底的了解了。
1.1.1. 解碼器:FrameDecoder
前面所講的解碼器,在獲取入站數據時,都是通過ByteBuf的基礎類型讀取方法,讀取到是基礎的數據類型,比如int整數。如果在解碼時,讀取的不是基礎類型,而是非常基礎的二進制數據,該如何處理呢?
大家都知道,TCP協議是個“流”性質協議,它的底層根據二進制緩沖區的實際情況進行包的划分,會把上層(Netty層)的ByteBuf包,進行重新的划分和重組,組成一幀一幀的二進制數據。換句話說,一個上層Netty中的 ByteBuf包,可能會被TCP底層拆分成多個二進制數據幀進行發送;也有可能,底層將多個小的ByteBuf包,封裝成一個大的底層數據幀發送出去。
問題來了:如何從底層的二進制數據幀中,界定出來上層數據包的邊界,也即是上層包的起點和末尾呢?別急,界定的辦法,還是很多的。比如說,簡單一點方法就是規定上層數據包的長度。例如,規定每個上層數據包的長度為100byte。再比如說,可以規定上層包的分割符號,比如換行符。無論采用什么方法,最為重要的是,發送方和接收方,在界定方法上必須保持一致。
Netty中,提供了幾個重要的可以直接使用的幀解碼器。這里先介紹一個最為基礎的,它就是LineBasedFrameDecoder。LineBasedFrameDecoder的工作原理很簡單,依次遍歷原始ByteBuf(代表底層幀)中的可讀字節,判斷看是否存在“\n”或者“\r\n”換行符,也就是上層包的邊界的分割符。如果有,就以此位置為結束位置,從可讀索引到結束位置區間的字節就組成了一行。同時,它支持配置上層包的最大長度。如果連續讀取到最大長度后仍然沒有發現換行符,就會拋出異常。
下面演示一下LineBasedFrameDecoder的使用,代碼如下:
/**
* create by 尼恩 @ 瘋狂創客圈
**/
package com.crazymakercircle.NettyTest;
//...
public class TestDecoder {
@Test
public void testLineBasedFrameDecoder() {
//...
ChannelInitializer i = new ChannelInitializer<EmbeddedChannel>() {
protected void initChannel(EmbeddedChannel ch) {
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
ch.pipeline().addLast(new StringDecoder());
ch.pipeline().addLast(new StringProcessHandler());
}
};
EmbeddedChannel channel = new EmbeddedChannel(i);
for (int j = 0; j < 100; j++) {
ByteBuf buf = Unpooled.buffer();
String s = "I am " + j;
buf.writeBytes(s.getBytes("UTF-8"));
buf.writeBytes("\r\n".getBytes("UTF-8"));
channel.writeInbound(buf);
}
//...
}
實例中,向channel寫入100個入站數據包,每一個入站包都以"\r\n"回車換行符作為結束。channel的LineBasedFrameDecoder 解碼器,會將"\r\n"作為分割符,分割出一個一個的入站ByteBuf,然后發送給StringDecoder。StringDecoder會將分割好的ByteBuf二進制數據,轉成字符串,發送給StringProcessHandler 。最后,由StringProcessHandler負責將字符串展示出來。
這里,LineBasedFrameDecoder 和StringDecoder 都是Netty自帶的類。特別要說下的,就是StringDecoder,它的作用是將接收到ByteBuf二進制數據,轉換成字符串。另外,LineBasedFrameDecoder ,是一個非常簡單的幀解碼器,包含此解碼器在內,Netty中比較常用的幀解碼器,大致如下:
(1)固定長度幀解碼器 - FixedLengthFrameDecoder
適用場景:每個上層數據包的長度,都是固定的,比如 100。在這種場景下,只需要把這個解碼器加到 pipeline 中,Netty 會把底層幀,拆分成一個個長度為 100 的數據包 (ByteBuf),發送到下一個 channelHandler入站處理器。
(2)行分割幀解碼器 - LineBasedFrameDecoder
適用場景:每個上層數據包,使用換行符或者回車換行符做為邊界分割符。發送端發送的時候,每個數據包之間以換行符/回車換行符作為分隔。在這種場景下,只需要把這個解碼器加到 pipeline 中,Netty 會使用換行分隔符,把底層幀分割成一個一個完整的應用層數據包,發送到下一站。前面的例子,已經對這個解碼器進行了演示。
(3)自定義分隔符幀解碼器 - DelimiterBasedFrameDecoder
DelimiterBasedFrameDecoder 是LineBasedFrameDecoder的通用版本。不同之處在於,這個解碼器,可以自定義分隔符,而不是局限於換行符。如果使用這個解碼器,在發送的時候,末尾必須帶上對應的分隔符。
(4)自定義長度幀解碼器 - LengthFieldBasedFrameDecoder
這是一種基於靈活長度的解碼器。在數據包中,加了一個長度字段(長度域),保存上層包的長度。解碼的時候,會按照這個長度,進行上層ByteBuf應用包的提取。
1.1.1. 難點:自定義長度幀解碼器
在前面的四個幀解碼器中,第四個解碼器LengthFieldBasedFrameDecoder(自定義長度幀解碼器)的參數比較多,比較難,同時也比較重要,這里對其進行重點介紹。
下面是一個簡單的使用實例,代碼如下:
/**
* create by 尼恩 @ 瘋狂創客圈
**/
package com.crazymakercircle.NettyTest;
public class TestDecoder {
//...
@Test
public void testLengthFieldBasedFrameDecoder() {
try {
LengthFieldBasedFrameDecoder spliter=new LengthFieldBasedFrameDecoder(1024,0,4,0,4);
ChannelInitializer i = new ChannelInitializer<EmbeddedChannel>() {
protected void initChannel(EmbeddedChannel ch) {
ch.pipeline().addLast(spliter);
ch.pipeline().addLast(new StringDecoder(Charset.forName("UTF-8")));
ch.pipeline().addLast(new StringProcessHandler());
}
};
EmbeddedChannel channel = new EmbeddedChannel(i);
for (int j = 0; j < 100; j++) {
ByteBuf buf = Unpooled.buffer();
String s = "呵呵,I am " + j;
byte[] bytes = s.getBytes("UTF-8");
buf.writeInt(bytes.length);
buf.writeBytes(bytes);
channel.writeInbound(buf);
}
//...
}
上面用到的自定義長度解碼器LengthFieldBasedFrameDecoder構造器,涉及5個參數,都與長度域(數據包中的長度字段)相關,具體介紹如下:
(1) maxFrameLength - 發送的數據包最大長度;
(2) lengthFieldOffset - 長度域偏移量,指的是長度域位於整個數據包字節數組中的下標;
(3) lengthFieldLength - 長度域的自己的字節數長度。
(4) lengthAdjustment – 長度域的偏移量矯正。 如果長度域的值,除了包含有效數據域的長度外,還包含了其他域(如長度域自身)長度,那么,就需要進行矯正。矯正的值為:包長 - 長度域的值 – 長度域偏移 – 長度域長。
(5) initialBytesToStrip – 丟棄的起始字節數。丟棄處於有效數據前面的字節數量。比如前面有4個節點的長度域,則它的值為4。
在上面的例子中,自定義長度解碼器的構造參數值如下:
LengthFieldBasedFrameDecoder spliter=new LengthFieldBasedFrameDecoder(1024,0,4,0,4);
第一個參數為1024,表示數據包的最大長度為1024;第二個參數0,表示長度域的偏移量為0,也就是長度域放在了最前面,處於包的起始位置;第三個參數為4,表示長度域占用4個字節;第四個參數為0,表示長度域保存的值,僅僅為有效數據長度,不包含其他域(如長度域)的長度;第五個參數為4,表示最終的取到的目標數據包,拋棄最前面的4個字節數據,長度域的值被拋棄。
為了更加清楚的說明一下上面的規則,調整一下例子中的代碼。在寫入通道前,在數據包的最前面,加上兩個字節,作為包頭Head。另外,寫入的長度值,包含長度域自身的長度,也就是加上4。 修改后的代碼如下:
/**
* create by 尼恩 @ 瘋狂創客圈
**/
//...
for (int j = 0; j < 100; j++) {
ByteBuf buf = Unpooled.buffer();
String s = j+ " is me ,呵呵" ;
byte[] bytes = s.getBytes("UTF-8");
buf.writeChar(100);
buf.writeInt(bytes.length+4);
buf.writeBytes(bytes);
}
//...
為了完成正確的解碼,需要調整自定義長度解碼器的構造參數值,調整如下:
LengthFieldBasedFrameDecoder spliter=new LengthFieldBasedFrameDecoder(1024,2,4,-4,6);
第一、第二、第三個參數比較簡單,不再啰嗦。
第四個參數長度域的矯正值為 -4,為什么呢? 它計算的方法是:包長(X+2)- 長度域的值(X) – 長度域偏移(2) – 長度域長(4)= -4 。
這里假定長度域的值為X,那么包長為X+2。因為在這個例子中,長度域的值,已經包括了長度域的長度值。長度域值與整個包長度相比,就少了前面的Header的2個字節。按照公式進行計算,最終的值為 2-2-4 = -4 。
第五個參數丟棄的起始字節數為6,為什么呢? 因為,最終的有效的應用層數據,需要去掉前面的6個字節。其中,包括2個字節的Header,4個字節的長度域長。
寫在最后
目前和幾個小伙伴一起,組織了一個高並發的實戰社群【瘋狂創客圈】,完成整個項目的完整的架構和開發實戰,歡迎參與。
瘋狂創客圈 億級流量 高並發IM 學習實戰
- Java (Netty) 聊天程序【 億級流量】實戰 開源項目實戰
- Netty 源碼、原理、JAVA NIO 原理
- Java 面試題 一網打盡
- 瘋狂創客圈 【 博客園 總入口 】