1. 程式人生 > >【Netty】(9)---Netty編解碼器

【Netty】(9)---Netty編解碼器

ext 就會 思想 eterm 固定 mit request對象 表示 ase

Netty編解碼器

在了解Netty編解碼之前,先了解Java的編解碼:

編碼(Encode)稱為序列化, 它將對象序列化為字節數組,用於網絡傳輸、數據持久化或者其它用途

解碼(Decode)稱為反序列化,它把從網絡、磁盤等讀取的字節數組還原成原始對象(通常是原始對象的拷貝),以方便後續的業務邏輯操作。

java序列化對象只需要實現java.io.Serializable接口並生成序列化ID,這個類就能夠通過java.io.ObjectInput和java.io.ObjectOutput序列化和反序列化。

Java序列化目的:1.網絡傳輸。2.對象持久化。

Java序列化缺點

:1.無法跨語言。 2.序列化後碼流太大。3.序列化性能太低。

Java序列化僅僅是Java編解碼技術的一種,由於它的種種缺陷,衍生出了多種編解碼技術和框架,這些編解碼框架實現消息的高效序列化。

下面我們來了解下Netty自己的編解碼器。

一、概念

在網絡應用中需要實現某種編解碼器,將原始字節數據與自定義的消息對象進行互相轉換。網絡中都是以字節碼的數據形式來傳輸數據的,服務器編碼數據後發送到客戶

端,客戶端需要對數據進行解碼。

netty提供了強大的編解碼器框架,使得我們編寫自定義的編解碼器很容易,也容易封裝重用。對於Netty而言,編解碼器由兩部分組成:編碼器、解碼器。

解碼器:負責將消息從字節或其他序列形式轉成指定的消息對象。

編碼器:將消息對象轉成字節或其他序列形式在網絡上傳輸。

Netty 的編(解)碼器實現了 ChannelHandlerAdapter,也是一種特殊的 ChannelHandler,所以依賴於 ChannelPipeline,可以將多個編(解)碼器鏈接在一起,以實現復雜

的轉換邏輯。

Netty裏面的編解碼: 解碼器:負責處理“入站 InboundHandler”數據編碼器:負責“出站 OutboundHandler” 數據

二、解碼器(Decoder)

解碼器負責 解碼“入站”數據從一種格式到另一種格式,解碼器處理入站數據是抽象ChannelInboundHandler的實現。實踐中使用解碼器很簡單,就是將入站數據轉換格式後

傳遞到ChannelPipeline中的下一個ChannelInboundHandler進行處理;這樣的處理時很靈活的,我們可以將解碼器放在ChannelPipeline中,重用邏輯。

對於解碼器,Netty中主要提供了抽象基類ByteToMessageDecoderMessageToMessageDecoder

技術分享圖片

抽象解碼器

1) ByteToMessageDecoder: 用於將字節轉為消息,需要檢查緩沖區是否有足夠的字節

2) ReplayingDecoder: 繼承ByteToMessageDecoder,不需要檢查緩沖區是否有足夠的字節,但是 ReplayingDecoder速度略慢於ByteToMessageDecoder,

同時不是所有的ByteBuf都支持。

選擇:項目復雜性高則使用ReplayingDecoder,否則使用 ByteToMessageDecoder

3)MessageToMessageDecoder: 用於從一種消息解碼為另外一種消息(例如POJO到POJO)

1、ByteToMessageDecoder解碼器

用於將接收到的二進制數據(Byte)解碼,得到完整的請求報文(Message)。

ByteToMessageDecoder是一種ChannelInboundHandler,可以稱為解碼器,負責將byte字節流住(ByteBuf)轉換成一種Message,Message是應用可以自己定義的一種

Java對象。

下面列出了ByteToMessageDecoder兩個主要方法:

protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out)//這個方法是唯一的一個需要自己實現的抽象方法,作用是將ByteBuf數據解碼成其他形式的數據。
decodeLast(ChannelHandlerContext, ByteBuf, List<Object>),//實際上調用的是decode(...)。

參數的作用如下:

Bytubuf:需要解碼的二進制數據。

List<Object>:解碼後的有效報文列表,我們需要將解碼後的報文添加到這個List中。之所以使用一個List表示,是因為考慮到粘包問題,因此入參的in中可能包含多個有效報文。

當然,也有可能發生了拆包,in中包含的數據還不足以構成一個有效報文,此時不往List中添加元素即可。

另外特別要註意的是,在解碼時,不能直接調用ByteBuf的readXXX方法來讀取數據,而是應該首先要判斷能否構成一個有效的報文。

案例,假設協議規定傳輸的數據都是int類型的整數

技術分享圖片

上圖中顯式輸入的ByteBuf中包含4個字節,每個字節的值分別為:1,2,3,4。我們自定義一個ToIntegerDecoder進行解碼,盡管這裏我看到了4個字節剛好可以構成一個int類

型整數,但是在真正解碼之前,我們並不知道ByteBuf包含的字節數能否構成完成的有效報文,因此需要首先判斷ByteBuf中剩余可讀的字節,是否大於等於4,如下:

public class ToIntegerDecoder extends ByteToMessageDecoder {
    @Override
   public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
    if (in.readableBytes() >= 4) {
        out.add(in.readInt());
    } }
}

只有在可讀字節數>=4的情況下,我們才進行解碼,即讀取一個int,並添加到List中。

在可讀字節數小於4的情況下,我們並沒有做任何處理,假設剩余可讀字節數為3,不足以構成1個int。那麽父類ByteToMessageDecoder發現這次解碼List中的元素沒有變化,

則會對in中的剩余3個字節進行緩存,等待下1個字節的到來,之後再回到調用ToIntegerDecoder的decode方法。

另外需要註意: 在ToIntegerDecoder的decode方法中,每次最多只讀取一個1個int。如果ByteBuf中的字節數很多,例如為16,那麽可以構成4個int,而這裏只讀取了1個int,

那麽剩余12字節怎麽辦?這個其實不用擔心,ByteToMessageDecoder再每次回調子類的decode方法之後,都會判斷輸入的ByteBuf中是否還有剩余字節可讀,如果還有,會再次

回調子類的decode方法,直到某個回調decode方法List中的元素個數沒有變化時才停止,元素個數沒有變化,實際上意味著子類已經沒有辦法從剩余的字節中讀取一個有效報文。

由於存在剩余可讀字節時,ByteToMessageDecoder會自動再次回調子類decode方法,因此筆者建議在實現ByteToMessageDecoder時,decode方法每次只解析一個有效報文

即可,沒有必要一次全部解析出來。

在推薦一個用代碼舉例: 自定義Decoder繼承ByteToMessageDecoder實現解碼的小案例(個人認為非常好)

ByteToMessageDecoder提供的一些常見的實現類:

  • FixedLengthFrameDecoder:定長協議解碼器,我們可以指定固定的字節數算一個完整的報文

  • LineBasedFrameDecoder: 行分隔符解碼器,遇到\n或者\r\n,則認為是一個完整的報文

  • DelimiterBasedFrameDecoder: 分隔符解碼器,與LineBasedFrameDecoder類似,只不過分隔符可以自己指定

  • LengthFieldBasedFrameDecoder:長度編碼解碼器,將報文劃分為報文頭/報文體,根據報文頭中的Length字段確定報文體的長度,因此報文提的長度是可變的

  • JsonObjectDecoder:json格式解碼器,當檢測到匹配數量的"{" 、”}”或”[””]”時,則認為是一個完整的json對象或者json數組。

這些實現類,都只是將接收到的二進制數據,解碼成包含完整報文信息的ByteBuf實例後,就直接交給了之後的ChannelInboundHandler處理。

