7.1 Dubbo调用介绍
如果我们手动写一个简单的RPC调用,一般需要把服务调用的信息传递给服务端,包括每次服务调用的一些共用信息包括服务调用接口、方法名、方法参数类型和方法参数值等,在传递方法参数值时需要先序列化对象并经过网络传输到服务端,在服务端接受后再按照客户端序列化的顺序再做一次反序列化,然后拼装成请求对象进行服务反射调用,最终将调用结果传给客户端。Dubbo的实现也基本是相同的原理,下图是Dubbo一次完整RPC调用中经过的步骤:
首先在客户端启动时,会从注册中心拉取和订阅对应的服务列表,Cluster会把拉取的服务列表聚合成一个Invoker,每次RPC调用前会通过Directory#list获取providers地址(已经生成好的Invoker地址),获取这些服务列表给后续路由和负载均衡使用。对应上图①中将多个服务提供者做聚合。在框架内部实现Directory接口的是RegistryDirectory类,它和接口名是一对一的关系(每一个接口都有一个RegistryDirectory实例),主要负责拉取和订阅服务提供者、动态配置和路由项。
在Dubbo发起服务调用时,所有路由和负载均衡都是在客户端实现的。客户端服务调用首先会触发路由操作,然后将路由结果得到的服务列表作为负载均衡参数,经过负载均衡后会选出一台机器进行RPC调用,这3个步骤一次对应图中②③④。客户端经过路由和负载均衡后,会将请求交给底层IO线程池(如Netty)进行处理,IO线程池主要处理读写、序列化和反序列化等逻辑,因此这里一定不能阻塞操作,Dubbo也提供参数控制(decode.in.io)参数,在处理反序列化对象时会在业务线程池中处理。在⑤中包含两种类似的线程池,一种是IO线程池(Netty),另一种是Dubbo业务线程池(承载业务方法调用)。
目前Dubbo将服务调用和Telnet调用做了端口复用,子啊编解码层面也做了适配。在Telnet调用时,会新建立一个TCP连接,传递接口、方法和json格式的参数进行服务调用,在编解码层面简单读取流中的字符串(因为不是Dubbo标准头报文),最终交给Telnet对应的Handler去解析方法调用。如果不是Telnet调用,则服务提供方会根据传递过来的接口、分组和版本信息查找Invoker对应的实例进行反射调用。在⑦中进行了端口复用,Telnet和正常RPC调用不一样的地方是序列化和反序列化使用的不是Hessian方式,而是直接使用fastjson进行处理。
讲解完主要调用原理,接下来开始探讨细节,比如Dubbo协议、编解码实现和线程模型等,本篇重点主要放在⑤⑥⑦。
7.2 Dubbo协议
Dubbo协议参考了现有的TCP/IP协议,每一次RPC调用包括协议头和协议体两部分。16字节长的报文头部主要包含魔数(0xdabb),以及当前请求报文是否是Request、Response、心跳和事件的信息,请求时也会携带当前报文体内序列化协议编号。除此之外,报文头还携带了请求状态,以及请求唯一标识和报文体长度。
偏移比特位 | 字段 | 描述 |
---|---|---|
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,60为SERVICE_NOT_FOUND,70为SERVICE_ERROR,80为SERVER_ERROR,90为CLIENT_ERROR,100为SERVER_THREADPOOL_EXHAUSTED_ERROR |
32 ~ 95 | 请求编号 | 这8个字节存储RPC请求的唯一ID,用来将请求和响应做关联 |
96 ~ 127 | 消息体长度 | 4个字节存储消息题长度 |
在消息体中,客户端严格按照序列化顺序写入消息,服务端也会遵循相同的顺序读取消息,客户端发起的请求消息体一次依次保存下列内容:Dubbo版本号、服务接口名、服务接口版本、方法名、参数类型、方法参数值和请求额外参数(attachment)。
服务端返回的响应消息体主要包含回值状态标记和返回值,其中回值状态标记包含6中:
状态值 | 状态符号 | 描述 |
---|---|---|
5 | RESPONSE_NULL_VALUE_WITH_ATTACHMENTS | 响应空值包含隐藏参数 |
4 | RESPONSE_VALUE_WITH_ATTACHMENTS | 响应结果包含隐藏参数 |
3 | RESPONSE_WITH_EXCEPTION_WITH_ATTACHMENTS | 异常返回包含隐藏参数 |
2 | RESPONSE_NULL_VALUE | 响应空值 |
1 | RESPONSE_VALUE | 响应结果 |
0 | RESPONSE_WITH_EXCEPTION_ | 异常返回 |
我们知道在网络通信中(TCP)需要解决网络粘包/解包的问题,常用的方法比如用回车、换行、固定长度和特殊分隔符进行处理,而Dubbo是使用特殊符号0xdabb魔法数来分割处理粘包问题。
在实际场景中,客户端会使用多线程并发调用服务,Dubbo如何做到正确响应调用线程呢?关键在于协议头的全局请求id标识,先看原理图:
当客户端多个线程并发请求时,框架内部会调用DefaultFuture对象的get方法进行等待。在请求发起时,框架内部会创建Request对象,这时候会被分配一个唯一id,DefaultFuture可以从Request中获取id,并将关联关系存储到静态HashMap中,就是上图中的Futures集合。当客户端收到响应时,会根据Response对象中的id,从Futures集合中查找对应DefaultFuture对象,最终会唤醒对应的线程并通知结果。客户端也会启动一个定时扫描线程去探测超时没有返回的请求。
6.3 编解码器的原理
先了解一下编解码器的类关系图:
如上,AbstractCodec主要提供基础能力,比如校验报文长度和查找具体编解码器等。TransportCodec主要抽象编解码实现,自动帮我们去调用序列化、反序列化实现和自动cleanup流。我们通过Dubbo编解码继承结构可以清晰看到,DubboCodec继承自ExchageCodec,它又再次继承了TelnetCodec实现。我们前面说过Telnet实现复用了Dubbo协议端口,其实就是在这层编解码做了通用处理。因为流中可能包含多个RPC请求,Dubbo框架尝试一次性读取更多完整报文编解码生成对象,也就是图中的DubboCountCodec,它的实现思想比较简单,依次调用DubboCodec去解码,如果能解码成完整报文,则加入消息列表,然后触发下一个Handler方法调用。
6.3.1 Dubbo协议编码器
编码器的作用是将Java对象转成字节流,主要分两部分,构造报文头部,和对消息体进行序列化处理。所有编辑码层实现都应该继承自ExchangeCodec,当Dubbo协议编码请求对象时,会调用ExchangeCodec#encode方法。我们来看下这个方法是如何对请求对象进行编码的:
如上,是Dubbo将请求对象转成字节流的过程,其中encodeRequestData方法是对RpcInvocation调用对象的编码,主要是对接口、方法、方法参数类型、方法参数等进行编码,在DubboCodec#encodeRequestData中对此方法进行了重写:
注意隐式参数是个HashMap,timeout和group等动态参数就放在这里。
处理完编码请求对象后,我们继续分析编码响应对象,在ExchangeCodec#encodeResponse中:
protected void encodeResponse(Channel channel, ChannelBuffer buffer, Response res) throws IOException {
int savedWriteIndex = buffer.writerIndex();
try {
// 获取指定或默认的序列化协议(Hessian2)
Serialization serialization = getSerialization(channel);
// 构造16字节头
byte[] header = new byte[HEADER_LENGTH];
// 设置2字节魔数
Bytes.short2bytes(MAGIC, header);
// 第3个字节(19~23)设置序列化编号
header[2] = serialization.getContentTypeId();
// 18设置事件标识,没有设置17调用方式应该是因为响应用不上
if (res.isHeartbeat()) {
header[2] |= FLAG_EVENT;
}
// 第4个字节设置响应状态
byte status = res.getStatus();
header[3] = status;
// 第5个字节设置id
Bytes.long2bytes(res.getId(), header, 4);
// 空出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 {
// 序列化响应,data一般是Result对象
encodeResponseData(channel, out, res.getResult(), res.getVersion());
}
} else {
out.writeUTF(res.getErrorMessage());
}
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
bos.flush();
bos.close();
int len = bos.writtenBytes();
// 检查是否超过默认8MB大小
checkPayload(channel, len);
// 写入消息长度到第13个字节
Bytes.int2bytes(len, header, 12);
// 定位指针到报文头部开始位置
buffer.writerIndex(savedWriteIndex);
// 写入完整报文头部到buffer
buffer.writeBytes(header);
// 设置writeIndex到消息体结束的位置
buffer.writerIndex(savedWriteIndex + HEADER_LENGTH + len);
} catch (Throwable t) {
// 如果编码失败,则复位buffer
buffer.writerIndex(savedWriteIndex);
// send error message to Consumer, otherwise, Consumer will wait till timeout.
// 将编码响应异常返送给consumer,否则客户端只能等待到超时
if (!res.isEvent() && res.getStatus() != Response.BAD_RESPONSE) {
Response r = new Response(res.getId(), res.getVersion());
r.setStatus(Response.BAD_RESPONSE);
// 数据包长度超过限制异常
if (t instanceof ExceedPayloadLimitException) {
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);
}
}
}
如上,响应编码与请求编码的逻辑基本大同小异,在编码出现异常时,会将异常响应返回给客户端,防止客户端只能一直等到超时。为了防止报错对象无法在客户端反序列化,在服务端会将异常信息转成字符串处理。对于响应体的编码,在DubboCodec#encodeResponseData方法中实现:
注意不管什么样的响应,都会先写入1个字节的标识符,具体的值和含义前面已经讲过。
6.3.2 Dubbo协议解码器
解码相对更复杂一些,分为2部分,第一部分是解码报文的头部,第二部分是解码报文体内容并将其转换成RpcInvocation对象。我们先看服务端接受到请求后的解码过程,具体解码实现在ExchangeCodec#decode方法:
protected Object decode(Channel channel, ChannelBuffer buffer, int readable, byte[] header) throws IOException {
// 如果加入流的起始处不是Dubbo魔数,
if (readable > 0 && header[0] != MAGIC_HIGH
|| readable > 1 && header[1] != MAGIC_LOW) {
int length = header.length;
// 如果还有数据可以读
if (header.length < readable) {
// 为header分配空间
header = Bytes.copyOf(header, readable);
// 将剩余的可读字节读到header中
buffer.readBytes(header, length, readable - length);
}
// 遍历读出来的header看有没有魔数
for (int i = 1; i < header.length - 1; i++) {
if (header[i] == MAGIC_HIGH && header[i + 1] == MAGIC_LOW) {
// 如果发现魔数,就把buffer读索引挪到这个魔数的位置,header只保留魔术之前的数据
buffer.readerIndex(buffer.readerIndex() - header.length + i);
header = Bytes.copyOf(header, i);
break;
}
}
// 交给父类TelnetCodec来解码
return super.decode(channel, buffer, readable, header);
}
// 如果可读数据小于16字节,期待更多数据
if (readable < HEADER_LENGTH) {
return DecodeResult.NEED_MORE_INPUT;
}
// 提取头部存储的报问体长度,并校验是否超过限制
int len = Bytes.bytes2int(header, 12);
checkPayload(channel, len);
// 验证是否可以读取完整报文体,不完整则期待更多数据
int tt = len + HEADER_LENGTH;
if (readable < tt) {
return DecodeResult.NEED_MORE_INPUT;
}
// limit input stream.
ChannelBufferInputStream is = new ChannelBufferInputStream(buffer, len);
try {
// 对报文体进行解码,此处is是完整的RPC调用报文体
return decodeBody(channel, is, header);
} finally {
// 如果解码过程又问题,则跳过这次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);
}
}
}
可以看出,解码过程中需要解决粘包和半包问题。接下来我们看一下DubboCodec对消息题解码的实现:
protected Object decodeBody(Channel channel, InputStream is, byte[] header) throws IOException {
byte flag = header[2], proto = (byte) (flag & SERIALIZATION_MASK);
// 从头部读取ID
long id = Bytes.bytes2long(header, 4);
if ((flag & FLAG_REQUEST) == 0) {
// 如果是响应体
...
} else {
// 如果是请求体
// 创建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)) {
// 直接在IO线程进行解码
inv = new DecodeableRpcInvocation(channel, req, is, proto);
inv.decode();
} else {
// 交给Dubbo业务线程池解码
inv = new DecodeableRpcInvocation(channel, req,
new UnsafeByteArrayInputStream(readMessageData(is)), proto);
}
data = inv;
}
// 把RpcInvocation数据放进Request
req.setData(data);
} catch (Throwable t) {
if (log.isWarnEnabled()) {
log.warn("Decode request failed: " + t.getMessage(), t);
}
// 解码失败,标记并存储异常
req.setBroken(true);
req.setData(t);
}
return req;
}
如上,如果默认配置在IO线程解码,直接调用decode方法;否则不做解码,延迟到业务线程池中解码。这里没有提到的是心跳和事件的解码,其实很简单,心跳报文是没有消息体的,事件又消息体,在使用Hessian2协议的情况下默认会传递字符R,当优雅停机时会通过发送readonly事件来通知客户端当前服务端不可用。
接下来,我们分析一下如何把消息体转换成RpcInvocation对象,具体在DecodeableRpcInvocation#decode方法中:
public Object decode(Channel channel, InputStream input) throws IOException {
ObjectInput in = CodecSupport.getSerialization(channel.getUrl(), serializationType)
.deserialize(channel.getUrl(), input);
// 读取Dubbo版本
String dubboVersion = in.readUTF();
request.setVersion(dubboVersion);
setAttachment(DUBBO_VERSION_KEY, dubboVersion);
// 读取调用接口
String path = in.readUTF();
setAttachment(PATH_KEY, path);
// 读取接口版本
setAttachment(VERSION_KEY, in.readUTF());
// 读取方法名称
setMethodName(in.readUTF());
// 读取方法参数类型
String desc = in.readUTF();
setParameterTypesDesc(desc);
try {
Object[] args = DubboCodec.EMPTY_OBJECT_ARRAY;
Class>[] pts = DubboCodec.EMPTY_CLASS_ARRAY;
if (desc.length() > 0) {
// if (RpcUtils.isGenericCall(path, getMethodName()) || RpcUtils.isEcho(path, getMethodName())) {
// pts = ReflectUtils.desc2classArray(desc);
// } else {
ServiceRepository repository = ApplicationModel.getServiceRepository();
ServiceDescriptor serviceDescriptor = repository.lookupService(path);
if (serviceDescriptor != null) {
MethodDescriptor methodDescriptor = serviceDescriptor.getMethod(getMethodName(), desc);
if (methodDescriptor != null) {
pts = methodDescriptor.getParameterClasses();
this.setReturnTypes(methodDescriptor.getReturnTypes());
}
}
if (pts == DubboCodec.EMPTY_CLASS_ARRAY) {
if (!RpcUtils.isGenericCall(path, getMethodName()) && !RpcUtils.isEcho(path, getMethodName())) {
throw new IllegalArgumentException("Service not found:" + path + ", " + getMethodName());
}
pts = ReflectUtils.desc2classArray(desc);
}
// }
args = new Object[pts.length];
for (int i = 0; i < args.length; i++) {
try {
// 循环读取方法参数值
args[i] = in.readObject(pts[i]);
} catch (Exception e) {
if (log.isWarnEnabled()) {
log.warn("Decode argument failed: " + e.getMessage(), e);
}
}
}
}
setParameterTypes(pts);
// 读取隐式参数
Map map = in.readAttachments();
if (map != null && map.size() > 0) {
Map attachment = getObjectAttachments();
if (attachment == null) {
attachment = new HashMap<>();
}
attachment.putAll(map);
setObjectAttachments(attachment);
}
// 处理异步参数回调,如果有则在服务端创建reference代理实例
for (int i = 0; i < args.length; i++) {
args[i] = decodeInvocationArgument(channel, this, pts, i, args[i]);
}
setArguments(args);
String targetServiceName = buildKey((String) getAttachment(PATH_KEY),
getAttachment(GROUP_KEY),
getAttachment(VERSION_KEY));
setTargetServiceUniqueName(targetServiceName);
} catch (ClassNotFoundException e) {
throw new IOException(StringUtils.toString("Read invocation data failed.", e));
} finally {
if (in instanceof Cleanable) {
((Cleanable) in).cleanup();
}
}
return this;
解码请求时,严格按照客户端写数据的顺序处理。
解码响应和解码请求类似,调用的同样是DubboCodec#decodeBody,就是上面省略的部分,这里就不赘述了,重点看下响应体的解码,即DecodeableRpcResult#decode方法:
6.4 ChannelHandler
如果读者熟悉Netty,就很容易理解Dubbo内部使用的ChannelHandler组件的原理,Dubbo内部使用了大量的Handler组成类似链表,依次处理具体逻辑,包括编解码、心跳时间戳和方法调用Handler等。因为Nettty每次创建Handler都会经过ChannelPipeline,大量的事件经过很多Pipeline会有较多开销,因此Dubbo会将多个Handler聚合成一个Handler。(个人表示,这简直bullshit)
6.5.1 核心Handler和线程模型
Dubbo的Channelhandler有5中状态:
- connected Channel已经被创建
- disconnected Channel已经被断开
- sent 消息被发送
- received 消息被接收
- caught 捕获到异常
Dubbo针对每个特性都会实现对应的ChannelHandler,在讲解Handler的指责之前,我们Dubbo有哪些常用的Handler:
- ExchangeHandlerAdapter 用于查找服务方法并调用
- HeaderExchangeHandler 封装处理Request/Response和Telnet调用能力
- DecodeHandler 支持在Dubbo业务线程池中做解码
- ChannelHandlerDispatcher 封装多Handler广播调用
- AllChannelHandler 支持Dubbo业务线程池调用业务方法
- HeartbeatHandler 支持心跳处理
- MultiMessageHandler 支持流中多消息报文批处理
- ConnectionOrderedChannelHandler 单线程池处理TCP的连接和断开
- MessageOnlyChannelHandler 仅在线程池处理接受报文,其他事件在IO线程处理。
- WrappedChannelHandler 基于内存key-value存储封装和共享线程池能力。
- NettyServerHandler 封装Netty服务端事件,处理连接、断开、读取、写入和异常等。
- NettyClientHandler 封装Netty客户端事件,处理连接、断开、读取、写入和异常等。
Dubbo提供了大量的Handler去承载特性和扩展,这些Handler最终会和底层通信框架做关联,比如Netty等。一次完整的RPC调用贯穿了一系列的Handler,如果直接挂载到底层通信框架(Netty),因为整个链路比较长,则需要大量链式查找和事件,不仅低效,而且浪费资源。
下图展示了同时具有入站和出站ChannelHandler的布局,如果一个入站事件被触发,比如连接或数据读取,那么它会从ChannelPipeline头部一直传播到ChannelPipeline的尾端。出站的IO事件将从ChannelPipeline最右边开始,然后向左传播。当然ChannelPipeline传播时,会检测入站的是否实现了ChannelInboundHandler,出站会检测是否实现了ChannelOutboundHandler,如果没有实现,则自动跳过。Dubbo框架中实现这两个接口类主要是NettyServerHandler和NettyClientHandler。Dubbo通过装饰者模式包装Handler,从而不需要将每个Handler都追加到Pipeline中。因此NettyServer和NettyClient中最多有3个Handler,分别是编码、解码和NettyHandler。
注意区分两种ChannelHandler,一种是netty的inbound和outBoundHandler,只需要3个编码、解码和NettyHandler(这个应该是dubbo使用netty3的实现,netty4的实现分开NettyServerHandler和NettyClientHandler);另一种是Dubbo定义的SPI接口也叫ChannelHandler,上面罗列的这么多Dubbo定义的ChannelHandler就是实现的spi接口,包括NettyClient这个类也实现了ChannelHandler这个SPI接口,它利用SPI包装扩展的方式将多个handler都包装起来。然后NettyHandler会持有NettyClient的引用,所以真正的处理逻辑都有NettyClient来处理,进而一层层的调用到每一层包装的handler,所以整个调用链看起来会相当的复杂。
讲完Handler的流转机制后,我们再来探讨RPC调用Provider方处理Handler的逻辑,在DubboProtocol中通过内部类继承自ExchangeHandlerAdapter,完成服务提供方Invoker实例的查找并进行服务的真实调用。
如上是触发业务方法调用的关键,在服务暴露时服务端已经按照特定规则(端口、接口名、接口版本和接口分组)把实例Invoker存储到HashMap中,客户端调用过来时必须携带相同信息构造的key,找到对应Exporter(里面持有Invoker)然后调用。
我们先跟踪getInvoker的实现,会发现服务端唯一标识的服务由4部分组成:端口、接口名、接口版本和接口分组。
Dubbo为了编织这些Handler,适应不同的场景,提供了一套可以定制的线程模型。为了使概念更清晰,我们描述的IO线程是指底层直接负责读写报文,比如Netty线程池。Dubbo中提供的线程池负责业务方法调用,我们称为业务线程。如果一些事件逻辑可以很快执行完成,比如做个标记这种简单操作,则可以直接在IO线程中处理。如果是比较耗时的处理,比如读写数据库等操作,则应该将耗时或者阻塞的任务转到业务线程上执行。因为IO线程用于接受请求,如果IO线程饱和,则不会接受新的请求。
我们先看一下Dubbo中是如何实现线程派发的:
如上,Dispatcher是线程池的派发器。这里需要注意的是,Dispatcher真实的职责是创建有线程派发能力的ChannelHandler,比如AllChannelHandler、MessageOnlyChannelHandler和ExecutionChannelHanlder,其本身并不具备线程派发能力。
Dispatcher属于Dubbo中的扩展点,这个扩展点用来动态产生Handler,以满足不同的场景,目前Dubbo支持一下6种策略调用:
分发策略 | 分发实现 | 描述 |
---|---|---|
all | AllDispatcher | 将所有IO事件交给业务线程池,Dubbo默认启用 |
connection | ConnectionOrderedDispatcher | 单独线程池处理连接断开事件,和业务线程池分开 |
direct | DirectDispatcher | 所有方法调用和事件处理在IO线程池,不推荐 |
execution | ExecutionDispatcher | 只在业务线程池处理接受请求,其他都在IO线程池 |
message | MessageOnlyChannelHandler | 只在业务线程池处理请求和响应,其他在IO线程池 |
mockdispatcher | MockDispatcher | 默认返回null |
具体需要按照使用场景不同启用不同的策略,建议使用默认策略,如果在TCP连接中需要做安全或校验,则可以使用ConnectionOrderedDispatcher策略。如果引入新的线程池,则不可避免的导致额外的线程切换,用户可在Dubbo配置中指定dispatcher属性让具体策略生效。
6.4.2 Dubbo请求响应Hanlder
在Dubbo内部,所有方法调用都被抽象成Request/Response,每次调用都会创建一个Request,如果是方法调用则返回一个Response对象。HeaderExceptionExchangeHandler就是用了处理这种场景,主要负责4中事情:
(1) 更新发送和读取请求时间戳。
(2) 判断请求格式或编解码是否有错,并响应客户端失败的具体原因。
(3) 处理Request请求和Response正常响应。
(4) 支持Telnet调用。
我们先来看一下HeaderExchangeHandler#received实现:
笔记记到这里,发现原书中这章就快结束了,但仍然对远程调用过程稀里糊涂的。。。就放弃继续记录了,还是期待下一篇看看官网的对远程调用的讲述吧。