手写一个RPC框架,我(小白)行,你(大佬)肯定行

手写一个RPC框架,我(小白)行,你(大佬)肯定行

最近在看八股文的时候看到一个场景题:你会如何设计一个RPC框架?

他的解答是:RPC框架的基本作用是远程调用,调用的过程就是网络通信,通信就需要通信协议吧,通信的过程中参数如何传递,就需要序列化技术了吧,我们在使用RPC的时候肯定需要注册中心吧,那么就需要服务发现服务调用吧,分布式微服务中肯定不止一个服务提供者,如何挑选其一,就需要负载均衡了吧,如何优雅的发起请求让调用方像本地调用一样,就需要动态代理了吧,当我们把问题一个个拆解出来之后,每一个不就是我们熟悉的八股文了嘛

对啊,这不就是八股文吗,我相信绝大部分人肯定都背过这些八股文,但是,八股文背会了就真的能写出来一个RPC框架吗,于是,理论存在,实践开始!

首先远程调用,总得有一个服务提供者和服务调用者吧,于是:

image-20230515171622323

image-20230515171649573

因为服务提供者和服务调用者都需要依赖CalculateService接口,于是创建一个公共的Common类来存放这个接口,里面就两个简单的方法

image-20230515171931647

image-20230515171952705

至此预备工作就绪

接下来就是如何让他们进行远程调用,也就是RPC框架的编写

首先得回忆一下我们是怎么使用RPC框架的,拿Dubbo举例,我们会先在服务的调用方所需要的服务上面加上一个@DubboReference注解,在服务的提供方的接口实现类上面加上@DubboService注解,最后在两边的配置文件里加上注册中心的信息,然后就可以调用了

我们可以画出他具体的调用流程图

image-20230517170611389

接下来梳理整体的大需求:

  1. 我们需要创建一个rpc框架包

  2. 使用了@Reference注解的接口,我们需要让他调用远程方法

  3. 使用了@Service注解的类,我们需要将他注册到注册中心

  4. 实现一个简易的注册中心,向外提供注册服务和获取服务信息的功能

一步一步来解决

首先得把大需求整理成小需求

—使用了@Reference注解的接口,我们需要让他调用远程方法,如下

public class Consumer {

    @AdagioReference
    private CalculateService calculateService;

    public static void main(String[] args) {
        Consumer consumer = new Consumer();
        Object test = consumer.test();
        System.out.println(test);
    }

