讀古今文學網 > Netty實戰 > 第10章 編解碼器框架 >

第10章 編解碼器框架

本章主要內容

  • 解碼器、編碼器以及編解碼器的概述
  • Netty的編解碼器類

就像很多標準的架構模式都被各種專用框架所支持一樣,常見的數據處理模式往往也是目標實現的很好的候選對象,它可以節省開發人員大量的時間和精力。

當然這也適應於本章的主題:編碼和解碼,或者數據從一種特定協議的格式到另一種格式的轉換。這些任務將由通常稱為編解碼器的組件來處理。Netty提供了多種組件,簡化了為了支持廣泛的協議而創建自定義的編解碼器的過程。例如,如果你正在構建一個基於Netty的郵件服務器,那麼你將會發現Netty對於編解碼器的支持對於實現POP3、IMAP和SMTP協議來說是多麼的寶貴。

10.1 什麼是編解碼器

每個網絡應用程序都必須定義如何解析在兩個節點之間來回傳輸的原始字節,以及如何將其和目標應用程序的數據格式做相互轉換。這種轉換邏輯由編解碼器處理,編解碼器由編碼器和解碼器組成,它們每種都可以將字節流從一種格式轉換為另一種格式。那麼它們的區別是什麼呢?

如果將消息看作是對於特定的應用程序具有具體含義的結構化的字節序列——它的數據。那麼編碼器是將消息轉換為適合於傳輸的格式(最有可能的就是字節流);而對應的解碼器則是將網絡字節流轉換回應用程序的消息格式。因此,編碼器操作出站數據,而解碼器處理入站數據。

記住這些背景信息,接下來讓我們研究一下Netty所提供的用於實現這兩種組件的類。

10.2 解碼器

在這一節中,我們將研究Netty所提供的解碼器類,並提供關於何時以及如何使用它們的具體示例。這些類覆蓋了兩個不同的用例:

  • 將字節解碼為消息——ByteToMessageDecoderReplayingDecoder
  • 將一種消息類型解碼為另一種——MessageToMessageDecoder

因為解碼器是負責將入站數據從一種格式轉換到另一種格式的,所以知道Netty的解碼器實現了ChannelInboundHandler也不會讓你感到意外。

什麼時候會用到解碼器呢?很簡單:每當需要為ChannelPipeline中的下一個Channel-InboundHandler轉換入站數據時會用到。此外,得益於ChannelPipeline的設計,可以將多個解碼器鏈接在一起,以實現任意複雜的轉換邏輯,這也是Netty是如何支持代碼的模塊化以及復用的一個很好的例子。

10.2.1 抽像類ByteToMessageDecoder

將字節解碼為消息(或者另一個字節序列)是一項如此常見的任務,以至於Netty為它提供了一個抽像的基類:ByteToMessageDecoder。由於你不可能知道遠程節點是否會一次性地發送一個完整的消息,所以這個類會對入站數據進行緩衝,直到它準備好處理。表10-1解釋了它最重要的兩個方法。

表10-1 ByteToMessageDecoder API

方  法

描  述

decode(
    ChannelHandlerContext ctx,
  ByteBuf in,
  List<Object> out)

這是你必須實現的唯一抽像方法。decode方法被調用時將會傳入一個包含了傳入數據的ByteBuf,以及一個用來添加解碼消息的List。對這個方法的調用將會重複進行,直到確定沒有新的元素被添加到該List,或者該ByteBuf中沒有更多可讀取的字節時為止。然後,如果該List不為空,那麼它的內容將會被傳遞給ChannelPipeline中的下一個ChannelInboundHandler

decodeLast(
    ChannelHandlerContext ctx,
  ByteBuf in,
  List<Object> out)

Netty提供的這個默認實現只是簡單地調用了decode方法。當Channel的狀態變為非活動時,這個方法將會被調用一次。可以重寫該方法以提供特殊的處理[1]

下面舉一個如何使用這個類的示例,假設你接收了一個包含簡單int的字節流,每個int都需要被單獨處理。在這種情況下,你需要從入站ByteBuf中讀取每個int,並將它傳遞給ChannelPipeline中的下一個ChannelInboundHandler。為了解碼這個字節流,你要擴展ByteToMessageDecoder類。(需要注意的是,原始類型的int在被添加到List中時,會被自動裝箱為Integer。)

