Dubbo源码学习之知识点分析(配原理图)

       Dubbo是阿里巴巴公司实现SOA治理的工具,最近听到有朋友公司用这个Dubbo,正好想了解一下源码。经过一小段时间分析,发现知识点非常多,很有价值。包括:动态代理,spring整合,各种设计模式,线程池,锁,netty这样基于nio的tcp框架,协议的设计,当然最重要的是一种解决问题的思路,就吧这些体会记录下来:

      网上已经看到一些源码分析,总感觉缺少点什么,看完文章还是感觉云里雾里的,可能是自己水平不够吧,所以想从其它角度,先少用代码,重点介绍理念与方案,用自己的话写出来,不一定正确,欢迎指导!!!

                  许多资料书只是工具书,只有在源码的综合使用过程中才能有更深的体会,才能碰到问题时,有直觉上的方案!
                  也许已经写过很多很多代码,真正有价值的代码真的多吗?Dubbo源码确实有助于提高整体水平!


一、Dubbo要干什么?
      远程跨应用调用。就是这边有接口,那边有实现。那么这边产生一个代理对象(即:Proxy.newProxyInstance)实现接口,对于任何请求,都把请求的东西(主要有方法名,参数类型,参数值)发出去。那边的应用会根据一些信息,找到一个exporter,而它持有一个invoker(即:new AbstractProxyInvoker,这个不是通常生成动态代理的标准invoker,而且它的方法invoke()也可以换成其它名字。),因为它持有真正的接口实现类,所以再根据方法名字反射得到值,再返回给请求方。

二、知识点
1.动态代理 
比如jdk动态代理,这个资料很多。简单的使用,就是有一个接口,写一个invoker,持有实现类。再通过Proxy.newProxyInstance返回一个代理对象,它实现了所有的接口,而所有的调用都变成invoke,并传来方法与参数。这时再对持有的实现类反射调用就可以了。在反射调用前后可以做点什么,这点很重要(事务啊,计时啊,推动工作流啊,读缓存啊.....)!!

2.层层包装
这算设计模式了。主要用在Dubbo中的各个handler之间。举个现实栗子,公司门口有人送信来,首先门卫那到,它有一个处理方式,如果是垃圾信就丢掉。重要的交给收发人,同时把送信人告诉收发人。收发人看到是简单问候,直接就让送信人回复了,如果是重要的信,再交给领导,同时把送信人信息告诉领导。领导拿到后,进行处理,结果直接给送信人,这样就处理完了。如果有一天要与歪果人打交道,在门卫收信前设置一个翻译人,他收到,如果是中文就给门卫,如果是外文,先翻译再给门卫,当然翻译接触送信了,所以这时由他把送信人信息给门卫。每个人都有一个handle方法,每个人除了领导都引用下一个处理人,同时把送信人信息交给对方。 有句话说,人的本质是各种社会关系的总和,感觉在面向对象的编程中,对象的本质是各种引用关系的总和。

3.内部类与模板方式
  一般抽象类中写公共方法,不同的了类中写不同的方法。如果一个方法中有一部分相同,一部分不同,那这个方法就拆成2个方法,不同的部分成为抽象方法在子类中实现。有时候这个不同,只有在使用时都知道有什么不同,那干脆就只在new这个抽象类时补充上不同的那个抽象方法,有点象匿名类(new AbstractProxyInvoker的时候就是这样。见JdkProxyFactory类。)。有时候这个不同的部分包装成实现通用接口的对象,那就是回调了,比如hibernateTemplate中经常用的。看情况选用吧。

4.从具体到抽象
通常一个大的应用中会用到已有的东东。为了灵活性,会使用不同的已发明的轮子,但轮子尺寸不一,咋办?包装啊。比如说:我要把消息给远方的朋友,我可以打电话,可以发email,可以QQ,可以写信。利用的都是已经有的通讯方式,不过感觉麻烦,我只想说一句话啊。那干脆雇佣几个人,一个会写信,一个会打电话,一个会email,那我只要选择其中一个人(好象是自动发现机制吧),对他们说句话,就OK了,其它事情他们搞定。Dubbo可以用netty,也可以用其它方式作为transport的工具,反正都是已有的轮子,包装起来用。Dubbo会有NettyClient,GrizzlyClient...反正他们都实现通用的方法。

