天天看点

Dubbo——Dubbo的服务调用过程总结摘要Dubbo大致流程调用具体的信息落地的调用流程Dubbo 协议需要约定序列化器粗略的调用流程图详细的调用流程调用流程-服务端端分析总结

摘要

主要是讲述Dubbo的服务调用过程。(https://juejin.cn/post/6875847496082391053)

Dubbo大致流程

首先我们已经知晓了远程服务的地址,然后我们要做的就是把我们要调用的方法具体信息告知远程服务,让远程服务解析这些信息。然后根据这些信息找到对应的实现类,然后进行调用,调用完了之后再原路返回,然后客户端解析响应再返回即可。

Dubbo——Dubbo的服务调用过程总结摘要Dubbo大致流程调用具体的信息落地的调用流程Dubbo 协议需要约定序列化器粗略的调用流程图详细的调用流程调用流程-服务端端分析总结

调用具体的信息

首先客户端肯定要告知要调用是服务端的哪个接口,当然还需要方法名、方法的参数类型、方法的参数值,还有可能存在多个版本的情况,所以还得带上版本号。由这么几个参数,那么服务端就可以清晰的得知客户端要调用的是哪个方法,可以进行精确调用!然后组装响应返回即可,我这里贴一个实际调用请求对象列子。

Dubbo——Dubbo的服务调用过程总结摘要Dubbo大致流程调用具体的信息落地的调用流程Dubbo 协议需要约定序列化器粗略的调用流程图详细的调用流程调用流程-服务端端分析总结

落地的调用流程

首先远程调用需要定义协议,也就是互相约定我们要讲什么样的语言,要保证双方都能听得懂。应用层一般有三种类型的协议形式,分别是:固定长度形式、特殊字符隔断形式、header+body 形式。

固定长度形式:指的是协议的长度是固定的,比如100个字节为一个协议单元,那么读取100个字节之后就开始解析。优点就是效率较高,无脑读一定长度就解析。缺点就是死板,每次长度只能固定,不能超过限制的长度,并且短了还得填充,在 RPC 场景中不太合适,谁晓得参数啥的要多长,定长了浪费,定短了不够。

特殊字符隔断形式:其实就是定义一个特殊结束符,根据特殊的结束符来判断一个协议单元的结束,比如用换行符等等。这个协议的优点是长度自由,反正根据特殊字符来截断,缺点就是需要一直读,直到读到一个完整的协议单元之后才能开始解析,然后假如传输的数据里面混入了这个特殊字符就出错了。

header+body 形式:也就是头部是固定长度的,然后头部里面会填写 body 的长度, body 是不固定长度的,这样伸缩性就比较好了,可以先解析头部,然后根据头部得到 body 的 len 然后解析 body。dubbo 协议就是属于 header+body 形式,而且也有特殊的字符 0xdabb ,这是用来解决 TCP 网络粘包问题的。

Dubbo 协议

Dubbo 支持的协议很多,我们就简单的分析下 Dubbo 协议。

Dubbo——Dubbo的服务调用过程总结摘要Dubbo大致流程调用具体的信息落地的调用流程Dubbo 协议需要约定序列化器粗略的调用流程图详细的调用流程调用流程-服务端端分析总结

协议分为协议头和协议体,可以看到 16 字节的头部主要携带了魔法数,也就是之前说的 0xdabb,然后一些请求的设置,消息体的长度等等。16 字节之后就是协议体了,包括协议版本、接口名字、接口版本、方法名字等等。其实协议很重要,因为从中可以得知很多信息,而且只有懂了协议的内容,才能看得懂编码器和解码器在干嘛,我再截取一张官网对协议的解释图。

需要约定序列化器

网络是以字节流的形式传输的,相对于我们的对象来说,我们对象是多维的,而字节流是一维的,我们需要把我们的对象压缩成一维的字节流传输到对端。然后对端再反序列化这些字节流变成对象。Dubbo 默认用的是 hessian2 序列化协议。所以实际落地还需要先约定好协议,然后再选择好序列化方式构造完请求之后发送。

粗略的调用流程图

Dubbo——Dubbo的服务调用过程总结摘要Dubbo大致流程调用具体的信息落地的调用流程Dubbo 协议需要约定序列化器粗略的调用流程图详细的调用流程调用流程-服务端端分析总结

简述一下就是客户端发起调用,实际调用的是代理类,代理类最终调用的是 Client (默认Netty),需要构造好协议头,然后将 Java 的对象序列化生成协议体,然后网络调用传输。服务端的

NettyServer

接到这个请求之后,分发给业务线程池,由业务线程调用具体的实现方法。但是这还不够,因为 Dubbo 是一个生产级别的 RPC 框架,它需要更加的安全、稳重。

详细的调用流程

Dubbo——Dubbo的服务调用过程总结摘要Dubbo大致流程调用具体的信息落地的调用流程Dubbo 协议需要约定序列化器粗略的调用流程图详细的调用流程调用流程-服务端端分析总结

前面已经分析过了客户端也是要序列化构造请求的,为了让图更加突出重点,所以就省略了这一步,当然还有响应回来的步骤,暂时就理解为原路返回,下文会再做分析。可以看到生产级别就得稳,因此服务端往往会有多个,多个服务端的服务就会有多个 Invoker,最终需要通过路由过滤,然后再通过负载均衡机制来选出一个 Invoker 进行调用。当然 Cluster 还有容错机制,包括重试等等。请求会先到达 Netty 的 I/O 线程池进行读写和可选的序列化和反序列化,可以通过

decode.in.io

控制,然后通过业务线程池处理反序列化之后的对象,找到对应 Invoker 进行调用。

调用流程-服务端端分析

服务端接收到请求之后就会解析请求得到消息,这消息又有五种派发策略:

Dubbo——Dubbo的服务调用过程总结摘要Dubbo大致流程调用具体的信息落地的调用流程Dubbo 协议需要约定序列化器粗略的调用流程图详细的调用流程调用流程-服务端端分析总结

默认走的是 all,也就是所有消息都派发到业务线程池中,我们来看下 AllChannelHandler 的实现。

Dubbo——Dubbo的服务调用过程总结摘要Dubbo大致流程调用具体的信息落地的调用流程Dubbo 协议需要约定序列化器粗略的调用流程图详细的调用流程调用流程-服务端端分析总结

就是将消息封装成一个 ChannelEventRunnable 扔到业务线程池中执行,ChannelEventRunnable 里面会根据 ChannelState 调用对于的处理方法,这里是

ChannelState.RECEIVED

,所以调用

handler.received

,最终会调用 HeaderExchangeHandler#handleRequest,我们就来看下这个代码。

Dubbo——Dubbo的服务调用过程总结摘要Dubbo大致流程调用具体的信息落地的调用流程Dubbo 协议需要约定序列化器粗略的调用流程图详细的调用流程调用流程-服务端端分析总结

这波关键点看到了吧,构造的响应先塞入请求的 ID,我们再来看看这个 reply 干了啥。

Dubbo——Dubbo的服务调用过程总结摘要Dubbo大致流程调用具体的信息落地的调用流程Dubbo 协议需要约定序列化器粗略的调用流程图详细的调用流程调用流程-服务端端分析总结

最后的调用我们已经清楚了,实际上会调用一个 Javassist 生成的代理类,里面包含了真正的实现类,之前已经分析过了这里就不再深入了,我们再来看看

getInvoker

这个方法,看看怎么根据请求的信息找到对应的 invoker 的。

Dubbo——Dubbo的服务调用过程总结摘要Dubbo大致流程调用具体的信息落地的调用流程Dubbo 协议需要约定序列化器粗略的调用流程图详细的调用流程调用流程-服务端端分析总结

关键就是那个 serviceKey, 还记得之前服务暴露将invoker 封装成 exporter 之后再构建了一个 serviceKey将其和 exporter 存入了 exporterMap 中吧,这 map 这个时候就起作用了!

这个 Key 就长这样:

Dubbo——Dubbo的服务调用过程总结摘要Dubbo大致流程调用具体的信息落地的调用流程Dubbo 协议需要约定序列化器粗略的调用流程图详细的调用流程调用流程-服务端端分析总结

找到 invoker 最终调用实现类具体的方法再返回响应整个流程就完结了,我再补充一下之前的图。

Dubbo——Dubbo的服务调用过程总结摘要Dubbo大致流程调用具体的信息落地的调用流程Dubbo 协议需要约定序列化器粗略的调用流程图详细的调用流程调用流程-服务端端分析总结

总结

今天的调用过程我再总结一遍应该差不多了。

首先客户端调用接口的某个方法,实际调用的是代理类,代理类会通过 cluster 从 directory 中获取一堆 invokers(如果有一堆的话),然后进行 router 的过滤(其中看配置也会添加 mockInvoker 用于服务降级),然后再通过 SPI 得到 loadBalance 进行一波负载均衡。这里要强调一下默认的 cluster 是 FailoverCluster ,会进行容错重试处理,这个日后再详细分析。现在我们已经得到要调用的远程服务对应的 invoker 了,此时根据具体的协议构造请求头,然后将参数根据具体的序列化协议序列化之后构造塞入请求体中,再通过 NettyClient 发起远程调用。

服务端 NettyServer 收到请求之后,根据协议得到信息并且反序列化成对象,再按照派发策略派发消息,默认是 All,扔给业务线程池。

业务线程会根据消息类型判断然后得到 serviceKey 从之前服务暴露生成的 exporterMap 中得到对应的 Invoker ,然后调用真实的实现类。

最终将结果返回,因为请求和响应都有一个统一的 ID, 客户端根据响应的 ID 找到存储起来的 Future, 然后塞入响应再唤醒等待 future 的线程,完成一次远程调用全过程。

继续阅读