今天,我们来看看dubbo消费的执行过程

首先,我们都知道dubbo是一个基于netty实现的RPC框架,底层通信是使用netty来实现的。在学习dubbo的时候,或许我们都会有下面的这些疑惑:

1、服务消费者只持有服务接口,我们的消费端在执行接口请求的时候获取到的接口实现是什么?

2、消费者是如何通过netty建立同服务端的通信的?

3、服务是怎么注册到注册中心的?

4、消费端怎么拉取服务?

5、服务的负载均衡是如何体现的?

等等这些问题都会困扰着我们,今天我们先来聊聊dubbo消费端的实现原理

现在,你可能已经自己通过官网的教程搭建了自己的dubbo demo服务,你在执行demo的时候会发现,服务消费者只持有服务接口,你是通过@Reference注解去获取的实现,你已经知道spring bean工厂会自动为用户创建代理实例,那么dubbo为我们的消费者创建的代理实现是什么呢?只要开启idea的调试模式,你就可以看到我们得到的实现其实是:com.alibaba.dubbo.rpc.proxy.InvokerInvocationHandler。该Handler实现了InvocationHandler,InvocationHandler是JDK动态代理实现的核心接口,如果你不了解动态代理,那建议你自己去了解一下。

回到正题,我们通过接口调用的方法都会被该Handler代理,该Handler源码如下:

public class InvokerInvocationHandler implements InvocationHandler {

	private final Invoker<?> invoker;

	public InvokerInvocationHandler(Invoker<?> handler) {
this.invoker = handler;
} public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
if (method.getDeclaringClass() == Object.class) {
return method.invoke(invoker, args);
}
if ("toString".equals(methodName) && parameterTypes.length == 0) {
return invoker.toString();
}
if ("hashCode".equals(methodName) && parameterTypes.length == 0) {
return invoker.hashCode();
}
if ("equals".equals(methodName) && parameterTypes.length == 1) {
return invoker.equals(args[0]);
}
return invoker.invoke(new RpcInvocation(method, args)).recreate();
} }

源码很简单,只有一个invoke方法,它是代理类和接口之间的桥梁。如果你再细心一点,会发现InvokerInvocationHandler中的Invoker实现类是com.alibaba.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker。它是dubbo invoker的默认实现,里面封装了服务降级等功能。到这里,你基本已经知道消费者究竟是怎么去调用服务的了,后面你只要继续跟着源码调试,服务是如何和netty建立联系的

消费者请求调用链:

proxy0#sayHello(String)
—> InvokerInvocationHandler#invoke(Object, Method, Object[])
—> MockClusterInvoker#invoke(Invocation)
—> AbstractClusterInvoker#invoke(Invocation)
—> FailoverClusterInvoker#doInvoke(Invocation, List<Invoker<T>>, LoadBalance)
—> Filter#invoke(Invoker, Invocation) // 包含多个 Filter 调用
—> ListenerInvokerWrapper#invoke(Invocation)
—> AbstractInvoker#invoke(Invocation)
—> DubboInvoker#doInvoke(Invocation)
—> ReferenceCountExchangeClient#request(Object, int)
—> HeaderExchangeClient#request(Object, int)
—> HeaderExchangeChannel#request(Object, int)
—> AbstractPeer#send(Object)
—> AbstractClient#send(Object, boolean)
—> NettyChannel#send(Object, boolean)
—> NioClientSocketChannel#write(Object)

在MockClusterInvoker是一个抽象类,它的默认实现是FailoverClusterInvoker,在MockClusterInvoker中,通过服务目录Directory列举服务列表,核心方法invoke如下:

public Result invoke(final Invocation invocation) throws RpcException {
checkWhetherDestroyed();
LoadBalance loadbalance = null;
//列举服务列表
List<Invoker<T>> invokers = list(invocation);
if (invokers != null && !invokers.isEmpty()) {
loadbalance = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(invokers.get(0).getUrl()
.getMethodParameter(invocation.getMethodName(), Constants.LOADBALANCE_KEY, Constants.DEFAULT_LOADBALANCE));
}
RpcUtils.attachInvocationIdIfAsync(getUrl(), invocation);
return doInvoke(invocation, invokers, loadbalance);
}

