深入理解Dubbo原理系列(四)- Dubbo核心调用和通信
- 一. Dubbo调用
- 二. Dubbo协议详解
- 2.1 Dubbo协议架构
- 2.2 编解码器
- 2.2.1 Dubbo请求Request编解码器
- Request编码器
- Request解码器
- 2.2.2 Dubbo响应Response编解码器
- Response编码器
- Response解码器
- 三. Telnet调用原理
- 四. Dubbo核心Handler和线程模型
- 4.1 核心Handler介绍
- 4.2 Dubbo请求响应Handler
- 总结
一. Dubbo调用
Dubbo调用流程图如下:
按照图中的标号顺序:
- 将多个服务提供者做一个聚合,
在Dubbo框架内部中,
Directory
接口的其中一个实现RegistryDirectory
类,它和接口名是一对一的关系(每一个接口都有一个RegistryDirectory
实例),主要负责拉取 和订阅服务提供者、动态配置和路由项。
- 客户端服务调用首先触发路由操作。
- 然后将路由结果得到的服务列表作为负载均衡的参数。
- 经过负载均衡后会选出一台机器进行RPC调用。
- 客户端经过路由和负载均衡后,则将请求交给底层的IO线程池来处理,这里包含两种:Netty、Dubbo业务线程池。
- 在编解码层读取流中的字符串,最终交给Telnet对应的
Handler
去解析方法调用。 - 进行端口复用,如果是Telnet调用(序列化反序列化使用的是fastjson),则先找到对应的
Invoker
进行方法调用。
而本篇文章主要将中心放到Dubbo协议、编解码实现和线程模型上。
二. Dubbo协议详解
2.1 Dubbo协议架构
首先,一次RPC调用,一般会分为:
- 协议头(16字节):主要携带了魔法数 (Oxdabb),以及当前请求报文类型(Request、Response)、心跳和事件等信息。
- 协议体
如下图所示(中间的内容表示对应比特位范围内存储的数据):
根据偏移比特位来进行划分:
偏移比特位 | 字段描述 | 具体标识 |
0-7 | 魔数高位 | 存储的是魔法数高位( |
8-15 | 魔数低位 | 存储的是魔法数高位( |
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 | 状态 | 看状态响应码表 |
32-95 | 请求编号 | 这8个字节存储RPC请求的唯一ID,用来将请求和响应做关联 |
96-127 | 消息体长度 | 占用的4个字节存储消息体长度。在一次RPC请求过程中,消息体中依次会存储7部分内容 |
状态响应码表(存储于协议报文头部的status中):
状态值 | 状态符号 | 意义 |
20 | OK | 正确返回 |
30 | CLIENT TIMEOUT | 客户端超时 |
31 | SERVERTIMEOUT | 服务端超时 |
40 | BADREQUEST | 请求报文格式错误 |
50 | BADRESPONSE | 响应报文格式错误 |
60 | SERVICE NOT FOUND | 未找到匹配的服务 |
70 | SERVICEERROR | 服务调用错误 |
80 | SERVER ERROR | 服务端内部错误 |
90 | CLIENTERROR | 客户端错误 |
100 | SERVER THREADPOOL EXHAUSTED ERROR | 服务端线程池满拒绝执行 |
到这里为止,讲的都是Dubbo协议头,那么再来看下Dubbo协议体存储什么东西
从比特位128之后,就是协议体的内容了,Dubbo协议中,其协议体的数据存储非常讲究,意思是存储的内容是严格按照一定顺序来的,如下:
- Dubbo version
- Service name
- Service version
- Method name
- Method parameter types
- Method arguments
- Attachments
2.2 编解码器
我们知道,数据要想在网络上进行传输,肯定需要某种协议进行约束和规范,比如TCP协议这种。但是数据在网络传输的时候,难免会遇到粘包、拆包的情况。对于主流协议的解决方案大致有4种:
- 将消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格。
- 在包尾增加回车换行符进行分割,例如FTP协议。
- 将消息分为消息头和消息体,消息头中包含表示消息总长度(或者消息体长度)的字段。
- 构造一个更复杂的应用层协议。
对于Dubbo而言,则是用特殊符号exdabb
魔法数来分割处理粘包问题的(具体细节后文会讲,结合代码来说)。而这一类实现肯定离不开编解码器的功劳。对于Dubbo中,所有的编解码层实现都应该继承Exchangecodec
类,Exchange
又扮演着把消息体解析为request和response的角色。因此来看下他的代码:
2.2.1 Dubbo请求Request编解码器
Request编码器
首先来看下入口方法,很明显会根据请求的类型来进行编码:
public class ExchangeCodec extends TelnetCodec {
@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);
}
}
}
1.请求对象编码encodeRequest()
,这里也可以看出来,
protected void encodeRequest(Channel channel, ChannelBuffer buffer, Request req) throws IOException {
// 1.获取指定或者默认的序列化协议(Hessian2)
Serialization serialization = getSerialization(channel);
// 2.构造16字节头 HEADER_LENGTH=16
byte[] header = new byte[HEADER_LENGTH];
// 3.占用2个字节来存储魔法数
Bytes.short2bytes(MAGIC, header);
// 4.在第3个字节(16位和19〜23位)分别存储请求标志和序列化协议序号
header[2] = (byte) (FLAG_REQUEST | serialization.getContentTypeId());
// 5.设置请求/响应标记
if (req.isTwoWay()) {
header[2] |= FLAG_TWOWAY;
}
if (req.isEvent()) {
header[2] |= FLAG_EVENT;
}
// 6.设置请求唯一标识
Bytes.long2bytes(req.getId(), header, 4);
// 7.用来跳过buffer头部的16个字节,用来序列化消息体的
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 {
// 8.序列化请求的调用,data一般是RpcInvocation
encodeRequestData(channel, out, req.getData(), req.getVersion());
}
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
bos.flush();
bos.close();
int len = bos.writtenBytes();
// 9.查是否超过默认8MB大小
checkPayload(channel, len);
// 10.将消息长度写入头部第12个字节的偏移量
Bytes.int2bytes(len, header, 12);
// 11.定位指针到协议头部开始的地方
buffer.writerIndex(savedWriteIndex);
// 12.写入完整的报文头到buffer
buffer.writeBytes(header);
// 13.写完后,定位指针到消息体结束的地方
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
}
2.编码请求对象体(对应上述代码中注释8),encodeRequestData()
方法,这部分主要就是对接口、方法、 方法参数类型、方法参数等进行编码。 ,对应DubboCodec
的encodeRequestData()
方法:
@Override
protected void encodeRequestData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
RpcInvocation inv = (RpcInvocation) data;
// 1.写入框架的版本
out.writeUTF(version);
// https://github.com/apache/dubbo/issues/6138
String serviceName = inv.getAttachment(INTERFACE_KEY);
if (serviceName == null) {
serviceName = inv.getAttachment(PATH_KEY);
}
// 2.写入调用的接口
out.writeUTF(serviceName);
// 3.写入接口指定的版本,默认是0.0.0
out.writeUTF(inv.getAttachment(VERSION_KEY));
// 4.写入方法名称
out.writeUTF(inv.getMethodName());
// 5.写入方法参数类型
out.writeUTF(inv.getParameterTypesDesc());
// 6.依次写入方法的参数值
Object[] args = inv.getArguments();
if (args != null) {
for (int i = 0; i < args.length; i++) {
out.writeObject(encodeInvocationArgument(channel, inv, i));
}
}
// 7.写入隐式参数
out.writeAttachments(inv.getObjectAttachments());
}
我们从方法中可以看出来,这和Dubbo消息体规定的写入顺序是一致的。接下来再来看下Request的解码器:
Request解码器
Request的解码器入口如下:
@Override
public Object decode(Channel channel, ChannelBuffer buffer) throws IOException {
int readable = buffer.readableBytes();
byte[] header = new byte[Math.min(readable, HEADER_LENGTH)];
buffer.readBytes(header);
return decode(channel, buffer, readable, header);
}
decode(channel, buffer, readable, header)
:
@Override
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
// 1.检查魔法数,处理流起始处并不是Dubbo魔法数0xdabb的场景
if (readable > 0 && header[0] != MAGIC_HIGH
|| readable > 1 && header[1] != MAGIC_LOW) {
int length = header.length;
// 2.若流中还有数据可以读取
if (header.length < readable) {
// 3.那么为header重新分配空间,用来存储流中所有可读字节
header = Bytes.copyOf(header, readable);
// 4.将剩余的字节读取到header中
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) {
// 5.将buffer读指针指向Dubbo报文头开始处:0xdabb
buffer.readerIndex(buffer.readerIndex() - header.length + i);
// 6.流起始处至下一个Dubbo报文之间的数据放到header中
header = Bytes.copyOf(header, i);
break;
}
}
// 7.主要用于解析header数据,比如用于Telnet
return super.decode(channel, buffer, readable, header);
}
// 8.如果读取数据长度小于16个字节,则进行等待,需要更多数据
if (readable < HEADER_LENGTH) {
return DecodeResult.NEED_MORE_INPUT;
}
// 10.取头部存储的报文长度,并校验长度是否超过限制
int len = Bytes.bytes2int(header, 12);
checkPayload(channel, len);
int tt = len + HEADER_LENGTH;
// 11.校验是否可以读取完整Dubbo报文,否则期待更多数据
if (readable < tt) {
return DecodeResult.NEED_MORE_INPUT;
}
// limit input stream.
ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);
try {
// 12.解码消息体,is参数是完整的RPC调用报文
return decodeBody(channel, is, header);
} finally {
// 13.如果解码过程有问题,则跳过这次RPC调用报文
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);
}
}
}
}
解码消息体:
@Override
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
long id = Bytes.bytes2long(header, 4);
if ((flag & FLAG_REQUEST) == 0) {
// 解码响应体分支
} else {
// 解码请求体分支
// 1.请求标志位被设置,创建Request对象
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()) {
ObjectInput in = CodecSupport.deserialize(channel.getUrl(), is, proto);
data = decodeEventData(channel, in);
} else {
DecodeableRpcInvocation inv;
if (channel.getUrl().getParameter(DECODE_IN_IO_THREAD_KEY, DEFAULT_DECODE_IN_IO_THREAD)) {
// 2.在I/O线程中直接解码
inv = new DecodeableRpcInvocation(channel, req, is, proto);
inv.decode();
} else {
// 3.交给Dubbo业务线程池解码
inv = new DecodeableRpcInvocation(channel, req,
new UnsafeByteArrayInputStream(readMessageData(is)), proto);
}
data = inv;
}
// 4.将Rpclnvocation作为Request的数据域
req.setData(data);
} catch (Throwable t) {
if (log.isWarnEnabled()) {
log.warn("Decode request failed: " + t.getMessage(), t);
}
// 5.解码失败,先做标记并存储异常
req.setBroken(true);
req.setData(t);
}
return req;
}
}
针对注释2和3,我们可以看到Dubbo解码过程中,把消息体转换成Rpclnvocation
对象,具体解码会触发 DecodeableRpcInvocation
的decode
方法,可以看出解码的流程严格按照编码的参数顺序来进行。
@Override
public Object decode(Channel channel, InputStream input) throws IOException {
ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType)
.deserialize(channel.getUrl(), input);
// 1.读取框架版本
String dubboVersion = in.readUTF();
request.setVersion(dubboVersion);
setAttachment(DUBBO_VERSION_KEY, dubboVersion);
String path = in.readUTF();
// 2.读取调用的接口
setAttachment(PATH_KEY, path);
// 3.读取接口指定的版本,默认是0.0.0
setAttachment(VERSION_KEY, in.readUTF());
// 4.读取方法名称
setMethodName(in.readUTF());
String desc = in.readUTF();
// 5.读取方法参数类型
setParameterTypesDesc(desc);
try {
// 。。。代码省略
// 依次读取方法的参数值
args[i] = in.readObject(pts[i]);
// 。。。代码省略
for (int i = 0; i < args.length; i++) {
// 处理异步参数的回调,如果有则在服务端创建一个reference代理实例
args[i] = decodeInvocationArgument(channel, this, pts, i, args[i]);
}
} catch (ClassNotFoundException e) {
throw new IOException(StringUtils.toString("Read invocation data failed.", e));
} finally {
if (in instanceof Cleanable) {
((Cleanable) in).cleanup();
}
}
return this;
}
针对Request的编解码,我们可以总结出以下结论:
结论1:对于Dubbo处理粘包拆包问题的解决:
- 在解码过程中,首先先判断此次传输的信息包的大小。
- 根据传输包的大小,确定本次传输的信息是否包含整个请求头,取与请求头固定长度比较最小值,然后读取相关信息到header中。如果此次信息包大于等于16字节,说明请求头是完整的。(查看解码器入口代码)
- 对于请求头的检查,会先检查魔法数,若请求头不完整则直接返回(可以说只有请求头是完整的,才会解析请求体)。
- 若发现拆包,直接返回,并进入等待状态。
结论2:为什么Dubbo协议要比普通的Http协议快点:
- 请求头较小,16字节,不会存储什么额外的信息。
- Http的编解码工作还会由Http服务器在做一层编解码,最后再由我们自己的应用来做一次编解码,比如转换成JSON。但是对于Dubbo一般不需要二次编码,而是直接编码二进制,然后传输(可参考Request编码部分)。
2.2.2 Dubbo响应Response编解码器
Response编码器
Response解码入口同样是:
public class ExchangeCodec extends TelnetCodec {
@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);
}
}
}
来看下encodeResponse()
方法:
protected void encodeResponse(Channel channel, ChannelBuffer buffer, Response res) throws IOException {
int savedWriteIndex = buffer.writerIndex();
try {
// 1.获取指定或默认的序列化协议(Hessian2)
Serialization serialization = getSerialization(channel);
// 2.构造 16 字节头
byte[] header = new byte[HEADER_LENGTH];
// 3.占用2个字节来存储魔法数
Bytes.short2bytes(MAGIC, header);
// 4.在第3个字节(16位和19〜23位)存储响应标志
header[2] = serialization.getContentTypeId();
if (res.isHeartbeat()) {
header[2] |= FLAG_EVENT;
}
// 5.在第4个字节存储响应状态
byte status = res.getStatus();
header[3] = status;
// 6.设置请求唯一标识
Bytes.long2bytes(res.getId(), header, 4);
// 7.空出16字节头部用于存储响应体报文
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()) {
encodeEventData(channel, out, res.getResult());
} else {
// 8.编码响应体
encodeResponseData(channel, out, res.getResult(), res.getVersion());
// ...省略
}
}
// 9.查是否超过默认的8MB大小
int len = bos.writtenBytes();
checkPayload(channel, len);
// 10.向消息长度写入头部第12个字节偏移量
Bytes.int2bytes(len, header, 12);
// 定位指针到报文头部开始位置
buffer.writerIndex(savedWriteIndex);
// 写入完整报文头部到 buffer
buffer.writeBytes(header);
// 写完后,定位指针到消息体结束的地方
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
} catch (Throwable t) {
// 若失败,则复位buffer、并将编码响应的异常发送给consumer,否则只能等待到超时
// 告知客户端数据包的长度发生限制或者编码失败的具体原因。
}
}
响应体的编码:DubboCodec.encodeResponseData()
:
@Override
protected void encodeResponseData(Channel channel, ObjectOutput out, Object data, String version) throws IOException {
Result result = (Result) data;
// 1.判断客户端请求的版本是否支持服务端参数返回
boolean attach = Version.isSupportResponseAttachment(version);
Throwable th = result.getException();
if (th == null) {
// 2.提取正常返回结果
Object ret = result.getValue();
if (ret == null) {
// 3.在编码结果前,先写一个字节标志
out.writeByte(attach ? RESPONSE_NULL_VALUE_WITH_ATTACHMENTS : RESPONSE_NULL_VALUE);
} else {
out.writeByte(attach ? RESPONSE_VALUE_WITH_ATTACHMENTS : RESPONSE_VALUE);
// 4.分别写一个字节标记和调用结果
out.writeObject(ret);
}
} else {
// 5.标记调用抛异常,并序列化异常
out.writeByte(attach ? RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS : RESPONSE_WITH_EXCEPTION);
out.writeThrowable(th);
}
if (attach) {
// 6.记录服务端Dubbo的版本,并返回服务端隐式参数
result.getObjectAttachments().put(DUBBO_VERSION_KEY, Version.getProtocolVersion());
out.writeAttachments(result.getObjectAttachments());
}
}
Response解码器
其实这里的部分和Request是一样的,接着上文中的代码,从注释12起:decodeBody()
方法,只是根据Request
和Response
类型不同,代码执行的if
条件分支不一样。本质上其实差不多(就不讲了)
@Override
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
// get request id.
long id = Bytes.bytes2long(header, 4);
if ((flag & FLAG_REQUEST) == 0) {
// 解码响应体
} else {
// 解码请求体
}
}
不同的是,响应体解码后,需要进行返回,最后将结果进行返回的时候,会触发DecodeableRpcResult
的decode
方法,主要针对不同的响应状态来对返回值的进行数据封装:
@Override
public Object decode(Channel channel, InputStream input) throws IOException {
if (log.isDebugEnabled()) {
Thread thread = Thread.currentThread();
log.debug("Decoding in thread -- [" + thread.getName() + "#" + thread.getId() + "]");
}
ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType)
.deserialize(channel.getUrl(), input);
byte flag = in.readByte();
switch (flag) {
// 返回结果标记为Null值
case DubboCodec.RESPONSE_NULL_VALUE:
break;
case DubboCodec.RESPONSE_VALUE:
// 读取方法调用返回值类型
handleValue(in);
break;
case DubboCodec.RESPONSE_WITH_EXCEPTION:
// 会保存读取的返回值异常结果
handleException(in);
break;
case DubboCodec.RESPONSE_NULL_VALUE_WITH_ATTACHMENTS:
// 读取返回值是Null,并且包含隐式参数
handleAttachment(in);
break;
case DubboCodec.RESPONSE_VALUE_WITH_ATTACHMENTS:
// 读取返回时不是Null,并且包含隐式参数
handleValue(in);
handleAttachment(in);
break;
case DubboCodec.RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS:
handleException(in);
handleAttachment(in);
break;
default:
throw new IOException("Unknown result flag, expect '0' '1' '2' '3' '4' '5', but received: " + flag);
}
if (in instanceof Cleanable) {
((Cleanable) in).cleanup();
}
return this;
}
最后的最后,响应码有这么几种:
状态值 | 状态符号 | 意义 |
5 | RESPONSE NULL VALUE WITH ATTACHMENTS | 响应空值包含隐藏参数 |
4 | RESPONSE VALUE WITH ATTACHMENTS | 响应结果包含隐藏参数 |
3 | RESPONSE WITH EXCEPTION WITH ATTACHMENTS | 异常返回包含隐藏参数 |
2 | RESPONSENULLVALUE | 响应空值 |
1 | RESPONSE VALUE | 响应结果 |
0 | RESPONSE WITH EXCEPTION | 异常返回 |
三. Telnet调用原理
上文主要是讲了Dubbo的一个编码解码的实现,其实,编解码器的处理有三种场景:
- 请求。
- 响应。
- Telnet调用。
对于Telnet调用,主要是将Telnet当做明文的字符串来处理,按照Dubbo的调用规范,解析成调用命令格式,然后查找对应的Invoker,发起方法调用即可。
1.在Dubbo中,Telnet指令解析被设置成了一个扩展点:TelnetHandler
,每个Telnet指令都会实现这个扩展点。
@SPI
public interface TelnetHandler {
// message包含处理命令之外的所有字符串参数。
String telnet(Channel channel, String message) throws RemotingException;
}
2.完成Telnet指令转发的核心实现类是TelnetHandlerAdapter
:
public class TelnetHandlerAdapter extends ChannelHandlerAdapter implements TelnetHandler {
@Override
public String telnet(Channel channel, String message) throws RemotingException {
// ...
if (message.length() > 0) {
int i = message.indexOf(' ');
if (i > 0) {
// 1.提取执行的命令
command = message.substring(0, i).trim();
// 2.提取命令后的所有字符串信息
message = message.substring(i + 1).trim();
} else {
command = message;
message = "";
}
} else {
command = "";
}
if (command.length() > 0) {
// 3.检查系统是否有命令对应的扩展点
if (extensionLoader.hasExtension(command)) {
if (commandEnabled(channel.getUrl(), command)) {
try {
// 4.如果有,那么交给具体的扩展点去执行
String result = extensionLoader.getExtension(command).telnet(channel, message);
if (result == null) {
return null;
}
buf.append(result);
} catch (Throwable t) {
buf.append(t.getMessage());
}
}
// ...
}
if (buf.length() > 0) {
// 5.在Telnet消息结尾追加回车和换行
buf.append("\r\n");
}
// ...
return buf.toString();
}
}
其实现总结就是:
- 将用户输入的指令识别成command,然后将剩余的内容解析成message。
- message则交给命令的实现者去处理。
3.完成Telnet的指令转发后,则实现命令的调用了,具体实现在于InvokeTelnetHandler.telnet()
方法:
public class InvokeTelnetHandler implements TelnetHandler {
public String telnet(Channel channel, String message) {
// ...
// 1.提取调用方法,由接口名、方法名组成
String method = message.substring(0, i).trim();
// 2.提取调用方法的参数值
String args = message.substring(i + 1, message.length() - 1).trim();
i = method.lastIndexOf(".");
if (i >= 0) {
// 3.提取方法前面的接口和法法名称
service = method.substring(0, i).trim();
method = method.substring(i + 1).trim();
}
List<Object> list;
try {
// 4.将参数JSON串转换成JSON对象
list = JSON.parseArray("[" + args + "]", Object.class);
} catch (Throwable t) {
return "Invalid json argument, cause: " + t.getMessage();
}
StringBuilder buf = new StringBuilder();
// ...
} else {
for (ProviderModel provider : ApplicationModel.allProviderModels()) {
// 5.以接口名、方法、参数值、类型等作为检索方法的条件进行方法的筛选
}
// ...
if (selectedProvider != null) {
if (invokeMethod != null) {
try {
// 6.将JSON参数值转换成JAVA对象
Object[] array = realize(list.toArray(), invokeMethod.getParameterTypes(),
invokeMethod.getGenericParameterTypes());
long start = System.currentTimeMillis();
AppResponse result = new AppResponse();
try {
// 7.根据查找到的Invoker、构造Rpclnvocation进行方法调用
Object o = invokeMethod.invoke(selectedProvider.getServiceInstance(), array);
// ...
return buf.toString();
}
}
- 当用户输入invoke指令的时候,会被转发到
InvokeTelnetHandler
类中进行Telnet调用。 - 提取方法调用信息、参数值、接口、方法等信息后,进行筛选,去查找对应的方法和
Invoker
对象。 - 在真正进行方法调用前,需要将JSON对象转换成Java对象,然后触发方法调用并返回结果值。
四. Dubbo核心Handler和线程模型
4.1 核心Handler介绍
我以前的文章也有提及,从代码实现也可以看到,Dubbo的实现和Netty息息相关(默认情况下),而Netty就有Channel管道这种概念,因此Dubbo内部自然有个组件叫做:ChannlHandler
。
Dubbo框架内部使用大量
Handler
组成类似链表,依次处理具体逻辑,比如编解码、心跳时间戳和方法调用Handler
等。因为Netty每次创建Handler
都会经过ChannelPipeline
, 大量的事件经过很多Pipeline
会有较多的开销,因此Dubbo会将多个Handler
聚合为一个Handler
。
来看下ChannlHandler的5种状态:
- connected:Channel已经被创建
- disconnected:Channel已经被断开
- sent:消息被发送
- received:消息被接收
- caught:捕获到异常
而Dubbo会针对每个特性都会实现对应的ChannelHandler
,这些Handler
最终会和底层通信框架做关联,比如Netty。因此接下来看下RPC调用服务方处理Handler的逻辑:在DubboProtocol
中通过内部类继承自ExchangeHandlerAdapte
,完成服务提供方Invoker实例的查找并进行服务的真实调用。
public class DubboProtocol extends AbstractProtocol {
private ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {
@Override
public CompletableFuture<Object> reply(ExchangeChannel channel, Object message) throws RemotingException {
// ...
Invocation inv = (Invocation) message;
// 1.查找invocation关联的Invoker,即查找当前已经暴露的服务。
// 在服务暴露时服务端已经按照特定规则(端口、接口名、接口版本和接口分组)把实例Invoker存储到HashMap中了
// 客户端调用过来时必须携带相同信息构造的key,找到对应Exporter然后调用
Invoker<?> invoker = getInvoker(channel, inv);
// ...
RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress());
// 2.调用业务方具体方法
Result result = invoker.invoke(inv);
return result.thenApply(Function.identity());
}
}
}
紧接着,跟进getInvoker(channel, inv)
方法,即服务端Invoker的查找过程:
Invoker<?> getInvoker(Channel channel, Invocation inv) throws RemotingException {
boolean isCallBackServiceInvoke = false;
boolean isStubServiceInvoke = false;
// 1.获取服务暴露协议的端口
int port = channel.getLocalAddress().getPort();
// 2.获取调用传递的接口
String path = (String) inv.getObjectAttachments().get(PATH_KEY);
// ...省略,主要是异步参数回调的一些处理逻辑
// 3.根据端口、接口名、接口分组和版本构造唯一的key
String serviceKey = serviceKey(
port,
path,
(String) inv.getObjectAttachments().get(VERSION_KEY),
(String) inv.getObjectAttachments().get(GROUP_KEY)
);
// 4.从存储着暴露服务的集合map中,根据上文得到的唯一key,获取实例Invoker(封装于Exporter对象中)
DubboExporter<?> exporter = (DubboExporter<?>) exporterMap.get(serviceKey);
// 5.返回所需要的Invoker实例即可
return exporter.getInvoker();
}
而Dubbo为了编织这些Handler,适应不同的场景,提供了一套可以定制的线程模型,一般有两种:
- I/O线程池:负责读写报文,如Netty。
- Dubbo业务线程池:负责处理一些耗时的事件,比如读写数据库的操作。
这是我从Dubbo官网上截图下来的,他代表Dubbo实现线程派发的一个流程:
在进行一个补充:
- Dispatcher:线程池派发器。是Dubbo中的扩展点,负责创建具有线程派发能力的
ChannelHandler
。
而这一类的ChannelHandler
目前有6种策略调用:
分发策略 | 分发实现类 | 作用 |
all | AllDispatcher | 将所有I/O事件交给Dubbo线程池处理,Dubbo默认启用 |
connection | ConnectionOrderedDispatcher | 单独线程池处理连接断开事件,和Dubbo线程池分开 |
direct | DirectDispatcher | 所有方法调用和事件处理在I/O线程中 |
execution | ExecutionDispatcher | 只在线程池处理接收请求,其他事件在I/O线程池中 |
message | MessageOnlyChannelHandler | 只在线程池处理请求和响应事件,其他事件在I/O线程池中 |
mockdispatcher | MockDispatcher | 默认返回Null |
4.2 Dubbo请求响应Handler
Dubbo毕竟是一个RPC框架,那么主要的肯定还是讲它的有关请求响应的Handler
了。在Dubbo框架内部,所有方法调用会被抽象成Request/Response,每次调用都会创建一个请求Request,如果是方法调用则会返回一个Response对象。Dubbo中使用HeaderExchangeHandler
类来进行处理:
- 更新发送和读取请求时间戳。
- 判断请求格式或编解码是否有错,并响应客户端失则的具体原因。
- 处理Request请求和Response正常响应。
- 支持上文所提到的Telnet调用。
请求响应Handler实现: 入口于HeaderExchangeHandler
类下的received
方法:
@Override
public void received(Channel channel, Object message) throws RemotingException {
final ExchangeChannel exchangeChannel = HeaderExchangeChannel.getOrAddChannel(channel);
// 接收请求
if (message instanceof Request) {
Request request = (Request) message;
if (request.isEvent()) {
// 处理处理readonly事件,在channel中打标
handlerEvent(channel, request);
} else {
if (request.isTwoWay()) {
// 处理方法的调用,并将结果返回给客户端
handleRequest(exchangeChannel, request);
} else {
handler.received(exchangeChannel, request.getData());
}
}
} else if (message instanceof Response) {
// 接收响应
handleResponse(channel, (Response) message);
} else if (message instanceof String) {
// 若客户端不支持Telnet调用
if (isClientSide(channel)) {
Exception e = new Exception("Dubbo client can not supported string message: " + message + " in channel: " + channel + ", url: " + channel.getUrl());
logger.error(e.getMessage(), e);
} else {
// 触发Telnet调用,并将结果返回
String echo = handler.telnet(channel, (String) message);
if (echo != null && echo.length() > 0) {
channel.send(echo);
}
}
} else {
handler.received(exchangeChannel, message);
}
}
额一看到上述的代码中,会针对message的类型(Response或者Request)类进行对应的处理:
- 处理请求报文:
handleRequest()
- 处理响应报文:
handleResponse()
handleRequest()
(HeaderExchangeHandler
类中):
void handleRequest(final ExchangeChannel channel, Request req) throws RemotingException {
Response res = new Response(req.getId(), req.getVersion());
if (req.isBroken()) {
Object data = req.getData();
String msg;
if (data == null) {
msg = null;
} else if (data instanceof Throwable) {
// 1.处理请求格式不正确(编解码),并把异常传换成字符串返回
msg = StringUtils.toString((Throwable) data);
} else {
msg = data.toString();
}
res.setErrorMessage("Fail to decode request due to: " + msg);
res.setStatus(Response.BAD_REQUEST);
channel.send(res);
return;
}
// find handler by message class.
Object msg = req.getData();
try {
// 2. 调用DubboProtocol的reply方法,进行方法调用()
CompletionStage<Object> future = handler.reply(channel, msg);
// ....
} catch (Throwable e) {
// 方法调用失败的处理逻辑,返回状态和错误信息
res.setStatus(Response.SERVICE_ERROR);
res.setErrorMessage(StringUtils.toString(e));
channel.send(res);
}
}
handleResponse()
的实现就比较简单:
static void handleResponse(Channel channel, Response response) throws RemotingException {
if (response != null && !response.isHeartbeat()) {
// 唤醒阻塞的线程并通知结果
DefaultFuture.received(channel, response);
}
}
这里做个解释:
- 因为Dubbo在发送请求的时候,会在
DefaultFuture
类中保存请求对象并阻塞请求线程。 - 那么进行
handleResponse()
响应返回的时候,会唤醒阻塞线程并将Response中的结果通知调用方。
总结
Dubbo客户端调用远程服务流程?:
1.将多个服务提供者进行聚合。
2.客户端中,经历路由、负载均衡。筛选出某一个机器进行RPC调用,将调用的请求交给底层Netty来处理。
3.在编解码层会进行请求的处理,最终交给Telnet对应的Handler进行处理。
4.服务端接收到请求,进行解码,进行处理并返回数据,客户端获得响应数据。
Dubbo协议讲了什么?
1.Dubbo协议分为协议头和协议体(保存具体的数据,并且数据按照一定顺序来存储)
2.编解码器根据Dubbo协议的规定,按照上述的顺序规范进行数据的编码、解码操作。
3.Dubbo利用魔法数exdabb
来处理粘包和拆包问题。若数据不够,则进入等待状态,只有完整的数据,其请求体才能够被解析。
3.Dubbo协议的优点:请求头较小,Dubbo一般不需要二次编码,传输效率高。
Dubbo中的Telnet调用是什么?
1.将Telnet当做明文的字符串来处理,按照Dubbo的调用规范,解析成调用命令格式,然后查找对应的Invoker,发起方法调用。
2.在Dubbo中,Telnet指令解析被设置成了一个扩展点:TelnetHandler。
Dubbo核心Handler说了什么?
1.ChannlHandler是Dubbo的一个核心组件,Dubbo会针对每个特性都会实现对应的ChannelHandler,这些Handler最终会和底层通信框架做关联。
2.而Dubbo为了编织这些Handler,适应不同的场景,提供了一套可以定制的线程模型,一般有两种:
----I/O线程池:负责读写报文。
----Dubbo业务线程池:负责业务的处理。
总的来说,将上面的话题串起来就是:
- 客户端发起一个RPC调用的请求,那么前提是对于客户端而言,有相关的服务可以使用,那么这个过程需要经历路由操作、负载均衡,最终选取某一个服务发起调用的请求。
- 在客户端中,会将调用请求Request通过编码器进行序列化操作。并将请求抽象成Request,这类请求交给
HeaderExchangeHandler
类来处理。 - 对于客户端而言的
HeaderExchangeHandler
会先判断请求的格式、编码是否有错,若没有错误则处理Request请求,并负责响应最终的结果且返回。 - 处理Request请求,则统一的调用
DubboProtocol
下的reply()
方法(此时请求线程会被堵塞,请求对象也会被保存于DefaultFuture中),主要做的事情是:
4.1 查找当前已经暴露的服务。在服务暴露时,服务端已经按照特定规则(端口、接口名、接口版本和接口分组)把实例Invoker存储到HashMap中了,因此客户端调用时必须携带相同信息构造的key,找到对应Invoker。
4.2 获得Invoker后,则调用其invoke方法。
- 相关的请求到达服务端后,服务端同样需要将请求进行解码。(此时客户端和服务端之间已经建立起了Netty连接,即存在一个IO线程池负责读写报文),服务端需要通过分发策略进行对应请求的处理,最后将结果进行返回。
- 对于服务端而言的
HeaderExchangeHandler
的handleResponse()
方法进行响应的返回,会唤醒阻塞线程并将Response中的结果通知调用方。
上面的流程是我的一个个人理解,若有不对,还望指正。