該設計如圖10-1所示。

每次從入站ByteBuf中讀取4字節,將其解碼為一個int,然後將它添加到一個List中。當沒有更多的元素可以被添加到該List中時,它的內容將會被發送給下一個Channel-InboundHandler

圖10-1 ToIntegerDecoder

代碼清單10-1展示了ToIntegerDecoder的代碼。

代碼清單10-1 ToIntegerDecoder類擴展了ByteToMessageDecoder

public class ToIntegerDecoder extends ByteToMessageDecoder {   ← --  擴展ByteToMessage-Decoder 類,以將字節解碼為特定的格式
  @Override
  public void decode(ChannelHandlerContext ctx, ByteBuf in,
    List<Object> out) throws Exception {  
    if (in.readableBytes >= 4) {   ← -- 檢查是否至少有4字節可讀(一個int的字節長度)
      out.add(in.readInt);   ← -- 從入站ByteBuf 中讀取一個int,並將其添加到解碼消息的List 中
    }
  }
}  

雖然ByteToMessageDecoder使得可以很簡單地實現這種模式,但是你可能會發現,在調用readInt方法前不得不驗證所輸入的ByteBuf是否具有足夠的數據有點繁瑣。在下一節中,我們將討論ReplayingDecoder,它是一個特殊的解碼器,以少量的開銷消除了這個步驟。

編解碼器中的引用計數

正如我們在第5章和第6章中所提到的,引用計數需要特別的注意。對於編碼器和解碼器來說,其過程也是相當的簡單:一旦消息被編碼或者解碼,它就會被ReferenceCountUtil.release(message)調用自動釋放。如果你需要保留引用以便稍後使用,那麼你可以調用ReferenceCountUtil.retain(message)方法。這將會增加該引用計數,從而防止該消息被釋放。

10.2.2 抽像類ReplayingDecoder

ReplayingDecoder擴展了ByteToMessageDecoder類(如代碼清單10-1所示),使得我們不必調用readableBytes方法。它通過使用一個自定義的ByteBuf實現,ReplayingDecoderByteBuf,包裝傳入的ByteBuf實現了這一點,其將在內部執行該調用[2]。

這個類的完整聲明是:

public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder  

類型參數S指定了用於狀態管理的類型,其中Void代表不需要狀態管理。代碼清單10-2展示了基於ReplayingDecoder重新實現的ToIntegerDecoder

代碼清單10-2 ToIntegerDecoder2類擴展了ReplayingDecoder

public class ToIntegerDecoder2 extends ReplayingDecoder<Void> {   ← --  擴展Replaying-Decoder<Void>以將字節解碼為消息
  @Override
  public void decode(ChannelHandlerContext ctx, ByteBuf in,  ← -- 傳入的ByteBuf 是ReplayingDecoderByteBuf
    List<Object> out) throws Exception {
    out.add(in.readInt);  ← --  從入站ByteBuf 中讀取一個int,並將其添加到解碼消息的List 中
  }
}  

和之前一樣,從ByteBuf中提取的int將會被添加到List中。如果沒有足夠的字節可用,這個readInt方法的實現將會拋出一個Error[3],其將在基類中被捕獲並處理。當有更多的數據可供讀取時,該decode方法將會被再次調用。(參見表10-1中關於decode方法的描述。)

請注意ReplayingDecoderByteBuf的下面這些方面:

  • 並不是所有的ByteBuf操作都被支持,如果調用了一個不被支持的方法,將會拋出一個UnsupportedOperationException
  • ReplayingDecoder稍慢於ByteToMessageDecoder

如果對比代碼清單10-1和代碼清單10-2,你會發現後者明顯更簡單。示例本身是很基本的,所以請記住,在真實的、更加複雜的情況下,使用一種或者另一種作為基類所帶來的差異可能是很顯著的。這裡有一個簡單的準則:如果使用ByteToMessageDecoder不會引入太多的複雜性,那麼請使用它;否則,請使用ReplayingDecoder

更多的解碼器

