• Dubbo漫谈之从服务消费端说起


    既然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
  • 相关阅读:
    每日日报
    HTML相关
    60-Shiro功能扩展(记住我)
    60--Shiro安全框架
    服务器项目部署简单操作
    61--DB项目--修改密码模块设计
    59-SpringAOP --Cache操作(注解)
    58-Spring AOP 异步操作
    58-Spring-AOP事务管理
    Terminal 执行 java 命令
  • 原文地址:https://www.cnblogs.com/johnvwan/p/15647312.html
Copyright © 2020-2023  润新知