本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。
前言
在服务消费者与服务提供者进行信息交换的过程中,需要使用到一些通信协议,以便于通信双方能够识别对方的信息。Dubbo框架支持多种协议,包括dubbo、rmi、hessian、http等,每种协议都有着各自适用的场景。
dubbo协议是Dubbo框架缺省和推荐的协议,采用单一长连接和 NIO 异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。
本文主要介绍dubbo协议报文格式和编解码细节。
报文格式
在网络七层模型中,TCP协议是传输层协议。dubbo协议是基于TCP协议建立的一种应用层协议,它参考了TCP协议,协议内容由header和body两部分组成。
其中,协议header格式如下:
| Bit | 类型 | 描述 |
|---|---|---|
| 0 - 15 | magic | 魔数,用于标识这是一个dubbo协议报文,固定为0xdabb。报文解码时会对魔数进行正确性校验。 |
| 16 - 23 | request flag | serializationId | 请求类型和序列化类型id的组合结果,高4位表示请求类型、低4位表示序列化方式。 |
| 24 - 31 | response status | 响应状态结果码,只有响应报文中才会设置该内容。 |
| 32 - 95 | request id | 请求id,作为一次请求的唯一标识,由于tcp传输内容有限,一次请求内容可能会分多次传输。 |
| 96 - 127 | body length | 请求body部分的内容长度,可以用来判断传输的内容是否完善。 |
编解码细节
在服务消费者与服务提供者进行信息交换的过程中,Dubbo需要对传输的数据进行编码和解码。Codec2接口声明了encode、decode两个方法,对应编码和解码规范;AbstractCodec中定义了一些公共方法,如获取序列化方式的方法;TransportCodec对应传输层的编解码;ExchangeCodec对应交换层的编解码;DubboCodec为应用层的编解码。
编码阶段
org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#encode
@Override
public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
// 1.对请求信息编码
if (msg instanceof Request) {
encodeRequest(channel, buffer, (Request) msg);
// 2.对响应信息编码
} else if (msg instanceof Response) {
encodeResponse(channel, buffer, (Response) msg);
// 3.对其它信息编码
} else {
super.encode(channel, buffer, msg);
}
}
org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#encodeRequest
protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
// 1.获取序列化扩展实现
Serialization serialization = getSerialization(channel, req);
// 2.构建dubbo协议头数组,长度16字节
byte[] header = new byte[HEADER_LENGTH];
// 3.设置魔数
Bytes.short2bytes(MAGIC, header);
// 4.设置请求类型和序列化标识
header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());
if (req.isTwoWay()) {
header[2] |= FLAG_TWOWAY;
}
if (req.isEvent()) {
header[2] |= FLAG_EVENT;
}
// 5.设置请求id
Bytes.long2bytes(req.getId(), header, 4);
// encode request data.
int savedWriteIndex = buffer.writerIndex();
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
if (req.isHeartbeat()) {
// heartbeat request data is always null
bos.write(CodecSupport.getNullBytesOf(serialization));
} else {
// 6.将请求数据序列化,写入缓存
ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
if (req.isEvent()) {
encodeEventData(channel, out, req.getData());
} else {
encodeRequestData(channel, out, req.getData(), req.getVersion());
}
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
}
bos.flush();
bos.close();
// 7.检查payload部分数据是否合法
int len = bos.writtenBytes();
checkPayload(channel, len);
Bytes.int2bytes(len, header, 12);
// 8.将协议头写入缓存
buffer.writerIndex(savedWriteIndex);
buffer.writeBytes(header); // write header.
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
}
对响应信息的编码和encodeRequest类似,区别主要是响应信息的编码,还需要设置响应状态结果码信息。
解码阶段
org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#decode(org.apache.dubbo.remoting.Channel, org.apache.dubbo.remoting.buffer.ChannelBuffer, int, byte[])
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
// 1.检查魔数信息,判断是不是dubbo协议
if (readable > 0 && header[0] != MAGIC_HIGH
|| readable > 1 && header[1] != MAGIC_LOW) {
int length = header.length;
if (header.length < readable) {
header = Bytes.copyOf(header, readable);
buffer.readBytes(header, length, readable - length);
}
for (int i = 1; i < header.length - 1; i++) {
if (header[i] == MAGIC_HIGH && header[i + 1] == MAGIC_LOW) {
buffer.readerIndex(buffer.readerIndex() - header.length + i);
header = Bytes.copyOf(header, i);
break;
}
}
return super.decode(channel, buffer, readable, header);
}
// 2.判断是否读取了一个完整的协议头
if (readable < HEADER_LENGTH) {
return DecodeResult.NEED_MORE_INPUT;
}
// 3.获取协议体数据长度
int len = Bytes.bytes2int(header, 12);
Object obj = finishRespWhenOverPayload(channel, len, header);
if (null != obj) {
return obj;
}
checkPayload(channel, len);
// 4.判断是否遇到了半包问题
int tt = len + HEADER_LENGTH;
if (readable < tt) {
return DecodeResult.NEED_MORE_INPUT;
}
// limit input stream.
ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);
try {
// 5.解析协议体数据部分
return decodeBody(channel, is, header);
} finally {
if (is.available() > 0) {
try {
if (logger.isWarnEnabled()) {
logger.warn("Skip input stream " + is.available());
}
StreamUtils.skipUnusedStream(is);
} catch (IOException e) {
logger.warn(e.getMessage(), e);
}
}
}
}
org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#decodeBody
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
// 1.获取请求id
long id = Bytes.bytes2long(header, 4);
if ((flag & FLAG_REQUEST) == 0) {
// 2.对响应信息进行解码
Response res = new Response(id);
if ((flag & FLAG_EVENT) != 0) {
res.setEvent(true);
}
// 3.获取响应结果码
byte status = header[3];
res.setStatus(status);
try {
if (status == Response.OK) {
Object data;
if (res.isEvent()) {
byte[] eventPayload = CodecSupport.getPayload(is);
if (CodecSupport.isHeartBeat(eventPayload, proto)) {
// heart beat response data is always null;
data = null;
} else {
data = decodeEventData(channel, CodecSupport.deserialize(channel.getUrl(), new ByteArrayInputStream(eventPayload), proto), eventPayload);
}
} else {
// 4.对响应协议体数据进行解码
data = decodeResponseData(channel, CodecSupport.deserialize(channel.getUrl(), is, proto), getRequestData(id));
}
res.setResult(data);
} else {
res.setErrorMessage(CodecSupport.deserialize(channel.getUrl(), is, proto).readUTF());
}
} catch (Throwable t) {
res.setStatus(Response.CLIENT_ERROR);
res.setErrorMessage(StringUtils.toString(t));
}
return res;
} else {
// 5.对请求信息进行解码
Request req = new Request(id);
req.setVersion(Version.getProtocolVersion());
req.setTwoWay((flag & FLAG_TWOWAY) != 0);
if ((flag & FLAG_EVENT) != 0) {
req.setEvent(true);
}
try {
Object data;
if (req.isEvent()) {
byte[] eventPayload = CodecSupport.getPayload(is);
if (CodecSupport.isHeartBeat(eventPayload, proto)) {
// heart beat response data is always null;
data = null;
} else {
data = decodeEventData(channel, CodecSupport.deserialize(channel.getUrl(), new ByteArrayInputStream(eventPayload), proto), eventPayload);
}
} else {
// 6.对请求协议体数据进行解码
data = decodeRequestData(channel, CodecSupport.deserialize(channel.getUrl(), is, proto));
}
req.setData(data);
} catch (Throwable t) {
// bad request
req.setBroken(true);
req.setData(t);
}
return req;
}
}
由于TCP传输层,不知道应用层协议业务包的概念,所以会出现半包及粘包问题。dubbo协议通过自定义协议header+body的形式,其中协议header的body length信息判断一次请求是否能够读取到一个完整的业务包,以此来解决半包及粘包问题。