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

2021-01-15 碼農沉思錄

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

相關焦點

  • 微服務RPC框架選美
    說到RPC框架,可能大家能想到一堆RPC開源框架,那麼在微服務平臺中,微服務間的服務調用,不可避免的會遇到一個問題,該選用哪一個RPC框架好呢?今天我們就請到三位RPC框架,來進行一場選美大賽,看看誰更適合微服務平臺中的服務間調用。
  • gRPC首頁、文檔和下載 - RPC 框架 - OSCHINA - 中文開源技術交流...
    gRPC 是一個高性能、開源和通用的 RPC 框架,面向移動和 HTTP/2 設計。目前提供 C、Java 和 Go 語言版本,分別是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持.
  • 搜狗開源srpc:自研高性能通用RPC框架
    9月15日,作為Workflow最重要的生態項目——srpc,一個基於其打造的輕量級RPC框架,也在GitHub上開源了。GitHub搜索「sogou srpc」即可找到該項目。一個性能更好的thrift/brpcsrpc與thrift/brpc是協議與IDL均互通的。
  • 從0 到 1:全面理解 RPC 遠程調用!
    光說不練嘴把式,接下來,我將分別用三種不同的方式全面地讓你搞明白 rpc 遠程調用是如何實現的。01、基於 xml-rpcPython實現 rpc,可以使用標準庫裡的 SimpleXMLRPCServer,它是基於XML-RPC 協議的。有了這個模塊,開啟一個 rpc server,就變得相當簡單了。
  • Golang 語言使用標準庫 net/rpc/jsonrpc 包跨語言遠程調用
    02Go 語言 net/rpc/jsonrpc 標準庫jsonrpc 是基於 TCP 協議,不支持 HTTP 協議。jsonrpc 採用 JSON 編解碼傳輸數據,而不是採用 gob 編解碼方式。其他方面和 net/rpc 一樣,可以通過閱讀「Go 使用標準庫 net/rpc 包」了解相關內容。03net/rpc/jsonrpc 怎麼使用?通過一個簡單的示例,我們演示 Go 語言標準庫 net/rpc/jsonrpc 的使用方法。
  • rpc、json Rpc和http區別
    帶索引數組參數的rpc調用--> {"jsonrpc": "2.0", "method": "subtract", "params": [42, 23], "id": 1}<-- {"jsonrpc": "2.0", "result": 19, "id": 1} 2.
  • 後端工程師,必須搞懂的 RPC 框架
    去年我面試一位高級後端工程師的時候,看他簡歷上寫著「熟練掌握 RPC 框架」,所以我就試探著問了他幾個原理方面的問題,比如,「大概說下 RPC 框架的核心原理」「、描述下序列化部分的邏輯」。但聊了半天,我發現他其實並不熟,他的回答基本都是在告訴我怎麼用,以及怎麼更好地用好這些框架。
  • smart-doc 1.9.7 發布,Java 零註解文檔生成工具
    smart-doc完全基於接口源碼分析來生成接口文檔,完全做到零註解侵入,你只需要按照java標準注釋編寫,smart-doc就能幫你生成一個簡易明了的markdown 或是一個像GitBook樣式的靜態html文檔。如果你已經厭倦了swagger等文檔工具的無數註解和強侵入汙染,那請擁抱smart-doc吧!
  • 微服務之RPC簡述
    比如說兩臺伺服器A,B,一個應用部署在A伺服器上,想要調用B伺服器上應用提供的函數/方法,由於不在一個內存空間,不能直接調用,就需要通過網絡來表達調用的語義和傳達調用的數據,而這種方式就是rpcRPC 的主要功能目標是讓構建分布式計算(應用)更容易,在提供強大的遠程調用能力時不損失本地調用的語義簡潔性。
  • 【行業資訊】SOFARPC v5.7.4 發布,螞蟻金服開源 Java RPC 框架
    SOFARPC 是一個高可擴展性、高性能、生產級的 Java RPC 框架。在螞蟻金服 SOFARPC 已經經歷了十多年及五代版本的發展。SOFARPC 致力於簡化應用之間的 RPC 調用,為應用提供方便透明、穩定高效的點對點遠程服務調用方案。
  • 瓷磚鋪貼,一個系統輕鬆搞定!
    一個系統讓你輕鬆搞定瓷磚鋪貼!科順瓷磚鋪貼系統▲科順瓷磚鋪貼系統3大系列、6款產品科順瓷磚鋪貼系統伴隨現代瓷磚發展應運而生,可應用於各類大尺寸、低吸水率的瓷磚,極大降低開裂脫落風險,無論是馬賽克還是玻化磚,科順瓷磚鋪貼系統都能助你輕鬆施工,填縫鋪貼一步到位。
  • 一斤綠豆一碗水,教你自製綠豆糕,細膩鬆軟,簡單幾步輕鬆搞定
    一斤綠豆一碗水,教你自製綠豆糕,細膩鬆軟,簡單幾步輕鬆搞定最近看電視劇對劇情不怎麼迷戀,但是對裡面人們吃的綠豆糕產生了興趣。看著她們一塊一塊地吃著,非常享受的樣子,自己也忍不住流起了口水。LCD歐高看起來十分的香嫩,顏色是淡綠色的,看著就讓人有了食慾。
  • 五分鐘學後端技術:如何學習Java工程師必須掌握的RPC
    RPC 框架是基於 HTTP 協議實現的,底層使用到了 Netty 框架的支持。Thrift:是 Facebook 的開源 RPC 框架,主要是一個跨語言的服務開發框架。 用戶只要在其之上進行二次開發就行,應用對於底層的 RPC 通訊等都是透明的。不過這個對於用戶來說需要學習特定領域語言這個特性,還是有一定成本的。
  • 能跑源碼,還提供數據集:這裡有一個入門企業級驗證碼識別項目
    前言網上關於驗證麼識別的開源項目眾多,但大多是學術型文章或者僅僅是一個測試 demo,那麼企業級的驗證碼識別究竟是怎樣的呢?前方高能預警,這是一個生產水準的驗證碼識別項目,筆者可以向你們保證,它一定會是各位所見過的文章中最實用的,你甚至可以不需要懂代碼寫代碼就能輕鬆使用它訓練一個 99 識別率的模型。這才是企業級應該有的樣子:算法開發負責框架,訓練只需要一個實習生。
  • 十行代碼就能搞定深度學習?飛槳框架高層API,輕鬆玩轉AI
    機器之心發布機器之心編輯部嚮往深度學習技術,可是深度學習框架太難學怎麼辦?百度傾心打造飛槳框架高層 API,零基礎也能輕鬆上手深度學習,一起來看看吧?針對不同的使用場景,飛槳框架提供了不同高層 API,從而降低開發難度,讓每個人都能輕鬆上手深度學習。我們先通過一個深度學習中經典的手寫數字分類任務,來簡單了解飛槳高層 API。然後再詳細的介紹每個模塊中所包含的 API。
  • rpc分布式服務 - CSDN
    第一章聊了【「為什麼要進行服務化,服務化究竟解決什麼問題」】第二章聊了【「微服務的服務粒度選型」】今天開始聊一些微服務的實踐,第一塊,RPC框架的原理及實踐,為什麼說要搞定微服務架構,先搞定RPC框架呢?
  • 一個人懶得做飯怎麼辦,不要點外賣,一個電飯鍋輕鬆搞定所有美食
    做飯需要一個完整的廚房嗎?答案是不一定的,其實只要你擁有一個普通的電飯鍋就可以了,小夥伴們真的以為電飯鍋只可以燜飯嗎?太天真了,電飯鍋如哆啦A夢的口袋,可以做出各種美食,零基礎的小白也可以做到零失誤。當你學會電飯鍋美食之後這些問題就迎刃而解了,只需要把食材簡單處理,放入電飯鍋半個小時就可以吃到美味佳餚了。小夥伴們動心了嗎?下面小編為大家精選五種電飯鍋美食,學會之後好好犒勞一下自己吧。
  • LOL無限火力站擼型英雄排名 站擼哪個英雄最強
    塔姆  站擼強度:五星  分帶強度:五星  被秀指數:兩星  打團強度:三星  秀兒指數(能秀指數):零  綜合強度:四星半  無限火力最強的硬加強指數,沒記錯的話造成130%的傷害,承受70%還是80傷害,簡單來說就是我很硬,還很痛,舔狗的春天。
  • 無人深空:從零開始賺錢攻略,輕鬆完成一個億的小目標!
    這裡小編給大家說一條完成完整的賺錢套路,從零開始,輕鬆完成一個億的小目標。前期賺錢遊戲前期賺錢是最困難的,這裡小編推薦兩種前期賺錢方法。1、採集嗡嗡作響的卵這個很好找,基本每個星球都會有,玩家可以在洞穴附近按F掃描找到它們,採集這個蛋是不會有怪物攻擊的,直接採集就可,一個的價格蛋價值9000貨幣,對於前期起步的玩家來說,這個價格還是很高的。