2、ReplayingDecoder 解碼器

ReplayingDecoder是byte-to-message解碼的一種特殊的抽象基類,byte-to-message解碼讀取緩沖區的數據之前需要檢查緩沖區是否有足夠的字節,使用

ReplayingDecoder就無需自己檢查;若ByteBuf中有足夠的字節,則會正常讀取;若沒有足夠的字節則會停止解碼

也正因為這樣的包裝使得ReplayingDecoder帶有一定的局限性

1) 不是所有的操作都被ByteBuf支持,如果調用一個不支持的操作會拋出DecoderException。

2) ByteBuf.readableBytes()大部分時間不會返回期望值

如果你能忍受上面列出的限制,相比ByteToMessageDecoder,你可能更喜歡ReplayingDecoder。在滿足需求的情況下推薦使用ByteToMessageDecoder,因為它的處理比較

簡單,沒有ReplayingDecoder實現的那麽復雜。ReplayingDecoder繼承於ByteToMessageDecoder,所以他們提供的接口是相同的。下面代碼是ReplayingDecoder的實現:

/**
 * Integer解碼器,ReplayingDecoder實現
 */
public class ToIntegerReplayingDecoder extends ReplayingDecoder<Void> {

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        out.add(in.readInt());
    }
}

3、MessageToMessageDecoder

ByteToMessageDecoder是將二進制流進行解碼後,得到有效報文。而MessageToMessageDecoder則是將一個本身就包含完整報文信息的對象轉換成另一個Java對象

舉例: 前面介紹了ByteToMessageDecoder的部分子類解碼後,會直接將包含了報文完整信息的ByteBuf實例交由之後的ChannelInboundHandler處理,此時,你可以在

ChannelPipeline中,再添加一個MessageToMessageDecoder,將ByteBuf中的信息解析後封裝到Java對象中,簡化之後的ChannelInboundHandler的操作。

另外:一些場景下,有可能你的報文信息已經封裝到了Java對象中,但是還要繼續轉成另外的Java對象,因此一個MessageToMessageDecoder後面可能還跟著另一個

MessageToMessageDecoder。一個比較容易的理解的類比案例是Java Web編程,通常客戶端瀏覽器發送過來的二進制數據,已經被web容器(如tomcat)解析成了一個

HttpServletRequest對象,但是我們還是需要將HttpServletRequest中的數據提取出來,封裝成我們自己的POJO類,也就是從一個Java對象(HttpServletRequest)轉換

成另一個Java對象(我們的POJO類)。

MessageToMessageDecoder的類聲明如下:

/**
  * 其中泛型參數I表示我們要解碼的消息類型。例前面,我們在ToIntegerDecoder中,把二進制字節流轉換成了一個int類型的整數。
  */
public abstract class MessageToMessageDecoder<I> extends ChannelInboundHandlerAdapter

類似的,MessageToMessageDecoder也有一個decode方法需要覆蓋 ,如下:

/**
* 參數msg,需要進行解碼的參數。例如ByteToMessageDecoder解碼後的得到的包含完整報文信息ByteBuf
* List<Object> out參數:將msg經過解析後得到的java對象,添加到放到List<Object> out中
*/
protected abstract void decode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;

例如,現在我們想編寫一個IntegerToStringDecoder,把前面編寫的ToIntegerDecoder輸出的int參數轉換成字符串,此時泛型I就應該是Integer類型。

技術分享圖片

integerToStringDecoder源碼如下所示

public class IntegerToStringDecoder extends MessageToMessageDecoder<Integer> {
    @Override
    public void decode(ChannelHandlerContext ctx, Integer msg List<Object> out) throws Exception {
        out.add(String.valueOf(msg));
    }
}

此時我們應該按照如下順序組織ChannelPipieline中ToIntegerDecoder和IntegerToStringDecoder 的關系:

