Netty in Action ——— The codec framework

本文是Netty文集中“Netty in action”系列的文章。主要是對Norman Maurer and Marvin Allen Wolfthal 的 《Netty in action》一書簡要翻譯,同時對重要點加上一些自己補充和擴展。

本章含蓋

  • 解碼器、編碼器、編解碼器綜述
  • Netty 的編解碼類

Netty提供可以簡化各種協議的自定義編解碼器創建的組件。

什么是編解碼器?

每個網絡應用都會定義端之間傳輸的二進制字節該如何被解析和轉換,從發送端到目標程序的數據類型。這個轉換邏輯通過編解碼器來完成,編解碼器包含了一個編碼器和一個解碼器,每個編解碼器將一個字節流從一個格式轉換為另一個格式。那么怎么區分它們了?
一個編碼器轉換消息為一個適當的格式用于傳輸(大部分情況下是一個字節流);對應的解碼器轉換網絡流為一個程序的消息格式。一個編碼器操作一個出站數據(outbound data),一個解碼器處理一個入站數據(inbound data)。

Decoders

Decoder 實現了ChannelInboundHandler。
解碼器類包含了兩個不同的使用場景:

  • 解碼字節到消息 —— ByteToMessageDecoder 和 ReplayingDecoder
  • 解碼一種消息類型到另外一種消息類型 —— MessageToMessageDecoder
    因為解碼器的責任是轉換入站數據從一種格式到另一種格式,Netty的解碼器實現了ChannelInboundHandler。
    因為Netty的ChannelPipeline的設計,你能夠鏈接多個解碼器去實現任意復雜的轉換邏輯,這是Netty支持代碼模塊化和重用的一個很好的例子。
ByteToMessageDecoder 抽象類

由于你不知道遠端是否會一次性發送一個完整的數據,ByteToMessageDecoder類緩存入站數據直到數據準備好可用于處理。



注意,decodeLast()方法是在當ByteBuf還有可讀數據時,默認調用decode()方法。

    /**
     * Is called one last time when the {@link ChannelHandlerContext} goes in-active. Which means the
     * {@link #channelInactive(ChannelHandlerContext)} was triggered.
     *
     * By default this will just call {@link #decode(ChannelHandlerContext, ByteBuf, List)} but sub-classes may
     * override this for some special cleanup operation.
     */
    protected void decodeLast(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        if (in.isReadable()) {
            // Only call decode() if there is something left in the buffer to decode.
            // See https://github.com/netty/netty/issues/4386
            decodeRemovalReentryProtection(ctx, in, out);
        }
    }

示例:


盡管ByteToMessageDecoder使得此模式實現簡單,你可能發現有一點比較煩人,就是在調用readInt()前必須校驗input ByteBuf是否有足夠的數據。下一章我們將討論ReplayingDecoder,一個特殊的解碼器,它能夠消除這個一步驟,只需要消耗很小的性能損耗。

編解碼器中的引用計數
正如我們在第五章和第六章所提到的,引用計數是需要特別注意的。在編碼器和解碼器情況下,這個過程是相當簡單的:一旦一個消息被編碼或解碼,它將自動被釋放通過調用ReferenceCountUtil.release(message)。如果你需要持有該引用以便后面使用,你能調用ReferenceCountUtil.retain(message)。該調用會增加引用計數,防止消息被釋放。

ReplayingDecoder 抽象類

ReplayingDecoder繼承了ByteToMessageDecoder,并將我們從調用readableBytes()中解放。它實現這個通過使用一個自定義ByteBuf的實現(ReplayingDecoderBuffer)來封裝入站ByteBuf。ReplayingDecoderBuffer在內部執行時調用。
public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder
參數S指定用于狀態管理的類型,Void表示不使用狀態管理。

示例:基于ReplayingDecoder來實現ToIntegerDecoder

??int從ByteBuf中提取,然后加到List中。如果有效字節不足,readInt()方法的實現會拋出一個Error異常,該異常會被捕獲并在基類中得到處理。decode()方法將再次被調用當更多的數據準備好讀取時


請注意ReplayingDecoder的這些方面:

  • 不是所有的ByteBuf操作都支持。如果一個不支持的方法被調用了,那么將拋出一個UnsupportedOperationException異常。
  • ReplayingDecoder略慢與ByteToMessageDecoder
    在現實環境總,在復雜情況下是使用ByteToMessageDecoder還是ReplayingDecoder的區別是很大的。

更多關于解碼器
下面的類處理更復雜的使用情況:

  • io.netty.handler.codec.LineBasedFrameDecoder —— 這個類用于Netty內部,使用'結束換行'控制字符( \n or \r\n )來解析消息數據
  • io.netty.handler.codec.http.HttpObjectDecoder —— 用于Http數據的解碼器
