《一起玩Dubbo》系列四之服务如何被调用

开发 架构
了解过rpc的大概都听过,rpc就是为了解决远程方法的本地调用的难题的,其实说穿了,就是为了解决方法在被调用到远程服被执行的流程问题,那么这个流程到底是怎么样的呢?

[[411936]]

了解过rpc的大概都听过,rpc就是为了解决远程方法的本地调用的难题的,其实说穿了,就是为了解决方法在被调用到远程服被执行的流程问题,那么这个流程到底是怎么样的呢?

同样的,我继续在 dubbo流程图 中继续绘画我的流程

首先是根据文章一起玩dubbo,先入个门搭建起demo,包括注册中心、服务消费方和服务提供方,接下来来撸撸整个过程

这边为了方便解说,先直接给个demo

这是服务提供方

  1. public class DemoServiceImpl implements DemoService { 
  2.  
  3.     @Override 
  4.     public String sayHello(String name) { 
  5.         System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "] Hello " + name + ", request from consumer: " + RpcContext.getContext().getRemoteAddress()); 
  6.         return "Hello " + name + ", response from provider: " + RpcContext.getContext().getLocalAddress(); 
  7.     } 

这是服务消费方

  1. public class Consumer { 
  2.  
  3.     public static void main(String[] args) { 
  4.         ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"META-INF/spring" + 
  5.                 "/dubbo-demo-consumer.xml"}); 
  6.         context.start(); 
  7.         DemoService demoService = (DemoService) context.getBean("demoService"); 
  8.         while (true) { 
  9.             try { 
  10.                 Thread.sleep(1000); 
  11.                 String hello = demoService.sayHello("world"); 
  12.                 System.out.println(hello); 
  13.  
  14.             } catch (Throwable throwable) { 
  15.                 throwable.printStackTrace(); 
  16.             } 
  17.         } 
  18.     } 

我断点了下这里

image-20210714015553406

走到服务消费方的最底层可以看到

在开始分析细节之前我们先在大脑风暴下大致流程

一次调用过程需要经历哪些步骤?

不用看dubbo代码都可以大概猜到:

  • 要知道远程服务的地址,
  • 把要调用的方法的具体信息告诉远程服务,让远程服务解析这些信息
  • 远程服务根据这些信息找到对应的实现类,进行调用,调用完了
  • 调用结果原路返回,然后客户端解析响应

第一点,我们通过前几篇文章已经知道,消费方在发起调用的时候已经知晓了远程服务的地址

那么要调用的方法的具体信息包括哪些呢?

客户端肯定要告诉服务方调用的哪个接口,所以需要方法名、方法的参数类型、方法的参数值,然后有可能存在多个版本的情况,所以还得带上版本号,有这些数据后,服务方就可以精准的调用具体的方法了。

我这边将上面调用的例子先贴出来

mdata也就是我上面说的那些数据。

看到这个Request这里,应该就清楚了远程调用的基本原理了。

这个时候很容易就想到另一个问题,消费方和提供方是如何通信的?

消费方和提供方如何通信?

其实很简单,就是消费方和提供方通过协议进行了通信罢了,dubbo的协议属于很常见的header+body 形式,而且也有特殊的字符 0xdabb,用来解决 TCP 网络粘包问题的。这种header是固定长度的,然后header里面填写 body 的长度是比较常见的做法,包括我司的游戏框架也是用这种模式。

我们可以看看dubbo协议的鬼样

可以看到,协议分为协议头和协议体,16 字节的头部主要携带了魔法数,也就是之前说的 0xdabb,然后一些请求的设置,消息体的长度等等,16 字节之后就是协议体了,包括协议版本、接口名字、接口版本、方法名字等等。

看到这里又很容易的引申出另一个问题了,协议是如何序列化的?

协议的序列化?

序列化的概念其实也简答, 在消费方先把Java对象转换为字节序列,这个过程也被称为对象的序列化,然后在服务方又把字节序列恢复为Java对象,这个过程称为对象的反序列化。

