Dubbo之从服务消费端说起

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

RPC框架分解

本地调用

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

远程调用

随着应用从单机发展到分布式,某个接口的实现可能部署到了其它机器上,这个时候要调用方法就变成了下图这样: Dubbo之从服务消费端说起  左边是调用方,一般把它叫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写的,这就是序列化存在的另外一个意义。 Dubbo之从服务消费端说起 

网络通信

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

代理

Consumer调用一个接口的方法,实际上是调用的一个接口实现的引用。本地方法调用的时候,无非两种方式,一种直接在类里初始化实例,就是new一个对象,另外一种依赖容器注入,比如使用Spring注入。对于远程接口,第一种肯定是不行了,所以只能选择容器来注入对象引用,这就给了RPC操作空间,RPC只要在容器中构造一个接口实现就可以了,在方法实现中调用远程网络接口。这个就是常说的代理(Proxy)了,Java中一般使用动态代理来实现。 最终的模块图应该是下面这样了,中间一部分全是对RPC的基本要求: Dubbo之从服务消费端说起  终于把做为一个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的。 Dubbo之从服务消费端说起
上一篇:现代操作系统:原理与实现配套实验ChCore-lab02


下一篇:LeetCode 2018. 判断单词是否能放入填字游戏内(模拟)