ChannelPipieline ch=....
    ch.addLast(new ToIntegerDecoder());
    ch.addLast(new IntegerToStringDecoder());

也就是說,前一個ChannelInboudHandler輸出的參數類型,就是後一個ChannelInboudHandler的輸入類型

特別註意,如果我們指定MessageToMessageDecoder的泛型參數為ByteBuf,表示其可以直接針對ByteBuf進行解碼,那麽其是否能替代ByteToMessageDecoder呢?

答案是不可以的。因為ByteToMessageDecoder除了進行解碼,還要會對不足以構成一個完整數據的報文拆包數據(拆包)進行緩存。而MessageToMessageDecoder

則沒有這樣的邏輯

因此通常的使用建議是,使用一個ByteToMessageDecoder進行粘包、拆包處理,得到完整的有效報文的ByteBuf實例,然後交由之後的一個或者多個

MessageToMessageDecoder對ByteBuf實例中的數據進行解析,轉換成POJO類。

三 編碼器(Encoder)

與ByteToMessageDecoder和MessageToMessageDecoder相對應,Netty提供了對應的編碼器實現MessageToByteEncoderMessageToMessageEncoder

二者都實現ChannelOutboundHandler接口。

技術分享圖片

相對來說,編碼器比解碼器的實現要更加簡單,原因在於解碼器除了要按照協議解析數據,還要要處理粘包、拆包問題;而編碼器只要將數據轉換成協議規定的二進制格式發送即可

1、抽象類MessageToByteEncoder

MessageToByteEncoder也是一個泛型類,泛型參數I表示將需要編碼的對象的類型,編碼的結果是將信息轉換成二進制流放入ByteBuf中。子類通過覆寫其抽象方法encode

來實現編碼,如下所示:

public abstract class MessageToByteEncoder<I> extends ChannelOutboundHandlerAdapter {
....
     protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception;
}

可以看到,MessageToByteEncoder的輸出對象out是一個ByteBuf實例,我們應該將泛型參數msg包含的信息寫入到這個out對象中。

MessageToByteEncoder使用案例:

public class IntegerToByteEncoder extends MessageToByteEncoder<Integer> {
    @Override
    protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) throws Exception {
        out.writeInt(msg);//將Integer轉成二進制字節流寫入ByteBuf中
    }
}

2、抽象類MessageToMessageEncoder

MessageToMessageEncoder同樣是一個泛型類,泛型參數I表示將需要編碼的對象的類型,編碼的結果是將信息放到一個List中。子類通過覆寫其抽象方法encode,來實現編碼,如下所示:

public abstract class MessageToMessageEncoder<I> extends ChannelOutboundHandlerAdapter {
   ...
   protected abstract void encode(ChannelHandlerContext ctx, I msg, List<Object> out) throws Exception;
   ...
}

與MessageToByteEncoder不同的,MessageToMessageEncoder編碼後的結果放到的out參數類型是一個List中。例如,你一次發送2個報文,因此msg參數中實際上包含了

2個報文,因此應該解碼出兩個報文對象放到List中。

MessageToMessageEncoder提供的常見子類包括:

  • LineEncoder:按行編碼,給定一個CharSequence(如String),在其之後添加換行符\n或者\r\n,並封裝到ByteBuf進行輸出,與LineBasedFrameDecoder相對應。

  • Base64Encoder:給定一個ByteBuf,得到對其包含的二進制數據進行Base64編碼後的新的ByteBuf進行輸出,與Base64Decoder相對應。

  • LengthFieldPrepender:給定一個ByteBuf,為其添加報文頭Length字段,得到一個新的ByteBuf進行輸出。Length字段表示報文長度,與LengthFieldBasedFrameDecoder相對應。

  • StringEncoder:給定一個CharSequence(如:StringBuilder、StringBuffer、String等),將其轉換成ByteBuf進行輸出,與StringDecoder對應。

