编程语言大多数都不清楚的技术

    作者:儒雅程序员更新于: 2020-01-13 10:21:22

    大神带你学编程,欢迎选课

    从0 开始手写一个 RPC 框架,大多数都不清楚的技术。编程语言原本是被设计成专门使用在计算机上的,但它们也可以用来定义算法或者数据结构。正是因为如此,程序员才会试图使程序代码更容易阅读。

    RPC 框架底层到底什么原理得知了RPC(远程过程调用)简单来说就是调用远程的服务就像调用本地方法一样,其中用到的知识有序列化和反序列化、动态代理、网络传输、动态加载、反射这些知识点。

    编程语言大多数都不清楚的技术_编程语言_Java视频_Javascript视频课程_课课家

    RPC 框架底层到底什么原理得知了RPC(远程过程调用)简单来说就是调用远程的服务就像调用本地方法一样,其中用到的知识有序列化和反序列化、动态代理、网络传输、动态加载、反射这些知识点。

    发现这些知识都了解一些。所以就想着试试自己实现一个简单的RPC框架,即巩固了基础的知识,也能更加深入的了解RPC原理。

    当然一个完整的RPC框架包含了许多的功能,例如服务的发现与治理,网关等等,本篇只是简单的实现了一个调用的过程。

    传参出参分析

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

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

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

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

    第一类是服务端可以根据这个信息找到相应的接口实现类和方法

    第二类是调用此方法传输的参数信息

    那么我们就根据要传输的两类信息进行分析,什么信息能够找到相应的实现类的相应的方法?要找到方法必须要先找到类,这里我们可以简单的用spring提供的Bean实例管理ApplicationContext进行类的寻找。

    所以要找到类的实例只需要知道此类的名字就行,找到了类的实例,那么如何找到方法呢?

    在反射中通过反射能够根据方法名和参数类型从而找到这个方法。那么此时第一类的信息我们就明了了,那么就建立相应的是实体类存储这些信息。

    1. @Data 
    2. public class Request implements Serializable { 
    3.  private static final long serialVersionUID = 3933918042687238629L; 
    4.  private String className; 
    5.  private String methodName; 
    6.  private Class [] parameTypes; 
    7.  private Object [] parameters; 

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

    上面我们分析了客户端应该传输什么信息给服务端,那么服务端处理完以后应该传什么样的返回值呢?

    这里我们只考虑最简单的情况,客户端请求的线程也会一直在等着,不会有异步处理这一说,所以这么分析的话就简单了,直接将得到的处理结果返回就行了。

    1. @Data 
    2. public class Response implements Serializable { 
    3.  private static final long serialVersionUID = -2393333111247658778L; 
    4.  private Object result; 

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

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

    上面我们分析了客户端向服务端发送的信息都有哪些?那么我们如何获得这些信息呢?

    首先我们调用的是接口,所以我们需要写自定义注解然后在程序启动的时候将这些信息加载在Spring容器中。

    有了这些信息那么我们就需要传输了,调用接口但是实际上执行的确实网络传输的过程,所以我们需要动态代理。那么就可以分为以下两步

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

    初始化信息阶段

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

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

    1. @Target({ElementType.TYPE}) 
    2. @Retention(RetentionPolicy.RUNTIME) 
    3. public @interface RpcClient { 

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

    1. public class RpcInitConfig implements ImportBeanDefinitionRegistrar{ 
    2.  @Override 
    3.  public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { 
    4.  ClassPathScanningCandidateComponentProvider provider = getScanner(); 
    5.  //设置扫描器 
    6.  provider.addIncludeFilter(new AnnotationTypeFilter(RpcClient.class)); 
    7.  //扫描此包下的所有带有@RpcClient的注解的类 
    8.  Set beanDefinitionSet = provider.findCandidateComponents("com.example.rpcclient.client"); 
    9.  for (BeanDefinition beanDefinition : beanDefinitionSet){ 
    10.  if (beanDefinition instanceof AnnotatedBeanDefinition){ 
    11.  //获得注解上的参数信息 
    12.  AnnotatedBeanDefinition annotatedBeanDefinition = (AnnotatedBeanDefinition) beanDefinition; 
    13.  String beanClassAllName = beanDefinition.getBeanClassName(); 
    14.  Map paraMap = annotatedBeanDefinition.getMetadata() 
    15.  .getAnnotationAttributes(RpcClient.class.getCanonicalName()); 
    16.  //将RpcClient的工厂类注册进去 
    17.  BeanDefinitionBuilder builder = BeanDefinitionBuilder 
    18.  .genericBeanDefinition(RpcClinetFactoryBean.class); 
    19.  //设置RpcClinetFactoryBean工厂类中的构造函数的值 
    20.  builder.addConstructorArgValue(beanClassAllName); 
    21.  builder.getBeanDefinition().setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); 
    22.  //将其注册进容器中 
    23.  registry.registerBeanDefinition( 
    24.  beanClassAllName , 
    25.  builder.getBeanDefinition()); 
    26.  } 
    27.  } 
    28.  } 
    29.  //允许Spring扫描接口上的注解 
    30.  protected ClassPathScanningCandidateComponentProvider getScanner() { 
    31.  return new ClassPathScanningCandidateComponentProvider(false) { 
    32.  @Override 
    33.  protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { 
    34.  return beanDefinition.getMetadata().isInterface() && beanDefinition.getMetadata().isIndependent(); 
    35.  } 
    36.  }; 
    37.  } 

    由于上面注册的是工厂类,所以我们建立一个工厂类RpcClinetFactoryBean继承Spring中的FactoryBean类,由其统一创建@RpcClient注解的代理类。推荐阅读:Spring零配置之@Configuration注解详解。

    1. @Data 
    2. public class RpcClinetFactoryBean implements FactoryBean { 
    3.  @Autowired 
    4.  private RpcDynamicPro rpcDynamicPro; 
    5.  private Class classType; 
    6.  public RpcClinetFactoryBean(Class classType) { 
    7.  this.classType = classType; 
    8.  } 
    9.  @Override 
    10.  public Object getObject(){ 
    11.  ClassLoader classLoader = classType.getClassLoader(); 
    12.  Object object = Proxy.newProxyInstance(classLoader,new Class[]{classType},rpcDynamicPro); 
    13.  return object; 
    14.  } 
    15.  @Override 
    16.  public Class getObjectType() { 
    17.  return this.classType; 
    18.  } 
    19.  @Override 
    20.  public boolean isSingleton() { 
    21.  return false
    22.  } 

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

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

    1. @Component 
    2. @Slf4j 
    3. public class RpcDynamicPro implements InvocationHandler { 
    4.  @Override 
    5.  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 
    6.  String requestJson = objectToJson(method,args); 
    7.  Socket client = new Socket("127.0.0.1", 20006); 
    8.  client.setSoTimeout(10000); 
    9.  //获取Socket的输出流,用来发送数据到服务端 
    10.  PrintStream out = new PrintStream(client.getOutputStream()); 
    11.  //获取Socket的输入流,用来接收从服务端发送过来的数据 
    12.  BufferedReader buf = new BufferedReader(new InputStreamReader(client.getInputStream())); 
    13.  //发送数据到服务端 
    14.  out.println(requestJson); 
    15.  Response response = new Response(); 
    16.  Gson gson =new Gson(); 
    17.  try{ 
    18.  //从服务器端接收数据有个时间限制(系统自设,也可以自己设置),超过了这个时间,便会抛出该异常 
    19.  String responsJson = buf.readLine(); 
    20.  response = gson.fromJson(responsJson, Response.class); 
    21.  }catch(SocketTimeoutException e){ 
    22.  log.info("Time out, No response"); 
    23.  } 
    24.  if(client != null){ 
    25.  //如果构造函数建立起了连接,则关闭套接字,如果没有建立起连接,自然不用关闭 
    26.  client.close(); //只关闭socket,其关联的输入输出流也会被关闭 
    27.  } 
    28.  return response.getResult(); 
    29.  } 
    30.  public String objectToJson(Method method,Object [] args){ 
    31.  Request request = new Request(); 
    32.  String methodName = method.getName(); 
    33.  Class[] parameterTypes = method.getParameterTypes(); 
    34.  String className = method.getDeclaringClass().getName(); 
    35.  request.setMethodName(methodName); 
    36.  request.setParameTypes(parameterTypes); 
    37.  request.setParameters(args); 
    38.  request.setClassName(getClassName(className)); 
    39.  GsonBuilder gsonBuilder = new GsonBuilder(); 
    40.  gsonBuilder.registerTypeAdapterFactory(new ClassTypeAdapterFactory()); 
    41.  Gson gson = gsonBuilder.create(); 
    42.  return gson.toJson(request); 
    43.  } 
    44.  private String getClassName(String beanClassName){ 
    45.  String className = beanClassName.substring(beanClassName.lastIndexOf(".")+1); 
    46.  className = className.substring(0,1).toLowerCase() + className.substring(1); 
    47.  return className; 
    48.  } 

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

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

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

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

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

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

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

    1. @Component 
    2. @Log4j 
    3. public class InitRpcConfig implements CommandLineRunner { 
    4.  @Autowired 
    5.  private ApplicationContext applicationContext; 
    6.  public static Map rpcServiceMap = new HashMap<>(); 
    7.  @Override 
    8.  public void run(String... args) throws Exception { 
    9.  Map beansWithAnnotation = applicationContext.getBeansWithAnnotation(Service.class); 
    10.  for (Object bean: beansWithAnnotation.values()){ 
    11.  Class clazz = bean.getClass(); 
    12.  Class[] interfaces = clazz.getInterfaces(); 
    13.  for (Class inter : interfaces){ 
    14.  rpcServiceMap.put(getClassName(inter.getName()),bean); 
    15.  log.info("已经加载的服务:"+inter.getName()); 
    16.  } 
    17.  } 
    18.  } 
    19.  private String getClassName(String beanClassName){ 
    20.  String className = beanClassName.substring(beanClassName.lastIndexOf(".")+1); 
    21.  className = className.substring(0,1).toLowerCase() + className.substring(1); 
    22.  return className; 
    23.  } 

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

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

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

    1. public Response invokeMethod(Request request){ 
    2.  String className = request.getClassName(); 
    3.  String methodName = request.getMethodName(); 
    4.  Object[] parameters = request.getParameters(); 
    5.  Class[] parameTypes = request.getParameTypes(); 
    6.  Object o = InitRpcConfig.rpcServiceMap.get(className); 
    7.  Response response = new Response(); 
    8.  try { 
    9.  Method method = o.getClass().getDeclaredMethod(methodName, parameTypes); 
    10.  Object invokeMethod = method.invoke(o, parameters); 
    11.  response.setResult(invokeMethod); 
    12.  } catch (NoSuchMethodException e) { 
    13.  log.info("没有找到"+methodName); 
    14.  } catch (IllegalAccessException e) { 
    15.  log.info("执行错误"+parameters); 
    16.  } catch (InvocationTargetException e) { 
    17.  log.info("执行错误"+parameters); 
    18.  } 
    19.  return response; 

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

    总结

    到现在一个简单的RPC就完成了,但是其中还有很多的功能需要完善,例如一个完整RPC框架肯定还需要服务注册与发现,而且双方通信肯定也不能是直接开启一个线程一直在等着,肯定需要是异步的等等的各种功能。

    后面随着学习的深入,这个框架也会慢慢增加一些东西。不仅是对所学知识的一个应用,更是一个总结。有时候学一个东西学起来觉得很简单,但是真正应用的时候就会发现各种各样的小问题。

    比如在写这个例子的时候碰到一个问题就是@Autowired的时候一直找不到SendMessage的类型,最后才发现是工厂类

    1. public Class getObjectType() { 
    2.  return this.getClass();; 

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

    编程语言(programming language)是一种被标准化的交流技巧,用来向计算机发出指令,定义计算机程序,让程序员能够准确地定义计算机所需要使用的数据,并精确地定义在不同情况下所应当采取的行动的一种计算机语言。 编程语言可以分成机器语言、汇编语言、高级语言三大类。计算机领域已发明了上千不同的编程语言,而且每年仍有新的编程语言诞生。

课课家教育

未登录