Простая структура RPC голыми руками

Java

доУдивительный фреймворк RPC, какой принцип лежит в его основе?Я узнал, что RPC (удаленный вызов процедуры) просто означает вызов удаленной службы точно так же, как вызов локального метода. Используемые в нем знания включают сериализацию и десериализацию, динамический прокси, передачу по сети, динамическую загрузку и отражение. Откройте для себя некоторые из этих знаний. Поэтому я подумал о том, чтобы попробовать реализовать простой RPC-фреймворк самостоятельно, то есть закрепить базовые знания и глубже понять принципы RPC. Конечно, полная структура RPC включает в себя множество функций, таких как обнаружение служб и управление ими, шлюзы и т. д. Эта статья представляет собой простую реализацию вызывающего процесса.

Исходящий анализ параметров

Простой запрос можно разделить на два шага.

Итак, основываясь на этих двух шагах, какую информацию мы должны отправить на сервер перед запросом? И какую информацию сервер должен вернуть клиенту после обработки?

Какую информацию мы должны отправить на сервер перед запросом?

Поскольку то, что мы вызываем на стороне клиента, является интерфейсом, предоставляемым серверной стороной, нам нужно передавать информацию, вызываемую клиентской стороной, тогда мы можем разделить информацию, которая должна быть передана, на две категории.

  • Первая категория заключается в том, что сервер может найти соответствующие классы и методы реализации интерфейса на основе этой информации.
  • Вторая категория — это информация о параметрах, передаваемая при вызове этого метода.

Затем мы анализируем в соответствии с двумя типами передаваемой информации, какую информацию может найти соответствующий метод соответствующего класса реализации? Чтобы найти метод, мы должны сначала найти класс.Здесь мы можем просто использовать экземпляр Bean, предоставленный Spring, для управления 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 при запуске программы. С этой информацией нам нужно передать, вызвать интерфейс, но фактически выполнить процесс сетевой передачи, поэтому нам нужен динамический прокси. Затем его можно разделить на следующие два шага

  • Этап информации об инициализации: зарегистрируйте ключ как имя интерфейса и значение как класс динамического интерфейса в контейнере Spring.
  • Фаза выполнения: через динамический прокси фактическое выполнение сетевой передачи

Этап информации об инициализации

Так как мы используем Spring в качестве управления бинами, нам нужно зарегистрировать интерфейс и соответствующий прокси-класс в контейнере 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Наследовать от весны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метод, когда фабричный класс внедряется в контейнер, какой тип класса возвращается этим методом, а затем какой тип класса зарегистрирован в контейнере.

Затем взгляните на созданный нами прокси-класс.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тип.

полный адрес проекта