- 作者:爱抄中间件代码的路人丙
源码地址:https://gitee.com/li-zhongwei/study-rpc-all
打个小广告,觉得有收获的小伙伴,希望给一个stater,这是笔者前进最大的动力!
阅读提醒:源码部分建议文章和代码一起食用,还有就是 这篇文章比较长,但是比较硬核的剖析我自己写的study-rpc框架!
- 这篇文章比较硬核,会涉及到一些spring和springBoot源码的东西,可能对于初学者不是很友好
- 建议对于不理解的点,大家可以百度查相关的资料,以及debug源码(非常有效,对于阅读源码)
第四章 study-rpc-all 部分核心源码分析
4.1 前言
前言,不管rpc框架多么复杂(虽然没有看过很多rpc框架的源码),它本身其实从宏观层面的角度看就是三个角色互相交互的一个过程,三个角色:客户端(服务消费者)、服务端(服务提供者)、注册中心(服务协调者或者维护者)
三者之间的关系简图:
有一些同学可能会发现,鄙人画的图跟好多开源框架dubbo、motan的介绍图好像啊? 像就对了,并不是说我是去抄了他们的图,因为rpc的本质就是三个角色的交互过程,不同rpc的落地,主要的区别在于他们的架构、以及落地的一些细节,当然现在很多开源的组件都会引入APM,来进行监控和预警,很多rpc开源组件都会看到APM的身影
接下来,我将从以上的4个交互流程进行梳理、剖析自己写的rpc框架,希望大家看了之后宏观上以及代码层面都能够有所收获!
4.2 注册中心启动
在注册中心的实现层面,因为选择的是zookeeper,所以这一块基本上不需要我们自己去写注册中心的代码,启动zk服务即可,如果想要自己去尝试实现注册中心可以去看一下zookeeper源码,或者也可以看一下第三章 我对rocketMQ注册中心 nameserver简单的分析,以及他们之间的对比,其他的注册中心应该也大差不差,区别主要在于他们的重心维度有一些区别
4.3 服务提供者启动和暴露服务流程
4.3.1 代码位置
大家会发现这个包下有一个接口和一个类,这是开源框架常见的一些代码实现的方式,接口:主要来抽象流程中的方法,实现类:则具体去实现流程中的细节
以服务提供者暴露服务为例:从注册流程的角度出发,我们可以抽象出来4个方法
- 初始化跟注册中心的连接
- 注册信息到注册中心 (registry)
- 去除自己在注册中心的信息
- 销毁跟注册中心的连接 (destroy)
4.3.2 Registry.class
//注册元信息 RemoteServerInfo类封装服务提供者的元信息
void registry(RemoteServerInfo serverInfo);
//销毁服务提供者相关的信息
void destroy();
我在具体实现的时候,只抽像了2个方法注册和销毁,其实更规范的话,应该是上面的4个方法(这个是复盘的时候,代码层面可以优化的地方,更加规范清晰)
注意:zk客户端,我用的是Curator API
大家可以先百度了解一下Curator 的基本用法
4.3.3 ZkRegister.class
核心方法代码
4.3.4 图解服务提供者启动和暴露流程(非常细,大家可以对照着源码食用)
(1)zkClient初始化流程
//构造函数
public ZkRegister(ZKServerConfig zkServerConfig){
if (zkServerConfig == null)
throw new IllegalArgumentException("zkServerConfig can't be null!");
this.zkServerConfig = zkServerConfig;
init();
}
public void init(){
//todo 参数校验
String zkPath = zkServerConfig.getZkPath ();
ExponentialBackoffRetry retryPolicy = new ExponentialBackoffRetry(1000, RETRY_TIMES);
zkClient = CuratorFrameworkFactory.builder() //zkPath注册中心的地址
//"ip:2181,ip2:2181,ip3:2181"
.connectString(zkPath)
.retryPolicy(retryPolicy)
.connectionTimeoutMs(ZK_CONNECT_TIMEOUT)
.sessionTimeoutMs(SESSION_TIMEOUT)
.build();
zkClient.start();
try {
//同步连接zk
zkClient.blockUntilConnected(10, TimeUnit.SECONDS);
log.info("zk server connect success!");
} catch (InterruptedException e) {
log.error("zk server connect fail,reason:【{}】",e.getMessage());
throw new RuntimeException(e);
}
log.info("zk server init success,info={}", zkServerConfig);
}
(2)服务提供者注册流程
参考代码RpcServerProcessor类的postProcessAfterInitialization方法使用(整合springboot做的,后面详细讲一下和springboot的整合)
当然自己也可以写测试用例,初始化一个ZKServerConfig实例,初始化一个Registry具体显现类实例,初始化元信息RemoteServerInfo,然后调用Registry的registry方法即可(测试的时候,保证zk服务一定要启动)
(3)registry方法实现细节(暴露服务)
/**
* 注册方法
* 创建一个接口节点信息
* */
public void registry(RemoteServerInfo serverInfo){
String env = zkServerConfig.getEnv ();
int port = zkServerConfig.getPort ();
// int weight = serverInfo.getWeight ();
String serviceInterfaceName = serverInfo.getServiceName();
// String server = zkServerConfig.getServer ();
if (zkClient == null){
//(1)检查是否初始化
log.error("zk server is null service:"+env+"-"+serviceInterfaceName);
throw new IllegalArgumentException ( "zk server can't be null" );
}
try {
//(2)检查对应zk节点是否创建
String envPath = env.startsWith ( "/" ) ? env : "/".concat ( env );
if (zkClient.checkExists().forPath(envPath) == null){
zkClient.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT)
.forPath(envPath);
}
String servicePath = serviceInterfaceName.startsWith ( "/" ) ? serviceInterfaceName : "/".concat ( serviceInterfaceName );
if (zkClient.checkExists().forPath(envPath+servicePath) == null){
zkClient.create()
.creatingParentsIfNeeded()
.withMode(CreateMode.PERSISTENT)
//权限
.withACL(ZooDefs.Ids.OPEN_ACL_UNSAFE)
.forPath(envPath+servicePath);
}
String ip = serverInfo.getIp();
if (StringUtils.isEmpty ( ip )) {
throw new IllegalArgumentException ( "ip can't be null" );
}
//(3)把封装的元信息转成json字符串
Gson gson = new Gson();
String jsonDataOfChild = gson.toJson(serverInfo);
//(4)创建临时节点
String childPath = envPath + servicePath + "/" + ip + ":" + port;
if (zkClient.checkExists().forPath(childPath) == null){
zkClient.create()
.creatingParentsIfNeeded()
//临时节点
.withMode(CreateMode.EPHEMERAL)
.forPath(childPath,jsonDataOfChild.getBytes(StandardCharsets.UTF_8));
}
}catch (Exception e){
log.error(e.getMessage(),e);
}
}
相信看到这里,大家应该会觉得其实服务暴露也不过如此嘛!
我还记得以前看尚硅谷的雷神讲dubbo的服务暴露的源码的时候,看的云里雾里,整个看下来就是一无所获(除了觉得雷神牛逼) 看到这里大家其实可以去github上看看一些开源的rpc框架的服务暴露流程,大家可能会发现其实都是一个套路,只是实现的一些细节不同、以及代码风格不同,这里想分享的点是:rpc原理都是相同的
4.3.5 关于使用其他注册中心实现服务暴露的建议
先说共同点:
- 不管什么注册中心,服务暴露的流程肯定类似
- 实现Registry接口
区别:
- 不同的注册中心,api不同,食用上也有区别
- 注册中心的机制不同
后面有时间的话可以写一写其他注册中心的事列参考,大家也可以参考一些开源框架的写法,反正流程上是类似的
4.4 消费者(注册中心)
关于消费者这块的代码,我在具体实现的时候跟负载路由的代码耦合度非常高,所以呢,有可能会分享的不是很清楚,我尽量去描述清楚实现的一些细节
4.4.1 代码位置
4.4.2 ICluster.class
这个接口其实就可以看做为消费者路由负载和资源维护的抽象
/**
* 销毁资源
* */
void destroy();
/**
* 获取服务提供者
* */
RemoteServerInfo get();
/**
* 获取资源池
* */
ServerObject getObjectForRemote();
/**
* 获取netty通信客户端
* */
NettyClient getNettyClient();
- destroy():销毁消费者维护的长连接资源(包括2块:服务提供者的长链接资源,注册中心的长链接资源)
- get():获取一个服务提供者的元信息
- getObjectForRemote:获取一个长连接资源池
- getNettyClient():获取一个netty通信客户端,大家看对应代码会发现NettyClient是一个单列
4.4.3 ZKClusterImpl.class((启动流程))
消费者启动流程图:
代码就不贴了,大家可以照着图一步一步的看代码,我把ZKClusterImpl类的一些核心属性说一下
/**
* 当前服务列表 注册中心拉下来的服务提供者的元信息
* */
private final List<RemoteServerInfo> serverList = new CopyOnWriteArrayList<>();
/**
* zk 集群
*注册中心 ip和端口
* */
private final String ipsAndPorts;
/**
* zk客户端
* */
public CuratorFramework zkClient;
/**
* 读写锁 后面可以讲一下为什么要用锁、以及为什么用读写锁,而不用其他的锁
* */
public ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
public Lock writeLock = lock.writeLock();
public Lock readLock = lock.readLock();
/**
* 孩子节点监听缓存 Curator监听机制的用法
* */
private PathChildrenCache pathChildrenCache;
大家会发现ZKClusterImpl类实现了一个父类AbstractCluster抽象类,我们来看一下AbstractCluster抽象类做了些什么事
4.4.4 AbstractCluster.class
首先先简单介绍一下AbstractCluster类的一些核心属性
/**
* 负载 提供负载策略
* */
public ILoadBalancer loadBalancer;
/**
* 服务名(或者说服务提供者的接口名)
* */
public String serviceName;
/**
* netty通信客户端
* */
private NettyClient nettyClient;
/**
* 是否异步todo 暂时还未实现
* */
private boolean isAsync;
/**
* 池化资源配置类
*
* GenericObjectPoolConfig:池配置
* AbandonedConfig:遗弃配置
* */
private GenericObjectPoolConfig genericObjectPoolConfig;
private AbandonedConfig abandonedConfig;
/**
* 消费者维护的:服务提供者的长连接资源
* */
public final ConcurrentHashMap<String, GenericObjectPool<io.netty.channel.Channel>> serverPollCache = new ConcurrentHashMap();
AbstractCluster主要提供了2个方法:createGenericObjectPool和destroyGenericObjectPool方法
- createGenericObjectPool:创建池化资源的方法
- destroyGenericObjectPool:销毁池化资源的方法
ZKClusterImpl是如何联动AbstractCluster类的:
public ZKClusterImpl(String ipsAndPorts, ILoadBalancer loadBalancer, String serviceName,String env, boolean async, int conTimeOut, int soTimeOut, GenericObjectPoolConfig genericObjectPoolConfig, AbandonedConfig abandonedConfig) {
super(loadBalancer, serviceName, async, conTimeOut, soTimeOut, genericObjectPoolConfig, abandonedConfig);
this.ipsAndPorts = ipsAndPorts;
this.env = env;
//验证合法性ipsAndPorts todo
initZKClient();
}
ZKClusterImpl在构造的时候回去先调父类的有参构造函数初始化
简单总结一下:AbstractCluster类主要做长连接池的创建和销毁动作
4.4.5 如何实现服务提供者动态扩缩容和上下线元信息维护
主要依靠以下2点实现
- 心跳包(todo)
- zk的Watcher监听回调机制
大家可以回到:消费者启动流程图
- 心跳包主要在dealChildren()和initScheduleTask()方法中实现
- Watcher监听主要在addDataChangeListener()方法中实现
这里主要介绍一下基于zk客户端API Curatord的pathChildrenCache实现:ZKClusterImpl类的addDataChangeListener()方法
/**
* 为孩子节点注册监听器(同步的方式)
path:需要注册监听的zk路径
pathChildrenCache:会监听当前节点以及其子节点的变化
* */
private void addDataChangeListener(String path) {
try {
pathChildrenCache = new PathChildrenCache(zkClient, path, true);
pathChildrenCache.getListenable().addListener(new ChildDataChangeListener());
//同步初始化cache BUILD_INITIAL_CACHE
//启动时同步初始化Cache,表示创建Cache后,就从服务器拉取对应的数据进行监听器注册;
pathChildrenCache.start(PathChildrenCache.StartMode.BUILD_INITIAL_CACHE);
} catch (Exception e) {
e.printStackTrace();
}
}
- 参数path:需要注册监听的zk路径
- pathChildrenCache类:会监听当前节点以及其子节点的变化
- ChildDataChangeListener类:具体的回调实现类
ChildDataChangeListener类实现核心:
/**
* 服务动态上下线核心:监听器
* */
private class ChildDataChangeListener implements PathChildrenCacheListener{
@Override
public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) {
try{
ChildData data = event.getData();
if (data == null)
return;
String dataJson = new String(data.getData(), StandardCharsets.UTF_8);
switch (event.getType()){
case CHILD_ADDED:
addServer(new Gson().fromJson(dataJson, RemoteServerInfo.class));
log.info("service:【{}】新服务节点上线,path:{},data:{}",serviceName,data.getPath(),dataJson);
break;
case CHILD_REMOVED:
removeServer(new Gson().fromJson(dataJson, RemoteServerInfo.class));
log.info("service:【{}】服务节点下线,path:{},data:{}",serviceName,data.getPath(),dataJson);
break;
case CHILD_UPDATED:
updateServer(new Gson().fromJson(dataJson, RemoteServerInfo.class));
log.info("service:【{}】服务节点更新,path:{},data:{}",serviceName,data.getPath(),dataJson);
break;
}
}catch (Exception e){
log.error("监听出现异常:",e);
e.printStackTrace();
}
}
}
当我们path监听的节点发生了变化就会触发ChildDataChangeListener的childEvent回调方法
- CHILD_ADDED:孩子节点添加,说明有新的服务提供者上线了
- CHILD_REMOVED:孩子节点移除,说明有新的服务提供者下线了
- CHILD_UPDATED:孩子节点信息修改,说明消费者维护的服务提供者修改了元信息
图解监听机制:(可以理解为简单的策略模式实现)
这三个方法比较简单,相信大家简单看看代码就能理解(就是在加锁的情况下,去维护serverList和池化资源)
心跳包todo,还待实现,后面有时间再去做
4.5 消费者rpc调用流程
4.5.1 消费者发起rpc调用流程
rpc的调用流程,通常是由消费者的统一代理类发起的远程调用
代码中主要使用JDK的动态代理Proxy,关于JDK的动态代理代码示例,大家可以百度了解一下
rpc远程调用流程图解:
图解主要的代码位置在ClientStubInvocationHandler类的invoke方法,还有一些细节比如:重试、超时等,大家可以看一下代码
简单介绍一下ClientStubInvocationHandler类的核心属性:
//节点(注册中心)
private ICluster cluster;
//负载
private ILoadBalancer iLoadBalancer;
//2种方式 决定使用直连还是zk注册中心
// 方式1:zk管理的动态集群,格式192.168.3.253:6666
private String zkPath;
// 方式2:指定的server列表,逗号分隔,#分隔权重,格式192.168.3.253:6666#10,192.168.3.253:6667#10
private String serverIpPorts;
//池化资源抛弃策略
private AbandonedConfig abandonedConfig;
还有一些其他的属性,都是关于org.apache.commons池化的配置,大家可以百度
4.5.2 消费者如何进行返回消息的处理
图解消息发送到接受流程:
后面可以跟大家图解一下异步调用的流程,以及为什么要提供异步调用
- 同步的缺点:简单来讲,跟同步IO类似,无法充分发挥消费者的调用性能
目前只支持同步调用,核心实现类
- RpcFuture类:主要依赖CountDownLatch实现发起调用的线程在未收到返回消息时进行阻塞或者超时阻塞操作
- DefaultResponseHandler类:实现了netty的SimpleChannelInboundHandler,进行消息的处理
- LocalRpcResponseCache类:有个map来维护返回的消息,key 请求id,value 返回的消息
RpcFuture.class
该类核心方法有3个:
- get(),一直阻塞,直到countDownLatch减少值为0
- get(long timeout, TimeUnit unit),阻塞指定时间
- setResponse(T response),执行countDownLatch.countDown();
代码的注释都非常详细,相信大家一看就懂,不懂就去了解一下countDownLatch 的用法
DefaultResponseHandler.class
这个跟netty有关,大家不懂netty的话,可以先去了解一下,暂时就理解为消息入站处理
LocalRpcResponseCache.class
- add():缓存请求id和RpcFuture实例的映射关系
- fillResponse():DefaultResponseHandler消息入站时,把返回消息设置到对应的RpcFuture实例上
大家可以在代码里看这2个方法使用地方,相信一看就明白
关于调用时候的负载和序列化,暂时没写,准备后面在设计模式能力提升章节说
4.6 整合springboot代码实现
在整合springboot的时候,主要利用了springboot的2个扩展点
- spring bean的生命周期的扩展点
- springboot-stater机制的扩展点
springboot 自动配置原理
springboot自动配置原理主要依赖启动类的注解
图解启动类的注解:
所以我们在写自己的stater的时候,只要遵守springboot自动配置原理的启动规则即可(后面会详细的介绍如何:我是如何写rpc的消费者和服务提供者的stater)
如果看springboot自动配置的源码的话,还是挺复杂的,我只是简单的图解了一下自动配置的一个大概过程
spring bean的生命周期(生每周期是指从bean的定义全部注册到beanFactory开始的)
bean的生命周期主要有4个阶段:
- 实例化
- 属性复制
- 初始化
- 销毁
简单图解一下bean的生命周期:(这里只是简单介绍一下生命周期的流程,方便大家了解我写的rpc是如何利用bean的生命周期的扩展进行服务暴露和服务拉取的)
图解spring bean的生命周期:(建议大家图解和源码一起食用,具体的方法都标注在图上了)
这个图有点大,我拆分成2张:
我这里只是简单的介绍了一下bean的生命周期,方便大家能够我利用spring的扩展点
4.6.1 study-rpc-server-stater(服务提供者启动流程)
- 自定义注解:RpcPrivider
- 自动配置类的前置属性:RpcServerProperties
- 自动配置类:RpcServerAutoConfig
- bean生命周期扩展类:RpcServerProcessor
下面就简单介绍一下这4个类,大家可以结合这代码看,我觉得应该很清晰了
RpcPrivider注解
基本上开源的rpc整合springboot都会提供这个区分消费者和服务提供者的注解,可以在使用的时候配置一些属性值
简单解释一下:RpcPrivider注解为什么用了spring的@Service注解,主要是想把这个注解放到spring容器管理
RpcServerProperties
@ConfigurationProperties(
prefix = "rpc.server"
)
通过这个注解,我们就可以在配置文件中,通过rpc.server的前缀去配置对应的属性值
RpcServerAutoConfig
自动配置核心类:主要用了以下三个注解,大家不了解可以百度
@Configuration:配置类注解
@EnableConfigurationProperties({RpcServerProperties.class}):前置类存在才生效注解
@ConditionalOnMissingBean:容器中没有这个bean,我就初始化
用@ConditionalOnMissingBean注解的好处在于,如果你不想用我的类,你可以自定义相关的类(扩展性很好)
RpcServerProcessor
实现了BeanPostProcessor的postProcessAfterInitialization方法(初始化后置通知)
实现了CommandLineRunner的run方法,(CommandLineRunner的run方法会在springboot启动流程的最后一个阶段执行,也就是所有的服务都已经暴露完成了)
- postProcessAfterInitialization方法
前面已经图解了bean的生命周期,我们知道每一个初始化后的bean都会执行所有xxxBeanPostProcessor的后置方法,所以我们只需要判断当前的bean是不是被RpcPrivider注解修饰即可,修饰的话:我们就进行服务暴露,代码很简单,大家看一下就能明白
- run方法
这个方法做了2件事:
第一件事,起了一个线程去启动nettyServer进行指定端口进行监听;
第二件事,注册了一个jvm的关闭钩子函数(进程关闭时会回调的方法:清除自己在注册中心的元信息,关闭nettyServer) 抄的rocketMQ中的源码
最后还是简单图解一下大概的启动流程:
4.6.2 study-rpc-client-stater(消费者启动流程)
消费者stater和服务提供者stater差不多,大家可能会看到,包的命名都是比较一致的
- 自定义注解:RpcAutowired
- 自动配置类的前置属性:RpcClientProperties
- 自动配置类:RpcClientAutoConfig
- bean生命周期扩展类:RpcClientProcessor
具体 细节就不描述了,参考服务提供者,大家看看代码应该也能够理解
详细剖析一下RpcClientProcessor类
//代理对象缓存 提高调用性能,减少反射反复创建的性能损耗
private final Map<String, Object> objectCache = new ConcurrentHashMap<>();
//代理工厂
private ProxyFactory proxyFactory;
//clien全局配置
private RpcClientProperties properties;
//spring上下文applicationContext 主要是想去容器中获取负载的实列
private ApplicationContext applicationContext;
-
扩展点BeanFactoryPostProcessor的后置通知postProcessBeanFactory(当所有的beanDefine加载完成后会执行这个方法)
postProcessBeanFactory()方法实现逻辑
(1)遍历每一个beanDefinition,获取对应的Class对象,判断是否被RpcAutowired自定义注解修饰
(2)如果被RpcAutowired自定义注解修饰,遍历对应Class的所有属性,找到具体被修饰的属性
(3)先看缓存objectCache是否有对应接口的代理类,有的话,直接通过反射把代理类赋值给该属性
(4)如果缓存没有,初始化一个ClientStubInvocationHandler实列,然后通过代理工厂生成一个代理类放入缓存并通过反射赋值给对应属性
简单图解一下消费者启动流程:
4.6.3 rpc远程调用流程
熟悉JDK动态代理的朋友都知道,当我们发起调用的时候,实际上会走我们实现的ClientStubInvocationHandler类的invoke方法
大家可以回到第四章的4.5.1 节,这里就不再描述了
具体代码位置:ClientStubInvocationHandler类的invoke方法