来源:https://juejin.im/post/5c4481a4f265da613438aec3

之前在牛逼哄哄的 RPC 框架,底层到底什么原理得知了RPC(远程过程调用)简单来说就是调用远程的服务就像调用本地方法一样,其中用到的知识有序列化和反序列化、动态代理、网络传输、动态加载、反射这些知识点。发现这些知识都了解一些。所以就想着试试自己实现一个简单的RPC框架,即巩固了基础的知识,也能更加深入的了解RPC原理。当然一个完整的RPC框架包含了许多的功能,例如服务的发现与治理,网关等等。本篇只是简单的实现了一个调用的过程。传参出参分析

一个简单请求可以抽象为两步

那么就根据这两步进行分析,在请求之前我们应该发送给服务端什么信息?而服务端处理完以后应该返回客户端什么信息?

在请求之前我们应该发送给服务端什么信息?

由于我们在客户端调用的是服务端提供的接口,所以我们需要将客户端调用的信息传输过去,那么我们可以将要传输的信息分为两类

  • 第一类是服务端可以根据这个信息找到相应的接口实现类和方法
  • 第二类是调用此方法传输的参数信息

那么我们就根据要传输的两类信息进行分析,什么信息能够找到相应的实现类的相应的方法?要找到方法必须要先找到类,这里我们可以简单的用Spring提供的Bean实例管理ApplicationContext进行类的寻找。所以要找到类的实例只需要知道此类的名字就行,找到了类的实例,那么如何找到方法呢?在反射中通过反射能够根据方法名和参数类型从而找到这个方法。那么此时第一类的信息我们就明了了,那么就建立相应的是实体类存储这些信息。

@Data
public class Request implements Serializable {
private static final long serialVersionUID = 3933918042687238629L;
private String className;
private String methodName;
private Class<?> [] parameTypes;
private Object [] parameters;
}
复制代码

服务端处理完以后应该返回客户端什么信息?

上面我们分析了客户端应该传输什么信息给服务端,那么服务端处理完以后应该传什么样的返回值呢?这里我们只考虑最简单的情况,客户端请求的线程也会一直在等着,不会有异步处理这一说,所以这么分析的话就简单了,直接将得到的处理结果返回就行了。

@Data
public class Response implements Serializable {
private static final long serialVersionUID = -2393333111247658778L;
private Object result;
}
复制代码

由于都涉及到了网络传输,所以都要实现序列化的接口

如何获得传参信息并执行?-客户端

上面我们分析了客户端向服务端发送的信息都有哪些?那么我们如何获得这些信息呢?首先我们调用的是接口,所以我们需要写自定义注解然后在程序启动的时候将这些信息加载在Spring容器中。有了这些信息那么我们就需要传输了,调用接口但是实际上执行的确实网络传输的过程,所以我们需要动态代理。那么就可以分为以下两步

  • 初始化信息阶段:将key为接口名,value为动态接口类注册进Spring容器中
  • 执行阶段:通过动态代理,实际执行网络传输

初始化信息阶段

由于我们使用Spring作为Bean的管理,所以要将接口和对应的代理类注册进Spring容器中。而我们如何找到我们想要调用的接口类呢?我们可以自定义注解进行扫描。将想要调用的接口全部注册进容器中。

创建一个注解类,用于标注哪些接口是可以进行Rpc的

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RpcClient {
} 复制代码

然后创建对于@RpcClient注解的扫描类RpcInitConfig,将其注册进Spring容器中

public class RpcInitConfig implements ImportBeanDefinitionRegistrar{

