Netty在RocketMQ中的應用----編解碼

RocketMQ中角色有Producer、Comsumer、Broker和NameServer,它們之間的通訊是通過Netty實現的。在之前的文章RocketMQ是如何通訊的?中,對RocketMQt通訊進行了一些介紹,但是底層Netty的細節涉及的比較少,這一篇將作爲其中的一個補充。

編碼

在RocketMQ中,消息的編解碼都在NettyEncoder和NettyDecoder中處理了,如下所示:
在這裏插入圖片描述
編碼的操作很簡單,返回報文中,body本身已經在業務處理過程中轉成了byte數組(例如json.getBytes()),不需要做額外處理。因此僅僅需要對報文頭進行編碼即可。

public void encode(ChannelHandlerContext ctx, RemotingCommand remotingCommand, ByteBuf out)
        throws Exception {
        try {
            ByteBuffer header = remotingCommand.encodeHeader(); // 編碼報文頭
            out.writeBytes(header); // 寫報文頭
            byte[] body = remotingCommand.getBody();
            if (body != null) {
                out.writeBytes(body); // 寫body
            }
        } catch (Exception e) {
            log.error("encode exception, " + RemotingHelper.parseChannelRemoteAddr(ctx.channel()), e);
            if (remotingCommand != null) {
                log.error(remotingCommand.toString());
            }
            RemotingUtil.closeChannel(ctx.channel());
        }
    }

接下來我們就看看報文頭是什麼。CustomerHeader是個接口,如下所示:

public interface CommandCustomHeader {
    void checkFields() throws RemotingCommandException;
}

接口中只有一個checkFields的方法,用於檢查報文頭字段。不同的請求,有不同的CustomerHeader。例如,發送消息的CustomerHeader是:

public class SendMessageRequestHeader implements CommandCustomHeader {
    @CFNotNull
    private String producerGroup;
    @CFNotNull
    private String topic;
    @CFNotNull
    private String defaultTopic;
    @CFNotNull
    private Integer defaultTopicQueueNums;
    @CFNotNull
    private Integer queueId;
    @CFNotNull
    private Integer sysFlag;
    @CFNotNull
    private Long bornTimestamp;
    @CFNotNull
    private Integer flag;
    @CFNullable
    private String properties;
    @CFNullable
    private Integer reconsumeTimes;
    @CFNullable
    private boolean unitMode = false;
    @CFNullable
    private boolean batch = false;
    private Integer maxReconsumeTimes;

    @Override
    public void checkFields() throws RemotingCommandException {
    }
 }

對報文頭編碼就包含了對CommandCustomerHeader的編碼,但最終攜帶的信息不僅僅是它。下面我們就看看 remotingCommand.encodeHeader()做了些什麼?

public ByteBuffer encodeHeader(final int bodyLength) {
        // 1> header length size
        int length = 4;// header數據長度域

        // 2> header data length
        byte[] headerData;
        headerData = this.headerEncode(); // 對customerHeader編碼

        length += headerData.length; // 報文頭長度域+header數據的長度

        // 3> body data length
        length += bodyLength;// 報文體如果有則加上長度

       // 分配的長度目前包括:總長度域(4) + 報文頭長度域(4) + 報文頭內容
        ByteBuffer result = ByteBuffer.allocate(4 + length - bodyLength);

        // length
        result.putInt(length); // 保存總長度

        // header length
        result.put(markProtocolType(headerData.length, serializeTypeCurrentRPC)); // 保存數據頭長度,這裏進行了進制轉換

        // header data
        result.put(headerData);// 保存報文頭數據

        result.flip();

        return result;
    }

在這個方法鍾,對CommandCustomerHead編碼重點有2個:
1-header編碼
2-數據長度計算
下面分別展開:

1-header編碼

在RemotingCommand中,cumstomerHead是被標註爲transient的,也就是不會被序列化。
在這裏插入圖片描述
實際上customerHeader的信息真正存放的地方是這裏:
在這裏插入圖片描述
對!你沒有猜錯,customerHeader最終會被轉換成map存放在extFields裏面。然後將整個RemotingCommand進行JSON序列化(根據序列化配置來,一般是JSON)。這個纔是真正的headerData部分。

2-數據長度計算

從encodeHeader方法中,我們可以看到header的數據包括了總長度域,處理了的數據頭長度域和headerData三部分。它們組成如下:

總長度域(4)+ 數據頭長度域(4)+ headerData。

其中數據頭長度域做了一點點處理,把序列化類型也保存進來了。

public static byte[] markProtocolType(int source, SerializeType type) {
        byte[] result = new byte[4];

        result[0] = type.getCode(); // 序列化類型,JSON或者RocketMQ
        result[1] = (byte) ((source >> 16) & 0xFF); // 2的16次方
        result[2] = (byte) ((source >> 8) & 0xFF); // 2的8次方
        result[3] = (byte) (source & 0xFF); // 256以下
        return result;
    }

