您当前的位置: 首页 >  dubbo

庄小焱

暂无认证

  • 4浏览

    0关注

    805博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Dubbo——生产者(Producer)原理

庄小焱 发布时间:2021-04-07 20:47:37 ,浏览量:4

摘要

主要是讲述Dubbo的服务调用过程。(《Dubbo系列》-Dubbo的服务调用过程 - 掘金)

Dubbo大致流程

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

调用具体的信息

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

落地的调用流程

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

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

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

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

Dubbo 协议

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

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

需要约定序列化器

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

粗略的调用流程图

简述一下就是客户端发起调用,实际调用的是代理类,代理类最终调用的是 Client (默认Netty),需要构造好协议头,然后将 Java 的对象序列化生成协议体,然后网络调用传输。服务端的 NettyServer 接到这个请求之后,分发给业务线程池,由业务线程调用具体的实现方法。但是这还不够,因为 Dubbo 是一个生产级别的 RPC 框架,它需要更加的安全、稳重。

详细的调用流程

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

调用流程-服务端端分析

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

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

就是将消息封装成一个 ChannelEventRunnable 扔到业务线程池中执行,ChannelEventRunnable 里面会根据 ChannelState 调用对于的处理方法,这里是 ChannelState.RECEIVED,所以调用 handler.received,最终会调用 HeaderExchangeHandler#handleRequest,我们就来看下这个代码。

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

最后的调用我们已经清楚了,实际上会调用一个 Javassist 生成的代理类,里面包含了真正的实现类,之前已经分析过了这里就不再深入了,我们再来看看getInvoker 这个方法,看看怎么根据请求的信息找到对应的 invoker 的。

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

这个 Key 就长这样:

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

总结

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

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

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

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

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

博文参考

关注
打赏
1657692713
查看更多评论
立即登录/注册

微信扫码登录

0.0825s