5.netty通讯 
netty是基于nio的通讯框架。基本原理不说了,重点体会NettyClient、NettyServer、NettyHandler、NettyChannel、以及和ChannelHandler的关系。NettyClient的两面性特别注意。NettyHandler构建的时候把NettyClient当成ChannelHandler来用,因为它持有ChannelHandler,而它本来是个client。
    NettyHandler可以监控任何nio通讯事件,监控到了,它就让引用的ChannelHandler(NettyClient)来处理事情,并把【来信通道】告诉处理的人。而处理的人就象前面介绍的门卫、收发人,领导那样依次处理。如果是要回复的处理(twoway),就直接用【来信通道】把最后的处理结果返回就OK了。

   从命名看功能:client一般有close(),connect(),reset()等方法,Channel一般有send()等功能,ChannelHandler一般是利用Channel来做些事情。

6. HeaderExchanger
当把客户端的调用信息,远程发送给服务器时,当然需要netty这样的通讯工具来帮忙。比如我们经常要让远方的朋友帮忙做事时,有时候让邮局送信,有时候让快递送信,有时候让镖局送东西。太麻烦了,我干脆在自己这边成立一个送信办事处,朋友那边成产一个办事处。办事处之间定时通讯一下(心跳),看着通讯有效不。服务点开张的时候,默认选择一个邮局(nettyclient)来用。办事处的通讯也有一些功能象client,也有一些功能象channel。所以就分成HeaderExchangeClient与HeaderExchangeChannel吧。办事处不象邮局那样处理各种各样的东东,我们自己的办事处只处理自己的request与response。主要的包括心跳请求,办事请求与相应的响应,还设计请求的同步与异步,同步时请求线程进入wait。这些格式就自己来设计了。

7.netty的encode/decode
前面讲netty后,缺少了一个重要步骤。netty传输时,读写都是对ChannelBuffer操作,就算是临时仓库吧。writeBytes、readBytes是ChannelBuffer的功能。得到的byte[]需要相互转换request与response。设计好的request转成byte[]是这样的,【头部】一共16位byte。01是叫magic码,2.3是请求标识或者twoway之类的,4-11是请求的ID,12到15是记录后面的数据的长度。response头部也是magic码,2是事件标识,3是响应码,其它不一一道来了。这样,来了请求或者响应,可以组成一堆byte[]放仓库,当仓库里来了一堆byte[],也可以分析出是啥东东。也有分析不出来的,返回DecodeResult.NEED_MORE_INPUT这个枚举对象。当你用netty实现httpserver时,你可以用现成的decode得到一个httprequest对象。

8 重要的对象关系
HeaderExchangeClient拥有一个NettyClient,还拥有一个HeaderExchangeChannel。但HeaderExchangeChannel做的事情还是找NettyClient来做,还记得前面说过NettyClient的两面性,即象client,又象channel。
   NettyClient更厉害了,它除了是client外,因为持有chanel(最核心的netty里的chanel),可以直接用chanel发送东西出去。构造的时候因为持有外部传给它的ChannelHandler,本来已经包装过了(new DecodeHandler(new HeaderExchangeHandler(handler))),它还再包装了一下(wrapChannelHandler(url, handler)),所以可以处理nettyhandle中的通道事务。nettyhandle是NettyClient生成的用来守着通道的,告诉它,有情况来找我,因为我持有ChannelHandler,我也可以变身ChannelHandler。