    public Object test(){
        return calculateService.add(1,2);
    }
}
  1. 我们需要有一个@Reference,那么注解里需要什么属性呢,那就要看这个注解到底需要实现点什么功能

    • 我们需要根据注解里的信息找到具体的服务提供方,最基本的,服务的名字,也可以有服务的版本

      @Target(ElementType.FIELD)
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      public @interface AdagioReference {
          String version() default "1.0";
      
          String name() default "";
          
          //后续可以拓展其他属性,比如负载均衡策略,序列化策略,容错策略等等
      }
      
    • rpc框架需要根据这个注解找到哪些接口是要远程调用的

      问题就是怎么找到,我们可以回想一下Spring里面的@ComponentScan注解,我们可以在该注解内传递一个扫描根路径,然后Spring会自动帮助我们去逐个扫描文件里面的类,类上面的属性、方法,那么有了这些,我们得知哪个接口有注解就很轻松了

      于是我们可以写一个类似于@ComponentScan的工具类:

      public class ClassUtils {
          public static String rootPath = new File("").getAbsolutePath();
          public static String classPath = Thread.currentThread().getContextClassLoader().getResource("").getPath();
      
          public static List<Class<?>> getClasses(File file,List<Class<?>> classList) throws ClassNotFoundException {
              if(file.isDirectory()){
                  File[] files = file.listFiles();
                  if(files != null){
                      for(File f:files){
                          getClasses(f,classList);
                      }
                  }
              }else {
                  String name = file.getName();
                  if(name.endsWith(".class")){
                      String classname = file.getPath().replace(".class","").replace(classPath.replace("/","\\").substring(1),"").replace("\\",".");
                      Class<?> clazz = Class.forName(classname);
                      classList.add(clazz);
                  }
              }
              return classList;
          }
      
          public static List<Class<?>> getClasses() throws ClassNotFoundException{
              return getClasses(new File(classPath),new ArrayList<>());
          }
      
          public static List<Class<?>> getClasses(String path) throws ClassNotFoundException{
              return getClasses(new File(path),new ArrayList<>());
          }
      }
      

      在上述工具类中我们可以指定扫描路径,去把该路径下的所有类扫描出来,之后我们只要根据需要去遍历这些类,找到里面带注解属性或方法,对其进行处理就行,此处我们是需要对所有加了@AdagioReference注解的属性,让他调用方法的时候,都是去调用远程方法

      那么现在的问题就是,我们怎么样让这个属性去调用远程方法而不调用自己的方法,还有一个点,我们的属性都还没有初始化,执行方法肯定会报空指针。当我们用Dubbo的时候是不是完全不用考虑这种问题,因为框架都已经帮我们处理好了。那么现在我们作为框架的开发者,就需要我们自己来处理这些问题。

      第一个问题:我们怎么样让这个属性去调用远程方法而不调用自己的方法,很显然这可以用动态代理来实现,并且我们都是对接口进行的代理,所以直接用JDK动态代理即可,但是不排除有人就喜欢用实现类来代理,这里就有一个拓展点,我们可以使用一个代理工厂,判断需要代理的到底是类还是接口,Dubbo内部就是使用这种方法(我自己还没做完这个拓展点,就不放上来了)

      public class JdkDynamicProxy extends AbstractDynamicProxy{
          public JdkDynamicProxy(Class<?> interfaceClass) {
              super(interfaceClass);
          }
      
          @Override
          public Object getProxy() {
              return Proxy.newProxyInstance(
                      interfaceClass.getClassLoader(),
                      new Class<?>[]{interfaceClass},
                      (proxy,method,args)->{
      					//增强逻辑
                      }
              );
          }
      }
      

      第二个问题:属性没有被初始化,Spring中两个重要思想之一的IOC就是用来解决这个问题的,但我们没有Spring,就手动的写一个简单Bean容器凑合一下

      public class BeanContainer {
          public static final ConcurrentHashMap<String,Object> BEAN_CONTAINER = new ConcurrentHashMap<>();
      
          public static Object getBean(String name){
              return BEAN_CONTAINER.get(name);
          }
          static {
              //初始化方法,去添加Bean
          }
      }
      

      现在整体的思路就非常的明确了,Consumer引入rpc框架使用@AdagioReference注解,当Consumer启动时,会自动扫描找到所有的带@AdagioReference的属性,给他们创建动态代理然后放到Bean容器里去。

      那么我们就先编写BeanContainer的初始化方法

      static {
          try {
              //上述的工具类
              List<Class<?>> classes = ClassUtils.getClasses();
              //遍历类对象里的属性对象,找到带注解的属性进行代理创建
              for (Class<?> clazz: classList) {
                  Field[] fields = clazz.getDeclaredFields();
                  for (Field field : fields) {
                      AdagioReference annotation = field.getAnnotation(AdagioReference.class);
                      //创建代理对象
                      String serverName = "".equals(annotation.name())?field.getName(): annotation.name();
                      //todo 判断到底要以什么接口创建代理,这里就可以尝试用代理工厂了
                      JdkDynamicProxy jdkDynamicProxy = new JdkDynamicProxy(field.getType());
                      Object proxy = jdkDynamicProxy.getProxy();
                      map.put(serverName,proxy);
                  }
              }
          } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
              throw new RuntimeException(e);
          }
      }
      

      上述方法有一个缺点就是:他是懒加载的,所以当我们第一次执行rpc调用的话会比较慢,所以此处可以参考Spring来解决(主要是我自己都还没看过Spring怎么解决的TAT)

      接下来就可以编写代理对象的增强方法了,我们的目标是让他去调用远程的方法,可以按照以下步骤走

      1. 根据注解里的信息向注册中心发起请求得到服务地址
      2. 获取到服务地址就可以发起请求

      那么就逐一编写

      —根据注解里的信息向注册中心发起请求得到服务地址

      注解里的信息?我们刚刚似乎只找到了注解所在位置,但没有去获取注解里面的信息吧,所以我们还得提前把信息保存下来,这个步骤可以在BeanContainer的初始化里写,因为他们都循环遍历了所有的类对象(不建议,还是得让他们各司其职),我写了一个工具类来实现注解信息导入

      /**
       * 用来缓存服务信息
       */
      @Data
      public class RpcCache {
          public static final ConcurrentHashMap<String,RpcConfig> map;
      
          static {
              map = new ConcurrentHashMap<>();
              try {
                  List<Class<?>> classList = ClassUtils.getClasses();
                  //工具类,用于执行注解信息导入
                  AnnotationUtils.rpcConfigExecute(map,classList);
              }catch (ClassNotFoundException e) {
                  throw new RuntimeException(e);
              }
          }
      }
      

      RpcConfig就是用于承载单个服务的注解里面的信息,和@AdagioReference里的属性对应就行,我在这里加了一个序列化格式,用于后面选定序列化方法

      /**
       * 服务信息
       */
      @Data
      public class RpcConfig {
          /**
           * 服务名称
           */
          private String name;
          /**
           * 服务版本
           */
          private String version;
          /**
           * 服务序列化格式
           */
          private String serializeType;
      }
      

      此外我们还缺少注册中心,这个放在后面写,先假设我们有一个帮我们发请求的工具包

      最后就是获取到目标地址该发请求了,告诉他我们需要调用哪一个接口的哪一个方法。哪一个接口?这直接传一个接口的名字就可以了;哪一个方法,这直接传一个名字可以吗?我们知道在java里是有方法重载的,如何区分重载的方法,是根据参数类型来区分的,那么是不是还需要传递一个参数类型的列表。这样我们就可以确定下来具体调用的是哪个接口的哪个方法了,但是我们有了方法没有参数还是不能调用的,所以还需要一个参数列表,综上,我们得到了调用方法的基本信息,在Dubbo中,这种信息有一个实体类来承载,叫Invoker,所以我们也定义一个自己的Invoker

      /**
       * 承载方法调用的信息的对象
       */
      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      public class Invoker implements Serializable {
          private static final long serialVersionUID = -1L;
      
          private String interfaceName;
      
          private String methodName;
      
          private Class<?>[] paramsTypes;
      
          private Object[] params;
      }
      

      有了可以发送的信息之后,我们是不是得让我们的rpc框架去帮助我们发送请求,那么发请求该怎么发,又有很多种选择,我们可以使用原生的URL,也可以使用Netty框架(Dubbo底层用的就是Netty),因为是手写rpc所以我们就简单一点,就发普通的http请求就行了(没错,主要是因为我不会Netty…)

      这里我就用HttpClient来发请求了

      发请求,发什么,怎么发,发给谁

      发什么,我们上面已经知道了,要调用接口方法就发它对应的Invoker就行了

      怎么发,我们传输的是一个对象,网络传输对象肯定需要将对象序列化,我们可以选择jdk自带的序列化,也可以用其他开源的更好的序列化工具,我这里就选用Dubbo底层也使用的Hessian2

      这里同样使用工厂模式打造一个序列化工厂,目前仅包含一个Hessain序列化器

      /**
       * 序列化器工厂
       */
      public class SerializerFactory {
          private static final Map<String, AbstractSerializer> SERIALIZER_MAP = new HashMap<>();
      
          public static AbstractSerializer getSerializer(String name){
              AbstractSerializer serializer = SERIALIZER_MAP.get(name);
              if (serializer == null) {
                  serializer = createSerializer(name);
                  SERIALIZER_MAP.put(name,serializer);
              }
              return serializer;
          }
      
          private static AbstractSerializer createSerializer(String name){
              if("Hessian".equals(name)){
                  return new HessianSerializer();
              }
              throw new IllegalArgumentException("不含有对应的序列化器");
          }
      }
      
      public class HessianSerializer implements AbstractSerializer {
      
          @Override
          public <T> byte[] serialize(T obj) {
              byte[] result;
              ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
              Hessian2Output hessian2Output = new Hessian2Output(byteArrayOutputStream);
              try {
                  hessian2Output.startMessage();
                  hessian2Output.writeObject(obj);
                  hessian2Output.flush();
                  hessian2Output.completeMessage();
                  result = byteArrayOutputStream.toByteArray();
                  return result;
              }catch (IOException e) {
                  e.printStackTrace();
              }finally {
                  try {
                      hessian2Output.close();
                      byteArrayOutputStream.close();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
              return new byte[0];
          }
      
          @Override
          public <T> T deserialize(byte[] bytes) {
              T result;
              ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
              Hessian2Input hessian2Input = new Hessian2Input(byteArrayInputStream);
              try {
                  hessian2Input.startMessage();
                  result = (T) hessian2Input.readObject();
                  hessian2Input.completeMessage();
                  return result;
              } catch (IOException e) {
                 e.printStackTrace();
              }finally {
                  try {
                      hessian2Input.close();
                      byteArrayInputStream.close();
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
              return null;
          }
      }
      

      发给谁,肯定发给已经获取到地址的某一个服务了

      至此,所有消费端发请求的步骤准备好了

      接下来就是实现发请求功能

      (proxy,method,args)->{
          //Invoker创建
          String name = interfaceClass.getSimpleName();
          Invoker invoker = new Invoker();
      
          RpcConfig rpcConfig = RpcCache.map.get(自己定义的key);
          invoker.setMethodName(method.getName());
          invoker.setParams(args);
          invoker.setSerializeType(rpcConfig.getSerializeType());
          invoker.setInterfaceName(name);
          invoker.setParamsTypes(method.getParameterTypes());
          //去注册中心里获取服务信息
          //此处的Register为自己编写的一个注册中心的starter
          RegisterClient registerClient = new RegisterClient("localhost","8081");
          RegisterData registerData = registerClient.getRegisterData(rpcConfig.getName(),rpcConfig.getVersion());
          //发送请求
          //可以做负载均衡,目前还没实现
          Set<String> paths = registerData.getPath();
          String path = new ArrayList<>(paths).get(0);
          //自定义的发请求工具类
          HttpClient client = new HttpClient();
          return client.doPost(path, invoker);
      }
      

      以下是client的doPost方法

      public Object doPost(String path,Invoker invoker){
          CloseableHttpClient client = HttpClients.createDefault();
          //基本信息配置
          //todo path不够优雅
          HttpPost httpPost = new HttpPost("http://"+path);
          httpPost.setHeader("serializeType",invoker.getSerializeType());
          //序列化工厂获取对应序列化器
          AbstractSerializer serializer = SerializerFactory.getSerializer(invoker.getSerializeType());
          //序列化invoker
          byte[] bytes = serializer.serialize(invoker);
          httpPost.setEntity(new ByteArrayEntity(bytes));
          try {
              //发送请求
              HttpEntity entity = client.execute(httpPost).getEntity();
              ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
              entity.writeTo(outputStream);
              byte[] byteArray = outputStream.toByteArray();
              return serializer.deserialize(byteArray);
          } catch (IOException e) {
              e.printStackTrace();
          }finally {
              try {
                  client.close();
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
          return null;
      }
      

      至此,消费者端的全部流程走完

  2. 使用了@AdagioService注解的类,我们需要将他注册到注册中心

    首先这个注解里需要有什么方法,那肯定和@AdagioReference里面几乎是对应的了,消费者方需要根据服务名称和版本号来查找到服务,所以服务方肯定得定义自己的名称和版本号,所以这两个是最基本的

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface AdagioService {
        String name() default "";
        String version() default "1.0";
    }
    

    如何查找并注册呢,这个就和@AdagioReference的查找和注册异曲同工了,这里就不放代码了,注册到注册中心也调用了自己编写的一个starter,发起一个请求到注册中心就行

    作为服务的提供者,肯定需要能接受请求吧,那么如何接受请求呢,可以用netty(如果我会的话),这里还是用Tomcat吧

    public class HttpServer {
        public void start(String hostname,Integer port){
            Tomcat tomcat = new Tomcat();
    
            Server server = tomcat.getServer();
            Service service = server.findService("Tomcat");
    
            Connector connector = new Connector();
            connector.setPort(port);
    
            StandardEngine engine = new StandardEngine();
            engine.setDefaultHost(hostname);
    
            StandardHost host = new StandardHost();
            host.setName(hostname);
    
            String contextPath = "";
            StandardContext context = new StandardContext();
            context.setPath(contextPath);
            context.addLifecycleListener(new Tomcat.FixContextListener());
    
            host.addChild(context);
            engine.addChild(host);
    
            service.setContainer(engine);
            service.addConnector(connector);
    
            tomcat.addServlet(contextPath,"dispatcher",new DispatcherServlet());
            context.addServletMappingDecoded("/*","dispatcher");
            
            try {
                //用于实现@AdagioService的类遍历及class对象缓存
                List<Class<?>> classList = ClassUtils.getClasses();
                AnnotationUtils.serverExecute(classList);
                tomcat.start();
                tomcat.getServer().await();
            }catch (LifecycleException | ClassNotFoundException e){
                e.printStackTrace();
            }
        }
    }
    

    以上代码启动了一个tomcat,并且配置了一个dispatcherServlet进行消息转发

    如下是dispatcherServlet代码

    public class DispatcherServlet extends HttpServlet {
        @Override
        protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            new HttpServerHandler().handler(req,resp);
        }
    }
    

    其中HttpServerHandler才是最终处理请求的部分,才是重点

    分析一下,我们接收到请求应该做什么

    第一,我们接收到的是一个Invoker对象,肯定得先反序列化

    第二,Invoker对象里包含了接口名,方法名,方法参数,参数类型,所以得用反射调用

    所以,如下就是HttpServerHandler的代码

    public class HttpServerHandler extends AbstractHandler{
        @Override
        public void handler(HttpServletRequest req, HttpServletResponse resp) {
            try {
                //todo 目前为可选的序列化方式,后续可添加自定义序列化方式
                //获取输入流并把输入流内容读到byte数组
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                IOUtils.copy(req.getInputStream(),bos);
                byte[] bytes = bos.toByteArray();
                String serializeType = req.getHeader("serializeType");
                AbstractSerializer serializer = SerializerFactory.getSerializer(serializeType);
                //反序列化
                Invoker invoker = serializer.deserialize(bytes);
                String interfaceName = invoker.getInterfaceName();
                Object[] params = invoker.getParams();
                String methodName = invoker.getMethodName();
                Class<?>[] types = invoker.getParamsTypes();
                //反射调用
                Class<?> specificClass = LocalRegister.getSpecificClass(interfaceName);
                Method method = specificClass.getMethod(methodName, types);
                Object obj = method.invoke(specificClass.newInstance(), params);
    
                //写入response中
                IOUtils.write(serializer.serialize(obj), resp.getOutputStream());
            } catch (IOException | NoSuchMethodException | InvocationTargetException |
                     IllegalAccessException | InstantiationException e) {
                throw new RuntimeException(e);
            }
        }
    }
    

    至此,服务调用端和服务提供段的代码均处理完毕,注册中心的代码就不放上来了,写的有点烂TAT

    那么就来调用试下吧

    1. 启动注册中心

      image-20230518152107784

    2. 启动服务提供者

      @AdagioService(name = "CalculateService")
      public class CalculateServiceImpl implements CalculateService {
          @Override
          public Integer add(Integer a, Integer b) {
              return a+b;
          }
      
          @Override
          public Integer minus(Integer a, Integer b) {
              return a-b;
          }
      }
      
      public class Provider {
          public static void main(String[] args) {
              HttpServer server = new HttpServer();
              server.start("localhost",8080);
          }
      }
      

      image-20230518152307050

    3. 启动服务消费者

      public class Consumer {
      
          @AdagioReference
          private CalculateService calculateService = (CalculateService) BeanContainer.getBean("calculateService");
      
          public static void main(String[] args) {
              Consumer consumer = new Consumer();
              Object test = consumer.test();
              System.out.println("运行结果为:"+test);
          }
      
          public Object test(){
              return calculateService.add(1,2);
          }
      }
      

      这里因为没有实现Bean的自动注入,所以就手动取Bean了

      我们此时调用的是add方法,结果应该返回3

      image-20230518152540479

      最终我们实现了远程调用

做个小小的总结,写了一个简易的rpc框架,其实我感觉一点也不简单,要考虑的点非常的多,真的需要全局的思维,并且还要给后续开发留下余地,让新功能的接入不影响老功能,有时考虑的太多反而不知道如何下手了,左右为难。

从看到这篇八股文,到我写出一个能实现基本功能的小框架,也花了一个多月的时间,收获肯定有,最起码的是我对Dubbo的执行过程更加的清楚了。还有写代码,打通思路很重要,在写的时候我经常会写着写着就不知道在写点什么东西了,包括类与类之间的调用,接下来该去写什么,都会突然间迷糊住,这次写一篇总结,也算把整体的流程梳理了一遍,印象应该很深刻了

然后再总结一下这里面还要修改的点和可以提高的点,还蛮多的

1.收发请求还是得用netty

2.动态代理工厂还没实现

3.负载均衡策略,可以用个策略模式

4.注册中心太不完善

5.可以添加服务的心跳检测

6.日志处理不够好,还没百分百还原类似于本地调用的感觉

7.还得手动注入Bean

8.有些值还是写死在代码里的,得动态获取

9.有些代码还是太臃肿,可以考虑复用

10…很多很多值得修改

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值