天天看點

基于socket和序列化的簡單rpc架構實作

這裡寫目錄标題

          • 1、什麼是RPC架構?
          • 2、http也能實作遠端過程調用,為什麼還要使用rpc架構?
          • 3、Springboot使用rpc的樣例
          • 4、一個簡單的rpc架構實作
1、什麼是RPC架構?

RPC:Remote Procedure Call,即遠端過程調用,通俗來講就是調用遠端應用(伺服器)上的方法。而rpc架構就是使得我們調用遠端伺服器上的方法就像調用本地方法一樣簡單友善。

2、http也能實作遠端過程調用,為什麼還要使用rpc架構?

關于這個問題,是我在學習和使用rpc架構中産生的疑惑,明明我們通過http協定get和post等接口就能實作遠端調用,還要rpc“多此一舉”幹嘛呢?

直到某天我在知乎上看見了相同的問題,疑惑頓解。簡單說來有以下幾點原因:

  • http協定封包含有大量備援,rpc架構如dubbo中自定義的通信協定更加精簡,效率更高
  • rpc架構搭配注冊中心具有更豐富的功能,如服務發現,負載均衡,熔斷降級等

想了解更多可參見既然有 HTTP 請求,為什麼還要用 RPC 調用?

3、Springboot使用rpc的樣例

架構這裡選擇的是dubbo,服務注冊中心選用zookeeper。

  • 首先導入pom相關依賴
  • 編寫client和provider的配置檔案

    client如下:

    基于socket和序列化的簡單rpc架構實作
    provider的配置除了服務名稱不一樣其他都一緻
  • 對provider服務類添加@Service注解,注意這個注解不是之前用的@Service注解,它是在dubbo下的:

這個注解的意思是,将我們的服務類注冊到注冊中心,注冊中心位址在配置檔案中。

服務類其他的代碼寫法和平時一樣,比如這裡有一個獲得所有使用者的方法:

@Override
    public List<UmsMember> getAllUser() {
        List<UmsMember> list= 		 userMapper.selectAll();//userMapper.selectAllUser();
        return list;
    }
           
  • 在client端如果想調用上面這個方法應該怎麼做呢?

    在以前我們使用@Autowired注解注入服務類對象就可,而現在使用的是@Reference注解,該注解也在dubbo下面:

import com.alibaba.dubbo.config.annotation.Reference;
...
...
  @Reference
    UserService userService;
           

調用provider方法也是一樣:

@RequestMapping("getAllUser")
    @ResponseBody
    public List<UmsMember>  getAllUser(){
        List<UmsMember > allUser = userService.getAllUser();
        return allUser;
    }
           

可以看出,使用rpc架構确實很簡單。

4、一個簡單的rpc架構實作

其實rpc的過程并沒有那麼複雜,我們要調用遠端的方法,應該怎麼做呢?

  • 首先得要告訴服務方,我們是要調用什麼方法,方法裡是什麼參數,希望拿到什麼結果?

    是以就用到了序列化機制,關于序列化,我在各大廠面經那篇文章中有介紹,這裡不多講。

    簡單說,序列化為我們将提供了網絡傳輸對象的方法,這裡我們的對象就封裝了方法名稱,參數等等。

  • 然後我們怎麼講對象進行網絡傳輸呢?于是我們這裡選擇socket進行通信。
  • 在服務方拿到我們序列化後的結果,需要進行反序列化,反序列化以後,它去調用它本地的方法,拿到結果,再将結果序列化發送給用戶端,用戶端拿到再反序列化。

    總的過程如下:

    [外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-7Tt1UjNQ-1588669665481)(https://s1.ax1x.com/2020/05/05/YFFWnO.png)]

下面通過具體代碼,進行一個簡單rpc編寫:

1、用戶端調用一個簡單的add(a,b)方法,實作a+b

public class Client {
    public static void main(String[] args) {
        Calculator calculator = new CalculatorImpl();
        int result = calculator.add(100, 200);
        System.out.println("rpc遠端調用的結果是:"+result);
    }
}

           

2、我們将序列化以及通信的邏輯封裝在calculatorImpl中:

注意這裡ip位址直接選擇的本機,端口号自定義,開啟socket,然後将socket包裝成輸入輸出流放入對象輸入輸出流中。

public class CalculatorImpl implements Calculator {
    private static final int PORT = 5566;
    @Override
    public int add(int a, int b) {
        try {
            Socket socket = new Socket("127.0.0.1", PORT); //實際情況會有多個provider,是以會有多個ip位址
            RequestSerializable requestSerializable = generateRequestSerializableObject(a, b);
            //包裝一個socket輸出流,在不同的場景有不同的輸出流,比如檔案輸出流
            ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
            outputStream.writeObject(requestSerializable);

            ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
            Object response = objectInputStream.readObject();//拿到反序列化結果
            if (response instanceof Integer) {
                return (Integer) response;
            }else {
                throw new InternalError();
            }
        } catch (IOException e) {
            e.printStackTrace();
            throw new InternalError();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
            throw new InternalError();
        }
    }
// RequestSerializable為方法以及參數的封裝類,需要實作Serialzable接口
    public RequestSerializable generateRequestSerializableObject(int a, int b) {
        RequestSerializable requestSerializable = new RequestSerializable();
        requestSerializable.setA(a);
        requestSerializable.setB(b);
        requestSerializable.setMethod("add");
        return requestSerializable;
    }

           

3、看看服務方是怎麼實作:

服務方隻需要指定相同的端口号,開啟監聽,直到監聽到用戶端的請求,拿到方法和參數,進行反序列化解析,然後調用本地的add方法,這裡的add方法就是一個簡單的return a+b;

public class Service {
    private static final int PORT = 5566;
    Calculator calculator = new CalculatorImpl();
    public static void main(String[] args) throws IOException {
        new Service().run();
    }
    private  void run() throws IOException {
        ServerSocket listener = new ServerSocket(PORT);
        try {
            while(true){
                Socket socket = listener.accept();
                System.out.println("連結到用戶端");
                try {
                    //反序列化
                    ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                    Object request = objectInputStream.readObject();
                    if(request instanceof RequestSerializable){
                        if("add".equals(((RequestSerializable) request).getMethod())){
                            int result = calculator.add(((RequestSerializable) request).getA(), ((RequestSerializable) request).getB());
                            //再将結果序列化發送回去
                            ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream());
                            outputStream.writeObject((Integer)result);
                        }
                    }else{
                        throw new InternalError();
                    }

                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                }finally {
                    socket.close();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            listener.close();
        }
    }
           

4、用戶端調用add(100,200),最後控制台列印結果如下:

服務端:

基于socket和序列化的簡單rpc架構實作

用戶端:

基于socket和序列化的簡單rpc架構實作

5、至此一個最簡單的rpc就實作完了,實際情況肯定是更複雜的,需要考慮負載均衡、服務注冊、長連接配接、連接配接池等功能。

參考:

如何實作一個簡單的RPC

另外:關于我的個人部落格網站.,歡迎通路