下面的這些類處理更加複雜的用例:

  • io.netty.handler.codec.LineBasedFrameDecoder——這個類在Netty內部也有使用,它使用了行尾控制字符(\n或者\r\n)來解析消息數據;

  • io.netty.handler.codec.http.HttpObjectDecoder——一個HTTP數據的解碼器。

io.netty.handler.codec子包下面,你將會發現更多用於特定用例的編碼器和解碼器實現。更多有關信息參見Netty的Javadoc。

10.2.3 抽像類MessageToMessageDecoder

在這一節中,我們將解釋如何使用下面的抽像基類在兩個消息格式之間進行轉換(例如,從一種POJO類型轉換為另一種):

public abstract class MessageToMessageDecoder<I>
  extends ChannelInboundHandlerAdapter  

類型參數I指定了decode方法的輸入參數msg的類型,它是你必須實現的唯一方法。表10-2展示了這個方法的詳細信息。

表10-2 MessageToMessageDecoder API

方  法

描  述

decode(
  ChannelHandlerContext ctx,
  I msg,
  List<Object> out)

對於每個需要被解碼為另一種格式的入站消息來說,該方法都將會被調用。解碼消息隨後會被傳遞給ChannelPipeline中的下一個ChannelInboundHandler

在這個示例中,我們將編寫一個IntegerToStringDecoder解碼器來擴展MessageTo-MessageDecoder<Integer>。它的decode方法會把Integer參數轉換為它的String表示,並將擁有下列簽名:

public void decode( ChannelHandlerContext ctx,
  Integer msg, List<Object> out ) throws Exception  

和之前一樣,解碼的String將被添加到傳出的List中,並轉發給下一個ChannelInboundHandler

該設計如圖10-2所示。

圖10-2 IntegerToStringDecoder

代碼清單10-3給出了IntegerToStringDecoder的實現。

代碼清單10-3 IntegerToStringDecoder

public class IntegerToStringDecoder extends
  MessageToMessageDecoder<Integer> {   ← --  擴展了MessageToMessageDecoder<Integer>
  @Override
  public void decode(ChannelHandlerContext ctx, Integer msg
    List<Object> out) throws Exception {
    out.add(String.valueOf(msg));  ← -- 將Integer 消息轉換為它的String 表示,並將其添加到輸出的List 中
  }
}  

HttpObjectAggregator

有關更加複雜的例子,請研究io.netty.handler.codec.http.HttpObjectAggregator類,它擴展了MessageToMessageDecoder<HttpObject>

10.2.4 TooLongFrameException類

由於Netty是一個異步框架,所以需要在字節可以解碼之前在內存中緩衝它們。因此,不能讓解碼器緩衝大量的數據以至於耗盡可用的內存。為了解除這個常見的顧慮,Netty提供了TooLongFrameException類,其將由解碼器在幀超出指定的大小限制時拋出。

為了避免這種情況,你可以設置一個最大字節數的閾值,如果超出該閾值,則會導致拋出一個TooLongFrameException(隨後會被ChannelHandler.exceptionCaught方法捕獲)。然後,如何處理該異常則完全取決於該解碼器的用戶。某些協議(如HTTP)可能允許你返回一個特殊的響應。而在其他的情況下,唯一的選擇可能就是關閉對應的連接。

代碼清單10-4展示了ByteToMessageDecoder是如何使用TooLongFrameException來通知ChannelPipeline中的其他ChannelHandler發生了幀大小溢出的。需要注意的是,如果你正在使用一個可變幀大小的協議,那麼這種保護措施將是尤為重要的。

代碼清單10-4 TooLongFrameException

public class SafeByteToMessageDecoder extends ByteToMessageDecoder {   ← --  擴展ByteToMessageDecoder以將字節解碼為消息
  private static final int MAX_FRAME_SIZE = 1024;
  @Override
  public void decode(ChannelHandlerContext ctx, ByteBuf in,
    List<Object> out) throws Exception {
      int readable = in.readableBytes;
      if (readable > MAX_FRAME_SIZE) {   ← -- 檢查緩衝區中是否有超過MAX_FRAME_SIZE個字節
        in.skipBytes(readable);  ← --  跳過所有的可讀字節,拋出TooLongFrame-Exception 並通知ChannelHandler
        throw new TooLongFrameException("Frame too big!");
    }
    // do something
    ...
  }
}  

