ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Dubbo之从服务消费端说起

2021-12-05 22:32:43  阅读:170  来源: 互联网

标签:Dubbo 调用 服务 说起 接口 RPC Invoker throws


既然Dubbo是一个RPC框架,我们就从分解一个RPC框架开始,然后把Dubbo中的模块和代码对应到RPC的每个部分上。

RPC框架分解

本地调用

本地进程内调用就不用多讲了,我们每天写的代码大部分都是这个
EchoService service = new EchoServiceImpl();
    service.sayHello("Dubbo"); 
虽然是调用的接口,但是实际上调用的是实现类的方法,所谓的面向对象的多态性。对于本地进程内调用来说,因为jvm可以直接加载到实现类,所以调用接口方法和调用实现类方法没有任何区别。

远程调用

随着应用从单机发展到分布式,某个接口的实现可能部署到了其它机器上,这个时候要调用方法就变成了下图这样:   左边是调用方,一般把它叫Consumer,Consumer这边只有接口定义(一般叫api),没有接口实现。Consumer发起请求之后,请求通过网络发送给右边的服务实现方,一般叫Provider。Provider收到请求,调用本地的实现类得到结果,然后把结果通过网络发回去,这样就叫一次远程调用了。当然,还有一种Provider不直接返回结果,而是通过异步回调的方式,原理差别不大。 如果上面这个过程全由用户自己实现的话,事还是很多的。如果有一个中间层干两件事,1)让Consumer端只要调用接口,不用管接口后面是本地实现类还是需要远程网络传输;2)Provider端只要把实现类放在那里,不用管调你的人是谁,按正常返回结果就好了,中间层把其它的事全做了,这个中间层就是RPC框架了。 当然,主流的框架在中间还提供了容错、监控这些功能,原则上说这些都不是RPC的本质功能。下面我们采用从中间往两边扩散的方式来分解下RPC的职责。

序列化

程序中接口调用传的是对象,实际上是一堆只有虚拟机能读的内存地址,而网络上能传的是0和1组成的字节数据,所以就有一个转换的过程,就是我们常说的序列化。常见的序列化协议如json、Protobuf等。加了序列化之后,用户调用时传参的对象实例,比如一个User,按照一个固定的格式转换,比如转成json字符串,provider侧拿到这个json,再把它转成一个User。所以User定义和接口定义一样,也是在双方约定的api定义中。这个定义不一定是个jar包,因为对方不见得是java写的,这就是序列化存在的另外一个意义。  

网络通信

网络上传输的是一个个数据包组成的数据流,数据在经过一个个网络中间节点的时候,会不断的被合并成大个数据包或者拆分成小的。所以要依赖网络协议来在对端重新组合数据包。流行的rpc框架一般都采用基于tcp协议的上层协议,比如http,当然也有很多跟Dubbo一样使用基于tcp的私有协议。封装网络协议的模块一般叫Codec,里面包含编码(encode)和解码(decode)方法。 所以上面的图就变成这样:   Codec只负责对象和0/1之间转换,但是网络通信还包括建立和Provider的链接并发送和接收数据包。这时候又会抽象出一个传输层(Transporter)专门负责建立和关闭链接,维护链接池,处理连接和断开的事件。在java中,netty是应用最多的框架。 所以,上面的图又变成这样了: 0 Transporter层通常不会直接接收Consumer发过来的对象,原因嘛肯定是针对每个对象都做协议适配和转换,这个得多少if-else。所以通常会封装一个Request和Response,我只接收Request并把它发出去,然后收回来response。这个Request随便想想的话应该有这么些属性:接口名、版本号、方法名、参数列表等等;而Response应该有执行状态Code和返回数据。 所以,上面的图变成下面这样: 0 

代理

Consumer调用一个接口的方法,实际上是调用的一个接口实现的引用。本地方法调用的时候,无非两种方式,一种直接在类里初始化实例,就是new一个对象,另外一种依赖容器注入,比如使用Spring注入。对于远程接口,第一种肯定是不行了,所以只能选择容器来注入对象引用,这就给了RPC操作空间,RPC只要在容器中构造一个接口实现就可以了,在方法实现中调用远程网络接口。这个就是常说的代理(Proxy)了,Java中一般使用动态代理来实现。 最终的模块图应该是下面这样了,中间一部分全是对RPC的基本要求:   终于把做为一个RPC框架的基本素养折腾清楚了,为了降低复杂性,这里面没有涉及集群及高可用等Dubbo为了支撑微服务相关的特性,这些部分会在RPC基础之后再涉及。

