從零開始,徒手擼一個簡單的 RPC 框架,輕鬆搞定!

2021-02-18 碼農沉思錄

得知了RPC(遠程過程調用)簡單來說就是調用遠程的服務就像調用本地方法一樣,其中用到的知識有序列化和反序列化、動態代理、網絡傳輸、動態加載、反射這些知識點。發現這些知識都了解一些。所以就想著試試自己實現一個簡單的RPC框架,即鞏固了基礎的知識,也能更加深入的了解RPC原理。當然一個完整的RPC框架包含了許多的功能,例如服務的發現與治理,網關等等。本篇只是簡單的實現了一個調用的過程。

傳參出參分析

一個簡單請求可以抽象為兩步

那麼就根據這兩步進行分析,在請求之前我們應該發送給服務端什麼信息?而服務端處理完以後應該返回客戶端什麼信息?

1、在請求之前我們應該發送給服務端什麼信息?

由於我們在客戶端調用的是服務端提供的接口,所以我們需要將客戶端調用的信息傳輸過去,那麼我們可以將要傳輸的信息分為兩類

第一類是服務端可以根據這個信息找到相應的接口實現類和方法

那麼我們就根據要傳輸的兩類信息進行分析,什麼信息能夠找到相應的實現類的相應的方法?要找到方法必須要先找到類,這裡我們可以簡單的用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;
}

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

上面我們分析了客戶端應該傳輸什麼信息給服務端,那麼服務端處理完以後應該傳什麼樣的返回值呢?這裡我們只考慮最簡單的情況,客戶端請求的線程也會一直在等著,不會有異步處理這一說,所以這麼分析的話就簡單了,直接將得到的處理結果返回就行了。

@Data
public class Response implements Serializable {
    private static final long serialVersionUID = -2393333111247658778L;
    private Object result;
}

由於都涉及到了網絡傳輸,所以都要實現序列化的接口

如何獲得傳參信息並執行?-客戶端

上面我們分析了客戶端向服務端發送的信息都有哪些?那麼我們如何獲得這些信息呢?首先我們調用的是接口,所以我們需要寫自定義註解然後在程序啟動的時候將這些信息加載在Spring容器中。有了這些信息那麼我們就需要傳輸了,調用接口但是實際上執行的確實網絡傳輸的過程,所以我們需要動態代理。那麼就可以分為以下兩步

初始化信息階段:將key為接口名,value為動態接口類註冊進Spring容器中1、初始化信息階段

由於我們使用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;
    }
}

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

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

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

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

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

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

@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存儲的就是接口名和其對應的實現類的對應關係。2、通過反射進行對應方法的執行

此時拿到了對應關係以後就能根據客戶端傳過來的信息找到相應的實現類中的方法。然後進行執行並返回信息就行

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