MessageToMessageDecoder 抽象類

使用MessageToMessageDecoder進行消息格式間的轉換。
public abstract class MessageToMessageDecoder<I> extends ChannelInboundHandlerAdapter
參數 “I" 指定了輸入消息的類型,作為decode的輸入消息類型參數,decode()是你唯一需要實現的方法。

示例:

一個更復雜的例子,請看io.netty.handler.codec.http.HttpObjectAggregator,該類間接繼承了MessageToMessageDecoder<HttpObject>.

TooLongFrameException 類

因為Netty是一個異步框架,你需要去緩存字節到內存中直到你能夠去解析它。所以,你不應該允許你的解碼器去緩存足夠的數據來耗盡可用內存。為了解決這個共同關心的問題,Netty提供了一個TooLongFrameException,如果一個幀的大小超過了指定大小則拋出該異常。
為了避免內存被耗盡,你能夠設置一個最大字節數的閾值,如果超過了這個閾值,將導致一個TooLongFrameException異常拋出( 并被ChannelHandler.exceptionCaught()捕獲)。然后由解碼器的用戶來決定如果處理該異常。一些協議,例如HTTP,允許你返回一個特殊的響應。在其他情況下,唯一的選擇可能就是關閉連接。

??顯示了如何使用TooLongFrameException來通知ChannelPipeline中的其他ChannelHandlers遇到一個超過幀大小的限度。注意,這種情況保護是特別重要的,如果你工作的協議具有任意幀大小。

Encoders

Encoder 實現了ChannelOutboundHandler。
Netty提供了一個集合的類來幫助你寫支持如下功能的編碼器:

  • 將一消息編碼為字節
  • 將一個消息編碼為另一個消息
MessageToByteEncoder 抽象類

你可能已經注意到,這個類只有一個方法,但decoder有兩個。這是因為解碼器經常需要產生一個最后消息在channel已經關閉前( 因此有了 decodeLast() 方法 )(注意,decodeLast會在channelInactive之前被調用)。我們清楚的知道編碼器是沒有這種情況的 —— 沒有必要在連接斷開后去產生一個消息。

示例:
MessageToMessageEncoder 抽象類

示例:

如果對特化的MessageToMessageEncoder感興趣,可以查看io.netty.handler.codec.protobuf.ProtobufEncoder類

codec抽象類

Netty的codec抽象類,將一個編碼器和解碼器捆綁成一對用于同時管理入站和出站消息的轉換。codec同時實現了ChannelInboundHandler 和 ChannelOutboundHandler。
為什么我們不是用這個復合類在所有時候,而是更傾向于將解碼和編碼分開了?因為將這兩個功能分開,無論何時都能最大程度上來保持代碼的重用性和可擴展性,這是Netty的一個基本理念。

ByteToMessageCodec 抽象類

ByteToMessageCodec 合并了ByteToMessageDecoder 和 MessageToByteEncoder。


任何 請求/響應 協議都適合使用ByteToMessageCodec。

MessageToMessageCodec 抽象類

public abstract class MessageToMessageCodec<INBOUND_IN, OUTBOUND_IN>

decode()方法轉換一個INBOUND_IN消息為一個OUTBOUND_IN類型,encode()則相反。
INBOUND_IN消息作為寫操作發送出去的類型,OUTBOUND_IN消息作為被應用處理的類型。

CombinedChannelDuplexHandler 類

如我們早前說的,合并一個解碼器和一個編碼器可能會對復用性造成影響。然后,這里提供了一個方式去避免這個損失且不用犧牲配置一個解碼器和一個編碼器為一個單元的便利性。使用CombinedChannelDuplexHandler來解決這個問題
public class CombinedChannelDuplexHandler<I extends ChannelInboundHandler, O extends ChannelOutboundHandler>
該類扮演一個包含有ChannelInboundHandler和一個ChannelOutboundHandler的容器。通過分別提供一個docoder類和一個encoder類,我們能夠實現編解碼器而不需要直接繼承一個codec抽象類。
也就是說,CombinedChannelDuplexHandler使用組合的方式,復用已經存在的Decoder和Encoder來實現編解碼器,這樣就保持了代碼的重用性和可擴展性。而如果是直接實現一個Codec抽象類的話,則是通過直接實現相關的encode、decode方法來實現編解碼器,這使得程序失去了代碼的重用性和可擴展性。

示例:



擴展

Q:ReplayingDecoder是如何做到,不用判斷字節數是否足夠就直接調用readXXX操作并能保證正確的邏輯了?
A:我們來簡單看下ReplayingDecoder中的一些實現:



callDecode()是一個寫循環實現,每次都會先記錄ByteBuf in當前的讀索引位置,然后將ByteBuf in封裝成一個ReplayingDecoderByteBuf對象(這是一個特殊的ByteBuf實現,它重寫了ByteBuf的各種readXXX、getXXX。這些方法在獲取真實的數據前會先判斷字節是否足夠,如果不足夠則會拋出一個Signal異常。)。然后將封裝好的ReplayingDecoderByteBuf對象傳遞給decodeRemovalReentryProtection方法(decodeRemovalReentryProtection方法底層會調用decode()方法),這樣一來當readXXX操作的時候數據不足的話就會拋出一個Signal異常。在catch{}語塊中就會將ByteBuf的readerIndex重置為本次解碼前的位置。
但也正是因為如此,在某些情況下ReplayingDecoder可能存在較差的性能。如,在網絡很慢且消息格式較復雜的情況下。比如,有個一消息格式為:“消息頭”+“消息體”兩部分組成一個完整的消息包。我們需要根據消息頭獲取消息體數據長度以獲取我們所需的數據。 但是了,因為網絡比較慢的關系,我們讀取到的ByteBuf可能不是一個完整的消息格式包(可能包含了消息頭以及部分的消息體),本次decode就無法解析出一個消息包(但是我們已經成功解碼處理消息頭的數據了),那么就會在catch中將ByteBuf的readerIndex重置。那么下次decdoe的時候,又需要重新解析一次消息(即,消息頭數據又需要重新進行一次解析)。如果依舊無法獲取一個完整的消息包,那么前面的操作將再執行一次。。。
當然,我們也是有辦法來解決這個問題的,那就是使用ReplayingDecoderByteBuf的checkpoint(T)方法來管理解碼器的狀態。嗯,這里舉個java doc中的例子來說明checkpoint(T)的使用。

public enum MyDecoderState {
    READ_LENGTH,
    READ_CONTENT;
}

public class IntegerHeaderFrameDecoder extends ReplayingDecoder<MyDecoderState> {

    private int length;

   public IntegerHeaderFrameDecoder() {
     // Set the initial state.
     super(MyDecoderState.READ_LENGTH);
   }

    @Override
   protected void decode(ChannelHandlerContext ctx, ByteBuf buf, List<Object> out) throws Exception {
     switch (state()) {
     case READ_LENGTH:
       length = buf.readInt();
       checkpoint(MyDecoderState.READ_CONTENT);
     case READ_CONTENT:
       ByteBuf frame = buf.readBytes(length);
       checkpoint(MyDecoderState.READ_LENGTH);
       out.add(frame);
       break;
     default:
       throw new Error("Shouldn't reach here.");
     }
   }
 }

首先,我們根據自定協議聲明好協議的各個狀態。這里就是“READ_LENGTH”和“READ_CONTENT”兩個狀態。然后在IntegerHeaderFrameDecoder解碼器的時候,設置初始狀態為“MyDecoderState.READ_LENGTH”。在decode方法中,我們根據不同的狀態來進行相應的操作:
一開始state為READ_LENGTH,則先進行消息頭部分的數據獲取,如果此時ByteBuf中的數據不足以獲取到消息頭的數據那么就會拋出一個Signal異常,由基類根據上面的邏輯進行readerIndex的重置;如果ByteBuf的數據足以獲取到消息頭,那么在獲取到消息頭的值后,執行『checkpoint(MyDecoderState.READ_CONTENT);』這步非常的重要,checkpoint方法會完成兩個操作:① 將調用decode方法前記錄的readerIndex初始值修改為當前ByteBuf的readerIndex值,② 將state狀態修改為MyDecoderState.READ_CONTENT。然后繼續state為MyDecoderState.READ_CONTENT情況的處理(注意,這里你會發現switch-case中沒有break語句,所以流程會走到下一個狀態)。這樣一來,當ByteBuf中的數據不足以讀取到完整的消息體的內容,基類在重置readerIndex的時候,不再是重置到讀取消息頭之前的位置了,而是重置到讀取完消息頭之后的位置。這樣,當decode再次被調用時,我們就無需再解碼一次消息頭了,這時state()方法返回的值已經是MyDecoderState.READ_CONTENT(因為我們上面在解碼完消息頭后通過checkpoint方法設置了狀態值為MyDecoderState.READ_CONTENT),流程也會從解碼消息體開始繼續進行。

后記

若文章有任何錯誤,望大家不吝指教:)

參考

《Netty in action》
圣思園《精通并發與Netty》

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容