一、什么是Decoder和Encoder
在Netty里面,有四個核心概念,它們分別是:
- Channel:一個客戶端與服務器通信的通道。
- ChannelHandler:業務邏輯處理器, 通常情況下,業務邏輯都是存在於ChannelHandler之中。
- ChannelInboundHandler:輸入處理器
- ChannelOutboundHandler:輸出處理器
- ChannelPipeline:用於存放ChannelHandler的雙向鏈表。
- ChannelContext:通信管道的上下文
它們之間的交互流程是:
- 事件傳遞給 ChannelPipeline 的第一個 ChannelHandler
- ChannelHandler 通過關聯的 ChannelHandlerContext 傳遞事件給 ChannelPipeline 中的 下一個
而我們要講的Decoder和Encoder,就是ChannelInboundHandler和ChannelOutboundHandler,分別用於在數據流進來的時候將字節碼轉換為消息對象和數據流出去的時候將消息對象轉換為字節碼。
二、解碼器Decoder
對於解碼器,Netty中主要提供了抽象基類ByteToMessageDecoder
和MessageToMessageDecoder
1. 抽象類 ByteToMessageDecoder
用於將接收到的二進制數據(Byte)解碼,得到完整的請求報文(Message)。
通常,ByteToMessageDecoder解碼后內容會得到一個ByteBuf實例列表,每個ByteBuf實例都包含了一個完整的報文信息。你可以直接把這些ByteBuf實例直接交給之后的ChannelInboundHandler處理,或者將這些包含了完整報文信息的ByteBuf實例解析封裝到不同的Java對象實例后,再交其處理。不管哪一種情況,之后的ChannelInboundHandler在處理時不需要再考慮粘包、拆包問題。
ByteToMessageDecoder提供的一些常見的實現類:
- FixedLengthFrameDecoder:定長協議解碼器,我們可以指定固定的字節數算一個完整的報文
- LineBasedFrameDecoder:行分隔符解碼器,遇到\n或者\r\n,則認為是一個完整的報文
- DelimiterBasedFrameDecoder:分隔符解碼器,與LineBasedFrameDecoder類似,只不過分隔符可以自己指定
- LengthFieldBasedFrameDecoder:長度編碼解碼器,將報文划分為報文頭/報文體,根據報文頭中的Length字段確定報文體的長度,因此報文提的長度是可變的
- JsonObjectDecoder:json格式解碼器,當檢測到匹配數量的"{" 、”}”或”[””]”時,則認為是一個完整的json對象或者json數組。
這些實現類,都只是將接收到的二進制數據,解碼成包含完整報文信息的ByteBuf實例后,就直接交給了之后的ChannelInboundHandler處理。之所以不將ByteBuf中的信息封裝到Java對象中,道理很簡單,Netty根本不知道開發者想封裝到什么對象中,甚至不知道報文中的具體內容是什么,因此不如直接把包含了完整報文信息的ByteBuf實例,交給開發人員來自己解析封裝。
當然也有例外,例如Netty提供的XmlDecoder,直接將二進制數據流解析成Aalto XML parser類庫中定義的xml對象。
我們也可以自定義ByteToMessageDecoder,此時需要覆蓋ByteToMessageDecoder的decode
方法:
protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;
參數說明:
- in:需要解碼的二進制數據。
- List<Object> out:解碼后的有效報文列表,我們需要將解碼后的報文添加到這個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方法每次只解析一個有效報文即可,沒有必要一次全部解析出來。
2. 抽象類 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類)。
除了一些公有協議的解碼器外,Netty提供的MessageToMessageDecoder實現類較少,主要是:
(1) StringDecoder
用於將包含完整的報文信息的ByteBuf轉換成字符串。我們可以將其與ByteToMessageDecoder的一些實現類聯合使用,以LineBasedFrameDecoder為例,其將二進制數據流按行分割后封裝到ByteBuf中。我們可以在其之后再添加一個StringDecoder,將ByteBuf中的數據轉換成字符串。
(2) Base64Decoder
用於Base64編碼。例如,前面我們提到LineBasedFrameDecoder、DelimiterBasedFrameDecoder等ByteToMessageDecoder實現類,是使用特殊字符作為分隔符作為解碼的條件。但是如果報文內容中如果本身就包含了分隔符,那么解碼就會出錯。此時,對於發送方,可以先使用Base64Encoder對報文內容進行Base64編碼,然后我們選擇Base64編碼包含的64種字符之外的其他特殊字符作為分隔符。在解碼時,首先特殊字符進行分割,然后通過Base64Decoder解碼得到原始的二進制字節流。
MessageToMessageDecoder的類聲明如下:
public abstract class MessageToMessageDecoder<I> extends ChannelInboundHandlerAdapter
其中泛型參數I表示我們要解碼的消息類型。例前面,我們在ToIntegerDecoder中,把二進制字節流轉換成了一個int類型的整數。
類似的,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提供了對應的編碼器實現MessageToByteEncoder
和MessageToMessageEncoder
,二者都實現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_IN
和OUTBOUND_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; }