9 重要的处理过程
层层包装的ChannelHandler处理channel事务,包装的同时还传递着channel供其使用。从这两句看的出来:new DecodeHandler(new HeaderExchangeHandler(handler) 与new MultiMessageHandler(new HeartbeatHandler(....)。最原始的Handler是DubboProtocol中的ExchangeHandlerAdapter的匿名子类,在它外面一直在包装。等到了nettyhandle用NettyClient来处理的时候,要一层一层的从外到里处理了。
   比如:HeartbeatHandler处理时,发现是心跳请求,直接产生一个心跳回复就OK了,用channel发出去。HeartbeatHandler不会再传递给内部了。如果不是心跳请求,传给HeaderExchangeHandler处理,并把channel给它。如果HeaderExchangeHandler发现是响应,它要把响应结果放好,并唤醒请求时等待的线程,说:返回值来了,醒醒,该干活了。如果发现是真正的请求,HeaderExchangeHandler还要把channel和请求转给DubboProtocol中最原始的Handler来处理。原始的Handler就不传递了,调用reply()来找到exporter,再找到invoker,处理后返回值。

10 java的SPI机制及dubbo扩展,及为啥扩展
    前面就提到自动发现ExtensionLoader.getExtensionLoader(...)这样的语句,使用的是SPI机制。SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。这么说吧,一般都是对接口编程,具体的实现类不同可以实现不同的功能。如果实现类让别人扩展,那只要告诉我实现此接口的类在哪?找到就可以实例化。我司的一个应用中,把实现类放在数据库中也达到此目标。SPI原理就是就放在META-INF\services里面就可以找到了,不足之处是把所有的实现都给你了。你也许只想用某个特殊的实现类呢?
      dubbo先实现了一个此接口的Adaptive类,实现的的方法是用代码写一个java文件(createAdaptiveExtensionClassCode()),再用代码编译(compiler.compile(code, classLoader)),再加载进来。这个Adpative类实现了接口的所有标注了adaptive的方法。当调用这个adaptive类的方法时,这个daptive类从方法的参数中提取一个重要的值,根据这个值再调用真正的实现类。这简直是灵活性++啊,当然难度^2啊。
     为啥这么做呢?还是先举个栗子,有人送东西来你家,你家好几个人,不知道谁来收。送东西的人说收货人信息封在里面,不能拆。怎么办,要么都过来拿,不是自己的东西的人就白走一趟。要么找个代理人,他可以收任何人的东西,收到后拆开,根据里面的真正的收货人,叫某某来拿。这个代理人就是daptive类。你可以直接定义这个代理人,情况比较多时,恩恩,这里存在某种看似重复的东东,那可以动态生成这个代理人嘛。为啥送货人不能直接看到关键的收货人信息呢?这么做简直就是.....是炫耀技术。
11 线程池
    前面提到了心跳,心跳是一种定时执行的任务,在HeaderExchangeClient中就有ScheduledThreadPool来不断发送心跳请求。java中有四种线程池,这个找资料都都搞明白。包括什么核心线程数,最大线程数,无界队列什么的...
    不过这里有几个值得学习的地方:首先,HeartBeatTask中有一个ChannelProvider接口定义,还持有实现这个接口的对象,构造Task时传入的。当要new HeartBeatTask放入线程池时,new的同时,实现这个类中接口的匿名类。高手们写代码就是高深。但想想为啥这么写呢?定时任务主要的功能是把心跳消息从各个通道发出去。提供通道他并不关心,最好由外部来实现,作为匿名类传进来就好了,省得绑定太密切,影响独立性。其实,ScheduledFuture作为启动线程池返回的对象,可以比如好的结束定时任务线程池heatbeatTimer.cancel(true);以前没怎么用过。

12 同步锁 ReentrantLock
    synchronized是比较常用的线程同步用法,要求高点就用concurrent里的东东。ConcurrentHashMap是多线程中用的比较多的容器,线程同步并且效率高。AtomicLong用来对request计数,使用cpu的什么cas保证线程安全。dubbo在请求后,就马上去取值,没有值就wait(),这里面有一个超时时间,就要用到ReentrantLock.newCondition()的这样的条件了。一旦返回值来了,就done.signal();通知等待的线程来读数据了。普通的wait(),notify()功能太少了吧。顺便提一下ChannelHandlerDispatcher中的CopyOnWriteArraySet吧,读写分享的思想,读不加锁,而且读应该远超过写的情况。如果写的开销比较大,不分开的话,要么读也加锁保证一致性,如果不加,读的会很混乱。写肯定要加锁,写的时候写复制出的东东,利用空间换时间。

三、把代码把知识点串起来
    下面开始把客户端产生代理,到得到返回值的整个过程,用代码串起来,当然上面的知识点也都串了起来,开始撸~
1. 配置到spring中,利用spring解析xml配置时初始化整个过程。
spring加载配置后执行afterPropertiesSet,就从
com.alibaba.dubbo.config.spring.ReferenceBean的afterPropertiesSet()开始跟踪。
      -->getObject()--->get();--->init();-->createProxy(map)--->invoker = refprotocol.refer(interfaceClass, urls.get(0));--->return (T) proxyFactory.getProxy(invoker);
     最后一个就是得到客户端的服务代理,重点是refprotocol.refer(interfaceClass, urls.get(0))方法得到invoker。下面直接到com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol看 refer(Class<T> serviceType, URL url) 方法。里面有个new DubboInvoker<T>(serviceType, url, getClients(url), invokers);方法。
2.产生invoker前做了很多事情
    注意:先看getClients(url)参数,--->initClient(url);---->Exchangers.connect(url ,requestHandler);(requestHandler是重要的处理武器,后面会被层层包装的,前面知识点有提到)---->getExchanger(url).connect(url, handler);(这里要找一个exchanger,再用connect方法)---(自动发现机制?)->HeaderExchanger.connect(URL url, ExchangeHandler handler)---->new HeaderExchangeClient(Transporters.connect(url, new DecodeHandler(new HeaderExchangeHandler(handler))));(看到了吧,出现了HeaderExchangeClient,它还持有一个传输者,还把那个处理武器包装后交给了它,后面继续找Transporters)--->getTransporter().connect(url, handler);--(自动发现机制?)->NettyTransporter.connect(URL url, ChannelHandler listener)---->new NettyClient(url, listener);---->NettyClient new的时候,在父类AbstractClient构造时--->doOpen();connect();
      这下都清楚了吧,在产生DubboInvoker之前,生成了HeaderExchangeClient,它持有NettyClient,同时把原始武器requestHandler包装后最好交给NettyClient,NettyClient打开,并开始连接server了。这部分只是介绍了DubboInvoker生成中的其中一个参数。下面就可以生成DubboInvoker了。
3.invoker做什么事情?
     DubboInvoker要用来生成代理对象的,所以它最重要的方法就是invoke(Invocation inv) ,这个方法在父类AbstractInvoker里。子类实现了个性化的doInvoke(final Invocation invocation)部分(模板模式)。参数为啥变成final了?哈哈,这个自己再找答案,这里不讲了。
    doInvoke(final Invocation invocation) 中,对于同步请求,就执行return (Result) currentClient.request(inv, timeout).get();这句。
其中currentClient就是前面getClients(url)中得到的,姑且认为就是HeaderExchangeClient吧,它用来把inv(包括调用接口、方法、参数、值等信息)发出去,还设置了超时间,半天不返回就不要了。最后一个get(),就是从com.alibaba.dubbo.remoting.exchange.support.DefaultFuture中取返回值,这时候就使用了lock,取不到值就等待,等被唤醒了就isDone()--->returnFromResponse();--->return res.getResult();。终于拿到返回值了。
4.如何把请求发出去?
currentClient.request(inv, timeout)是上面过程中的最重要的一个方法了。
    request(Object request, int timeout) ---> channel.request(request, timeout);--->channel.send(req);
详细说一下上面的:currentClient的请求,转交给channel(HeaderExchangeChannel)来办(channel从名字上就知道是干这事情的),channel对request处理了一下先,req.setData(request);就是new了一个新的req,把传过来的request当成了req的数据部分了,就是加了一个【头,又叫header】。新的请求对象已经组装完成,HeaderExchangeChannel接到任务后也要用它的channel发出去。一环套一环啊,自己的事情都不自己做,都找别人做,还好不开工资的。HeaderExchangeChannel拥有的channel是啥?前面知识点有介绍过就是nettyClient啊,这个具有两面性的东东。看来HeaderExchangeClient不仅拥有它,还把它给了HeaderExchangeChannel来使唤。
     nettyClient自己也有nettyChannel,为啥HeaderExchangeChannel不直接引用呢?我们知道netty是一个整体。nettyClient统一收个口子,如果不用netty,只换一下client就行了。符合耦合度低,内聚度高的原则。
5.现在nettyCient如何把请求发出去?
nettyClient --->父类AbstractPeer的send(Object message)--->AbstractClient中的send(Object message, boolean sent)--->Channel channel = getChannel();再channel.send(message, sent); 其中的getChannel();中返回的是NettyChannel.getOrAddChannel(c, getUrl(), this);说明NettyChannel持有netty最核心的channel(org.jboss.netty.channel.Channel ),NettyChannel.send时用这句:ChannelFuture future = channel.write(message);--->write是这个核心channel的发信息的方法。
  上面客户端的过程介绍完了,下面是服务器的介绍了。nettyClient发出去的,当然nettyServer要收。nettyClient当然也要收nettyServer返回的结果了。
6.现在nettyServer如何启动
    在nettyServer收请求时,首先应该是服务器那边启动了,nettyServer监听各种请求。所以看看服务器启动过程。同样中服务端的spring启动开始。com.alibaba.dubbo.config.spring.ServiceBean.afterPropertiesSet()开始跟踪吧。--->export()--->doExport()--->doExportUrls();--->doExportUrlsFor1Protocol(protocolConfig, registryURLs);--->
Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));--->protocol.export(invoker);--->exporters.add(exporter);
其中的proxyFactory.getInvoker就是new AbstractProxyInvoker<T>(proxy, type, url) ,同时实现里面的抽象方法doInvoke。当收到客户端的调用Invocation inv后,可以知道调用的方法名、参数类型,参数值,同时它持有proxy这个接口实现类。那剩下的就是简单的反射得到最后的结果了。invoker有了,下面是protocol.export(invoker);把invoker用协议暴露出来。那就看看com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol中的export方法。
    export中主要做了两件事。DubboExporter<T> exporter = new DubboExporter<T>(invoker, key, exporterMap);与openServer(url);。前面是exporter持有invoker并放入一个exporterMap中存下来。后面继续跟踪---->createServer(url)--->server = Exchangers.bind(url, requestHandler);
