徒手擼一個簡單的RPC框架

不學無數的程式設計師發表於2019-01-20

之前在牛逼哄哄的 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 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的型別。

完整專案地址

相關文章