LengthFieldBasedFrameDecoder

LengthFieldBasedFrameDecoder 詳解

瘋狂創客圈 Java 分佈式聊天室【 億級流量】實戰系列之 -31【 博客園 總入口html


 

 

寫在前面

​ 你們好,我是做者尼恩。目前和幾個小夥伴一塊兒,組織了一個高併發的實戰社羣【瘋狂創客圈】。正在開始高併發、億級流程的 IM 聊天程序 學習和實戰數組

有的小夥伴對幀解碼器FrameDecoder ,尤爲是LengthFieldBasedFrameDecoder(自定義長度幀解碼器) 不是太瞭解,尤爲是以爲LengthFieldBasedFrameDecoder 參數多,不理解。markdown

這裏單獨撰文,對LengthFieldBasedFrameDecoder 的參數,進行重點介紹。看完以後,就會完全的瞭解了。併發

1.1.1. 解碼器:FrameDecoder

前面所講的解碼器,在獲取入站數據時,都是經過ByteBuf的基礎類型讀取方法,讀取到是基礎的數據類型,好比int整數。若是在解碼時,讀取的不是基礎類型,而是很是基礎的二進制數據,該如何處理呢?分佈式

你們都知道,TCP協議是個「流」性質協議,它的底層根據二進制緩衝區的實際狀況進行包的劃分,會把上層(Netty層)的ByteBuf包,進行從新的劃分和重組,組成一幀一幀的二進制數據。換句話說,一個上層Netty中的 ByteBuf包,可能會被TCP底層拆分紅多個二進制數據幀進行發送;也有可能,底層將多個小的ByteBuf包,封裝成一個大的底層數據幀發送出去。高併發

問題來了:如何從底層的二進制數據幀中,界定出來上層數據包的邊界,也便是上層包的起點和末尾呢?別急,界定的辦法,仍是不少的。好比說,簡單一點方法就是規定上層數據包的長度。例如,規定每一個上層數據包的長度爲100byte。再好比說,能夠規定上層包的分割符號,好比換行符。不管採用什麼方法,最爲重要的是,發送方和接收方,在界定方法上必須保持一致。post

Netty中,提供了幾個重要的能夠直接使用的幀解碼器。這裏先介紹一個最爲基礎的,它就是LineBasedFrameDecoder。LineBasedFrameDecoder的工做原理很簡單,依次遍歷原始ByteBuf(表明底層幀)中的可讀字節,判斷看是否存在「\n」或者「\r\n」換行符,也就是上層包的邊界的分割符。若是有,就以此位置爲結束位置,從可讀索引到結束位置區間的字節就組成了一行。同時,它支持配置上層包的最大長度。若是連續讀取到最大長度後仍然沒有發現換行符,就會拋出異常。學習

下面演示一下LineBasedFrameDecoder的使用,代碼以下:atom

/**
 * 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負責將字符串展現出來。spa

這裏,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);

第1、第2、第三個參數比較簡單,再也不囉嗦。

第四個參數長度域的矯正值爲 -4,爲何呢? 它計算的方法是:包長(X+2)- 長度域的值(X) – 長度域偏移(2) – 長度域長(4)= -4 。

這裏假定長度域的值爲X,那麼包長爲X+2。由於在這個例子中,長度域的值,已經包括了長度域的長度值。長度域值與整個包長度相比,就少了前面的Header的2個字節。按照公式進行計算,最終的值爲 2-2-4 = -4 。

第五個參數丟棄的起始字節數爲6,爲何呢? 由於,最終的有效的應用層數據,須要去掉前面的6個字節。其中,包括2個字節的Header,4個字節的長度域長。

相關文章
相關標籤/搜索