Netty實戰十之編解碼器框架

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

1、什麼是編解碼器

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

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

2、解碼器

——將字節解碼爲消息——ByteToMessageDecoder和ReplayingDecoder

——將一種消息類型解碼爲另一種——MessageToMessageDecoder

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

每當需要爲ChannelPipeline中的下一個ChannelInboundHandler轉換入站數據時會用到。此外,得益於ChannelPipeline的設計,可以將多個解碼器鏈接在一起,以實現任意複雜的轉換邏輯,這也是Netty是如何支持代碼的模塊化以及複用的例子。

3、抽象類ByteToMessageDecoder

將字節解碼爲消息是一項如此常見的任務,以至於Netty爲他提供了一個抽象的基類:ByteToMessageDecoder。由於你不可能知道遠程節點是否會一次性地發送一個完整的消息,所以這個類會對入站數據進行緩衝。

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

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

public class ToIntegerDecoder extends ByteToMessageDecoder{
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext,                          ByteBuf in, List<Object> out) throws Exception {        //檢查是否至少有4字節可讀(一個int的字節長度)
        if (in.readableBytes() >= 4){            //從入站ByteBuf中讀取一個int,並將其添加到解碼消息的List中
            out.add(in.readInt());
        }
    }
}

雖然ByteToMessageDecoder使得可以很簡單地實現這種模式,但是你可能會發現,在調用readint()方法前不得不驗證所輸入的ByteBuf是否具有足夠的數據有點繁瑣。

4、抽象類ReplayingDecoder

ReplayingDecoder擴展了ByteToMessageDecoder類,使得我們不必調用readableBytes()方法。它通過使用一個自定義的ByteBuf實現,ReplayingDecoderByteBuf,包裝傳入的ByteBuf實現了這一點,其將在內部執行該調用。

public abstract class ReplayingDecoder extends ByteToMessageDecoder

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


public class ToIntegerDecoder2 extends ReplayingDecoder<Void>{
    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext,                          ByteBuf in, List<Object> out) throws Exception {
        out.add(in.readInt());
    }
}

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

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

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

——ReplayingDecoder稍慢於ByteToMessageDecoder

——如果使用ByteToMessageDecoder不會引入太多的複雜性,那麼請使用它;否則,請使用ReplayingDecoder

5、抽象類MessageToMessageDecoder

public abstract class MessageToMessageDecoder extends ChannelInboundHandlerAdapter

類型參數I指定了decode()方法的輸入參數msg的類型,它是你必須實現的唯一方法。

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

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

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


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

6、TooLongFrameException類

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

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

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


public class SafeByteToMessageDecoder extends ByteToMessageDecoder{
    private static final int MAX_FRAME_SIZE = 1024;    @Override
    protected void decode(ChannelHandlerContext channelHandlerContext,                          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
    }
}

7、抽象類MessageToByteEncoder

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

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


public class ShortToByteEncoder extends MessageToByteEncoder<Short>{
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext,                          Short msg, ByteBuf out) throws Exception {        //將Short寫入ByteBuf中
        out.writeShort(msg);
    }
}

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

8、抽象類MessageToMessageEncoder

我們將展示對於出站數據將如何從一種消息編碼爲另一種。MessageToMessageEncoder類的encoder()方法提供了這種能力。

以下代碼,編碼器將每個出站Integer的String表示添加到了該List中。


public class IntegerToStringEncoder extends MessageToMessageEncoder<Integer>{
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext,                          Integer msg, List<Object> out) throws Exception {        //將Integer轉換爲String,並將其添加到List中
        out.add(String.valueOf(msg));
    }
}

9、抽象的編解碼器類

在同一個類中管理入站和出站數據和消息的轉換是很有用的。Netty的抽象編解碼器類正好用於這個目的,因爲它們每個都將捆綁一個解碼器/編碼器對,以處理我們一直在學習的這兩種類型的操作。

通過儘可能地將這兩種功能分開,最大化了代碼的可重用性和可擴展性,這是Netty設計的一個基本原則。

10、抽象類ByteToMessageCodec

場景:我們需要將字節編碼爲某種形式的消息,可能是POJO,隨後再次對它進行編碼。ByteToMessageCodec將爲我們處理好了這一切,因爲它結合了ByteToMessageDecoder以及他的逆向MessageToByteEncoder。

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

11、CombinedChannelDuplexHandler類

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

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

這個類充當了ChannelInboundHandler和ChannelOutboundHandler(該類的類型參數I和O)的容器。通過提供分別繼承瞭解碼器類和編碼器類的類型,我們可以實現一個編解碼器,而又不必直接擴展抽象的編解碼器類。

首先,讓我們研究代碼中的ByteToCharDecoder。注意,該實現擴展了ByteToMessageDecoder,因爲它要從ByteBuf中讀取字符。


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

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

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


public class CharToByteEncoder extends MessageToByteEncoder<Character>{
    @Override
    protected void encode(ChannelHandlerContext channelHandlerContext,                          Character msg, ByteBuf out) throws Exception {        //將Character解碼爲char,並將其寫入到出站ByteBuf中
        out.writeChar(msg);
    }
}

既然我們有了解碼器和編碼器,我們將會結合它們來構建 一個編解碼器。如以下代碼所示。

在某些情況下,通過這種方式結合實現相對於使用編解碼器類的方式來說可能更加的簡單也更加的靈活。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章