    @Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider provider = getScanner();
//设置扫描器
provider.addIncludeFilter(new AnnotationTypeFilter(RpcClient.class));
//扫描此包下的所有带有@RpcClient的注解的类
Set<BeanDefinition> beanDefinitionSet = provider.findCandidateComponents("com.example.rpcclient.client");
for (BeanDefinition beanDefinition : beanDefinitionSet){
if (beanDefinition instanceof AnnotatedBeanDefinition){
//获得注解上的参数信息
AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) beanDefinition;
String beanClassAllName = beanDefinition.getBeanClassName();
Map<String, Object> paraMap = annotatedBeanDefinition.getMetadata()
.getAnnotationAttributes(RpcClient.class.getCanonicalName());
//将RpcClient的工厂类注册进去
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(RpcClinetFactoryBean.class);
//设置RpcClinetFactoryBean工厂类中的构造函数的值
builder.addConstructorArgValue(beanClassAllName);
builder.getBeanDefinition().setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
//将其注册进容器中
registry.registerBeanDefinition(
beanClassAllName ,
builder.getBeanDefinition());
}
}
}
//允许Spring扫描接口上的注解
protected ClassPathScanningCandidateComponentProvider getScanner() {
return new ClassPathScanningCandidateComponentProvider(false) {
@Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent();
}
};
}
}
复制代码

由于上面注册的是工厂类,所以我们建立一个工厂类RpcClinetFactoryBean继承Spring中的FactoryBean类,由其统一创建@RpcClient注解的代理类

如果对FactoryBean类不了解的可以参见FactoryBean讲解

@Data
public class RpcClinetFactoryBean implements FactoryBean { @Autowired
private RpcDynamicPro rpcDynamicPro; private Class<?> classType; public RpcClinetFactoryBean(Class<?> classType) {
this.classType = classType;
} @Override
public Object getObject(){
ClassLoader classLoader = classType.getClassLoader();
Object object = Proxy.newProxyInstance(classLoader,new Class<?>[]{classType},rpcDynamicPro);
return object;
} @Override
public Class<?> getObjectType() {
return this.classType;
} @Override
public boolean isSingleton() {
return false;
}
} 复制代码

注意此处的getObjectType方法,在将工厂类注入到容器中的时候,这个方法返回的是什么Class类型那么注册进容器中就是什么Class类型。

然后看一下我们创建的代理类rpcDynamicPro

@Component
@Slf4j
public class RpcDynamicPro implements InvocationHandler { @Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String requestJson = objectToJson(method,args);
Socket client = new Socket("127.0.0.1", 20006);
client.setSoTimeout(10000);
//获取Socket的输出流,用来发送数据到服务端
PrintStream out = new PrintStream(client.getOutputStream());
//获取Socket的输入流,用来接收从服务端发送过来的数据
BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream()));
//发送数据到服务端
out.println(requestJson);
Response response = new Response();
Gson gson =new Gson();
try{
//从服务器端接收数据有个时间限制(系统自设,也可以自己设置),超过了这个时间,便会抛出该异常
String responsJson = buf.readLine();
response = gson.fromJson(responsJson, Response.class);
}catch(SocketTimeoutException e){
log.info("Time out, No response");
}
if(client != null){
//如果构造函数建立起了连接,则关闭套接字,如果没有建立起连接,自然不用关闭
client.close(); //只关闭socket,其关联的输入输出流也会被关闭
}
return response.getResult();
} public String objectToJson(Method method,Object [] args){
Request request = new Request();
String methodName = method.getName();
Class<?>[] parameterTypes = method.getParameterTypes();
String className = method.getDeclaringClass().getName();
request.setMethodName(methodName);
request.setParameTypes(parameterTypes);
request.setParameters(args);
request.setClassName(getClassName(className));
GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapterFactory(new ClassTypeAdapterFactory());
Gson gson = gsonBuilder.create();
return gson.toJson(request);
} private String getClassName(String beanClassName){
String className = beanClassName.substring(beanClassName.lastIndexOf(".")+1);
className = className.substring(0,1).toLowerCase() + className.substring(1);
return className;
}
} 复制代码

我们的客户端已经写完了,传给服务端的信息我们也已经拼装完毕了。剩下的工作就简单了,开始编写服务端的代码。

服务端处理完以后应该返回客户端什么信息?-服务端

服务端的代码相比较客户端来说要简单一些。可以简单分为下面三步

  • 拿到接口名以后,通过接口名找到实现类
  • 通过反射进行对应方法的执行
  • 返回执行完的信息