还记得client时,用的是Exchangers.connect生成HeaderExchangeClient吗?是了,这里用Exchangers.bind生成服务端,记着这里也传入了requestHandler这个武器,用来处理request的。继续跟踪--->getExchanger(url).bind(url, handler);--->HeaderExchanger.bind(URL url, ExchangeHandler handler)--->new HeaderExchangeServer(Transporters.bind(url, new DecodeHandler(new HeaderExchangeHandler(handler)))); (武器同样被层层包装)--->Transporters.bind--->NettyTransporter.bind(URL url, ChannelHandler listener)--->new NettyServer(url, listener);(nettyserver已经生成了,武器也传给它了)--->super(url, ChannelHandlers.wrap(handler, ExecutorUtil.setThreadName(url, SERVER_THREAD_POOL_NAME)));(武器又被包装了)--->doOpen();。好了,最后的nettyServer已经启动了。
6.nettyServer如何处理客户端的请求呢?
    注意看doOpen中的pipeline.addLast("decoder", adapter.getDecoder());pipeline.addLast("encoder", adapter.getEncoder());pipeline.addLast("handler", nettyHandler);这三句。这点要随便找点简单的netty通讯就知道了。server会收到数据,也会发数据,有上行下行过程,在pipeline中被处理。处理包括编码、解码,还有后续处理。
   比如收到了客户端的请求了,前面的知识点中介绍了解码的过程,就是从buffer中拿一堆byte[],按着头部16位看数据情况(包括2位magiccode,reqid,还有数据长度;body中的真正的数据),标准的请求过来了,就转成从byte[]转为request(dubbo定义的request协议)过程。
   再看nettyHandler,当请求过来了,也转码好了,就轮到nettyHandler来处理了。nettyHandler就是new NettyHandler(getUrl(), this);,其中的this是啥?就是NettyServer。nettyHandler主要处理4种情况:channelConnected/channelDisconnected/messageReceived/writeRequested/exceptionCaught,重点介绍收到请求怎么办?messageReceived方法中是handler.received(channel, e.getMessage());handler是啥?就是NettyServer。说明nettyHandler是NettyServerr的探子,NettyServer当open后,让nettyHandler监控各种情况,再回来向NettyServer汇报,当然NettyServer会处理的。
   NettyServer的父类AbstractPeer中有处理--->received(Channel ch, Object msg)--->handler.received(ch, msg);。前面刚说过NettyServer在nettyHandler中是当作handler来处理的。现在它内部处理时,又调用它自己的handler来处理。说是我对我的探子来说我是处理者,处理的时候我又用我内部的处理者处理。内部的处理者是谁呢?就是前面new NettyServer时传入的那个层层包装的武器啊(就是handler.received(ch, msg);,把通道和数据都交给武器来处理了)。
