什麼是編解碼器
每個網絡應用程序都必須定義如何解析在兩個節點之間來回傳輸的原始字節,以及如何將其和 目標應用程序的數據格式做相互轉換。這種轉換邏輯由編解碼器處理,編解碼器由編碼器和解碼 器組成,它們每種都可以將字節流從一種格式轉換爲另一種格式。那麼它們的區別是什麼呢?
如果將消息看作是對於特定的應用程序具有具體含義的結構化的字節序列——它的數據。
- 編碼器是將消息轉換爲適合於傳輸的格式(最有可能的就是字節流);
- 解碼器則是將 網絡字節流轉換回應用程序的消息格式。
因此,編碼器操作出站數據,而解碼器處理入站數據。
解碼器
Netty 所提供的解碼器類覆蓋了兩個不同的用例:
- 將字節解碼爲消息——
ByteToMessageDecoder
和ReplayingDecoder;
- 將一種消息類型解碼爲另一種——
MessageToMessageDecoder
。
因爲解碼器是負責將入站數據從一種格式轉換到另一種格式的,所以Netty 的解碼器實現了 ChannelInboundHandler
也不會讓你感到意外。
什麼時候會用到解碼器呢?很簡單:每當需要爲 ChannelPipeline 中的下一個 ChannelInboundHandler 轉換入站數據時會用到
。此外,得益於 ChannelPipeline 的設計,可以將 多個解碼器鏈接在一起,以實現任意複雜的轉換邏輯,這也是 Netty 是如何支持代碼的模塊化以及 複用的一個很好的例子。
抽象類ByteToMessageDecoder
將字節解碼爲消息(或者另一個字節序列)是一項如此常見的任務,以至於 Netty 爲它提供了一個 抽象的基類:ByteToMessageDecoder。由於你不可能知道遠程節點是否會一次性地發送一個完整 的消息,所以這個類會對入站數據進行緩衝,直到它準備好處理
。
下面舉一個如何使用這個類的示例,假設你接收了一個包含簡單 int 的字節流,每個 int 都需要被單獨處理。在這種情況下,你需要從入站 ByteBuf 中讀取每個 int,並將它傳遞給 ChannelPipeline 中的下一個 ChannelInboundHandler。爲了解碼這個字節流,你要擴展 ByteToMessageDecoder 類。(需要注意的是,原子類型的 int 在被添加到 List 中時,會被自動裝箱爲 Integer
。)
每次從入站 ByteBuf 中讀取 4 字節,將其解碼爲一個 int,然後將它添加到一個 List 中。 當沒有更多的元素可以被添加到該 List 中時,它的內容將會被髮送給下一個 Channel- InboundHandler。
//擴展了ByteToMessageDecoder,以將字節解碼爲特定的格式
public class ToIntegerDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//檢查是否至少有4字節可讀(1個int的字節長度)
if (in.readableBytes() >= 4) {
//從入站ByteBuf中讀取一個int,並將其添加到解碼消息的List中
out.add(in.readInt());
}
}
}
雖然 ByteToMessageDecoder 使得可以很簡單地實現這種模式,但是你可能會發現,在調 用 readInt()方法前不得不驗證所輸入的 ByteBuf 是否具有足夠的數據有點繁瑣。下面說的 ReplayingDecoder,它是一個特殊的解碼器,以少量的開銷消除了這個步驟。
編碼器中的引用計數:
對於編碼器和解碼器來說,其過程 也是相當的簡單:
一旦消息被編碼或者解碼,它就會被 ReferenceCountUtil.release(message)調用 自動釋放。如果你需要保留引用以便稍後使用,那麼你可以調用 ReferenceCountUtil.retain(message) 方法。這將會增加該引用計數,從而防止該消息被釋放。
抽象類ReplayingDecoder
ReplayingDecoder擴展了ByteToMessageDecoder類,使 得我們不必調用readableBytes()方法。它通過使用一個自定義的ByteBuf實現, ReplayingDecoderByteBuf,包裝傳入的ByteBuf實現了這一點,其將在內部執行該調用。
//完整聲明
public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder
//類型參數 S 指定了用於狀態管理的類型,其中 Void 代表不需要狀態管理。
//擴展ReplayingDecoder<Void>以將字節解碼爲消息
public class ToIntegerDecoder2 extends ReplayingDecoder<Void> {
//傳入的ByteBuf是ReplayingDecoderByteBuf
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//從入站ByteBuf中讀取一個int,並將其添加到解碼消息的List中
out.add(in.readInt());
}
}
和之前一樣,從ByteBuf中提取的int將會被添加到List中。如果沒有足夠的字節可用,這 個readInt()方法的實現將會拋出一個Error,其將在基類中被捕獲並處理。當有更多的數據可供讀取時,該decode()方法將會被再次調用。
請注意 ReplayingDecoderByteBuf 的下面這些方面:
- 並不是所有的 ByteBuf 操作都被支持,如果調用了一個不被支持的方法,將會拋出一個
UnsupportedOperationException
; - ReplayingDecoder 稍慢於 ByteToMessageDecoder。
更多解碼器:
io.netty.handler.codec.LineBasedFrameDecode
r——這個類在 Netty 內部也有使用,它使用了行尾控制字符(\n
或者\r\n
)來解析消息數據;io.netty.handler.codec.http.HttpObjectDecoder
——一個 HTTP 數據的解碼器。
抽象類MessageToMessageDecoder
使用該抽象基類可以使消息在
//完整聲明
public abstract class MessageToMessageDecoder<I> extends ChannelInboundHandlerAdapter
示例:
我們將編寫一個 IntegerToStringDecoder 解碼器來擴展 MessageToMessageDecoder。它的 decode()方法會把 Integer 參數轉換爲它的 String 表示:
解 碼 的 S t r i n g 將 被 添 加 到 傳 出 的 L i s t 中 ,並 轉 發 給 下 一 個 C h a n n e l I n b o u n d H a n d l e r 。
public class IntegerToStringDecoder extends MessageToMessageEncoder<Integer> {
@Override
protected void encode(ChannelHandlerContext ctx, Integer msg, List<Object> out) throws Exception {
//將Integer消息轉換爲它的String表示,並將其添加到輸出的List中
out.add(String.valueOf(msg));
}
}
TooLongFrameException類
由於 Netty 是一個異步框架,所以需要在字節可以解碼之前在內存中緩衝它們。因此,不能 讓解碼器緩衝大量的數據以至於耗盡可用的內存。爲了解除這個常見的顧慮,Netty 提供了 TooLongFrameException 類,其將由解碼器在幀超出指定的大小限制時拋出。
爲了避免這種情況,你可以設置一個最大字節數的閾值,如果超出該閾值,則會導致拋出一 個 TooLongFrameException(隨後會被 ChannelHandler.exceptionCaught()方法捕 獲
)。然後,如何處理該異常則完全取決於該解碼器的用戶。某些協議(如 HTTP)可能允許你 返回一個特殊的響應。而在其他的情況下,唯一的選擇可能就是關閉對應的連接。
示例:
使用 TooLongFrameException 來通知 ChannelPipeline 中的其他 ChannelHandler 發生了幀大小溢出的。需要注意的是, 如果你正在使用一個可變幀大小的協議,那麼這種保護措施將是尤爲重要的:
public class SafeByteToMessageDecoder extends ByteToMessageDecoder {
public static final int MAX_FRAME_SIZE = 1024;
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
int readable = in.readableBytes();
//檢查緩衝區是否有超過MAX_FRAME_SIZE個字節
if (readable > MAX_FRAME_SIZE) {
//跳過所有的可讀字節,拋出TooLongFrameException並通知ChannelHandler
in.skipBytes(readable);
throw new TooLongFrameException("Frame too big!");
}
//do something
}
}
編碼器
編碼器實現了 ChannelOutboundHandler,並將出站數據從 一種格式轉換爲另一種格式,和我們方纔學習的解碼器的功能正好相反。Netty 提供了一組類, 用於幫助你編寫具有以下功能的編碼器:
- 將消息編碼爲字節;
- 將消息編碼爲消息;
抽象類MessageToByteEncoder
這個類只有一個方法,而解碼器有兩個。原因是解碼器通常需要在 Channel 關閉之後產生最後一個消息(因此也就有了 decodeLast()方法
。這顯然不適用於編碼器的場景——在連接被關閉之後仍然產生一個消息是毫無意義的
。
示例:
上圖展示了 ShortToByteEncoder,其接受一個 Short 類型的實例作爲消息,將它編碼 爲Short的原子類型值,並將它寫入ByteBuf中,其將隨後被轉發給ChannelPipeline中的 下一個 ChannelOutboundHandler。每個傳出的 Short 值都將會佔用 ByteBuf 中的 2 字節。
public class ShortToByteEncoder extends MessageToByteEncoder<Short> {
@Override
protected void encode(ChannelHandlerContext ctx, Short msg, ByteBuf out) throws Exception {
//將Short寫入ByteBuf
out.writeShort(msg);
}
}
Netty 提供了一些專門化的 MessageToByteEncoder,你可以基於它們實現自己的編碼器。 WebSocket08FrameEncoder
類提供了一個很好的實例。你可以在 io.netty.handler. codec.http.websocketx 包中找到它。
抽象類MessageToMessageEncoder
MessageToMessageEncoder 類的 encode()方法提供了將入站數據從一個消息格式解碼爲另一種。
示例:
使用 IntegerToStringEncoder 擴展了 MessageToMessageEncoder。編碼器將每個出站 Integer 的 String 表示添加到了該 List 中。
public class IntegerToStringEncoder extends MessageToMessageEncoder {
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, List out) throws Exception {
out.add(String.valueOf(msg));
}
}
抽象的編解碼器類
雖然我們一直將解碼器和編碼器作爲單獨的實體討論,但是你有時將會發現在同一個類中管理 入站和出站數據和消息的轉換是很有用的。Netty 的抽象編解碼器類正好用於這個目的,因爲它們每 個都將捆綁一個解碼器/編碼器對,
以處理我們一直在學習的這兩種類型的操作。正如同你可能已經 猜想到的,這些類同時實現了 ChannelInboundHandler 和 ChannelOutboundHandler 接口
。
爲什麼我們並沒有一直優先於單獨的解碼器和編碼器使用這些複合類呢?因爲通過儘可能 地將這兩種功能分開,最大化了代碼的可重用性和可擴展性,這是 Netty 設計的一個基本原則。
抽象類ByteToMessageCodec
讓我們來研究這樣的一個場景:我們需要將字節解碼爲某種形式的消息,可能是 POJO,隨 後再次對它進行編碼。ByteToMessageCodec 將爲我們處理好這一切,因爲它結合了 ByteToMessageDecoder 以及它的逆向——MessageToByteEncoder。
任何的請求/響應協議都可以作爲使用ByteToMessageCodec的理想選擇。例如,在某個 SMTP的實現中,編解碼器將讀取傳入字節,並將它們解碼爲一個自定義的消息類型,如 SmtpRequest。而在接收端,當一個響應被創建時,將會產生一個SmtpResponse,其將被 編碼回字節以便進行傳輸。
抽象類MessageToMessageCodec
通過使用 MessageToMessageCodec,我們可以在一個單個的 類中實現該轉換的往返過程。MessageToMessageCodec 是一個參數化的類,定義如下:
public abstract class MessageToMessageCodec<INBOUND_IN,OUTBOUND_IN>
decode()方法是將INBOUND_IN類型的消息轉換爲OUTBOUND_IN類型的消息,而 encode()方法則進行它的逆向操作。將INBOUND_IN類型的消息看作是通過網絡發送的類型, 而將OUTBOUND_IN類型的消息看作是應用程序所處理的類型,將可能有所裨益。
示例:
WebSocket 協議
下面關於 MessageToMessageCodec 的示例引用了一個新出的 WebSocket 協議,這個協議能實現 Web 瀏覽器和服務器之間的全雙向通信。
我們的WebSocketConvertHandler 在參數化MessageToMessageCodec時將使用INBOUND_IN類型的WebSocketFrame,以及 OUTBOUND_IN類型的MyWebSocketFrame,後者是WebSocketConvertHandler本身的一個 靜態嵌套類。
public class WebSocketConvertHandler
extends MessageToMessageCodec<WebSocketFrame, WebSocketConvertHandler.MyWebSocketFrame> {
@Override
protected void encode(ChannelHandlerContext ctx, MyWebSocketFrame msg, List<Object> out) throws Exception {
//實例化一個指定子類型的WebSocketFrame
ByteBuf payload = msg.getData().duplicate().retain();
switch (msg.getType()) {
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);
}
}
//將WebSocketFrame解碼爲MyWebSocketFrame,並設置FrameType
@Override
protected void decode(ChannelHandlerContext ctx, WebSocketFrame msg, List<Object> out) throws Exception {
ByteBuf paload = msg.content().duplicate().retain();
if (msg instanceof BinaryWebSocketFrame) {
out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.BINARY, paload));
} else
if (msg instanceof CloseWebSocketFrame) {
out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.CLOSE, paload));
} else
if (msg instanceof PingWebSocketFrame) {
out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.PING, paload));
} else
if (msg instanceof PongWebSocketFrame) {
out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.PONG, paload));
} else
if (msg instanceof TextWebSocketFrame) {
out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.TEXT, paload));
} else
if (msg instanceof ContinuationWebSocketFrame) {
out.add(new MyWebSocketFrame(MyWebSocketFrame.FrameType.CONTINUATION, paload));
} else {
throw new IllegalStateException("Unsupported websocket msg " + msg);
}
}
public static final class MyWebSocketFrame {
public enum FrameType {
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;
}
}
}
CombinedChannelDuplexHandler類
正如我們前面所提到的,結合一個解碼器和編碼器可能會對可重用性造成影響。但是,有一 種方法既能夠避免這種懲罰,又不會犧牲將一個解碼器和一個編碼器作爲一個單獨的單元部署所 帶來的便利性。
CombinedChannelDuplexHandler 提供了這個解決方案,其聲明爲:
public class CombinedChannelDuplexHandler
<I extends ChannelInboundHandler, O extends ChannelOutboundHandler>
這個類充當了 ChannelInboundHandler 和 ChannelOutboundHandler(該類的類型 參數 I 和 O)的容器
。通過提供分別繼承瞭解碼器類和編碼器類的類型,我們可以實現一個編解碼器,而又不必直接擴展抽象的編解碼器類。
示例:
public class ByteToCharDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
while (in.readableBytes() >= 2) {
out.add(in.readChar());
}
}
}
這裏的 decode()方法一次將從 ByteBuf 中提取 2 字節,並將它們作爲 char 寫入到 List 中,其將會被自動裝箱爲 Character 對象。
public class CharToByteEncoder extends MessageToByteEncoder<Character> {
@Override
protected void encode(ChannelHandlerContext ctx, Character msg, ByteBuf out) throws Exception {
out.writeChar(msg);
}
}
將 Character 轉換回字節。這個類擴 展了 MessageToByteEncoder,因爲它需要將 char 消息編碼到 ByteBuf 中。這是通過直接 寫入 ByteBuf 做到的。
//通過該解碼器和編碼器實現參數化CombinedByteCharCodec
public class CombinedChannelDuplexHandler extends
io.netty.channel.CombinedChannelDuplexHandler<ByteToCharDecoder, CharToByteEncoder> {
public CombinedChannelDuplexHandler() {
//將委託實例傳遞給父類
super(new ByteToCharDecoder(), new CharToByteEncoder());
}
}