到目前為止,我們已經探討了解碼器的常規用例,以及Netty所提供的用於構建它們的抽像基類。但是解碼器只是硬幣的一面。硬幣的另一面是編碼器,它將消息轉換為適合於傳出傳輸的格式。這些編碼器完備了編解碼器API,它們將是我們的下一個主題。

10.3 編碼器

回顧一下我們先前的定義,編碼器實現了ChannelOutboundHandler,並將出站數據從一種格式轉換為另一種格式,和我們方才學習的解碼器的功能正好相反。Netty提供了一組類,用於幫助你編寫具有以下功能的編碼器:

  • 將消息編碼為字節;
  • 將消息編碼為消息[4]。

我們將首先從抽像基類MessageToByteEncoder開始來對這些類進行考察。

10.3.1 抽像類MessageToByteEncoder

前面我們看到了如何使用ByteToMessageDecoder來將字節轉換為消息。現在我們將使用MessageToByteEncoder來做逆向的事情。表10-3展示了該API。

表10-3 MessageToByteEncoder API

方  法

描  述

encode(
  ChannelHandlerContext ctx,
  I msg,
  ByteBuf out)

encode方法是你需要實現的唯一抽像方法。它被調用時將會傳入要被該類編碼為ByteBuf的(類型為I的)出站消息。該ByteBuf隨後將會被轉發給ChannelPipeline中的下一個ChannelOutboundHandler

你可能已經注意到了,這個類只有一個方法,而解碼器有兩個。原因是解碼器通常需要在Channel關閉之後產生最後一個消息(因此也就有了decodeLast方法)。這顯然不適用於編碼器的場景——在連接被關閉之後仍然產生一個消息是毫無意義的。

圖10-3展示了ShortToByteEncoder,其接受一個Short類型的實例作為消息,將它編碼為Short的原始類型值,並將它寫入ByteBuf中,其將隨後被轉發給ChannelPipeline中的下一個ChannelOutboundHandler。每個傳出的Short值都將會佔用ByteBuf中的2字節。

ShortToByteEncoder的實現如代碼清單10-5所示。

代碼清單10-5 ShortToByteEncoder

public class ShortToByteEncoder extends MessageToByteEncoder<Short> {   ← --  擴展了MessageToByteEncoder
  @Override
  public void encode(ChannelHandlerContext ctx, Short msg, ByteBuf out)
    throws Exception {
    out.writeShort(msg);  ← --  將Short 寫入ByteBuf 中
  }
}  

Netty提供了一些專門化的MessageToByteEncoder,你可以基於它們實現自己的編碼器。WebSocket08FrameEncoder類提供了一個很好的實例。你可以在io.netty.handler. codec.http.websocketx包中找到它。

圖10-3 ShortToByteEncoder

10.3.2 抽像類MessageToMessageEncoder

你已經看到了如何將入站數據從一種消息格式解碼為另一種。為了完善這幅圖,我們將展示對於出站數據將如何從一種消息編碼為另一種。MessageToMessageEncoder類的encode方法提供了這種能力,如表10-4所示。

表10-4 MessageToMessageEncoder API

名  稱

描  述

encode(
  ChannelHandlerContext ctx,
  I msg,

  List<Object> out)

這是你需要實現的唯一方法。每個通過write方法寫入的消息都將會被傳遞給encode方法,以編碼為一個或者多個出站消息。隨後,這些出站消息將會被轉發給ChannelPipeline中的下一個ChannelOutboundHandler

為了演示,代碼清單10-6使用IntegerToStringEncoder擴展了MessageToMessage-Encoder。其設計如圖10-4所示。

圖10-4 IntegerToStringEncoder

如代碼清單10-6所示,編碼器將每個出站IntegerString表示添加到了該List中。

代碼清單10-6 IntegerToStringEncoder

public class IntegerToStringEncoder
  extends MessageToMessageEncoder<Integer> {   ← --  擴展了MessageToMessageEncoder
  @Override
  public void encode(ChannelHandlerContext ctx, Integer msg
    List<Object> out) throws Exception {
    out.add(String.valueOf(msg));  ← --  將Integer 轉換為String,並將其添加到List 中
  }
}  