Dubbo RPC组件

下面按照上面对RPC的拆解来分析下Dubbo对应的模块,这里只是为了展现一个大体的框架,不会对每个模块做详细的解析,所以有个大概印象就可以了。后续分析到具体模块的源码,可以回来翻查一下。 为了和思维方式对上,我们从左至右过一下RPC框架图,先从客户端调用接口开始。

代理

Proxy

第一步是接口要有个代理实现,下面是Dubbo官方的Consumer例子,因为绝大多数人都是Dubbo和Spring一起用的,所以选用SpringBoot的例子:
@EnableAutoConfiguration
public class DubboAutoConfigurationConsumerBootstrap {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Reference(version = "1.0.0", url = "dubbo://127.0.0.1:12345")
    private DemoService demoService;

    public static void main(String[] args) {
        SpringApplication.run(DubboAutoConfigurationConsumerBootstrap.class).close();
    }

    @Bean
    public ApplicationRunner runner() {
        return args -> logger.info(demoService.sayHello("mercyblitz"));
    }
}
代码中DemoService就是远程API接口,Dubbo使用自定义的@Reference注解来注入,被注入的自然就是Dubbo的Proxy实现了,对这个接口的调用,会通过Proxy将请求发到127.0.0.1的12345端口。 Dubbo通过调用代理工厂ProxyFactory的getProxy来获得一个接口的代理实现类。
@SPI("javassist")
public interface ProxyFactory {
    @Adaptive({PROXY_KEY})
    <T> T getProxy(Invoker<T> invoker) throws RpcException;
} 
可以看到,这个代理工厂也是一个接口,Dubbo中提供了两种实现,一种使用的JDK中的动态代理实现(JdkProxyFactory),这个就是用的java.lang.reflect.Proxy.newProxyInstance()方法,关于这个的具体原理和用法网上很多,这里就不解释了。 Dubbo默认采用的是另一个实现JavassistProxyFactory,Javaassist是一个运行期字节码工具,它允许在运行时动态编译代码成class。Dubbo默认做法就是在初始化时生成一个接口实现类的源代码,通过javaassist编译后加载,Consumer实际上就是调用的这个类。 上面的源代码在dubbo-rpc的dubbo-rpc-api模块中。  

代理实现原理

代理有了,做的事情无非就是按传输层要求的格式把调用参数封装成一个Request,通过传输层发出去,比如通过http调用的话就封装成一个HttpRequest。但是在Dubbo这里没有这么简单,因为Dubbo是需要支持多种协议的,每个接口用的网络接口调用Client可能都不一样,而且还要保证扩展性,方便以后添加新的协议实现。 有3个类需要重点关注下:Protocol,Invoker和Invocation。 
  • Invoker

    Invoker是调用动作封装类,作用类似于java.lang.Runnable相对于Thread。 调用它的invoke()方法发起一次调用,这次调用有可能是远程有可能是本地,对调用方来说是透明的。一般来说,每个远程服务接口都会对应有一个Invoker实例。

public interface Invoker<T> extends Node {
    /**
     * 获取Invoker调用的target接口类
     */
    Class<T> getInterface();
    /**
     * 发起一次调用
     */
    Result invoke(Invocation invocation) throws RpcException;
} 
  • Invocation

    invoke() 操作的输入参数,就是把真实调用的method,输入参数等封装一下。下面是它定义的部分方法:

public interface Invocation {
    ...
    /**
     * 获取方法名
     */
    String getMethodName();
    /**
     * 获取接口名
     */
    String getServiceName();
    /**
     * 参数类型
     */
    Class<?>[] getParameterTypes();
   /**
     * 调用参数
     */
    Object[] getArguments();
     ...
}
  • Protocol

    Dubbo对接口协议的抽象,主要是两个功能,对于Provider来说通过Protocol把本地接口暴露成一个指定协议Server;对于Consumer来说可以获取一个针对特定接口协议的Invoker

@SPI("dubbo")
public interface Protocol {
    //暴露服务接口
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

     //获取接口调用的Invoker
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
}
看完上面几个接口,把关系捋一下,Consumer获取到接口的Proxy后,调用方法时,Proxy根据远程服务的配置协议找对应的Protocol实现,默认是DubboProtocol。然后,调用Protocol的refer方法获取到Invoker实现,比如从DubboProtocol获取到的就是DubboInvoker。然后,将Consumer原始的输入参数封装成一个Invocation(对应于远程调用就是RpcInvocation),调用Invoker的invoke() 方法。 注意以上只是逻辑流程,不是真正的代码调用流程,因为代码实现中对象都是提前初始化的,初始化的代码流程和逻辑流程不是完全一样。 

