1. 前言
前面已经分析了Dubbo是如何暴露以及引用远程服务的,接下来自然是服务调用的实现细节了,但是在那之前,有必要先了解Dubbo的网络通信协议。 ?
在单机系统中,调用本地方法获取结果很简单,因为同一个进程里内存是共享的。但是在分布式系统中,远程方法调用就显得较为复杂,必须通过网络将内存中的数据传输给对方,数据以何种格式传输呢?这就需要双方约定一个协议。
2. 通讯协议细节
这是Dubbo官方文档给的图,可以看出协议主要分为两块:16字节定长的Header、变长的Body。
2.1 协议头和协议体
Dubbo通讯协议头Header由16字节组成,Header设计的非常紧凑,能用bit表示就绝不用byte,内容如下:
Bit | 字段 | 说明 |
---|
0~15 | Magic Number | 魔数,固定0xdabb | 16 | Req/Res | 0=Response,1=Request | 17 | 2Way | 仅在Request在有用。是否期望服务器返回数据 | 18 | Event | 是否事件信息,如心跳 | 19~23 | Serialization ID | 序列化类型ID | 24~31 | Status | 响应状态 | 32~95 | Request ID | 请求唯一标识 | 96~127 | Data Length | Body的长度 | 变长部分 | Body | 对象序列化的byte[] |
Body部分是变长的,长度DataLength已经写在Header里了。Dubbo可以根据DataLength解决TCP粘包/拆包的问题,如果读取的字节数不足16,说明连一个完整的Header都没有接收到,此时会等待对端发送更多的数据。读取到一个完整的Header,会解析出DataLength,然后判断Body是否完整,不完整同样会等待对端传输更多的数据,完整则解析Body进行后续的请求处理。
2.2 编解码和序列化
Dubbo对于请求用Request类描述,对于响应用Response描述。 Dubbo针对协议头Header部分,使用Codec编解码,针对Body部分,使用序列化的方式。 ?
当Consumer向Provider发起RPC调用时,会先创建RpcInvocation对象,再创建Request对象。然后利用NettyClient将数据发送到对端。我们知道,网络传输的总是字节序列,所以Request对象会经过编码再发送,对应的方法是ExchangeCodec#encodeRequest() ,Dubbo会根据协议的格式向Channel先写入Header,再写入Body。
protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
Serialization serialization = getSerialization(channel);
byte[] header = new byte[HEADER_LENGTH];
Bytes.short2bytes(MAGIC, header);
header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());
if (req.isTwoWay()) {
header[2] |= FLAG_TWOWAY;
}
if (req.isEvent()) {
header[2] |= FLAG_EVENT;
}
Bytes.long2bytes(req.getId(), header, 4);
int savedWriteIndex = buffer.writerIndex();
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH);
ChannelBufferOutputStream bos = new ChannelBufferOutputStream(buffer);
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();
int len = bos.writtenBytes();
checkPayload(channel, len);
Bytes.int2bytes(len, header, 12);
buffer.writerIndex(savedWriteIndex);
buffer.writeBytes(header);
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
}
Provider响应的Response,也是需要编码再发送的,处理流程和Request差不多,这里就不贴代码了。 ?
对于Body部分,RpcInvocation该如何编码呢?我们以DubboCodec为例,事实上Dubbo并不会将整个RpcInvocation对象序列化输出,而是按照固定的格式,将重要信息写入到ObjectOutput。 格式如下:
- 协议Version
- ServiceName
- ServiceVersion
- MethodName
- ParameterTypesDesc
- Arguments序列化
- Attachments序列化
Provider按照约定的格式,依次将这些数据再读取出来即可。
protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
RpcInvocation inv = (RpcInvocation) data;
out.writeUTF(version);
String serviceName = inv.getAttachment(INTERFACE_KEY);
if (serviceName == null) {
serviceName = inv.getAttachment(PATH_KEY);
}
out.writeUTF(serviceName);
out.writeUTF(inv.getAttachment(VERSION_KEY));
out.writeUTF(inv.getMethodName());
out.writeUTF(inv.getParameterTypesDesc());
Object[] args = inv.getArguments();
if (args != null) {
for (int i = 0; i < args.length; i++) {
out.writeObject(encodeInvocationArgument(channel, inv, i));
}
}
out.writeAttachments(inv.getObjectAttachments());
}
3. 总结
Dubbo通讯协议分为Header和Body两部分,Header固定由16个字节组成,设计的非常紧凑,尽可能的用bit表示,包含:魔数、请求响应标识、2Way标识、Event标识、序列化ID、Status、RequestID、DataLength。Body则是变长的,长度记录在Header最后4字节里,Dubbo可以基于此来解决TCP的粘包/拆包问题。 Header部分采用Codec编解码,Body部分使用序列化,具体取决于使用的的序列化实现方案,默认是Hessian2。
|