dubbo默认使用的是 hessian2 序列化协议,hessian2是阿里对于hessian进行行了修改的版本,应该还不错。

大致总结下,消费方发起调用,在那一刻,实际调用的是代理类,代理类最终调用的是Client,Client将 Java 的对象序列化生成协议体,然后通过网络传输给服务方,服务方Server接到这个请求之后,分发给业务线程池,由业务线程调用具体的实现方法。

先see see官网图吧

分析下消费方的调用链路

我们先看看服务消费方的调用逻辑,大家可以对着我这张图来

好了,我继续说

可以看到调用的接口生成的代理类是

而在invoke的时候会先释放掉部分不需要拦截的方法啦,比如toString什么的,这样正常吧,这些方法确实不需要拦截的嘛

看看RpcInvocation是什么

可以看到生成的 RpcInvocation 包含了方法名、参数类和参数值什么的。

接下来往里进一步看看MockClusterInvoker#invoke 代码,先解释下为啥会进来了MockClusterInvoker,看过文章 想学dubbo的看过来,2万字整理服务引入流程 应该可以理解这个过程,这个过程可以认为是套娃吧,A套B,B套C,一直套到最外层的invoker就是MockClusterInvoker,如果不理解这个过程可以往回看我的文章,很肝却很实用

这里可以看到就是判断配置里面有没有配置mock,mock 的话后续再展开说,继续看看this.invoker.invoke 的实现,实际上会调用 AbstractClusterInvoker#invoker

这里倒是涉及到了一个模板方法的设计模式,其实很简单,就是在抽象类中定好代码的执行骨架,之后将具体的实现延迟到子类中,由子类来决定逻辑,这样可以在不改变整体执行步骤的情况下修改步骤里面的实现,减少了重复的代码,也利于扩展,符合了开闭原则。

接下来看看

做了啥,这一步算是比较重要吧,单独拎出来讲讲

这里其实就是先由路由过滤一波,然后返回invoker

继续看看doInvoke的流程,我们默认使用的是 FailoverClusterInvoker,也就是失败自动切换的容错方式,

这里说说为啥默认是这个哦,其实从实际应用上来说,失败后自动切换下个服务实例还是比较符合场景的,如果想替换其他模式可以在xml里边配置

那我们继续看看那doInvoke的实现

  1. public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException { 
  2.     List<Invoker<T>> copyinvokers = invokers; 
  3.     checkInvokers(copyinvokers, invocation); 
  4.     String methodName = RpcUtils.getMethodName(invocation); 
  5.     int len = getUrl().getMethodParameter(methodName, Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1; 
  6.     if (len <= 0) { 
  7.         len = 1; 
  8.     } 
  9.     // retry loop. 
  10.     RpcException le = null; // last exception. 
  11.     List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size()); // invoked invokers. 
  12.     Set<String> providers = new HashSet<String>(len); 
  13.     // 重试次数 
  14.     for (int i = 0; i < len; i++) { 
  15.         //Reselect before retry to avoid a change of candidate `invokers`. 
  16.         //NOTE: if `invokers` changed, then `invoked` also lose accuracy. 
  17.         if (i > 0) { 
  18.             checkWhetherDestroyed(); 
  19.             copyinvokers = list(invocation); 
  20.             // check again 
  21.             checkInvokers(copyinvokers, invocation); 
  22.         } 
  23.         // 通过负载选择了一个invoker 
  24.         Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked); 
  25.         invoked.add(invoker); 
  26.         // 上下文保留了调用过的invoker 
  27.         RpcContext.getContext().setInvokers((List) invoked); 
  28.         try { 
  29.             // 发起调用 
  30.             Result result = invoker.invoke(invocation); 
  31.             if (le != null && logger.isWarnEnabled()) { 
  32.                 logger.warn("Although retry the method " + methodName 
  33.                         + " in the service " + getInterface().getName() 
  34.                         + " was successful by the provider " + invoker.getUrl().getAddress() 
  35.                         + ", but there have been failed providers " + providers 
  36.                         + " (" + providers.size() + "/" + copyinvokers.size() 
  37.                         + ") from the registry " + directory.getUrl().getAddress() 
  38.                         + " on the consumer " + NetUtils.getLocalHost() 
  39.                         + " using the dubbo version " + Version.getVersion() + ". Last error is: " 
  40.                         + le.getMessage(), le); 
  41.             } 
  42.             return result; 
  43.         } catch (RpcException e) { 
  44.             if (e.isBiz()) { // biz exception. 
  45.                 throw e; 
  46.             } 
  47.             le = e; 
  48.         } catch (Throwable e) { 
  49.             le = new RpcException(e.getMessage(), e); 
  50.         } finally { 
  51.             providers.add(invoker.getUrl().getAddress()); 
  52.         } 
  53.     } 
  54.     throw new RpcException(le != null ? le.getCode() : 0, "Failed to invoke the method " 
  55.             + methodName + " in the service " + getInterface().getName() 
  56.             + ". Tried " + len + " times of the providers " + providers 
  57.             + " (" + providers.size() + "/" + copyinvokers.size() 
  58.             + ") from the registry " + directory.getUrl().getAddress() 
  59.             + " on the consumer " + NetUtils.getLocalHost() + " using the dubbo version " 
  60.             + Version.getVersion() + ". Last error is: " 
  61.             + (le != null ? le.getMessage() : ""), le != null && le.getCause() != null ? le.getCause() : le); 

