白话Dubbo(一):从RPC说起之服务消费端

前言

之前用过两年的Dubbo,虽然对原理有所了解,但是一直没整体看一遍源码。最近因为新冠疫情在家呆着,打算把源码过一遍。最好的办法,当然 是先看下网上的源码解读,但是找了几篇发现有个共同的问题,就是都是按??槔捶纸獾?。比如先讲SPI,后面Transport、Protocol 等等一层层讲下去,直接看很难连贯起来,最终还是对照着源码自己搞清楚之间的关系。
所以,后面的几篇文章打算从另一个角度来解读,采用从整体分解到细节的方式,可能不会有大段的源码,但是看完这个再去看其它的源码解读文章,会有比较大的帮助,所以叫白话Dubbo。

RPC框架分解

既然Dubbo是一个RPC框架,我们就从分解一个RPC框架开始,然后把Dubbo中的??楹痛攵杂Φ絉PC的每个部分上。

本地调用

本地进程内调用就不用多讲了,我们每天写的代码大部分都是这个

    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的私有协议。封装网络协议的??橐话憬蠧odec,里面包含编码(encode)和解码(decode)方法。
所以上面的图就变成这样:


Codec

Codec只负责对象和0/1之间转换,但是网络通信还包括建立和Provider的链接并发送和接收数据包。这时候又会抽象出一个传输层(Transporter)专门负责建立和关闭链接,维护链接池,处理连接和断开的事件。在java中,netty是应用最多的框架。
所以,上面的图又变成这样了:


Transporter

Transporter层通常不会直接接收Consumer发过来的对象,原因嘛肯定是针对每个对象都做协议适配和转换,这个得多少if-else??。所以通常会封装一个Request和Response,我只接收Request并把它发出去,然后收回来response。这个Request随便想想的话应该有这么些属性:接口名、版本号、方法名、参数列表等等;而Response应该有执行状态Code和返回数据。
所以,上面的图变成下面这样:


Transporter2

代理

Consumer调用一个接口的方法,实际上是调用的一个接口实现的引用。本地方法调用的时候,无非两种方式,一种直接在类里初始化实例,就是new一个对象,另外一种依赖容器注入,比如使用Spring注入。对于远程接口,第一种肯定是不行了,所以只能选择容器来注入对象引用,这就给了RPC操作空间,RPC只要在容器中构造一个接口实现就可以了,在方法实现中调用远程网络接口。这个就是常说的代理(Proxy)了,Java中一般使用动态代理来实现。
最终的??橥加Ω檬窍旅嬲庋耍屑湟徊糠秩嵌訰PC的基本要求:


代理

终于把做为一个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 {

    /**
     * create proxy.
     *
     * @param invoker
     * @return proxy
     */
    @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个类需要重点关注下:ProtocolInvokerInvocation。

  • 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 Consumer

到这里左半部分就结束了,本来想用一篇文章把整个链路说完的,写起来才发现有点多,Dubbo服务提供方的??榉纸饩头旁谙乱黄?。

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,100评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,308评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,718评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,275评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,376评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,454评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,464评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,248评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,686评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,974评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,150评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,817评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,484评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,140评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,374评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,012评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,041评论 2 351

推荐阅读更多精彩内容