Java併發編程學習-日記8、netty的編碼和解碼

Netty解碼器

ByteToMessageDecoder

一個標準的解碼器將輸入類型爲ByteBuf緩衝區的數據進行解碼,輸出一個一個的Java POJO對象。Netty內置了這個解碼器,叫作ByteToMessageDecoder,位在Netty的io.netty.handler.codec包中。所有的Netty中的解碼器,都是Inbound入站處理器類型,都直接或者間接地實現了ChannelInboundHandler接口。

ByteToMessageDecoder解碼的流程,大致具體可以描述爲:

  • 首先,它將上一站傳過來的輸入到Bytebuf中的數據進行解碼,解碼出一個List<Object>對象列表;
  • 然後,迭代List<Object>列表,逐個將Java POJO對象傳入下一站Inbound入站處理器。

如果要實現一個自己的ByteBuf解碼器,流程大致如下:

(1)首先繼承ByteToMessageDecoder抽象類。

(2)然後實現其基類的decode抽象方法。將ByteBuf到POJO解碼的邏輯寫入此方法。將Bytebuf二進制數據,解碼成一個一個的Java POJO對象。

(3)在子類的decode方法中,需要將解碼後的Java POJO對象,放入decode的List<Object>實參中。這個實參是ByteTo-MessageDecoder父類傳入的,也就是父類的結果收集列表。在流水線的過程中,ByteToMessageDecoder調用子類decode方法解碼完成後,會將List<Object>中的結果,一個一個地分開傳遞到下一站的Inbound入站處理器。

自定義整數解碼器:

public class Byte2IntegerDecoder extends ByteToMessageDecoder {

    @Override

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

        while (in.readableBytes() >= 4) {

            int i = in.readInt();

            Logger.info("解碼出一個整數: " + i);

            out.add(i);

        }

    }

}

ByteToMessageDecoder傳遞給下一站的是解碼之後的Java POJO對象,不是ByteBuf緩衝區。

(1)ByteBuf緩衝區由誰負責進行引用計數和釋放管理的呢?基類ByteToMessageDecoder負責解碼器的ByteBuf緩衝區的釋放工作,它會調用ReferenceCountUtil.release(in)方法,將之前的ByteBuf緩衝區的引用數減1。

(2)如果這個ByteBuf被釋放了,在後面還需要用到,怎麼辦呢?可以在decode方法中調用一次ReferenceCountUtil .retain(in)來增加一次引用計數。

 ReplayingDecoder     

 ReplayingDecoder類是ByteToMessageDecoder的子類。其作用是:在讀取ByteBuf緩衝區的數據之前,需要檢查緩衝區是否有足夠的字節。若ByteBuf中有足夠的字節,則會正常讀取;反之,如果沒有足夠的字節,則會停止解碼。

public class Byte2IntegerDecoder extends ByteToMessageDecoder {

    @Override

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

        int i = in.readInt();

        Logger.info("解碼出一個整數: " + i);

        out.add(i);

    }}

 

8     ReplayingDecoder基類的關鍵技術就是偷樑換柱,在將外部傳入的ByteBuf緩衝區傳給子類之前,換成了自己裝飾過的ReplayingDecoderBuffer緩衝區。

  • ReplayingDecoderBuffer類型的讀取方法與ByteBuf類型的讀取方法相比,做了什麼樣的功能增強呢?主要是進行二進制數據長度的判斷,如果長度不足,則拋出異常。這個異常會反過來被ReplayingDecoder基類所捕獲,將解碼工作停止。
  • ReplayingDecoder的作用,遠遠不止於進行長度判斷,它更重要的作用是用於分包傳輸的應用場景。
  • ReplayingDecoder類型和所有的子類都需要保存狀態信息,都有狀態,不適合在不同的通道之間共享。

一個案例解析兩個整數,然後求和最爲解碼結果:

public class IntegerAddDecoder extends ReplayingDecoder<IntegerAddDecoder.Status> {

    enum Status {

        PARSE_1, PARSE_2

    }

    private int first;

    private int second;

    public IntegerAddDecoder() {

        //構造函數中,需要初始化父類的state 屬性,表示當前階段

        super(Status.PARSE_1);

    }

    @Override

    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {

        switch (state()) {

            case PARSE_1:

                //從裝飾器ByteBuf 中讀取數據

                first = in.readInt();

                //第一步解析成功,

                // 進入第二步,並且設置讀指針斷點爲當前的讀取位置

                checkpoint(Status.PARSE_2);

                break;

            case PARSE_2:

                second = in.readInt();

                Integer sum = first + second;

                out.add(sum);

                checkpoint(Status.PARSE_1);

                break;

            default:

                break;

        }

    }

}

checkpoint(Status)方法有兩個作用:

(1)設置state屬性的值,更新一下當前的狀態。

(2)還有一個非常大的作用,就是設置“讀斷點指針”。“讀斷點指針”是ReplayingDecoder類的另一個重要的成員,它保存着裝飾器內部ReplayingDecoderBuffer成員的起始讀指針,有點兒類似於mark標記。當讀數據時,一旦可讀數據不夠,ReplayingDecoderBuffer在拋出ReplayError異常之前,ReplayingDecoder會把讀指針的值還原到之前的checkpoint(IntegerAddDecoder.Status)方法設置的“讀斷點指針”(checkpoint)。於是乎,在ReplayingDecoder下一次讀取時,還會從之前設置的斷點位置開始。

