关闭

Dubbo源码学习之知识点分析(续)

1660人阅读 评论(4) 收藏 举报

    在上一篇里以文字为主,介绍了dubbo中的很多知识点,在本篇文章中,主要以代码为主,把整个的过程弄清楚。

    上一篇地址是:http://blog.csdn.net/herriman/article/details/51525151

    首先补充上篇遗留的知识点。


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,但包罗万象真的好吗?一些产品,特别是一些内部产品,真的需要做成又大又全的黑盒子给别人用吗?如果用约定去简化很多扩展,或者直接产生多种版本,给别人一个相对简单的白盒子,难度降低后,项目中技术人员可以根据特殊情况完善白盒子,类似于把产品开发的实力,一部分转移到使用产品的项目中,会不会项目更好?



0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:9196次
    • 积分:163
    • 等级:
    • 排名:千里之外
    • 原创:6篇
    • 转载:0篇
    • 译文:0篇
    • 评论:8条
    最新评论