那么我们就根据这三步进行编写代码

拿到接口名以后,通过接口名找到实现类

如何通过接口名拿到对应接口的实现类呢?这就需要我们在服务端启动的时候将其对应信息加载进去

@Component
@Log4j
public class InitRpcConfig implements CommandLineRunner {
@Autowired
private ApplicationContext applicationContext; public static Map<String,Object> rpcServiceMap = new HashMap<>(); @Override
public void run(String... args) throws Exception {
Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(Service.class);
for (Object bean: beansWithAnnotation.values()){
Class<?> clazz = bean.getClass();
Class<?>[] interfaces = clazz.getInterfaces();
for (Class<?> inter : interfaces){
rpcServiceMap.put(getClassName(inter.getName()),bean);
log.info("已经加载的服务:"+inter.getName());
}
}
} private String getClassName(String beanClassName){
String className = beanClassName.substring(beanClassName.lastIndexOf(".")+1);
className = className.substring(0,1).toLowerCase() + className.substring(1);
return className;
}
} 复制代码

此时rpcServiceMap存储的就是接口名和其对应的实现类的对应关系。

通过反射进行对应方法的执行

此时拿到了对应关系以后就能根据客户端传过来的信息找到相应的实现类中的方法。然后进行执行并返回信息就行

public Response invokeMethod(Request request){
String className = request.getClassName();
String methodName = request.getMethodName();
Object[] parameters = request.getParameters();
Class<?>[] parameTypes = request.getParameTypes();
Object o = InitRpcConfig.rpcServiceMap.get(className);
Response response = new Response();
try {
Method method = o.getClass().getDeclaredMethod(methodName, parameTypes);
Object invokeMethod = method.invoke(o, parameters);
response.setResult(invokeMethod);
} catch (NoSuchMethodException e) {
log.info("没有找到"+methodName);
} catch (IllegalAccessException e) {
log.info("执行错误"+parameters);
} catch (InvocationTargetException e) {
log.info("执行错误"+parameters);
}
return response;
}
复制代码

现在我们两个服务都启动起来并且在客户端进行调用就发现只是调用接口就能调用过来了。

总结

到现在一个简单的RPC就完成了,但是其中还有很多的功能需要完善,例如一个完整RPC框架肯定还需要服务注册与发现,而且双方通信肯定也不能是直接开启一个线程一直在等着,肯定需要是异步的等等的各种功能。后面随着学习的深入,这个框架也会慢慢增加一些东西。不仅是对所学知识的一个应用,更是一个总结。有时候学一个东西学起来觉得很简单,但是真正应用的时候就会发现各种各样的小问题。比如在写这个例子的时候碰到一个问题就是@Autowired的时候一直找不到SendMessage的类型,最后才发现是工厂类RpcClinetFactoryBean中的getObjectType中的返回类型写错了,我之前写的是

    public Class<?> getObjectType() {
return this.getClass();;
} 复制代码

这样的话注册进容器的就是RpcClinetFactoryBean类型的而不是SendMessage的类型。

完整项目地址

作者:不学无数的程序员
链接:https://juejin.im/post/5c4481a4f265da613438aec3

