徒手寫一個簡單的RPC框架

徒手擼一個簡單的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 annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
    ClassPathScanningCandidateComponentProvider provider = getScanner();
    // 掃描註解器
    provider.addIncludeFilter(new AnnotationTypeFilter(RpcClient.class));
    Set<BeanDefinition> beanDefinitionSet = provider.findCandidateComponents("xyz.amazingwu.blog.rpc");
    for (BeanDefinition beanDefinition : beanDefinitionSet) {
      if (beanDefinition instanceof AnnotatedBeanDefinition) {
        String beanClassAllName = beanDefinition.getBeanClassName();
        //將RpcClient的工廠類註冊進去
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(RpcClientFactoryBean.class);
        //設置RpcClientFactoryBean工廠類中的構造函數的值
        builder.addConstructorArgValue(beanClassAllName);
        builder.getBeanDefinition().setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
        //將其註冊進容器中
        beanDefinitionRegistry.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註解的代理類

@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);
    Long threadId = Thread.currentThread().getId();
    // 使用自己封裝的netty客戶端發送消息
    NettyClientConnect nettyClientConnect = new NettyClientConnect();
    // 發起連接併發送請求消息
    nettyClientConnect.connect(requestJson, threadId);
    return nettyClientConnect.getResponse(threadId).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.setParamTypes(parameterTypes);
    request.setParameters(args);
    //  className遵循服務端的設計
    request.setClassName(getClassName(className));
    return JSON.toJSONString(request);
  }
 
  private String getClassName(String beanClassName) {
    String className = beanClassName.substring(beanClassName.lastIndexOf(".") + 1);
    className = className.substring(0, 1).toLowerCase() + className.substring(1);
    return className;
  }
}

NettyClientConnect的實現如下:

@Slf4j
public class NettyClientConnect {

  private final static NioEventLoopGroup workGroup = new NioEventLoopGroup();

  private final static Map<Long, MessageFuture> futureMap = new ConcurrentHashMap<>();
  private CountDownLatch countDownLatch = new CountDownLatch(1);
  private Throwable throwable;

  public void connect(String requestJson, Long threadId) {
    throwable = null;
    Bootstrap bootstrap = new Bootstrap();
    ChannelFuture channelFuture = bootstrap.group(workGroup).channel(NioSocketChannel.class).handler(new ChannelInitializer<Channel>() {
      @Override
      protected void initChannel(Channel channel) {
        channel.pipeline()
            // 服務端返回消息string解碼
            .addLast(new StringDecoder())
            // 服務端返回消息反序列化(此處爲了簡化起見,使用了JSON序列化)
            .addLast(new SimpleChannelInboundHandler<String>() {
              @Override
              protected void channelRead0(ChannelHandlerContext channelHandlerContext, String s) throws Exception {
                Response response = JSON.parseObject(s, Response.class);
                MessageFuture messageFuture = futureMap.get(threadId);
                messageFuture.setMessage(response);
                countDownLatch.countDown();
              }
              @Override
              public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
                futureMap.put(threadId, new MessageFuture());
              }
              @Override
              public void channelActive(ChannelHandlerContext ctx) {
                ctx.writeAndFlush(Unpooled.wrappedBuffer(requestJson.getBytes()));
              }
            });
      }
    }).option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 2000)
        .connect("127.0.0.1", 10090);
    channelFuture.addListener((ChannelFutureListener) channelFuture1 -> {
      // 檢查操作的狀態
      if (channelFuture1.isSuccess()) {
        log.info("connect success");
      } else {
        // 如果發生錯誤,則訪問描述原因的Throwable
        throwable = channelFuture1.cause();
        log.error("connect error", throwable);
        // 連接異常時釋放鎖
        countDownLatch.countDown();
      }
    });
  }

  public Response getResponse(Long threadId) throws Throwable {
    MessageFuture messageFuture = null;
    try {
      countDownLatch.await();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    if (throwable != null) {
      throw throwable;
    }
    messageFuture = futureMap.get(threadId);
    System.out.println(JSON.toJSON(messageFuture));
    futureMap.remove(threadId);
    return messageFuture.getMessage();
  }
}

我們的客戶端已經寫完了,傳給服務端的信息我們也已經拼裝完畢了。剩下的工作就簡單了,開始編寫服務端的代碼。

服務端處理完以後應該返回客戶端什麼信息?-服務端

服務端的代碼相比較客戶端來說要簡單一些。可以簡單分爲下面三步

  • 拿到接口名以後,通過接口名找到實現類
  • 通過反射進行對應方法的執行
  • 返回執行完的信息

那麼我們就根據這三步進行編寫代碼

拿到接口名以後,通過接口名找到實現類

如何通過接口名拿到對應接口的實現類呢?這就需要我們在服務端啓動的時候將其對應信息加載進去

@Component
@Slf4j
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());
      }
    }
    startPort();
  }

  private String getClassName(String beanClassName) {
    String className = beanClassName.substring(beanClassName.lastIndexOf(".") + 1);
    className = className.substring(0, 1).toLowerCase() + className.substring(1);
    return className;
  }
    
  // netty的服務端設計
  public void startPort() throws IOException {
    ServerBootstrap serverBootstrap = new ServerBootstrap();
    NioEventLoopGroup boos = new NioEventLoopGroup();
    NioEventLoopGroup worker = new NioEventLoopGroup();
    serverBootstrap
        .group(boos, worker)
        .channel(NioServerSocketChannel.class)
        .childHandler(new ChannelInitializer<NioSocketChannel>() {
          @Override
          protected void initChannel(NioSocketChannel ch) {
            ch.pipeline().addLast(new StringDecoder());
            ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
              @Override
              protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
                //獲得實現類處理過後的返回值
                String invokeMethodMes = CommonDeal.getInvokeMethodMes(msg);
                ctx.writeAndFlush(Unpooled.wrappedBuffer(invokeMethodMes.getBytes()));
              }
            });
          }
        }).bind(10090);
  }
}

此時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的類型。

完整項目地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章