關於有趣的MessageToMessageEncoder的專業用法,請查看io.netty.handler. codec.protobuf.ProtobufEncoder類,它處理了由Google的Protocol Buffers規範所定義的數據格式。

10.4 抽像的編解碼器類

雖然我們一直將解碼器和編碼器作為單獨的實體討論,但是你有時將會發現在同一個類中管理入站和出站數據和消息的轉換是很有用的。Netty的抽像編解碼器類正好用於這個目的,因為它們每個都將捆綁一個解碼器/編碼器對,以處理我們一直在學習的這兩種類型的操作。正如同你可能已經猜想到的,這些類同時實現了ChannelInboundHandlerChannelOutboundHandler接口。

為什麼我們並沒有一直優先於單獨的解碼器和編碼器使用這些復合類呢?因為通過盡可能地將這兩種功能分開,最大化了代碼的可重用性和可擴展性,這是Netty設計的一個基本原則。

在我們查看這些抽像的編解碼器類時,我們將會把它們與相應的單獨的解碼器和編碼器進行比較和參照。

10.4.1 抽像類ByteToMessageCodec

讓我們來研究這樣的一個場景:我們需要將字節解碼為某種形式的消息,可能是POJO,隨後再次對它進行編碼。ByteToMessageCodec將為我們處理好這一切,因為它結合了ByteToMessageDecoder以及它的逆向——MessageToByteEncoder。表10-5列出了其中重要的方法。

任何的請求/響應協議都可以作為使用ByteToMessageCodec的理想選擇。例如,在某個SMTP的實現中,編解碼器將讀取傳入字節,並將它們解碼為一個自定義的消息類型,如SmtpRequest[5]。而在接收端,當一個響應被創建時,將會產生一個SmtpResponse,其將被編碼回字節以便進行傳輸。

表10-5 ByteToMessageCodec API

方 法 名 稱

描  述

decode(
  ChannelHandlerContext ctx,
  ByteBuf in,
  List<Object>)

只要有字節可以被消費,這個方法就將會被調用。它將入站ByteBuf轉換為指定的消息格式,並將其轉發給ChannelPipeline中的下一個ChannelInboundHandler

decodeLast(
  ChannelHandlerContext ctx,
  ByteBuf in,
  List<Object> out)

這個方法的默認實現委託給了decode方法。它只會在Channel的狀態變為非活動時被調用一次。它可以被重寫以實現特殊的處理

encode(
  ChannelHandlerContext ctx,
  I msg,
  ByteBuf out)

對於每個將被編碼並寫入出站ByteBuf的(類型為I的)消息來說,這個方法都將會被調用

10.4.2 抽像類MessageToMessageCodec

在10.3.1節中,你看到了一個擴展了MessageToMessageEncoder以將一種消息格式轉換為另外一種消息格式的例子。通過使用MessageToMessageCodec,我們可以在一個單個的類中實現該轉換的往返過程。MessageToMessageCodec是一個參數化的類,定義如下:

public abstract class MessageToMessageCodec<INBOUND_IN,OUTBOUND_IN>  

表10-6列出了其中重要的方法。

表10-6 MessageToMessageCodec的方法

方 法 名 稱

描  述

protected abstract decode(
  ChannelHandlerContext ctx,
  INBOUND_IN msg,
  List<Object> out)

這個方法被調用時會被傳入INBOUND_IN類型的消息。它將把它們解碼為OUTBOUND_IN類型的消息,這些消息將被轉發給ChannelPipeline中的下一個Channel- InboundHandler

protected abstract encode(
  ChannelHandlerContext ctx,
  OUTBOUND_IN msg,
  List<Object> out)

對於每個OUTBOUND_IN類型的消息,這個方法都將會被調用。這些消息將會被編碼為INBOUND_IN類型的消息,然後被轉發給ChannelPipeline中的下一個ChannelOutboundHandler

decode方法是將INBOUND_IN類型的消息轉換為OUTBOUND_IN類型的消息,而encode方法則進行它的逆向操作。將INBOUND_IN類型的消息看作是通過網絡發送的類型,而將OUTBOUND_IN類型的消息看作是應用程序所處理的類型,將可能有所裨益[6]。

