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個字節被丟棄了,所以後面的解碼邏輯完全正確!