这个调用稍微总结一下就是FailoverClusterInvoker 拿到 Directory 返回的 Invoker 列表,并且经过路由之后,通过LoadBalance 从 Invoker 列表中选择一个 Invoker,也就是负载均衡啦,最后FailoverClusterInvoker会将参数传给选择出的那个 Invoker 实例的 invoke 方法,进行真正的远程调用。

后面发起调用的这个 invoke 又是调用抽象类中的 invoke 然后再调用子类的 doInvoker,抽象类中的方法很简单我就不展示了,我们直接看子类 DubboInvoker 的 doInvoke 方法。

  1. protected Result doInvoke(final Invocation invocation) throws Throwable { 
  2.     RpcInvocation inv = (RpcInvocation) invocation; 
  3.     final String methodName = RpcUtils.getMethodName(invocation); 
  4.     inv.setAttachment(Constants.PATH_KEY, getUrl().getPath()); 
  5.     inv.setAttachment(Constants.VERSION_KEY, version); 
  6.  
  7.     ExchangeClient currentClient; 
  8.     // 选择client 
  9.     if (clients.length == 1) { 
  10.         currentClient = clients[0]; 
  11.     } else { 
  12.         currentClient = clients[index.getAndIncrement() % clients.length]; 
  13.     } 
  14.     try { 
  15.         // 是否异步 
  16.         boolean isAsync = RpcUtils.isAsync(getUrl(), invocation); 
  17.         // 是否oneway方式发送,也就是需不需要返回值 
  18.         boolean isOneway = RpcUtils.isOneway(getUrl(), invocation); 
  19.         // 超时时间 
  20.         int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT); 
  21.        // 不需要返回值 
  22.         if (isOneway) { 
  23.             boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false); 
  24.             // 协议发送 
  25.             currentClient.send(inv, isSent); 
  26.             // future直接是Null 
  27.             RpcContext.getContext().setFuture(null); 
  28.             // 返回空的结果 
  29.             return new RpcResult(); 
  30.         } else if (isAsync) { 
  31.             // 异步发送 
  32.             ResponseFuture future = currentClient.request(inv, timeout); 
  33.             // 设置future 
  34.             RpcContext.getContext().setFuture(new FutureAdapter<Object>(future)); 
  35.             // 返回空结果 
  36.             return new RpcResult(); 
  37.         } else { 
  38.             // 同步发送 
  39.             RpcContext.getContext().setFuture(null); 
  40.             // 直接调用了future.get去等待 
  41.             return (Result) currentClient.request(inv, timeout).get(); 
  42.         } 
  43.     } catch (TimeoutException e) { 
  44.         throw new RpcException(RpcException.TIMEOUT_EXCEPTION, "Invoke remote method timeout. method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e); 
  45.     } catch (RemotingException e) { 
  46.         throw new RpcException(RpcException.NETWORK_EXCEPTION, "Failed to invoke remote method: " + invocation.getMethodName() + ", provider: " + getUrl() + ", cause: " + e.getMessage(), e); 
  47.     } 

这里可以看到调用的方式有三种,分别是 oneway、异步、同步,我分别说说

  • oneway是比较常见的方式了,就是当我们不关心请求是否发送成功的情况下,就用 oneway 的方式发送,这种方式消耗最小。
  • 异步调用,我们可以看到其实 Dubbo 天然支持异步的,client 发送请求之后会得到一个 ResponseFuture,然后把 future 包装一下塞到上下文中,这样用户就可以从上下文中拿到这个 future,然后调用方可以做了一波操作之后再调用 future.whenComplete什么的异步做点什么。
  • 同步调用,Dubbo 底层也帮我们做了,可以看到在 Dubbo 源码中就调用了 future.get,所以给我们的感觉就是我调用了这个接口的方法之后就阻塞住了,必须要等待结果到了之后才能返回,所以就是同步的。

那么这个回调是怎么做的?

其实很简单的,就是在调用的时候生成一个唯一的id,将回调和这个id缓存起来,然后将这个id传递到服务方,服务方在处理好业务后将结果和这个id重新发回到消费方,消费方拿到回调触发即可。

我们看看代码层面的

看看DefaultFuture是什么

看到啦,里边生成了唯一id,然后放到FUTURES这个并发容器里边,我们看看用的地方

这里比较清楚了吧,在收到返回的协议后将future拿出来去触发,基于这种思路,很多做回调的都可以用这种设计思路。

到这里服务消费方怎么去触发rpc的这个行为基本上就到这了,其实还是很清晰的,先是起服订阅的时候层层封装了invoker,然后搞出了一个代理对象注入到我们的接口中,然后在调用接口的时候就一个个调用invoker啦,最后就是发协议给服务提供方。

爱了爱了,简单清晰的逻辑。

接下来说说服务提供方的调用流程。

分析下提供方的调用电路

同样的,我们先看看服务提供方的调用链

这个流程也是特别长的,我这边只拎几个重点出来,先看下HeaderExchangeHandler,handleRequest

这里很容易理解啦,就是把request对象中的data取出来传到DubboProtocol.requestHandler中,这个data就是前面的解码后的DecodeableRpcInvocation对象它是Invocation接口的一个实现,我们可以看看里边有啥

可以看到调用信息都在这里啦,接下来就简单了,根据这些参数拿到对应的对象反射调用下就可以了,接下来看看DubboProtocol比较核心的reply方法

  1. @Override 
  2. public Object reply(ExchangeChannel channel, Object message) throws RemotingException { 
  3.     if (message instanceof Invocation) { 
  4.         Invocation inv = (Invocation) message; 
  5.         // 根据调用的参数拿到对应的invoker,其实就是之前服务暴露的时候有说过的Exporter里边取的 
  6.         Invoker<?> invoker = getInvoker(channel, inv); 
  7.         // 这里边是对callback回来的一些处理,先不管 
  8.         if (Boolean.TRUE.toString().equals(inv.getAttachments().get(IS_CALLBACK_SERVICE_INVOKE))) { 
  9.             String methodsStr = invoker.getUrl().getParameters().get("methods"); 
  10.             boolean hasMethod = false
  11.             if (methodsStr == null || methodsStr.indexOf(",") == -1) { 
  12.                 hasMethod = inv.getMethodName().equals(methodsStr); 
  13.             } else { 
  14.                 String[] methods = methodsStr.split(","); 
  15.                 for (String method : methods) { 
  16.                     if (inv.getMethodName().equals(method)) { 
  17.                         hasMethod = true
  18.                         break; 
  19.                     } 
  20.                 } 
  21.             } 
  22.             if (!hasMethod) { 
  23.                 logger.warn(new IllegalStateException("The methodName " + inv.getMethodName() 
  24.                         + " not found in callback service interface ,invoke will be ignored." 
  25.                         + " please update the api interface. url is:" 
  26.                         + invoker.getUrl()) + " ,invocation is :" + inv); 
  27.                 return null
  28.             } 
  29.         } 
  30.         RpcContext.getContext().setRemoteAddress(channel.getRemoteAddress()); 
  31.         // 最后invoke一下啦 
  32.         return invoker.invoke(inv); 
  33.     } 
  34.     throw new RemotingException(channel, "Unsupported request: " 
  35.             + (message == null ? null : (message.getClass().getName() + ": " + message)) 
  36.             + ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress()); 

getInvoker的逻辑也简单,之前的文章服务暴露有说过这个过程啦,其实就是从一个DubboProtocol.exporterMap内找到一个Exporter,再从里边取出invoker,那么key是啥呢,key其实是由URL生成的serviceKey,此时通过Invocation中的信息就可还原该serviceKey并且找到对应的Exporter和Invoker。再看看之前提过的 JavassistProxyFactory,这是一个给提供方的服务对象生成代理的工厂类

这个也说过啦,调用invoker.invoke时,通过反射调用最终的服务实现执行相关逻辑,入口就是这里了。因为这块之前的文章比较详细的说过,这里就不重复了。

到了这一步,调用就已经技术了,我们再看看调用结束后怎么将结果返回给服务消费方。

调用结束后,服务提供方方就会创建一个Response对象返回给服务消费方,那么自然在执行服务实现时会出现两种结果:成功和失败

如果成功的话,则把返回值设置到Response的result中,Response的status设置成OK

如果失败,把失败异常设置到Response的errorMessage中,status设置成SERVICE_ERROR

我们会回到HeaderExchangeHandler.received中的代码来看看,在handleRequest之后,调用channel.send把Response发送到客户端,这个channel封装客户端-服务端通信链路,最终会调用Netty框架,把响应写回到客户端。

惯例总结下

终于将调用这个过程说完啦,其实思路还是比较清晰的,不过最好是自己全程断点细看下啦,可以学到很多东西的。

说说后续安排:

  • SPI
  • dubbo中的AOP机制
  • 服务治理
  • ....

 

 

 

等好几个模块,最后就是带大家撸一个RPC框架了,还是那句话,想学dubbo的可以持续关注这一系列。

 

责任编辑:武晓燕 来源: 稀饭下雪
相关推荐

2021-05-13 06:21:26

Dubbo框架RPC

2021-06-01 08:29:08

dubbo线程池服务暴露

2021-08-27 07:06:09

DubboDocker技术

2023-03-02 07:44:39

pixijsWebGL

2023-06-28 07:49:02

2023-11-13 22:27:53

Mapping数据库

2022-02-23 14:43:50

索引数据库mysql

2012-11-08 17:33:53

智慧云

2013-11-21 10:52:34

NVIDIAIBM超级计算机

2023-11-13 12:48:32

语言DSL

2023-11-30 15:23:07

聚合查询数据分析

2021-03-04 08:06:17

Redis面试模型

2021-10-20 16:13:05

鸿蒙HarmonyOS应用

2024-07-29 08:24:43

2022-02-02 21:17:19

Eslint字段配置

2024-06-07 14:54:55

2022-05-20 12:14:50

ZuulSpringClou

2011-09-07 22:59:07

联想一体机

2021-05-17 10:50:15

系统调用内核

2015-08-21 09:48:11

女朋友编程学习编程
点赞
收藏

51CTO技术栈公众号