這些MessageToMessageEncoder實現類最終輸出的都是ByteBuf,因為最終在網絡上傳輸的都要是二進制數據。

四、編碼解碼器Codec

編碼解碼器: 同時具有編碼與解碼功能,特點同時實現了ChannelInboundHandler和ChannelOutboundHandler接口,因此在數據輸入和輸出時都能進行處理。

Netty提供提供了一個ChannelDuplexHandler適配器類,編碼解碼器的抽象基類 ByteToMessageCodec 、MessageToMessageCodec都繼承與此類,如下:

ByteToMessageCodec內部維護了一個ByteToMessageDecoder和一個MessageToByteEncoder實例,可以認為是二者的功集合,泛型參數I是接受的編碼類型:

public abstract class ByteToMessageCodec<I> extends ChannelDuplexHandler {
    private final TypeParameterMatcher outboundMsgMatcher;
    private final MessageToByteEncoder<I> encoder;
    private final ByteToMessageDecoder decoder = new ByteToMessageDecoder(){…}
  
    ...
    protected abstract void encode(ChannelHandlerContext ctx, I msg, ByteBuf out) throws Exception;
    protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
    ...
}

MessageToMessageCodec內部維護了一個MessageToMessageDecoder和一個MessageToMessageEncoder實例,可以認為是二者的功集合,泛型參數

INBOUND_INOUTBOUND_IN分別表示需要解碼和編碼的數據類型。

public abstract class MessageToMessageCodec<INBOUND_IN, OUTBOUND_IN> extends ChannelDuplexHandler {
   private final MessageToMessageEncoder<Object> encoder= ...
   private final MessageToMessageDecoder<Object> decoder =…
   ...
   protected abstract void encode(ChannelHandlerContext ctx, OUTBOUND_IN msg, List<Object> out) throws Exception;
   protected abstract void decode(ChannelHandlerContext ctx, INBOUND_IN msg, List<Object> out) throws Exception;
}

其他編解碼方式

使用編解碼器來充當編碼器和解碼器的組合失去了單獨使用編碼器或解碼器的靈活性,編解碼器是要麽都有要麽都沒有。你可能想知道是否有解決這個僵化問題

的方式,還可以讓編碼器和解碼器在ChannelPipeline中作為一個邏輯單元。幸運的是,Netty提供了一種解決方案,使用CombinedChannelDuplexHandler。雖然這個

類不是編解碼器API的一部分,但是它經常被用來簡歷一個編解碼器。

如何使用CombinedChannelDuplexHandler來結合解碼器和編碼器呢?下面我們從兩個簡單的例子看了解。

/**
 * 解碼器,將byte轉成char
 */
public class ByteToCharDecoder extends ByteToMessageDecoder {
 
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        while(in.readableBytes() >= 2){
            out.add(Character.valueOf(in.readChar()));
        }
    }
/**
 * 編碼器,將char轉成byte
 */
public class CharToByteEncoder extends MessageToByteEncoder<Character> {
 
    @Override
    protected void encode(ChannelHandlerContext ctx, Character msg, ByteBuf out) throws Exception {
        out.writeChar(msg);
    }
}
/**
 * 繼承CombinedChannelDuplexHandler,用於綁定解碼器和編碼器
 */
public class CharCodec extends CombinedChannelDuplexHandler<ByteToCharDecoder, CharToByteEncoder> {
    public CharCodec(){
        super(new ByteToCharDecoder(), new CharToByteEncoder());
    }
}

從上面代碼可以看出,使用CombinedChannelDuplexHandler綁定解碼器和編碼器很容易實現,比使用*Codec更靈活。

如果一個人充滿快樂,正面的思想,那麽好的人事物就會和他共鳴,而且被他吸引過來。同樣,一個人老帶悲傷,倒黴的事情也會跟過來。

——在自己心情低落的時候,告誡自己不要把負能量帶給別人。(大校19)

【Netty】(9)---Netty編解碼器