徒手撸一个简单的RPC框架的更多相关文章

  1. 【RPC】手撸一个简单的RPC框架实现

      涉及技术   序列化.Socket通信.Java动态代理技术,反射机制   角色   1.服务提供者:运行在服务端,是真实的服务实现类   2.服务发布监听者:运行在RPC服务端,1将服务端提供的 ...

  2. 动手实现一个简单的 rpc 框架到入门 grpc (上)

    rpc 全称 Remote Procedure Call 远程过程调用,即调用远程方法.我们调用当前进程中的方法时很简单,但是想要调用不同进程,甚至不同主机.不同语言中的方法时就需要借助 rpc 来实 ...

  3. 动手实现一个简单的 rpc 框架到入门 grpc (下)

    之前手动实现了一次简陋的 rpc 调用,为了简单使用了 json 编码信息,其实这是非常不可靠的,go 中 json 解析会有一些问题,比如整数会变成浮点数,而且 json 字符串比较占空间. gRP ...

  4. 一个简单的"RPC框架"代码分析

    0,服务接口定义---Echo.java /* * 定义了服务器提供的服务类型 */ public interface Echo { public String echo(String string) ...

  5. 一个简单的RPC框架

    一个 系统模型 二.数据库代码实现 1. mkdir database cd database vim dbInit.c /* * * Database Init tool * */ #include ...

  6. 一个简单的RPC框架实现

    package com.rpc; public interface EchoService { String echo(String ping); } package com.rpc; public ...

  7. 自己用 Netty 实现一个简单的 RPC

    目录: 需求 设计 实现 创建 maven 项目,导入 Netty 4.1.16. 项目目录结构 设计接口 提供者相关实现 消费者相关实现 测试结果 总结 源码地址:github 地址 前言 众所周知 ...

  8. 最简单的RPC框架实现

    通过java原生的序列化,Socket通信,动态代理和反射机制,实现一个简单的RPC框架,由三部分组成: 1.服务提供者,运行再服务端,负责提供服务接口定义和服务实现类 2.服务发布者,运行再RPC服 ...

  9. 教你用 Netty 实现一个简单的 RPC!

    众所周知,dubbo 底层使用了 Netty 作为网络通讯框架,而 Netty 的高性能我们之前也分析过源码,对他也算还是比较了解了. 今天我们就自己用 Netty 实现一个简单的 RPC 框架. 1 ...

随机推荐

  1. 使用python对工作簿每个sheet表进行数据可视化展示(本案例是从第2个sheet开始循环读取也就是索引为1的表)

    # 导入相关模块from pyecharts.charts import Barfrom pyecharts import options as optsfrom pyecharts.charts i ...

  2. war项目依赖war项目

    还没有看,立个flag:https://my.oschina.net/u/588379/blog/1817077

  3. 基于 apache-arrow 的 duckdb rust 客户端

    背景 duckdb 是一个 C++ 编写的单机版嵌入式分析型数据库.它刚开源的时候是对标 SQLite 的列存数据库,并提供与 SQLite 一样的易用性,编译成一个头文件和一个 cpp 文件就可以在 ...

  4. 网络损伤仪WANsim--不同的部署方式

    网络损伤仪WANsim的业务口在逻辑上是不存在IP地址与MAC地址的,所以,WANsim可以串接在测试拓扑中的任意位置,只需要确保有流量通过WANsim即可. 不同的拓扑结构会对测试的结果造成影响.在 ...

  5. CF833B-线段树优化DP

    CF833B-线段树优化DP 题意 将一个长为\(n\)的序列分成\(k\)段,每段贡献为其中不同数字的个数,求最大贡献和. 思路 此处感谢@gxy001 聚铑的精彩讲解 先考虑暴力DP,可以想到一个 ...

  6. erase

    erase详细解释及原理 我们先定义一个字符串string string.erase(iterator) iterator表示要删除元素的迭代器. string.erase(it_begin,it_e ...

  7. 【贪心+排序】排队接水 luogu-1223

    题目描述 有n个人在一个水龙头前排队接水,假如每个人接水的时间为Ti,请编程找出这n个人排队的一种顺序,使得n个人的平均等待时间最小. 分析 注意要开longlong AC代码 #include &l ...

  8. CFS靶机

    nmap扫描 nmap -sV -p1-65535 192.168.1.135 thinkphp5.0版本 找到poc进行测试 http://192.168.1.135/index.php?s=ind ...

  9. SQL Server添加字段语法

    通用式: alter table [表名] add [字段名] 字段属性 default 缺省值 default 是可选参数增加字段: alter table [表名] add 字段名 smallin ...

  10. [JS]异步任务之事件循环

    前言 常常会听到单线程和多线程这两个名词,单线程即一个时间段内程序从上到下执行任务,多线程即一个时间段内程序同时执行多个任务. 然而 JavaScript 是单线程的,它不像 Java 那样新开启一个 ...