网络协议
通过前面文章的介绍,我们已经知道Dubbo服务调用的流程,Dubbo框架最终通过借助Netty网络框架实现数据的发送和接收。每次消费者服务发起远程接口调用,Dubbo框架都需要将包括服务调用接口、方法名、方法参数类型和方法参数值等信息传递给提供者服务,并且在传递方法参数值时需要先序列化对象成字节数组才能进行网络传输,提供者服务接收到数据之后需要经过反序列化才能读取到这些信息,然后拼装成请求对象通过代理对象进行反射调用,最后将调用的结果返回给消费者服务。
我们知道网络中传输的数据都是字节数组,要想消费者和提供者能够正确的解析这些字节数组,转换成Java对象,就需要一个约定的编解码规则,这种规则称为网络协议或者数据包结构。Dubbo自定义了一个网络协议,消费者和提供者双方按照该网络协议进行数据传递和接收,通过网络协议不仅可以正确收发信息,还可以解决网络传输中出现的粘包、拆包问题。
假设现在消费者服务向提供者服务连续发送了两个请求(数据包),用packet1和packet2来表示,那么提供者服务收到的请求可能有三种情况:
第一种情况,提供者服务正常收到两个数据包,即没有发生拆包和粘包的现象。
第二种情况,提供者服务只收到一个数据包,这一个数据包中包含了消费者服务发送的两个数据包的信息,这种现象即为粘包。
第三种情况,这种情况有两种表现形式,提供者服务收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。
有了网络协议,Dubbo就可以按照协议解析成一个个完整的数据包,出现粘包拆包的时候,顺序处理每一个包,后面的半包等待接收到完整的数据包之后再处理。
下面我们就来分析一下Dubbo的网络协议(数据包结构):
Dubbo的数据包分为消息头和消息体,消息头总共占16个字节,下表为消息头各个位(bit)的具体含义:
偏移量(Bit) | 字段 | 作用 |
---|---|---|
0 ~ 7 | 魔数高位 | 0xda00 |
8 ~ 15 | 魔数低位 | 0xbb |
16 | 数据包类型 | 0 - Response, 1 - Request |
17 | 调用方式 | 仅在第16位被设为1的情况下有效,0 - 单向调用,1 - 双向调用 |
18 | 事件标识 | 0 - 当前数据包是请求或响应包,1 - 当前数据包是心跳包 |
19 ~ 23 | 序列化器编号 | 2 - Hessian2Serialization 3 - JavaSerialization 4 - CompactedJavaSerialization 6 - FastJsonSerialization 7 - NativeJavaSerialization 8 - KryoSerialization 9 - FstSerialization |
24 ~ 31 | 状态 | 20 - OK 30 - CLIENT_TIMEOUT 31 - SERVER_TIMEOUT 40 - BAD_REQUEST 50 - BAD_RESPONSE … |
32 ~ 95 | 请求编号 | 共8字节,运行时生成 |
96 ~ 127 | 消息体长度 | 运行时计算 |
我们可以看到Dubbo使用魔法数0xdabb(固定值)作为数据包的开头,魔法数占两个字节;数据包类型、调用方式、事件标识和序列化器编号组合占用一个字节;状态值Status占用一个字节;请求编号就是前面文章提到的请求ID,用于标识该请求对于服务全局唯一,请求ID是一个Long类型,占8个字节;消息头的最后四个字节用来存储消息体长度Data Length,Dubbo将用一个Integer类型变量来表示Data Length。
消息体的内容不是定长的,但是内容顺序是确定的,消息体内容按顺序依次为:Dubbo版本号、服务接口名、服务接口版本、方法名、参数类型、方法参数值和请求额外参数(attachment)。
编码器-编码请求对象
了解了Dubbo协议内容之后,我们来分析下Dubbo是如何根据协议进行编解码的。我们先从消费者服务说起,消费者服务通过NettyClient的doOpen()
方法设置编解码器:
@Override
protected void doOpen() throws Throwable {
NettyHelper.setNettyLoggerFactory();
bootstrap = new ClientBootstrap(channelFactory);
bootstrap.setOption("keepAlive", true);
bootstrap.setOption("tcpNoDelay", true);
bootstrap.setOption("connectTimeoutMillis", getTimeout());
final NettyHandler nettyHandler = new NettyHandler(getUrl(), this);
bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
@Override
public ChannelPipeline getPipeline() {
NettyCodecAdapter adapter = new NettyCodecAdapter(getCodec(), getUrl(), NettyClient.this);
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("decoder", adapter.getDecoder());//编码器
pipeline.addLast("encoder", adapter.getEncoder());//解码器
pipeline.addLast("handler", nettyHandler);
return pipeline;
}
});
}
//实现了ChannelHandler接口
private final ChannelHandler encoder = new InternalEncoder();
//实现了ChannelHandler接口
private final ChannelHandler decoder = new InternalDecoder();
先分析编码器encoder,进入NettyCodecAdapter内部类InternalEncoder的encode()
方法:
@Sharable
private class InternalEncoder extends OneToOneEncoder {
@Override
protected Object encode(ChannelHandlerContext ctx, Channel ch, Object msg) throws Exception {
com.alibaba.dubbo.remoting.buffer.ChannelBuffer buffer =
com.alibaba.dubbo.remoting.buffer.ChannelBuffers.dynamicBuffer(1024); //@1
NettyChannel channel = NettyChannel.getOrAddChannel(ch, url, handler);//@2
try {
codec.encode(channel, buffer, msg);//@3
} finally {
NettyChannel.removeChannelIfDisconnected(ch);
}
return ChannelBuffers.wrappedBuffer(buffer.toByteBuffer());
}
}
代码@1:构建数据缓冲区ChannelBuffer实例buffer。
代码@2:获取NettyChannel实例,该实例缓存在静态集合channelMap中。
代码@3:codec是扩展接口Codec2类型变量,实例类型是DubboCountCodec。
@SPI
public interface Codec2 {
@Adaptive({Constants.CODEC_KEY})
void encode(Channel channel, ChannelBuffer buffer,
Object message) throws IOException;
@Adaptive({Constants.CODEC_KEY})
Object decode(Channel channel, ChannelBuffer buffer) throws IOException;
enum DecodeResult {
NEED_MORE_INPUT, SKIP_SOME_INPUT
}
}
public final class DubboCountCodec implements Codec2 {
private DubboCodec codec = new DubboCodec();
@Override
public void encode(Channel channel, ChannelBuffer buffer,
Object msg) throws IOException {
codec.encode(channel, buffer, msg);
}
@Override
public Object decode(Channel channel,
ChannelBuffer buffer) throws IOException {
//......省略部分代码
return result;
}
}
public class DubboCodec extends ExchangeCodec implements Codec2
DubboCodec继承了ExchangeCodec并实现了Codec2扩展接口,进入ExchangeCodec的encode()
方法:
@Override
public void encode(Channel channel, ChannelBuffer buffer, Object msg) throws IOException {
if (msg instanceof Request) {
encodeRequest(channel, buffer, (Request) msg);
} else if (msg instanceof Response) {
encodeResponse(channel, buffer, (Response) msg);
} else {
super.encode(channel, buffer, msg);
}
}
该方法内容很简单,根据msg的类型执行不同的处理方法,进入encodeRequest(channel, buffer, (Request) msg)
:
protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
Serialization serialization = getSerialization(channel); //@1
// header.
byte[] header = new byte[HEADER_LENGTH]; //@2
// set magic number.
Bytes.short2bytes(MAGIC, header); //@3
// set request and serialization flag.
header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId()); //@4
if (req.isTwoWay()) header[2] |= FLAG_TWOWAY;
if (req.isEvent()) header[2] |= FLAG_EVENT;
// set request id.
Bytes.long2bytes(req.getId(), header, 4); //@5
// encode request data.
int savedWriteIndex = buffer.writerIndex();//@6
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
ObjectOutput out = serialization.serialize(channel.getUrl(), bos);//@7
if (req.isEvent()) {
encodeEventData(channel, out, req.getData());
} else {
encodeRequestData(channel, out, req.getData());
}
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
bos.flush();
bos.close();
int len = bos.writtenBytes();//@8
checkPayload(channel, len);
Bytes.int2bytes(len, header, 12);//@9
// write
buffer.writerIndex(savedWriteIndex);//@10
buffer.writeBytes(header); // write header.
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
}
代码@1:根据channel获取序列化器,默认序列化器是Hessian2Serialization。
public static Serialization getSerialization(URL url) {
return ExtensionLoader.getExtensionLoader(Serialization.class).
getExtension(url.getParameter(Constants.SERIALIZATION_KEY,
Constants.DEFAULT_REMOTING_SERIALIZATION));
}
代码@2:创建字节数组header,HEADER_LENGTH
的值为16,用于存储数据包的消息头数据。
代码@3:往header数组填充魔法数MAGIC
,MAGIC
值为0xdabb。
代码@4:数据包第三个字节存储了数据包类型、调用方式、事件标识和序列化器编号。
代码@5:从Request对象中取出Long类型id值,填充到header数组的5-12位。
代码@6:获取buffer当前的写位置,赋值给savedWriteIndex
,接着更新buffer的writerIndex
,跳过了当前写位置HEADER_LENGTH
个字节空间。目的是先往缓冲区写数据包的消息体。
代码@7:构建ObjectOutput
实例,如果request是事件类型(没有返回值),则执行encodeEventData()
方法,否则执行encodeRequestData()
方法,这部分内容后面会详细介绍。
代码@8:通过第@7步,往缓冲区填充消息体信息,len
值为写入的消息体的字节长度,checkPayload()
方法用来检测消息体长度是否超过限制(默认最大8M)。
代码@9:获取到消息体的长度len
之后,填充给header数组的13-16位。
代码@10:缓冲区writerIndex
(写指针)移动到savedWriteIndex
位置,也就是一开始的位置,目的是为了写消息头header数组。写完header之后将writerIndex
设置为savedWriteIndex + HEADER_LENGTH + len
,此时writerIndex
的位置为开始位置再往后移动一个完整数据包字节长度的位置,这样一个完整的请求数据包就已经写到了缓冲区buffer
中了。
下面我们来分析一下消息体是如何被写入缓冲区的,进入encodeRequestData()
方法:
@Override
protected void encodeRequestData(Channel channel, ObjectOutput out, Object data) throws IOException {
RpcInvocation inv = (RpcInvocation) data;
//@1
out.writeUTF(inv.getAttachment(Constants.DUBBO_VERSION_KEY, DUBBO_VERSION));
out.writeUTF(inv.getAttachment(Constants.PATH_KEY));
out.writeUTF(inv.getAttachment(Constants.VERSION_KEY));
out.writeUTF(inv.getMethodName());//@2
out.writeUTF(ReflectUtils.getDesc(inv.getParameterTypes()));//@3
Object[] args = inv.getArguments();
if (args != null)
for (int i = 0; i < args.length; i++) {
out.writeObject(encodeInvocationArgument(channel, inv, i));//@4
}
out.writeObject(inv.getAttachments());//@5
}
该方法按照Dubbo网络协议对消息体内容的顺序规定依次写入相关内容:
代码@1:序列化写入Dubbo版本号、服务接口名称和服务接口版本号。
代码@2:序列化写入方法名称。
代码@3:序列化写入方法参数类型。
代码@4:序列化写入参数值。
代码@5:序列化写入额外attachments。
编码器-编码响应对象
下面我们来分析一下提供者服务是如何编码响应结果的,进入ExchangeCodec的encodeResponse()
方法:
protected void encodeResponse(Channel channel, ChannelBuffer buffer, Response res) throws IOException {
int savedWriteIndex = buffer.writerIndex();//@1
try {
Serialization serialization = getSerialization(channel);//@2
// header.
byte[] header = new byte[HEADER_LENGTH];//@3
// set magic number.
Bytes.short2bytes(MAGIC, header);//@4
//@5
// set request and serialization flag.
header[2] = serialization.getContentTypeId();
if (res.isHeartbeat()) header[2] |= FLAG_EVENT;
// set response status.
byte status = res.getStatus();
header[3] = status;
// set request id.
Bytes.long2bytes(res.getId(), header, 4);//@6
//@7
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
ObjectOutput out = serialization.serialize(channel.getUrl(), bos);
// encode response data or error message.
if (status == Response.OK) {
if (res.isHeartbeat()) {
encodeHeartbeatData(channel, out, res.getResult());
} else {
encodeResponseData(channel, out, res.getResult());
}
} else out.writeUTF(res.getErrorMessage());
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
bos.flush();
bos.close();
//@8
int len = bos.writtenBytes();
checkPayload(channel, len);
Bytes.int2bytes(len, header, 12);
//@9
// write
buffer.writerIndex(savedWriteIndex);
buffer.writeBytes(header); // write header.
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
} catch (Throwable t) {
// clear buffer
buffer.writerIndex(savedWriteIndex);//@10
// send error message to Consumer, otherwise, Consumer will wait till timeout.
if (!res.isEvent() && res.getStatus() != Response.BAD_RESPONSE) {
Response r = new Response(res.getId(), res.getVersion());
r.setStatus(Response.BAD_RESPONSE);//@11
if (t instanceof ExceedPayloadLimitException) {//@12
logger.warn(t.getMessage(), t);
try {
r.setErrorMessage(t.getMessage());
channel.send(r);
return;
} catch (RemotingException e) {
logger.warn("Failed to send bad_response info back: " +
t.getMessage() + ", cause: " + e.getMessage(), e);
}
} else {
// FIXME log error message in Codec and handle in caught() of IoHanndler?
logger.warn("Fail to encode response: " + res +
", send bad_response info instead, cause: " + t.getMessage(), t);
try {
r.setErrorMessage("Failed to send response: " + res + ", cause: "
+ StringUtils.toString(t));
channel.send(r);
return;
} catch (RemotingException e) {
logger.warn("Failed to send bad_response info back: " +
res + ", cause: " + e.getMessage(), e);
}
}
}
// Rethrow exception
if (t instanceof IOException) {
throw (IOException) t;
} else if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else if (t instanceof Error) {
throw (Error) t;
} else {
throw new RuntimeException(t.getMessage(), t);
}
}
}
代码@1:获取当前缓冲区写位置,赋值给savedWriteIndex
。
代码@2:获取序列化器。
代码@3:创建长度为16的消息头数组header
。
代码@4:填充魔法值0xdabb
。
代码@5:这里只需要往header
数组填充序列化编号和状态值。
代码@6:将Long类型请求Id填充到header
数组的5-12位。
代码@7:这步骤大家应该已经熟悉了,缓冲区同样先跳过16个字节先写消息体。
代码@8:获取消息体长度,检测消息体长度限制,将len
值填充到header
数组的13-16位。
代码@9:这部分内容还是和encodeRequest()
一样的,调整writerIndex
位置,写入消息头内容。
代码@10:如果编码过程出现异常,那么缓冲区的writerIndex
恢复到开始位置。
代码@11:返回状态值为Response.BAD_RESPONSE
的响应结果。
代码@12:如果是ExceedPayloadLimitException
异常,即消息体长度超过限制,那么将该异常返回给消费者服务。
下面我们分析下@7步骤的encodeResponseData()
方法:
@Override
protected void encodeResponseData(Channel channel, ObjectOutput out, Object data) throws IOException {
Result result = (Result) data;
Throwable th = result.getException(); //@1
if (th == null) {
Object ret = result.getValue();
if (ret == null) {
out.writeByte(RESPONSE_NULL_VALUE);//@2
} else {
out.writeByte(RESPONSE_VALUE);//@3
out.writeObject(ret);
}
} else {
out.writeByte(RESPONSE_WITH_EXCEPTION);//@4
out.writeObject(th);
}
}
代码@1:从结果对象result
中获取异常信息,赋值给th
,如果th
不为空,那么执行步骤@4。
代码@2:如果响应对象ret
为null
,那么只往缓冲区写一个字节RESPONSE_NULL_VALUE
,代表响应结果为空。
代码@3:当响应结果不为空时,先往缓冲区写一个字节RESPONSE_VALUE
,代表包含响应结果,然后再往缓冲区写入结果对象ret
。
代码@4:响应结果发生异常时,先往缓冲区写一个字节RESPONSE_WITH_EXCEPTION
,代表响应结果异常,然后再往缓冲区写入异常对象。