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 篇原创文章 · 获赞 85 · 访问量 59万+

猜你喜欢

转载自blog.csdn.net/GAMEloft9/article/details/102936809