请求传输

继续按照前面的RPC流程图往右走,下一个模块就是传输模块了。Dubbo的传输层模块抽象层数较多,这里只列举比较关键的部分。

Client

作为Consumer和服务提供端通信,针对不同协议都会提供Cient,Dubbo将其抽象为ExchangeClient,用于发送Request并将收到结果转化成Response。
public interface ExchangeClient extends Client, ExchangeChannel {
}
ExchangeClient接口是一个组合接口,组合了Client和ExchangeChannel 。
public interface Client extends Endpoint, Channel, Resetable, IdleSensible {
    /**
     * reconnect.
     */
    void reconnect() throws RemotingException;
    ...
}

public interface ExchangeChannel extends Channel {
    /**
     * 发送请求
     */
    CompletableFuture<Object> request(Object request, int timeout, ExecutorService executor) throws RemotingException;

    ExchangeHandler getExchangeHandler();

    /**
     * 关闭Channel
     */
    @Override
    void close(int timeout);
}
从上面两个接口定义,可以看出Client负责连接的建立,ExchangeChannel负责请求的异步发送(request())以及结果的接收(ExchangeHandler)。Dubbo中接口的默认实现类分别是HeaderExchangeClient和HeaderExchangeHandler。 Client仅仅属于传输层的一半,对于整个传输层的抽象则是Exchanger接口:
@SPI(HeaderExchanger.NAME)
public interface Exchanger {

    /**
     * 开启一个服务端Server
     */
    @Adaptive({Constants.EXCHANGER_KEY})
    ExchangeServer bind(URL url, ExchangeHandler handler) throws RemotingException;

    /**
     * 获取Client
     */
    @Adaptive({Constants.EXCHANGER_KEY})
    ExchangeClient connect(URL url, ExchangeHandler handler) throws RemotingException;
} 

Transporter

对于Client来说,发送和接收是Request和Response,它所依赖的底层数据的传输抽象为Transporter。
@SPI("netty")
public interface Transporter {

    /**
     * Bind a server.
     */
    @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
    RemotingServer bind(URL url, ChannelHandler handler) throws RemotingException;

    /**
     * Connect to a server.
     */
    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
    Client connect(URL url, ChannelHandler handler) throws RemotingException;
}
看起来是不是和Exchanger一样,其实本来就是一样的,只是在Exchanger层存在Request和Response的概念,而Transporter层则只有Object的概念。也就是说,Transporter层只管传数据,而没有交互的概念,不管数据背后的意义。Exchanger是应用传输层,Transporter是数据传输层。Transporter的默认实现类是NettyTransporter。 到现在位置,我们把上面的RPC的图又往右推了一步,上一节中的Invoker执行时,创建一个ExchangeClient,请求参数被封装成一个Request发出,正常的话就可以收到一个Response,Response中包含了调用方法的返回值。

编码

Transporter在收到数据后,需要按协议来打成数据包,Dubbo中负责编码的是Codec接口。 @SPI public interface Codec2 { @Adaptive({Constants.CODEC_KEY}) void encode(Channel channel, ChannelBuffer buffer, Object message) throws IOException; @Adaptive({Constants.CODEC_KEY}) Object decode(Channel channel, ChannelBuffer buffer) throws IOException; enum DecodeResult { NEED_MORE_INPUT, SKIP_SOME_INPUT } } 接口简单直接,一个编码,一个解码,默认实现类是ExchangeCodec。 

序列化

终于走到最后一步了, 序列化的意义上面已经说了,Dubbo对序列化的抽象接口是Serialization,默认实现类是Hessian2Serialization。
@SPI("hessian2")
public interface Serialization {
    /**
     * Get content type unique id
     */
    byte getContentTypeId();
    /**
     * content type
     */
    String getContentType();
    /**
     * 序列化
     */
    @Adaptive
    ObjectOutput serialize(URL url, OutputStream output) throws IOException;
    /**
     * 反序列化
     */
    @Adaptive
    ObjectInput deserialize(URL url, InputStream input) throws IOException;

Consumer端总结

以上部分已经把Dubbo中对应RPC各个模块分解清楚了,一图胜千言,还是对应RPC的图画一个Dubbo的。 0

标签:Dubbo,调用,服务,说起,接口,RPC,Invoker,throws
来源: https://www.cnblogs.com/johnvwan/p/15647312.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有