看完今天的服务调用流程,基本上Dubbo的核心流程是完全串联起来的。Dubbo整体运行的概念你心里应该已经有了。这个系统建立起来,也是为了RPC。会有进一步的了解。简单想想大概的流程。在分析Dubbo的服务调用流程之前,我们先想一下如果我们自己实现一个调用流程需要经过哪些步骤。首先我们已经知道了远程服务的地址,接下来我们要做的就是根据我们想要的信息找到对应的实现类,然后调用它。调用完成后,以同样的方式返回,然后客户端解析response并返回。.调用具体信息,client应该告诉server包含哪些具体信息?首先,客户端必须告知要调用服务器的哪个接口。当然还需要方法名、方法参数类型、方法参数值,而且可能有多个版本,所以版本号一定要带上。有了这几个参数,服务端就可以清楚的知道客户端要调用哪个方法,并且可以进行精准的调用!然后只需组装响应并返回它。这里我贴一个实际调用请求对象的例子。data就是我说的数据,其他都是framework,包括协议版本,调用方式等,这个下面会分析。至此,大家就知道大概的意思了,就是一个普通的远程调用,告知请求的参数,然后服务端解析参数找到对应的实现调用,然后返回。落地调用流程以上是想象中的调用流程,真正的落地调用流程并没有这么简单。首先,远程调用需要定义一个协议,即约定我们要说什么语言,并保证双方都能听懂。比如我会说英文和中文,你也会说英文和中文。我们必须达成协议,选择一种语言,例如,用中文交谈。有人说这是错误的。我能用中文听懂你的英语。.那是因为你的大脑很聪明,它可以智能地识别交流的语言,而电脑却不行。如果你想想你的代码写了print1,它还能打印2吗?也就是说,计算机是死板的,我们的程序告诉它该做什么,它就直截了当地去做。需要一个协议,所以首先双方需要定义一个协议,这样计算机才能解析出正确的信息。三种常见的协议形式应用层一般有三种协议形式,即:定长形式、特殊字符定界形式、header+body形式。定长形式:表示协议的长度是固定的。比如100字节是一个协议单元,那么读取100字节就开始解析。优点是效率比较高,读到一定长度就可以不用脑子解析了。缺点是死板,每次只能固定长度,不能超过限定长度,短了就必须补,不适合RPC场景。.特殊字符隔离形式:其实就是定义一个特殊的终止符,根据这个特殊的终止符来判断一个协议单元的结束,比如使用换行符等。这个协议的优点是长度是自由的。反正就是按照特殊字符截断了。缺点是需要一直读到读完一个完整的协议单元才能开始解析。那么,如果传输的数据中混入了这个特殊字符,就会出错。header+body格式:即header是定长的,然后在header中填充body的长度。主体不是固定长度的,因此可扩展性更好。可以先解析header,再根据header获取。body的len然后解析body。dubbo协议属于header+body的形式,还有一个特殊字符0xdabb,用来解决TCP网络中的粘包问题。Dubbo协议Dubbo支持的协议非常多,下面简单分析一下Dubbo协议。协议分为协议头和协议体。可以看到16字节的header主要是携带magicnumber,也就是之前提到的0xdabb,然后是一些请求设置,消息体的长度等等。16字节之后是协议体,包括协议版本、接口名称、接口版本、方法名称等。其实协议很重要,因为从中可以了解到很多信息,只有理解其中的内容该协议可以让我们了解编码器和解码器在做什么。我会在官方网站上拍照解释协议。需要约定的是,serializer网络是以字节流的形式传输的。相对于我们的对象,我们的对象是多维的,而字节流是一维的。我们需要将我们的对象压缩成一个一维的词Throttle传输给peer。然后对等方将这些字节流反序列化为对象。序列化协议其实从上图中的协议我们可以知道Dubbo支持很多种序列化。我不会详细分析每个协议,而是粗略地分析一下序列化的类型。序列化大致分为两类,一类是字符型,一类是二进制流。字符类型的代表是XML和JSON。字符类型的优点是容易调试,对人友好。我们一眼就能知道哪个参数对应那个字段。缺点是传输效率低,冗余的东西很多,比如JSON括号。对于网络传输,传输时间变长,占用带宽变大。另一大类是二进制流类型,它是机器友好的,它的数据更紧凑,因此占用的字节更少,传输速度更快。缺点是调试困难,肉眼无法识别,必须使用专用工具进行转换。我不会深入到更深的地方。连载的方法还是很多的,后面再说。Dubbo默认使用hessian2序列化协议。所以实际落地还是需要先约定好协议,然后选择序列化方式构造请求并发送。粗略的调用流程图,我们看一下官网的图。简而言之,客户端发起调用,真正调用的是代理类。代理类最终调用Client(默认Netty)。需要构造协议头,然后序列化Java对象生成协议体,然后进行网络调用。服务器端的NettyServer收到这个请求后,分发给业务线程池,由业务线程调用具体的实现方法。但这还不够,因为Dubbo是一个生产级的RPC框架,需要更加安全和稳定。详细的调用过程之前已经分析过了。客户端还需要序列化构造请求。为了使图更突出,省略了这一步。当然,还有一个回应的步骤。暂且理解为原路返回。下面再分析一下。可见生产层面一定要稳定,所以往往会有多个服务器,多个服务器的服务就会有多个Invoker。最后需要进行路由过滤,然后通过负载均衡机制选择一个Invoker进行调用。当然Cluster也有容错机制,包括重试等。请求会先到达Netty的I/O线程池进行读写和可选的序列化反序列化,可以通过decode.in.io进行控制,然后通过业务线程池对反序列化后的对象进行处理,找到对应的Invoker进行打电话。调用流程-客户端源码分析客户端调用代码。Stringhello=demoService.sayHello("world");调用具体接口会调用生成的代理类,代理类会生成一个RpcInvocation对象来调用MockClusterInvoker#invoke方法。此时生成的RpcInvocation如下图所示,包括方法名、参数类和参数值。那么我们再来看看MockClusterInvoker#invoke代码。可以看出是判断配置中是否配置了mock。如果使用mock,则不会进行分析。我们来看看this.invoker.invoke的实现,它实际上会调用AbstractClusterInvoker#invoker。模板方法这实际上是一种非常常见的设计模式,模板方法。如果你经常看源码,就知道这种设计模式实在是太常见了。模板方法其实就是在抽象类中定义代码的执行骨架,然后将具体的实现延迟到子类中,子类可以自定义个性化的实现,也就是说可以在不改变整体执行的情况下修改步骤steps的实现,减少了重复代码,也有利于扩展,符合开闭原则。在代码中,doInvoke是由子类实现的。上面的一些步骤是每个子类都需要的,所以抽成抽象类。路由和负载均衡得到Invoker。让我们看一下列表(调用)。其实就是通过方法名找到Invoker,然后过滤服务的路由。还有一个MockInvoker。然后带着这些Invoker,进行一波loadbalance选择,得到一个Invoker。我们默认使用FailoverClusterInvoker,就是故障自动切换的容错方式。其实路由、集群、负载均衡是独立的模块。如果我们展开的内容还有很多,那么就需要另外写一篇了。在本文中,它们将首先用作黑盒。简单概括就是FailoverClusterInvoker获取Directory返回的Invoker列表,经过路由后,会让LoadBalance从Invoker列表中选择一个Invoker。最后,FailoverClusterInvoker会将参数传递给选中的Invoker实例的invoke方法,进行真正的远程调用。让我们简单看一下FailoverClusterInvoker#doInvoke。为了突出重点,我删除了很多方法。发起调用的invoke调用抽象类中的invoke,然后调用子类的doInvoker。抽象类中的方法很简单,就不展示了。影响不大。看看子类DubboInvoker的doInvoke方法就知道了。三种调用方式从上面的代码我们可以看出,调用分为三种,分别是oneway、asynchronous、synchronous。oneway还是很常见的,就是当你不关心你的请求是否发送成功时,你可以使用oneway来发送。这种方法耗钱最少,什么都不用记,什么都不用关心。异步调用,其实Dubbo天生就是异步的,可以看到client发送请求后会得到一个ResponseFuture,然后把future包装到context中,这样用户就可以从context中得到future,然后用户可以做一个wave操作后,调用future.get并等待结果。同步调用,这个是我们最常用的,也就是Dubbo框架帮我们把异步转为同步。从代码中我们可以看到dubbo源码中调用了future.get,所以用户感觉我调用了这个接口的方法之后是阻塞的,必须要等待结果到达才返回,所以它是同步的。可见Dubbo本质上是异步的。之所以有同步,是因为框架帮我们扭转了局面。同步和异步的区别其实就是future.get是在用户代码中调用还是在框架代码中调用。回到源码,currentClient.request的源码如下:组装请求,构造future,然后调用NettyClient发送请求。我们再来看看DefaultFuture的内部结构。有没有想过一个问题,因为是异步的,future存起来response回来了怎么找对应的future呢?这就是秘密!只需使用唯一ID。可以看到Request会生成一个全局唯一的ID,然后future会把自己和这个ID存储在一个ConcurrentHashMap中。这个ID发送给服务端之后,服务端也会返回这个ID,这样以后就可以通过这个ID在ConcurrentHashMap中找到对应的以后,这样整个连接就正确完整了!我们再看一下最后收到响应的代码,应该很清楚了。先看下一个响应的消息:看到这个ID,最后会调用DefaultFuture#received方法。为了让大家看得更清楚,我再画一张图:客户端调用的主要流程到这里差不多就清楚了,但是还有很多细节,后面的文章会讲到,不然太乱了复杂的。发起请求的调用链如下图:处理请求响应的调用链如下图调用流程-服务端源码分析服务端收到请求后解析请求得到一条消息,这条消息有五种分发策略:默认是all,即所有消息都分发到业务线程池。我们来看一下AllChannelHandler的实现。就是将消息封装成一个ChannelEventRunnable,丢到业务线程池中执行。ChannelEventRunnable会根据ChannelState调用相应的处理方法,这里是ChannelState.RECEIVED,所以调用handler.received最终会调用HeaderExchangeHandler#handleRequest,我们来看看这段代码。你可以看到这一波的关键点。构造出来的response中先填充了request的ID,然后我们看看这个reply做了什么。我们已经明确了最后的呼吁。实际上,它会调用Javassist生成的一个代理类,其中包含真正的实现类。前面已经分析过了,这里不再深入。下面我们看一下getInvoker方法,看看是如何根据请求的信息找到对应的invoker的。关键是serviceKey。请记住,在服务暴露之前,调用程序被封装到一个导出器中,然后构建一个serviceKey并与导出器一起存储在exporterMap中。这张地图现在可以用了!ThisKey看起来是这样的:找到调用者,最后调用实现类的具体方法并返回响应。整个过程就结束了。我将添加以前的图表。总结一下今天的调用流程,再总结一遍,应该差不多了。首先是客户端调用接口的某个方法,真正调用的是代理类。proxy类会通过cluster从目录中获取一堆invoker(如果有的话),然后进行router过滤(由于服务降级配置也会添加mockInvoker使用),然后通过SPI获取loadBalance用于一波负载均衡。这里要强调一下,默认集群是FailoverCluster,会进行容错重试处理,后面会详细分析。现在我们已经获取了要调用的远程服务对应的invoker,接下来根据具体的协议构造请求头,然后根据具体的序列化协议将参数序列化后构造插入到请求体中,以及然后通过NettyClient发起远程调用。服务端NettyServer收到请求后,根据协议获取信息并反序列化为对象,然后根据dispatch策略分发消息。默认是All,丢给业务线程池。业务线程会根据消息类型进行判断然后获取serviceKey从之前服务暴露生成的exporterMap中获取对应的Invoker,然后调用真正的实现类。最后返回结果,因为request和response有统一的ID,client根据response的ID找到存储的Future,然后插入response,唤醒等待future的线程,完成整个过程的远程调用。而且我还讲了模板方法的设计模式。当然这里面还隐藏着很多设计模式,比如责任链,装饰器等等,不用特意挑出来,在源码中太常见了,基本无处不在。