服务目录Directory已知的实现有RegistryDirectory和StaticDirectiry,默认使用的是RegistryDirectory,在进行服务调用的时候会从这里面去获取可用的服务列表,如果想要了解更多,推荐阅读dubbo官网服务列表一章,里面有非常详细的介绍。

可以看到,获取服务列表之后会从系统扩展中加载默认的负载均衡实现,然后继续往下执行到子类FailoverClusterInvoker的模板方法doInvoke,该方法会重新执行服务列举并检查服务的可用性,之后通过负载均衡策略选择具体服务,dubbo默认负载均衡策略是随机RandomLoadBalance。关键代码如下:

public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
List<Invoker<T>> copyinvokers = invokers;
// 服务检查
checkInvokers(copyinvokers, invocation);
int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
if (len <= 0) {
len = 1;
}
// retry loop.
RpcException le = null; // last exception.
List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size()); // invoked invokers.
Set<String> providers = new HashSet<String>(len);
for (int i = 0; i < len; i++) {
//Reselect before retry to avoid a change of candidate `invokers`.
//NOTE: if `invokers` changed, then `invoked` also lose accuracy.
if (i > 0) {
// i>0,说明第一次服务调用失败,需要重新检查服务列表
checkWhetherDestroyed(); //如果服务已经销毁,抛出异常
copyinvokers = list(invocation);//重新列举服务
// check again
checkInvokers(copyinvokers, invocation);//检查服务是否为空
}
//负载均衡获取服务
Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);
invoked.add(invoker);//将获取的服务添加到已执行列表
RpcContext.getContext().setInvokers((List) invoked);
try {
//服务请求
Result result = invoker.invoke(invocation);
if (le != null && logger.isWarnEnabled()) {
logger.warn("Although retry the method " + invocation.getMethodName()
+ " in the service " + getInterface().getName()
+ " was successful by the provider " + invoker.getUrl().getAddress()
+ ", but there have been failed providers " + providers
+ " (" + providers.size() + "/" + copyinvokers.size()
+ ") from the registry " + directory.getUrl().getAddress()
+ " on the consumer " + NetUtils.getLocalHost()
+ " using the dubbo version " + Version.getVersion() + ". Last error is: "
+ le.getMessage(), le);
}
return result;
} catch (RpcException e) { }
// 异常处理部分省略。。。
} }

通过负载均衡获取到具体服务后,执行服务调用,到AbstractInvoker的invoke方法,主要设置一些attachment的信息。重点来看看实现类DubboInvoke的doInvoke方法,如下:

@Override
protected Result doInvoke(final Invocation invocation) throws Throwable {
RpcInvocation inv = (RpcInvocation) invocation;
final String methodName = RpcUtils.getMethodName(invocation);
//设置路径和版本
inv.setAttachment(Constants.PATH_KEY, getUrl().getPath());
inv.setAttachment(Constants.VERSION_KEY, version); //ExchangeClient
ExchangeClient currentClient;
if (clients.length == 1) {
currentClient = clients[0];
} else {
currentClient = clients[index.getAndIncrement() % clients.length];
}
try {
//从配置中获取是否同步执行
boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
//是否单向执行
boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
//请求超时时间
int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
if (isOneway) {
//如果单向执行,发起调用后立即返回一个空RpcResult
boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
currentClient.send(inv, isSent);
RpcContext.getContext().setFuture(null);
return new RpcResult();
} else if (isAsync) {
//如果异步,调用后立即返回一个空的RpcResult
ResponseFuture future = currentClient.request(inv, timeout);
RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));
return new RpcResult();
} else {
//否则,同步等待
RpcContext.getContext().setFuture(null);
return (Result) currentClient.request(inv, timeout).get();
}
} catch (TimeoutException e) {
throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
} catch (RemotingException e) {
throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e);
}
}

