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

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

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

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

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

这是服务提供方

public class DemoServiceImpl implements DemoService {

    @Override
    public String sayHello(String name) {
        System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "] Hello " + name + ", request from consumer: " + RpcContext.getContext().getRemoteAddress());
        return "Hello " + name + ", response from provider: " + RpcContext.getContext().getLocalAddress();
    }
}

这是服务消费方

public class Consumer {

    public static void main(String[] args) {
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"META-INF/spring" +
                "/dubbo-demo-consumer.xml"});
        context.start();
        DemoService demoService = (DemoService) context.getBean("demoService");
        while (true) {
            try {
                Thread.sleep(1000);
                String hello = demoService.sayHello("world");
                System.out.println(hello);

            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
    }
}

我断点了下这里

image-20210714015553406

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

image-20210714015755764

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

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

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

  • 要知道远程服务的地址,

  • 把要调用的方法的具体信息告诉远程服务,让远程服务解析这些信息

  • 远程服务根据这些信息找到对应的实现类,进行调用,调用完了

  • 调用结果原路返回,然后客户端解析响应

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

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

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

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

image-20210714215428485

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

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

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

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

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

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

image-20210714220141931

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

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

协议的序列化?

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

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

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

先see see官网图吧

image-20210714221940413

分析下消费方的调用链路

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

image-20210714015755764

好了,我继续说

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

image-20210714222312343

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

image-20210714222141035

看看RpcInvocation是什么

image-20210714222355534

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

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

image-20210714222613092

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

image-20210714222949861

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

接下来看看

image-20210714223131956

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

image-20210714223258911

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

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

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

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

public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
    List<Invoker<T>> copyinvokers = invokers;
    checkInvokers(copyinvokers, invocation);
    String methodName = RpcUtils.getMethodName(invocation);
    int len = getUrl().getMethodParameter(methodName, 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) {
            checkWhetherDestroyed();
            copyinvokers = list(invocation);
            // check again
            checkInvokers(copyinvokers, invocation);
        }
        // 通过负载选择了一个invoker
        Invoker<T> invoker = select(loadbalance, invocation, copyinvokers, invoked);
        invoked.add(invoker);
        // 上下文保留了调用过的invoker
        RpcContext.getContext().setInvokers((List) invoked);
        try {
            // 发起调用
            Result result = invoker.invoke(invocation);
            if (le != null && logger.isWarnEnabled()) {
                logger.warn("Although retry the method " + methodName
                        + " 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) {
            if (e.isBiz()) { // biz exception.
                throw e;
            }
            le = e;
        } catch (Throwable e) {
            le = new RpcException(e.getMessage(), e);
        } finally {
            providers.add(invoker.getUrl().getAddress());
        }
    }
    throw new RpcException(le != null ? le.getCode() : 0, "Failed to invoke the method "
            + methodName + " in the service " + getInterface().getName()
            + ". Tried " + len + " times of the 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 != 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 方法。

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 currentClient;
    // 选择client
    if (clients.length == 1) {
        currentClient = clients[0];
    } else {
        currentClient = clients[index.getAndIncrement() % clients.length];
    }
    try {
        // 是否异步
        boolean isAsync = RpcUtils.isAsync(getUrl(), invocation);
        // 是否oneway方式发送,也就是需不需要返回值
        boolean isOneway = RpcUtils.isOneway(getUrl(), invocation);
        // 超时时间
        int timeout = getUrl().getMethodParameter(methodName, Constants.TIMEOUT_KEY, Constants.DEFAULT_TIMEOUT);
       // 不需要返回值
        if (isOneway) {
            boolean isSent = getUrl().getMethodParameter(methodName, Constants.SENT_KEY, false);
            // 协议发送
            currentClient.send(inv, isSent);
            // future直接是Null
            RpcContext.getContext().setFuture(null);
            // 返回空的结果
            return new RpcResult();
        } else if (isAsync) {
            // 异步发送
            ResponseFuture future = currentClient.request(inv, timeout);
            // 设置future
            RpcContext.getContext().setFuture(new FutureAdapter<Object>(future));
            // 返回空结果
            return new RpcResult();
        } else {
            // 同步发送
            RpcContext.getContext().setFuture(null);
            // 直接调用了future.get去等待
            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);
    }
}

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

  • oneway是比较常见的方式了,就是当我们不关心请求是否发送成功的情况下,就用 oneway 的方式发送,这种方式消耗最小。

  • 异步调用,我们可以看到其实 Dubbo 天然支持异步的,client 发送请求之后会得到一个 ResponseFuture,然后把 future 包装一下塞到上下文中,这样用户就可以从上下文中拿到这个 future,然后调用方可以做了一波操作之后再调用 future.whenComplete什么的异步做点什么。

  • 同步调用,Dubbo 底层也帮我们做了,可以看到在 Dubbo 源码中就调用了 future.get,所以给我们的感觉就是我调用了这个接口的方法之后就阻塞住了,必须要等待结果到了之后才能返回,所以就是同步的。

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

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

我们看看代码层面的

image-20210717144959403

看看DefaultFuture是什么

image-20210717145144137

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

image-20210717145302362

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

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

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

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

分析下提供方的调用电路

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

image-20210717150216620

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

image-20210717160411314

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

image-20210717160800818

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

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

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

image-20210717161805781

这个也说过啦,调用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的可以持续关注这一系列。

dubbo还不知道怎么发音,3.0.0版本已经来了


想学dubbo的看过来,2万字整理服务引入流程《一起玩dubbo系列第三篇》


一起玩dubbo,万字长文揭秘服务暴露


一起玩dubbo,先入个门


小饭饭:某游戏大厂高级开发,专门和主管抬杠的小组长。 想学dubbo的可以微信搜一搜:稀饭下雪,第一时间阅读Caffeine、dubbo等优秀框架的源码分析和教材讲解以及实际应用,有需要java面试资料的也可以关注一波,回复java资源获取我整理的专项资料。

点个在看,支持一下小饭饭啦

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值