7.层层包装的武器如何处理呢?
  下面两句:
handler=new DecodeHandler(new HeaderExchangeHandler(handler)),再被下一句包装
ChannelHandlers.wrap(handler, ExecutorUtil.setThreadName(url, SERVER_THREAD_POOL_NAME))
  其中中线程池处理的后一句不多说,里面有一个HeartbeatHandler。收到的消息已经是netty解码过的,不是byte[]了,已经是request消息了(头和body都解码了,body中是放的DecodeableRpcResult),HeartbeatHandler发现isHeartbeatRequest(message),就new Response(req.getId(), req.getVersion();channel.send(res);,直接用通道发回去了,里面的武器都用不上了。如果是标准的调用响应,就从DecodeHandler开始吧。--->decode( ((Response)message).getResult());(netty解码后是消息body中的DecodeableRpcResult,消息体内才是调用结果,要再解码Decode出方法的参数类型,值等信息)--->DecodeableRpcResult.decode()--->handler.received(channel, message);(再交给下一步的handle处理)--->HeaderExchangeHandler--->handleResponse(channel, (Response) message);(方法内可以看到其它一些功能,如果是请求,还有其它的处理方法)--->DefaultFuture.received(channel, response);--->future.doReceived(response);--->lock.lock();response = res;done.signal();lock.unlock();。
8. 数据终于回来了
  最高请求的时候,我们分析的是同步请求,那个请求线程如果不超时,还在那里等着呢。上面看到返回值线程锁定后,置好返回值,就发出了信息给等待线程的那个get()方法。这下完整的过程都结束了。

四、结束语
    看完基本的代码感悟颇深,看了许多java知识书,设计模式书,感觉都是工具书或者入门书,只有看这样的源码,再深入体会设计开发人员的思想,把很多知识点融合在一起,才有一个质的提高。
    前两天看到一点hadoop的源码分析中的各部分之间的同步调用,线程等待也是这样,互相借鉴嘛。hadoop中通讯只用nio的实现,因为都是内部的东东,不需要留下很多口子给别人扩展,简洁有效。在我看来dubbo在技术的道路的走的很远,很高,但原始的功能只是实现一个远程rpc,但包罗万象真的好吗?一些产品,特别是一些内部产品,真的需要做成又大又全的黑盒子给别人用吗?如果用约定去简化很多扩展,或者直接产生多种版本,给别人一个相对简单的白盒子,难度降低后,项目中技术人员可以根据特殊情况完善白盒子,类似于把产品开发的实力,一部分转移到使用产品的项目中,会不会项目更好?


     
   dubbo协议的核心就是实现了headExchange层(头交换层)的Request,Response的定义与encoding,decoding,以及异步转同步,请求与响应的处理以及此层的心跳。它的下层由不同的传输层(如netty)来实现,对不同的传输工具进行了包装。

猜你喜欢

转载自herman-liu76.iteye.com/blog/2308561
今日推荐