瘋狂創客圈 Java 分佈式聊天室【 億級流量】實戰系列之 -31【 博客園 總入口 】html
你們好,我是做者尼恩。目前和幾個小夥伴一塊兒,組織了一個高併發的實戰社羣【瘋狂創客圈】。正在開始高併發、億級流程的 IM 聊天程序 學習和實戰數組
有的小夥伴對幀解碼器FrameDecoder ,尤爲是LengthFieldBasedFrameDecoder(自定義長度幀解碼器) 不是太瞭解,尤爲是以爲LengthFieldBasedFrameDecoder 參數多,不理解。markdown
這裏單獨撰文,對LengthFieldBasedFrameDecoder 的參數,進行重點介紹。看完以後,就會完全的瞭解了。併發
前面所講的解碼器,在獲取入站數據時,都是經過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應用包的提取。
在前面的四個幀解碼器中,第四個解碼器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個字節的長度域長。