ExchangeClient接口的实现是HeaderExchangeClient,request方法很简单,如下

public ResponseFuture request(Object request, int timeout) throws RemotingException {
return channel.request(request, timeout);
}

就是做了一次请求转发,channel是ExchangeChannel,默认实现是HeaderExchangeChannel,HeaderExchangeChannel的request方法如下:

public ResponseFuture request(Object request, int timeout) throws RemotingException {
if (closed) {
throw new RemotingException(this.getLocalAddress(), null, "Failed to send request " + request + ", cause: The channel " + this + " is closed!");
}
// create request.
Request req = new Request();
req.setVersion("2.0.0");//版本号
req.setTwoWay(true);//双向通信
req.setData(request);//请求数据
DefaultFuture future = new DefaultFuture(channel, req, timeout);//Future
try {
channel.send(req);//Dubbo封装的通信Channel
} catch (RemotingException e) {
future.cancel();
throw e;
}
return future;
}

这里就是构造Request请求体,然后传递给NettyChannel,NettyChannel封装了netty的channel,通过该channel将数据请求写入到TCP请求中传递给服务端。NettyChannel的send方法如下

public void send(Object message, boolean sent) throws RemotingException {
super.send(message, sent); boolean success = true;
int timeout = 0;
try {
//数据请求
ChannelFuture future = channel.write(message);//此处的channel才是org.jboss.netty.channel.Channel
if (sent) {
//等待请求结果
timeout = getUrl().getPositiveParameter(Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
success = future.await(timeout);
}
Throwable cause = future.getCause();
if (cause != null) {
throw cause;
}
} catch (Throwable e) {
throw new RemotingException(this, "Failed to send message " + message + " to " + getRemoteAddress() + ", cause: " + e.getMessage(), e);
} if (!success) {
throw new RemotingException(this, "Failed to send message " + message + " to " + getRemoteAddress()
+ "in timeout(" + timeout + "ms) limit");
}
}

到这里,关于dubbo消费请求的基本流程已经走完,继续往下就是netty层面的东西了,有兴趣的童鞋可以自行寻找netty相关教程

dubbo入门之服务消费的更多相关文章

  1. dubbo入门之微服务客户端服务端配置

    正常一个服务不会只做客户端或者只做服务端,一般的微服务都是服务与服务相互调用,那么,应该怎么配置呢?接着之前的dubbo入门之helloWorld,我们再改改配置,即可实现正常的微服务架构.与之前相比 ...

  2. Spring Cloud Alibaba(四)实现Dubbo服务消费

    本项目演示如何使用 Spring Cloud Alibaba 完成 Dubbo 的RPC调用. Spring Cloud与Dubbo Spring Cloud是一套完整的微服务架构方案 Dubbo是国 ...

  3. Dubbo学习笔记10:Dubbo服务消费方启动流程源码分析

    同理我们看下服务消费端启动流程时序图: 在<Dubbo整体架构分析>一文中,我们提到服务消费方需要使用ReferenceConfig API来消费服务,具体是调用代码(1)get()方法来 ...

  4. Dubbo学习笔记4:服务消费端泛化调用与异步调用

    本文借用dubbo.learn的Dubbo API方式来解释原理. 服务消费端泛化调用 前面我们讲解到,基于Spring和基于Dubbo API方式搭建简单的分布式系统时,服务消费端引入了一个SDK二 ...

  5. 分布式服务框架dubbo入门实例

    dubbo是一个分布式的服务架构,可直接用于生产环境作为SOA服务框架. 官网首页:http://dubbo.io/ ,官方用户指南 http://dubbo.io/User+Guide-zh.htm ...

  6. dubbo入门教程-从零搭建dubbo服务

    [原创 转载请注明出处] 本文是学习了dubbo之后自己手动写的,比较通俗,很多都是自己学习之后的理解,写的过程中没有参考任何文章. 另外dubbo也有官方文档,但是比较官方,也可以多看看dubbo的 ...

  7. Dubbo入门到精通学习笔记(二):Dubbo管理控制台、使用Maven构建Dubbo的jar包、在Linux上部署Dubbo privider服务(shell脚本)、部署consumer服务

    文章目录 Dubbo管理控制台 1.Dubbo管理控制台的主要作用: 2.管理控制台主要包含: 3.管理控制台版本: 安装 Dubbo 管理控制台 使用Maven构建Dubbo服务的可执行jar包 D ...

  8. Dubbo之服务消费

    Dubbo的服务消费主要包括两个部分.第一大步是ReferenceConfig类的init方法调用Protocol的refer方法生成Invoker实例,这是服务消息的关键.第二大步是把Invoker ...

  9. 源码分析Dubbo服务消费端启动流程

    通过前面文章详解,我们知道Dubbo服务消费者标签dubbo:reference最终会在Spring容器中创建一个对应的ReferenceBean实例,而ReferenceBean实现了Spring生 ...

随机推荐

  1. maven环境变量配置及测试

    首先要确认,你是否已经安装了jdk,并且已经成功配置了环境变量. 和java配置类似,计算机右键属性->高级系统设置->环境变量. 添加如下变量: 变量名:maven_home 变量值:C ...

  2. Ubuntu16.04+cuda9.0安装教程

    1.安装NVIDIA驱动 首先去官网(http://www.nvidia.cn/Download/index.aspx?lang=cn)查找适配自己电脑GPU的驱动,我的电脑驱动版本如下: 执行如下语 ...

  3. sql语句练习50题(Mysql版-详加注释)

    表名和字段 1.学生表       Student(s_id,s_name,s_birth,s_sex) --学生编号,学生姓名, 出生年月,学生性别 2.课程表       Course(c_id, ...

  4. 使用 Audacity 录制声卡声音

    在Linux中使用 Audacity 录制电脑播放的声音非常简单,其实主要设置不在 Audacity 上,而是要设置好输入设备并选择对录音输入源. 首先确认输入设备中 内置音频的Monitor 没有被 ...

  5. shell script 二 判断符号【】 shift 偏移量 if then fi

    判断符号[]类似于test.但是[]有通配符及正则表达式,为了区分,利用[]来做判断时,前后都需要加空格来区分.又一个坑 [ -z "$HOME" ];echo $? 例: 1 r ...

  6. Fiddler,对数据进行抓包,拦截,修改等操作

    转载....... 一.fiddler设置fiddler默认是只能抓取http网络格式的,所以我们要先设置下使fiddler可以获取到https网络格式 首先tools→options→https进去 ...

  7. xshell本地上传文件到Ubuntu上及从Ubuntu上下载文件到本地

    1.第一种方法是最常用的 :如果下载了Xshell和Xftp,Ctrl+Alt+F就可以选择文件的互传了!(虚拟机/云服务器通用)--只要相互间能ping得通. 2.第二种方法 :ubuntu环境下安 ...

  8. 【Nacos】本地集群部署

    关于Nacos已经展开了四篇入门文章: 初探Nacos(一)-- 单机模式启动 初探Nacos(二)-- SpringCloud使用Nacos的服务注册与发现 初探Nacos(三)-- SpringB ...

  9. 基础(一):SCSI硬盘与IDE硬盘有什么区别

    硬盘接口是硬盘与主机系统间的连接部件,作用是在硬盘缓存和主机内存之间传输数据.不同的硬盘接口决定着硬盘与计算机之间的连接速度,在整个系统中,硬盘接口的优劣直接影响着程序运行快慢和系统性能好坏.从整体的 ...

  10. URAL 1996. Cipher Message 3(KMP+fft)

    传送门 解题思路 因为要完全匹配,所以前七位必须保证相同,那么就可以把前7位提出来做一遍\(kmp\)匹配,最后的答案一定在这些位置里.考虑最后一位,可以把最后一位单独取出来,要计算的是最后一位相同的 ...