相關焦點

  • 從 0 開始手寫一個 RPC 框架,輕鬆搞定!
    得知了RPC(遠程過程調用)簡單來說就是調用遠程的服務就像調用本地方法一樣,其中用到的知識有序列化和反序列化、動態代理、網絡傳輸、動態加載、反射這些知識點。發現這些知識都了解一些。所以就想著試試自己實現一個簡單的RPC框架,即鞏固了基礎的知識,也能更加深入的了解RPC原理。
  • 一文探討 RPC 框架中的服務線程隔離
    Kirito 推薦語:最近秋招開始了,很多學生開始準備起了秋招,有很多人想知道進一些有名的網際網路公司實習有什麼要求,正好最近跟一位阿里春招的實習小夥子聊了一些 RPC 相關的知識點,於是我把這篇他的思考轉發過來,給大家參考下,我覺得有這樣的實力,進大廠實習應該是沒有問題的。
  • 用 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
  • RPC 框架,底層到底什麼原理?
    RPC框架的概念RPC(Remote Procedure Call)–遠程過程調用,通過網絡通信調用不同的服務,共同支撐一個軟體系統,微服務實現的基石技術。使用RPC可以解耦系統,方便維護,同時增加系統處理請求的能力。
  • 搜狗開源 srpc:自研高性能通用 RPC 框架
    現如今,搜狗又宣布開源 Workflow 的生態項目——srpc,一個基於 Workflow 打造的輕量級 RPC 框架。一個性能更好的 thrift/brpcsrpc 與 thrift/brpc 是協議與 IDL 均互通的。
  • go-zero 1.1.2 發布,web 和 rpc 框架
    go-zero 是一個集成了各種工程實踐的 web 和 rpc 框架。通過彈性設計保障了大並發服務端的穩定性,經受了充分的實戰檢驗。go-zero 包含極簡的 API 定義和生成工具 goctl,可以根據定義的 API 文件一鍵生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代碼,並可直接運行。
  • 不推廣月入過萬零擼項目鏈信吉柚小視頻
    但是後面就非常簡單了。除了部分軟體實名認證會收取1-2塊錢費用以外,不需要任何投資就可以做。我這邊不定時會更新項目,零擼越來越多,簡簡單單跟著老四做就完事了。老四聯繫微568399046保證大家財源滾滾!  一,鏈信。每天點五個贊,回答一個選擇題一分鐘不到即可完成交任務。不推廣,零投資月擼300多。堅持玩一年每月幾千。  二,好玩吧。
  • 從0 到 1:全面理解 RPC 遠程調用!
    有了這個模塊,開啟一個 rpc server,就變得相當簡單了。答案是很多,很多web框架其自身都自己實現了json-rpc,但我們要獨立這些框架之外,要尋求一種較為乾淨的解決方案,我查找到的選擇有兩種第一種是 jsonrpclibpip install jsonrpclib -i https://pypi.douban.com/simple
  • 原來芒果班戟做法這麼簡單!一個平底鍋就輕鬆搞定 香甜美味寶寶...
    原來芒果班戟做法這麼簡單!一個平底鍋就輕鬆搞定 香甜美味寶寶放心吃時間:2020-04-30 16:08   來源:今日頭條   責任編輯:沫朵 川北在線核心提示:原標題:原來芒果班戟做法這麼簡單!一個平底鍋就輕鬆搞定 香甜美味寶寶放心吃 天氣熱了以後,各種各樣的水果都開始上市了,現在好多水果都是 鮮,最香甜的時候,不吃可就太虧了。說到我最喜歡的水果,那 是芒果無疑了。我本身不怎麼愛吃酸、像草莓、菠蘿這種稍   原標題:原來芒果班戟做法這麼簡單!
  • 包皮擼起來就不用挨一刀嗎
    最近,有很多家長諮詢,「聽說,如果能把孩子的包皮擼起來就不用挨一刀?這是真的嗎?」三歲前千萬別硬擼男孩三歲前大部分是生理性包莖,這是正常的。此時若「強擼」包皮,就會造成包皮與龜頭之間出現縫隙,細菌就可進入,有可能造成感染。上翻不僅造成疼痛,還會因刺激而造成局部損傷,引起局部腫脹,增加孩子的心理負擔。三歲開始包皮逐漸開放,80%的孩子到8歲左右,包皮都能夠上翻。
  • 懶人版滷雞爪,一個電飯煲輕鬆搞定,味道堪比滷味店,簡單零難度
    不過說起滷雞爪,可能有人會苦惱「為什麼自己做的滷雞爪,沒有滷味店賣的好吃」,其實最關鍵的原因就在於滷汁的配方,這也是每個滷味店的鎮店之寶,那今天猴妹也給大家分享一款滷雞爪的配方,滷汁用量詳細,做法也超級簡單,可以說是懶人版滷雞爪了,因為這款配方不需要煸炒香料和炒糖色那些麻煩的步驟,它只需要把所有食材和調味料通通倒入電飯煲就OK了,然後一個煮飯按鈕,就什麼都不用管了
  • 早餐雞蛋餅這樣做,比薯片酥脆好吃,簡單幾步輕鬆搞定,零失敗!
    今天寶媽給大家分享一個零難度零失敗的早餐雞蛋餅的獨特做法,3個雞蛋1碗麵粉,不加黃油不用打發,拌勻這樣做,比薯片酥脆好吃,簡單幾步輕鬆搞定,一次就能做成功,簡單零失敗,感興趣的朋友,趕緊和寶媽一起來看看這個芝麻花生雞蛋餅的做法吧!
  • 軟糯香甜的懶人蛋糕,做法簡單,零失敗,只需一個平底鍋就搞定
    軟糯香甜的懶人蛋糕,做法簡單,零失敗,只需一個平底鍋就搞定。今天我就教大家一種蘭科的最新做法,只需要一個平底鍋就搞定了。現在很多家庭裡面都有平底鍋吧!其實用平底鍋也可以做出軟糯香甜的蛋糕,非常適合懶人做喲!做法也是超級簡單的,最重要的是零失敗。我們一起來看一下吧!今天做個蛋糕,跟平常做的不一樣,我們不需要用烤箱,只需要一個平底鍋就可以搞定。
  • Spring集成RabbitMQ簡單實現RPC
    correlationId 用來表示發送消息和返回消息的標誌,來區分是否是一個調用下面一步步來實現RPC首先貼出spring配置文件代碼<?-- durable是否持久化 exclusive:是否排外的--><rabbit:queue durable="true" auto-delete="false" exclusive="false" name="rpc.bao.goods"/><!
  • 眾包翻譯文檔分享 |《 gRPC 官方文檔中文版》
    《gRPC 官方文檔中文版》日前在開源中國眾包平臺翻譯完成,現發布與各位 OSCer 共享:http://doc.oschina.net/grpcgRPC  是一個高性能、開源和通用的 RPC 框架,面向移動和 HTTP/2 設計。
  • 2天擼出一個Instagram, 小白轉 web 變香餑餑!
    抱著死馬當活馬醫的心態,小陳參加了九章算法 《Django 項目速成班》, 僅僅2天就擼出了一個Instagram全棧項目,而且憑藉這個項目拿到了 Facebook 的面試邀約!這裡還有小陳整理的一份《Django求職大禮包》,含有他的學習筆記、面試題整理。
  • 號稱10萬人民幣一個?零擼可以別花錢買!
    最近我在朋友圈看到了這樣的一張圖,有早期挖礦的PI幣會員也有上百萬個,如果200塊一賣就2個億人民幣到手。Pi Network 是全球首款手機免費挖礦的數字貨幣,19年3月14日,pi幣問世,app上線。19年5月22日,用戶數量突破1萬,挖礦速度第一次減半。19年7月10日,用戶數量突破10萬,挖礦速度第二次減半。19年11月1日,用戶數量突破100萬,挖礦速度第三次減半。
  • 照樣可以吃到烤土豆片,步驟簡單,在家用炒鍋輕鬆搞定!
    照樣可以吃到烤土豆片,步驟簡單,在家用炒鍋輕鬆搞定!夏日炎炎,很多人喜歡結束一天工作後,坐在燒烤攤上,吹著小風,擼幾根串,緩解一天的勞累。燒烤攤,除了必點的肉串,像烤茄子、烤金針菇、烤韭菜、烤土豆片等這樣的素食美味也是不能少的。今天,就來跟大家分享——烤土豆片。
  • java簡單的封裝翻譯框架
    1,準備工作:註冊一個百度帳號,在http://api.fanyi.baidu.com/api/trans/product/apidoc個網址裡面拿到appid和key值2,直接開始擼代碼:>java簡單的封裝翻譯框架goodit 2018-02-06 11:421,準備工作:
  • 微服務與RPC
    所以,業內對微服務的實現,基本是確定一個組織邊界,在該邊界內,使用RPC; 邊界外,使用Restful。這個邊界,可以是業務、部門,甚至是全公司。二、 RPC技術選型RPC技術選型上,原則也是選擇自己熟悉的,或者公司內部內定的框架。 如果是新業務,則現在可選的框架其實也不多,卻也足夠讓人糾結。