雖然這個編解碼器可能看起來有點高深,但是它所處理的用例卻是相當常見的:在兩種不同的消息API之間來回轉換數據。當我們不得不和使用遺留或者專有消息格式的API進行互操作時,我們經常會遇到這種模式。

WebSocket協議

下面關於MessageToMessageCodec的示例引用了一個新出的WebSocket協議,這個協議能實現Web瀏覽器和服務器之間的全雙向通信。我們將在第12章中詳細地討論Netty對於WebSocket的支持。

代碼清單10-7展示了這樣的對話[7]可能的實現方式。我們的WebSocketConvertHandler在參數化MessageToMessageCodec時將使用INBOUND_IN類型的WebSocketFrame,以及OUTBOUND_IN類型的MyWebSocketFrame,後者是WebSocketConvertHandler本身的一個靜態嵌套類。

代碼清單10-7 使用MessageToMessageCodec

public class WebSocketConvertHandler extends
  MessageToMessageCodec<WebSocketFrame,
  WebSocketConvertHandler.MyWebSocketFrame> {
  @Override
  protected void encode(ChannelHandlerContext ctx,    ← --  將MyWebSocketFrame 編碼為指定的WebSocketFrame子類型
    WebSocketConvertHandler.MyWebSocketFrame msg,
    List<Object> out) throws Exception {
    ByteBuf payload = msg.getData.duplicate.retain;
    switch (msg.getType) {  ← --  實例化一個指定子類型的WebSocketFrame
      case BINARY:
        out.add(new BinaryWebSocketFrame(payload));
        break;
      case TEXT:
        out.add(new TextWebSocketFrame(payload));
        break;
      case CLOSE:
        out.add(new CloseWebSocketFrame(true, 0, payload));
        break;
      case CONTINUATION:
        out.add(new ContinuationWebSocketFrame(payload));
        break;
      case PONG:
        out.add(new PongWebSocketFrame(payload));
        break;
      case PING:
        out.add(new PingWebSocketFrame(payload));
        break;
      default:
        throw new IllegalStateException(
          "Unsupported websocket msg " + msg);
    }
  }

  @Override
  protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg,  ← --  將WebSocketFrame 解碼為MyWebSocketFrame,並設置FrameType
    List<Object> out) throws Exception {
      ByteBuf payload = msg.content.duplicate.retain;
      if (msg instanceof BinaryWebSocketFrame) {
        out.add(new MyWebSocketFrame(
          MyWebSocketFrame.FrameType.BINARY, payload));
      } else
      if (msg instanceof CloseWebSocketFrame) {
        out.add(new MyWebSocketFrame (
          MyWebSocketFrame.FrameType.CLOSE, payload));
      } else
      if (msg instanceof PingWebSocketFrame) {
        out.add(new MyWebSocketFrame (
          MyWebSocketFrame.FrameType.PING, payload));
      } else
      if (msg instanceof PongWebSocketFrame) {
        out.add(new MyWebSocketFrame (
          MyWebSocketFrame.FrameType.PONG, payload));
      } else
      if (msg instanceof TextWebSocketFrame) {
        out.add(new MyWebSocketFrame (
          MyWebSocketFrame.FrameType.TEXT, payload));
      } else
      if (msg instanceof ContinuationWebSocketFrame) {
        out.add(new MyWebSocketFrame (
          MyWebSocketFrame.FrameType.CONTINUATION, payload));
      } else
      {
        throw new IllegalStateException(
          "Unsupported websocket msg " + msg);
    }
  }

  public static final class MyWebSocketFrame { ← --  聲明WebSocketConvertHandler所使用的OUTBOUND_IN 類型 
    public enum FrameType {  ← -- 定義擁有被包裝的有效負載的WebSocketFrame的類型 
      BINARY,
      CLOSE,
      PING,
      PONG,
      TEXT,
      CONTINUATION
    }
    private final FrameType type;
    private final ByteBuf data;

    public MyWebSocketFrame(FrameType type, ByteBuf data) {
      this.type = type;
      this.data = data;
    }

    public FrameType getType {
      return type;
    }

    public ByteBuf getData {
      return data;
    }
  }
}  

10.4.3 CombinedChannelDuplexHandler類

