從 0 開始手寫一個 RPC 框架,輕鬆搞定!

2021-02-18 芋道源碼

得知了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的類型。

相關焦點

  • 從零開始,徒手擼一個簡單的 RPC 框架,輕鬆搞定!
    所以就想著試試自己實現一個簡單的RPC框架,即鞏固了基礎的知識,也能更加深入的了解RPC原理。當然一個完整的RPC框架包含了許多的功能,例如服務的發現與治理,網關等等。本篇只是簡單的實現了一個調用的過程。傳參出參分析一個簡單請求可以抽象為兩步
  • 從0 開始手寫一個 Mybatis 框架,三步搞定!
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫繼上一篇手寫SpringMVC之後,我最近趁熱打鐵,研究了一下Mybatis。MyBatis框架的核心功能其實不難,無非就是動態代理和jdbc的操作,難的是寫出來可擴展,高內聚,低耦合的規範的代碼。
  • 一文探討 RPC 框架中的服務線程隔離
    Kirito 推薦語:最近秋招開始了,很多學生開始準備起了秋招,有很多人想知道進一些有名的網際網路公司實習有什麼要求,正好最近跟一位阿里春招的實習小夥子聊了一些 RPC 相關的知識點,於是我把這篇他的思考轉發過來,給大家參考下,我覺得有這樣的實力,進大廠實習應該是沒有問題的。
  • 從0 到 1:全面理解 RPC 遠程調用!
    有了這個模塊,開啟一個 rpc server,就變得相當簡單了。答案是很多,很多web框架其自身都自己實現了json-rpc,但我們要獨立這些框架之外,要尋求一種較為乾淨的解決方案,我查找到的選擇有兩種第一種是 jsonrpclibpip install jsonrpclib -i https://pypi.douban.com/simple
  • 用 Python 編寫簡單的 gRPC 服務
    RPC是遠程過程調用(Remote Procedure Call)的縮寫形式,可以理解為RPC就是要像調用本地的函數一樣去調遠程函數,gRPC就是Google開源的RPC框架。這裡寫個簡單的Python gRPC示例,能實現加法和乘法的計算器:版本信息:Python 3.6.8grpcio 1.25.0grpcio-tools 1.25.0nginx version: nginx/1.14.0開始環境準備安裝gRPC相關的庫,grpcio-tools
  • 搜狗開源 srpc:自研高性能通用 RPC 框架
    現如今,搜狗又宣布開源 Workflow 的生態項目——srpc,一個基於 Workflow 打造的輕量級 RPC 框架。一個性能更好的 thrift/brpcsrpc 與 thrift/brpc 是協議與 IDL 均互通的。
  • RPC 框架,底層到底什麼原理?
    RPC框架的概念RPC(Remote Procedure Call)–遠程過程調用,通過網絡通信調用不同的服務,共同支撐一個軟體系統,微服務實現的基石技術。使用RPC可以解耦系統,方便維護,同時增加系統處理請求的能力。
  • go-zero 1.1.2 發布,web 和 rpc 框架
    go-zero 是一個集成了各種工程實踐的 web 和 rpc 框架。通過彈性設計保障了大並發服務端的穩定性,經受了充分的實戰檢驗。go-zero 包含極簡的 API 定義和生成工具 goctl,可以根據定義的 API 文件一鍵生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代碼,並可直接運行。
  • pomelo 0.8 發布,網易遊戲伺服器框架
    rpc調用方式改進當進行rpc調用的時候,增加了跳過路由計算而直接將調用發送到一個具體的伺服器或者廣播到一類伺服器的調用方式,代碼實例如下:// routeapp.rpc.<ServerType>.<Remote>.
  • 從 0 開始手寫一個Tomcat,7 步搞定!
    Tomcat 是非常流行的 Web Server,它還是一個滿足 Servlet 規範的容器。那麼想一想,Tomcat 和我們的 Web 應用是什麼關係?第二,進行請求的分發要知道一個 Tomcat 可以為多個 Web 應用提供服務,那麼很顯然,Tomcat 可以把 URL 下發到不同的Web應用。
  • 網易開源遊戲伺服器框架 pomelo 0.9 版發布
    ## pomelo rpc支持zeromq通信在pomelo 0.9中提供了基於zmq的rpc調用,開發者可以根據需要選擇原有的pomelo-rpc或者pomelo-rpc-zeromq。基於zeromq和原有的pomelo-rpc的性能對比測試結果可以參考:具體使用方法:1. 安裝zeromq2.
  • 基於Nest.js + React 的開發框架 Notadd 2.0 Beta2 發布
    前言    大多數 node.js 框架都沒解決架構問題,使得 node.js 沒能像 spring
  • 萬字長文 | 從實踐到原理,帶你參透 gRPC
    簡述gRPC 是一個高性能、開源和通用的 RPC 框架,面向移動和 HTTP/2 設計。gRPC Server 開始 lis.Accept,直到 Stop 或 GracefulStop。Clientfunc main() {    conn, err := grpc.Dial(":"+PORT, grpc.WithInsecure())    ...
  • 十行代碼就能搞定深度學習?飛槳框架高層API,一起輕鬆玩轉AI
    機器之心發布機器之心編輯部嚮往深度學習技術,可是深度學習框架太難學怎麼辦?百度傾心打造飛槳框架高層 API,零基礎也能輕鬆上手深度學習,一起來看看吧?我們先通過一個深度學習中經典的手寫數字分類任務,來簡單了解飛槳高層 API。然後再詳細的介紹每個模塊中所包含的 API。
  • 眾包翻譯文檔分享 |《 gRPC 官方文檔中文版》
    《gRPC 官方文檔中文版》日前在開源中國眾包平臺翻譯完成,現發布與各位 OSCer 共享:http://doc.oschina.net/grpcgRPC  是一個高性能、開源和通用的 RPC 框架,面向移動和 HTTP/2 設計。
  • 以太坊Truffle框架搭建一氣呵成無坑版教程
    本文約1600字+,閱讀(觀看)需要15分鐘Truffle框架搭建的教程網上也有好多,但是隨著truffle版本的更新,truffle.js改名truffle-config.js、truffle命令升級、truffle與testrpc埠不一致、瀏覽器錢包插件等問題,導致原有的教程不再適用
  • 微服務與RPC
    所以,業內對微服務的實現,基本是確定一個組織邊界,在該邊界內,使用RPC; 邊界外,使用Restful。這個邊界,可以是業務、部門,甚至是全公司。二、 RPC技術選型RPC技術選型上,原則也是選擇自己熟悉的,或者公司內部內定的框架。 如果是新業務,則現在可選的框架其實也不多,卻也足夠讓人糾結。
  • Spring集成RabbitMQ簡單實現RPC
    xml version="1.0" encoding="UTF-8"?/schema/context/spring-context-4.0.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop-4.0.xsdhttp
  • 教孩子寫遊戲:手寫一個最最簡單的Windows程序框架
    這一節課,我們就用這個集成工作環境寫一個Windows程序的框架。Visual Studio是為了方便程式設計師編程用的,所以,它能主動生成各種各樣的程序框架,而我們這一節所講的Windows框架也是可以自動生成的。在Visual C++中,這個Windows框架被稱為MFC,是一個非常強大的、微軟提供Windows框架程序。
  • Google 高性能 RPC 框架 gRPC 1.0.0 發布
    GTK 重磅更新,4.0 大版本發布!