實際上上面所做就是把長度域進制換一下,好騰出一個字節存序列化類型。
這樣轉換後,真正的長度就是len = result[1] * 2的16次方 + result[2] * 2的8次方 + rsult[3]。

解碼

編碼說完了,我們再來說說解碼。解碼的代碼如下所示:

public static RemotingCommand decode(final ByteBuffer byteBuffer) {
        int length = byteBuffer.limit();
        int oriHeaderLen = byteBuffer.getInt();
        int headerLength = getHeaderLength(oriHeaderLen);

        byte[] headerData = new byte[headerLength];
        byteBuffer.get(headerData);

        RemotingCommand cmd = headerDecode(headerData, getProtocolType(oriHeaderLen));

        int bodyLength = length - 4 - headerLength;
        byte[] bodyData = null;
        if (bodyLength > 0) {
            bodyData = new byte[bodyLength];
            byteBuffer.get(bodyData);
        }
        cmd.body = bodyData;

        return cmd;
    }

解碼的代碼,說實話剛開始我沒有看懂,根據編碼,總長度應該就在前面四個字節裏面。但是!但是!它通過byteBuffer.limit()拿到了總長度!然後byteBuffer.getInt(),這個應該也是總長度,但是它卻直接當作了我們的header長度域的數據,然後獲取數據頭長度和編碼類型了。
總長度-4(數據長度域)-數據頭長度拿到了報文體長度,然後直接獲取bodyData。
然後我翻了下Remoting裏的測試代碼:

    @Test
    public void testEncodeAndDecode_FilledBody() {
        System.setProperty(RemotingCommand.REMOTING_VERSION_KEY, "2333");

        int code = 103; //org.apache.rocketmq.common.protocol.RequestCode.REGISTER_BROKER
        CommandCustomHeader header = new SampleCommandCustomHeader();
        RemotingCommand cmd = RemotingCommand.createRequestCommand(code, header);
        cmd.setBody(new byte[] { 0, 1, 2, 3, 4});

        ByteBuffer buffer = cmd.encode();

        //Simulate buffer being read in NettyDecoder
        buffer.getInt();
        byte[] bytes = new byte[buffer.limit() - 4];
        buffer.get(bytes, 0, buffer.limit() - 4);
        buffer = ByteBuffer.wrap(bytes);

        RemotingCommand decodedCommand = RemotingCommand.decode(buffer);

        assertThat(decodedCommand.getSerializeTypeCurrentRPC()).isEqualTo(SerializeType.JSON);
        assertThat(decodedCommand.getBody()).isEqualTo(new byte[]{ 0, 1, 2, 3, 4});
    }

測試代碼的中的cmd.encode()並不是Encoder的原方法,但是邏輯是一樣的。重點是,模擬解碼的時候,通過buffer.getInt()把前面四個字節給忽略了!這剛好就是編碼中的總長度域的數據。這樣,再調用 RemotingCommand.decode(buffer)的時候,就符合解碼邏輯了!

這說明,在解碼器的哪裏,把前面的4個字節已經忽略了!是的,你沒有猜錯,在NettyDecoder的初始化方法裏面,已經申明瞭丟棄前面4個字節!

public NettyDecoder() {
        super(FRAME_MAX_LENGTH, 0, 4, 0, 4);
    }

我們的NettyDecoder繼承的LengthFieldBasedFrameDecoder,這是一種基於靈活長度的解碼器。在數據包中,加了一個長度字段(長度域),保存上層包的長度。解碼的時候,會按照這個長度,進行上層ByteBuf應用包的提取。
自定義長度解碼器LengthFieldBasedFrameDecoder構造器,涉及5個參數,都與長度域(數據包中的長度字段)相關,具體介紹如下:

(1) maxFrameLength - 發送的數據包最大長度;

(2) lengthFieldOffset - 長度域偏移量,指的是長度域位於整個數據包字節數組中的下標;

(3) lengthFieldLength - 長度域的自己的字節數長度。

(4) lengthAdjustment – 長度域的偏移量矯正。 如果長度域的值,除了包含有效數據域的長度外,還包含了其他域(如長度域自身)長度,那麼,就需要進行矯正。矯正的值爲:包長 - 長度域的值 – 長度域偏移 – 長度域長。

(5) initialBytesToStrip – 丟棄的起始字節數。丟棄處於有效數據前面的字節數量。比如前面有4個節點的長度域,則它的值爲4。
我們的NettyDecoder的構造函數中,initialBytesToStrip=4,因此頭部的前4個字節被丟棄了,所以後面的解碼邏輯完全正確!

發佈了379 篇原創文章 · 獲贊 87 · 訪問量 59萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章