正如我們前面所提到的,結合一個解碼器和編碼器可能會對可重用性造成影響。但是,有一種方法既能夠避免這種懲罰,又不會犧牲將一個解碼器和一個編碼器作為一個單獨的單元部署所帶來的便利性。CombinedChannelDuplexHandler提供了這個解決方案,其聲明為:

public class CombinedChannelDuplexHandler
  <I extends ChannelInboundHandler,
  O extends ChannelOutboundHandler>  

這個類充當了ChannelInboundHandlerChannelOutboundHandler(該類的類型參數IO)的容器。通過提供分別繼承了解碼器類和編碼器類的類型,我們可以實現一個編解碼器,而又不必直接擴展抽像的編解碼器類。我們將在下面的示例中說明這一點。

首先,讓我們研究代碼清單10-8中的ByteToCharDecoder。注意,該實現擴展了ByteTo-MessageDecoder,因為它要從ByteBuf中讀取字符。

代碼清單10-8 ByteToCharDecoder

public class ByteToCharDecoder extends ByteToMessageDecoder {   ← --  擴展了ByteToMessageDecoder
  @Override
  public void decode(ChannelHandlerContext ctx, ByteBuf in,
    List<Object> out) throws Exception {
      while (in.readableBytes >= 2) {  ← --  將一個或者多個Character對像添加到傳出的List 中
        out.add(in.readChar); 
    }
  }
}  

這裡的decode方法一次將從ByteBuf中提取2字節,並將它們作為char寫入到List中,其將會被自動裝箱為Character對象。

代碼清單10-9包含了CharToByteEncoder,它能將Character轉換回字節。這個類擴展了MessageToByteEncoder,因為它需要將char消息編碼到ByteBuf中。這是通過直接寫入ByteBuf做到的。

代碼清單10-9 CharToByteEncoder

public class CharToByteEncoder extends
  MessageToByteEncoder<Character> {   ← --  擴展了MessageToByteEncoder
  @Override
  public void encode(ChannelHandlerContext ctx, Character msg,
    ByteBuf out) throws Exception {
    out.writeChar(msg);  ← -- 將Character 解碼為char,並將其寫入到出站ByteBuf 中
  }
}  

既然我們有了解碼器和編碼器,我們將會結合它們來構建一個編解碼器。代碼清單10-10展示了這是如何做到的。

代碼清單10-10 CombinedChannelDuplexHandler<I,O>

public class CombinedByteCharCodec extends
  CombinedChannelDuplexHandler<ByteToCharDecoder, CharToByteEncoder> {   ← --  通過該解碼器和編碼器實現參數化CombinedByteCharCodec
  public CombinedByteCharCodec {
    super(new ByteToCharDecoder, new CharToByteEncoder);  ← -- 將委託實例傳遞給父類
  }
}  

正如你所能看到的,在某些情況下,通過這種方式結合實現相對於使用編解碼器類的方式來說可能更加的簡單也更加的靈活。當然,這可能也歸結於個人的偏好問題。

10.5 小結

在本章中,我們學習了如何使用Netty的編解碼器API來編寫解碼器和編碼器。你也瞭解了為什麼使用這個API相對於直接使用ChannelHandlerAPI更好。

你看到了抽像的編解碼器類是如何為在一個實現中處理解碼和編碼提供支持的。如果你需要更大的靈活性,或者希望重用現有的實現,那麼你還可以選擇結合他們,而無需擴展任何抽像的編解碼器類。

在下一章中,我們將討論作為Netty框架本身的一部分的ChannelHandler實現和編解碼器,你可以利用它們來處理特定的協議和任務。


[1] 比如用來產生一個LastHttpContent消息。——譯者注

[2] 指調用readableBytes方法。——譯者注

[3] 這裡實際上拋出的是一個Signal,詳見io.netty.util.Signal類。——譯者注

[4] 另外一種格式的消息。——譯者注

[5] 位於基於Netty的SMTP/LMTP客戶端項目中(https://github.com/normanmaurer/niosmtp)。——譯者注

[6] 即有助於理解這兩個類型簽名的實際意義。——譯者注

[7] 指Web瀏覽器和服務器之間的雙向通信。——譯者注