如何獲取字符串的長度信息呢?

這個問題和程序所使用的具體傳輸協議是強相關的。一般來說,在Netty中進行字符串的傳輸,可以採用普通的Header-Content內容傳輸協議: (1)在協議的Head部分放置字符串的字節長度。Head部分可以用一個整型int來描述即可。(2)在協議的Content部分,放置的則是字符串的字節數組。

通過ReplayingDecoder解碼器,可以正確地解碼分包後的ByteBuf數據包。但是,在實際的開發中,不太建議繼承這個類,原因是:

(1)不是所有的ByteBuf操作都被ReplayingDecoderBuffer裝飾類所支持,可能有些ByteBuf操作在ReplayingDecoder子類的decode實現方法中被使用時就會拋出ReplayError異常。

(2)在數據解析邏輯複雜的應用場景,ReplayingDecoder在解析速度上相對較差。

MessageToMessageDecoder<I>

將POJO解解碼爲另一個POJO的解碼器基類MessageToMessageDecoder<I>。

NETTY中開箱即用的Decoder:

(1)固定長度數據包解碼器——FixedLengthFrameDecoder 適用場景:每個接收到的數據包的長度,都是固定的,例如100個字節。

(2)行分割數據包解碼器——LineBasedFrameDecoder 適用場景:每個ByteBuf數據包,使用換行符(或者回車換行符)作爲數據包的邊界分割符。

(3)自定義分隔符數據包解碼器——DelimiterBasedFrameDecoder。DelimiterBasedFrameDecoder是LineBasedFrameDecoder按照行分割的通用版本。

(4)自定義長度數據包解碼器——LengthFieldBasedFrameDecoder 這是一種基於靈活長度的解碼器。在ByteBuf數據包中,加了一個長度字段,保存了原始數據包的長度。

Netty編譯器

首先,編碼器是一個Outbound出站處理器,負責處理“出站”數據;其次,編碼器將上一站Outbound出站處理器傳過來的輸入(Input)數據進行編碼或者格式轉換,然後傳遞到下一站ChannelOutboundHandler出站處理器。編碼器是ChannelOutboundHandler出站處理器的實現類。

public class Integer2ByteEncoder extends MessageToByteEncoder<Integer> {

    @Override

    public void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out)

            throws Exception {

        out.writeInt(msg);

        Logger.info("encoder Integer = " + msg);

    }

}

具有相互配套邏輯的編碼器和解碼器能否放在同一個類中呢?

答案是肯定的:這就要用到Netty的新類型——Codec類型。ByteToMessageCodec同時包含了編碼encode和解碼decode兩個抽象方法。都需要自己實現。編碼器和解碼器如果要結合起來,除了繼承的方法之外,還可以通過組合的方式實現。與繼承相比,組合會帶來更大的靈活性:編碼器和解碼器可以捆綁使用,也可以單獨使用。Netty提供了一個新的組合器——CombinedChannelDuplexHandler基類。

public class Byte2IntegerCodec extends ByteToMessageCodec<Integer> {

    @Override

    public void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) throws Exception {

        out.writeInt(msg);

        System.out.println("write Integer = " + msg);

    }

    @Override

    public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {

        if (in.readableBytes() >= 4) {

            int i = in.readInt();

            System.out.println("Decoder i= " + i);

            out.add(i);

        }

    }

}

public class IntegerDuplexHandler extends CombinedChannelDuplexHandler<Byte2IntegerDecoder,

 Integer2ByteEncoder>{

    public IntegerDuplexHandler() {

        super(new Byte2IntegerDecoder(), new Integer2ByteEncoder());

    }

}

對於對性能要求不是太高的服務器程序,可以選擇JSON系列的序列化框架;對於性能要求比較高的服務器程序,則應該選擇傳輸效率更高的二進制序列化框架,目前的建議是Protobuf。Netty也提供了相應的編解碼器,爲Protobuf解決了有關Socket通信中“半包、粘包”等問題。

底層網絡是以二進制字節報文的形式來傳輸數據的。讀數據的過程大致爲:當IO可讀時,Netty會從底層網絡將二進制數據讀到ByteBuf緩衝區中,再交給Netty程序轉成Java POJO對象。寫數據的過程大致爲:這中間編碼器起作用,是將一個Java類型的數據轉換成底層能夠傳輸的二進制ByteBuf緩衝數據。解碼器的作用與之相反,是將底層傳遞過來的二進制ByteBuf緩衝數據轉換成Java能夠處理的Java POJO對象。

在Netty中,分包的方法,主要有兩種方法:

(1)可以自定義解碼器分包器:基於ByteToMessageDecoder或者ReplayingDecoder,定義自己的進程緩衝區分包器。

(2)使用Netty內置的解碼器。如使用Netty內置的LengthFieldBasedFrameDecoder自定義分隔符數據包解碼器,對進程緩衝區ByteBuf進行正確的分包。

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