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个字节被丢弃了,所以后面的解码逻辑完全正确!