文章目录
1 dubbo的RPC远程调用整体过程简单回顾
上篇文章《【dubbo源码解析】— dubbo的服务暴露+服务消费(RPC调用)底层原理深入探析》非常详细的介绍了dubbo的RPC远程调用过程,这里做一个简单的回顾。
【首先】 需要明确的是,在dubbo的RPC远程调用过程中,最核心的动作
是如下两个:
- (1)服务端通过protocol. export(Invoker) —> 将服务暴露出去
- (2)消费端通过protocol. refer(【接口】,【URL】) —> 获取到
【可以调用远程服务的一个对象】
【其次】 这里再从整体上串联一下上篇文章中讲到的消费端和服务端之间的通信过程。
如上图所示:
- (1)服务端发布一个服务的过程为: 先将【ref】(即具体要发布的服务)、【接口】和【URL】封装为一个Invoker,然后交给protocol对象进行真正的服务发布
- (2)消费端获取@Reference注解标注的bean的过程为: protocol对象先拿着【URL】(即服务端发布服务的URL)、【接口】获取到
【可以调用远程服务的对象】
,然后再将其和【URL】、【接口】封装为一个Invoker对象,最后再拿着这个Invoker做一个【可以调用远程服务的对象】
的代理对象作为@Reference注解标注的bean。
- (3)消费端@Reference注解标注的bean进行方法调用的过程为:
- ① bean为一个代理类 ,进行方法调用时会将 调用的方法(Method) 和 调用的参数(args) 封装起来,并将其作为参数调用消费端Invoker的invoke方法
- ② 调用消费端Invoker的invoke方法实际上就是拿着
【可以调用远程服务的对象】
、 调用的方法(Method) 和 调用的参数(args) 三个信息,通过反射调用【可以调用远程服务的对象】
实际的方法 - ③ 之后就会直接调用服务端代理类的相应方法 —》(至于为什么会调到,这里还是上两篇文章的观点,没必要深究)
- ④服务端的代理类会
再次
将调用的 调用的方法(Method) 和 调用的参数(args) 进行封装,并将其作为参数调用服务端Invoker的invoke方法 - ⑤ 与④类似,调用服务端Invoker的invoke方法实际上就是拿着【ref】、调用的方法(Method) 和 调用的参数(args) 三个信息,通过反射调用【ref】实际的方法
画个简图对上诉过程进行总结,如下:
2 动态服务发布、注册中心集成进RPC链条的思想
可以看到,按照上面的逻辑整个RPC链条已成闭环
。
但是这里还有一个比较重要的问题,就是我们实际开发中,其实根本就不必指定服务端的URL(除非直连的情况)。 —> 因此接下来的问题就是:
在整个RPC链条已成闭环的情况下,如何将动态服务发布、注册中心集成进整个RPC链条中!!!
按照开闭原则, dubbo希望只增加代码来完成功能的增强, 杜绝更改代码,如何做呢? 我前面铺垫的 SPI 思想(《【dubbo源码解析】 — dubbo spi 机制(@SPI、@Adaptive)详解》)此时就展示出它的威力了。
首先应该明确的是 protocol 对象本身就只是个 SPI 适配对象
。它可以根据 URL的不同, 选择不同的 protocol 实现类。 于是, dubbo 在此基础上, 进行了一层嵌套: 将服务的注册/发现功能, 抽象成一个 protocol 协议, 即RegistryProtocol(它并不像rmi、dubbo、http等协议一样对应一个真正的协议, 其实就是用来做服务注册与发现的),并在 RegistryProtocol 对象内, 再次嵌套一个真实的 protocol进行具体的服务发布动作,加上RegistryProtocol后dubbo的整个RPC结构大致如下图:
即:
(1)服务端发布一个服务的过程变为 先将【ref】(即具体要发布的服务)、【接口】和【URL】(注意这里的URL是包括注册中心、具体服务发布地址的url
)封装为一个Invoker —> 交给RegistryProtocol对象 —> RegistryProtocol会先将这个Invoker进行进一步封装,并交给各个具体的协议进行服务的暴漏 —> 然后还要将服务的暴露地址等发布给注册中心
(2)消费端获取@Reference注解标注的bean的过程变为: RegistryProtocol对象先拿着【URL】(这里的URL为注册中心的URL)去获取注册中心对象 —> 然后拿着获取到的注册中心对象、url、接口等去注册中心订阅并获取相应服务的url地址列表(服务端可能会有多个
)—> 然后拿着获取到的地址列表和接口获取一个个的【可以调用远程服务的对象】
—> 然后将这些对象和url、接口封装为一个个的Invoker,并将其放到一个Map容器(RegistryDirectory类的urlInvokerMap
)缓存起来—> 最后将这些信息进行再次封装为一个Invoker—》最后再拿着这个Invoker做一个【可以调用远程服务的对象】
的代理对象作为@Reference注解标注的bean。
上面仅仅简单描诉了在引入注册中心后 :服务端发布一个服务的过程+ 消费端引入一个服务的过程。
其实还有很多细节,没有进行具体的描述,比如说RegistryDirectory
其实不仅仅只是一个拥有盛放【可以调用远程服务对象】
的Invoker 的 Map容器的对象,它还是一个监听器、比如说最外层包装的Invoker其实是一个拒绝策略相关的Invoker、比如说其实这之间还有负载均衡策略、Filter的逻辑等等。。。 —> 有兴趣的可以细究一下源码!!!
3 实现一个自己的加入注册中心的dubbo服务端 + 消费端
3.1 引入注册中心jar包
本文使用zookeeper作为注册中心,需要在前面几篇文章的基础上引入如下包:
<!-- zookeeper注册中心 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
<version>${dubbo.version}</version>
</dependency>
3.2 实现一个自己的加入注册中心的dubbo服务端
ExtensionLoader<Protocol> protocolLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
ExtensionLoader<ProxyFactory> proxyLoader = ExtensionLoader.getExtensionLoader(ProxyFactory.class);
//注册中心服务--zk
final URL registryUrl = URL.valueOf("registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?registry=zookeeper");
//支持的协议:dubbo,http,hessian,rmi
URL serviceUrl = URL.valueOf("dubbo://127.0.0.1:9001/com.nrsc.service.DemoService");
@Test
public void serverRpc() throws IOException {
DemoService service = new ZkDemoServiceImpl();
//生成代理工厂
// --- 由URL确定到底是动态代理工厂(JdkProxyFactory)还是静态代理工厂(JavassistProxyFactory)
// --- 默认情况下为静态代理工厂
ProxyFactory proxy = proxyLoader.getAdaptiveExtension();
//下面这两句话完全可以放在外面 ---> 如果写在外面,这里的代码就和上文讲到的RPC完整链条的代码一致了
//这里为了测试消费端可以动态监测到服务端的发布/下线,所以写在了这里
serviceUrl = serviceUrl.setPort(9001);
URL newRegistryUrl = registryUrl.addParameter(Constants.EXPORT_KEY, serviceUrl.toFullString());
Invoker<DemoService> serviceInvoker = proxy.getInvoker(service, DemoService.class, newRegistryUrl);
//获取具体的协议
Protocol protocol = protocolLoader.getAdaptiveExtension();
Exporter<DemoService> exporter = protocol.export(serviceInvoker);
System.out.println("server 启动协议:" + serviceUrl.getProtocol());
// 保证服务一直开着
System.in.read();
exporter.unexport();
}
/****
* 除了接口外,其他和serverRpc1()一样,主要用来测试消费端可以动态监测到服务端的发布/下线
* @throws IOException
*/
@Test
public void serverRpc2() throws IOException {
DemoService service = new ZkDemoServiceImpl();
//生成代理工厂
// --- 由URL确定到底是动态代理工厂(JdkProxyFactory)还是静态代理工厂(JavassistProxyFactory)
// --- 默认情况下为静态代理工厂
ProxyFactory proxy = proxyLoader.getAdaptiveExtension();
//下面这两句话完全可以放在外面 ---> 如果写在外面,这里的代码就和上文讲到的RPC完整链条的代码一致了
//这里为了测试消费端可以动态监测到服务端的发布/下线,所以写在了这里
serviceUrl = serviceUrl.setPort(9002);
URL newRegistryUrl = registryUrl.addParameter(Constants.EXPORT_KEY, serviceUrl.toFullString());
Invoker<DemoService> serviceInvoker = proxy.getInvoker(service, DemoService.class, newRegistryUrl);
//获取具体的协议
Protocol protocol = protocolLoader.getAdaptiveExtension();
Exporter<DemoService> exporter = protocol.export(serviceInvoker);
System.out.println("server 启动协议:" + serviceUrl.getProtocol());
// 保证服务一直开着
System.in.read();
exporter.unexport();
}
3.3 实现一个自己的加入注册中心的dubbo消费端
@Test
public void clientRpc() throws IOException {
Protocol protocol = protocolLoader.getAdaptiveExtension();
//生成代理工厂
ProxyFactory proxy = proxyLoader.getAdaptiveExtension();
//由代理工厂生成Invoker对象
Invoker<DemoService> referInvoker = protocol.refer(DemoService.class, registryUrl);
//生成DemoService的代理类
DemoService service = proxy.getProxy(referInvoker);
Map<String, String> info = new HashMap();
info.put("target", "orderService");
info.put("methodName", "getOrderInfo");
info.put("arg", "1");
Map<String, String> result = service.getHelloInfo(info);
System.out.println(result);
// 保证服务一直开着 ,测试消费端可以动态监测到服务端的发布/下线
System.in.read();
}
3.4 测试 — 【在加入注册中心后消费端可调通服务端、消费端可监测到服务端的上线和下线】
(1)启动serverRPC
(2)消费端启动,并可以调通serverRPC提供的服务
(3)启动serverRPC服务,消费端可以监测到有新的服务发布了
这里涉及到了一个非常重要的对象【RegistryDirectory】
,本文仅简单提一下,有兴趣的可以深挖:
- (1)每一个要发布或引入的服务对应一个
【RegistryDirectory】
— 或者说一个接口对应一个【RegistryDirectory】
- (2)
【RegistryDirectory】
不仅缓存了每个服务端URL对应的Invoker(这些Invoker里封装了【可以调用远程服务的对象】
),还是一个监听器,当有新的服务端上线或下线时,它都能监听到,并以此动态的创建或删除Invoker对象。- (3)同时它还封装了 Dubbo 的整体发布订阅逻辑的模板 —> 创建该对象后需要将注册中心对象set到该对象里,真正与注册中心进行交互的发布与订阅其实是由具体的注册中心对象(如ZK的注册中心对象为
ZookeeperRegistry
)实现的。- (4)在
【RegistryDirectory】
中, 可以看到有很多的缓存容器,如urlInvokerMap、methodInvokerMap、cachedInvokerUrls 等用来缓存服务的信息。也就是说, notify方法(下图中的方法,当有服务上线或下线时,注册中心会触发消费端的该方法) 会更改这些缓存信息, 且Dubbo在 rpc 过程中, 则会直接使用这些缓存的信息。
这里还要再强调一下,
在 Dubbo 中, URL 是整个服务发布和调用流程的串联信息,它包含了服务的基本信息(服务名、 服务方法、 版本、 分组), 注册中心配置, 应用配置等等信息, 还包括在 dubbo 的消费端发挥作用的各种组件信息如: filter、 loadbalance、 cluster 等等。
当消费端的 notify方法中收到这些 url 信息时, 意味着这些组件信息也就可以得到了。
4 最后简单地从整体上描述一下dubbo的服务注册与发现机制
服务提供者在暴露服务时, 会向注册中心注册自己, 具体就是在 ${serviceinterface}/providers
目录下添加 一个节点(临时), 服务提供者需要与注册中心保持长连接, 一旦连接断掉(重试连接) 会话信息失效后, 注册中心会认为该服务提供者不可用(提供者节点会被删除)。
消费者在启动时, 首先也会向注册中心注册自 己, 具体为在${interfaceinterface}/consumers
目录下创建一个节点。同时消费者会订阅${service interface}/ [ providers、 configurators、 routers ]
这三个目录,这些目录下的节点删除、 新增事件都会通知消费者, 根据通知, 重构服务调用器(Invoker)。
end!