RPC核心概念理解

ice_moss發表於2022-04-22

介紹

本文主要介紹RPC是什麼, 為什麼要使用RPC,使用RPC需要解決問題及RPC使用例項

RPC是什麼

RPC(Remote Procedure Call)遠端過程呼叫協議,一種透過網路從遠端計算機上請求服務,而不需要了解底層網路技術的協議。RPC它假定某些協議的存在,例如TPC/UDP等,為通訊程式之間攜帶資訊資料。在OSI網路七層模型中,RPC跨越了傳輸層和應用層,RPC使得開發,包括網路分散式多程式在內的應用程式更加容易。

簡單一點說:就是向遠端伺服器傳送請求,做業務處理或任務計算等(即程式),就是想要把呼叫遠端伺服器中方法的過程和像呼叫本地方法一樣簡單。

為什麼要使用RPC

當我們無法在一個程式內,甚至透過本第地呼叫的方式滿足我們的需求時,比如我們的通訊系統,甚至不同的組織間的通訊,由於計算能力需要橫向擴充套件,需要在多臺機器組成的叢集上部署應用,這才能是我們進行遠端的通訊;又或者說電商系統,我們不可能靠著本地呼叫就能滿足我們的需求,例如提交訂單的處理方法,庫存處理方法等在本地呼叫,在肯定是不行的,這是使用者發起相應的請求由我們遠端伺服器去做業務處理和計算的。所以RPC就顯得如此重要了。

使用RPC需要解決問題

這裡我們來看,我們想象一下當我們在遠端呼叫(也就是使用RPC)的過程中,我們需要執行的函式或者方法在遠端機器上,例如我們要呼叫遠端計算機的add方法,下面就會有這幾個問題:

  1. Call ID對映。 我們要怎麼告訴遠端計算機我們需要呼叫的是add方法,而不是reduce方法, mult方法,divi方法等,在本地呼叫中,函式體是直接透過函式指標來指定的,當我們呼叫本地add方法時,編譯器會自動給我們呼叫到它相應的函式指標,而在遠端呼叫中,使用指標明顯是不行的,因為兩個程式的地址不同。所以,在RPC中,所有函式都必須有一個唯一的ID,這個ID在所有程式中都是唯一確定的,客戶端在呼叫時都必須附加上這個ID,然後我們還需要在客戶端和服務端分別維護一個{ 函式 <—–> Call ID } 的對應表,兩者的表不一定需要完全相同,但相同的函式對應的Call ID必須相同,當客戶端需要進行遠端呼叫時,它就查一下這個表,找出相應的Call ID,然後把它傳給服務端,服務端也透過查表,來確定客戶端需要呼叫的函式,然後執行相應函式的程式碼。

  2. 序列化及反序列化。客戶端怎麼把引數值傳給遠端的函式呢?在本地呼叫中,我們只需要把引數壓到棧裡,然後讓函式自己去棧裡讀就行。但是在遠端過程呼叫時,客戶端跟服務端是不同的程式,不能透過記憶體來傳遞引數。甚至有時候客戶端和服務端使用的都不是同一種語言(比如服務端用C++,客戶端用Java或者Go)。這時候就需要客戶端把引數先轉成一個位元組流,傳給服務端後,再把位元組流轉成自己能讀取的格式。這個過程叫序列化和反序列化。同理,從服務端返回的值也需要序列化反序列化的過程。

    整個流程如下:

  1. 網路傳輸。遠端呼叫往往用在網路上,客戶端和服務端是透過網路連線的。所有的資料都需要透過網路傳輸,因此就需要有一個網路傳輸層。網路傳輸層需要把Call ID和序列化後的引數位元組流傳給服務端,然後再把序列化後的呼叫結果傳回客戶端。只要能完成這兩者的,都可以作為傳輸層使用。因此,它所使用的協議其實是不限的,能完成傳輸就行。儘管大部分RPC框架都使用TCP協議,但其實UDP也可以,而gRPC乾脆就用了HTTP2。Java的Netty也屬於這層的東西。

解決了上面三個問題,我們就能實現RPC了,現在我們再看看,客戶端和服務端在RPC中的工作是什麼:

客戶端(client):

1. 將這個呼叫對映為Call ID。這裡假設用最簡單的字串當Call ID的方法,例如這裡可以:http://127.0.0.1:8080/add?a=1&b=1,即直接使用add作為path,又或者http://127.0.0.1:8080/?method=add&a=1&b=12. 將Call ID,a和b序列化。可以直接將它們的值以二進位制形式打包

3.2中得到的資料包傳送給ServerAddr,這需要使用網路傳輸層

4. 等待伺服器返回結果

4. 如果伺服器呼叫成功,那麼就將結果反序列化,並賦給total

服務端(service):

1. 在本地維護一個Call ID到函式指標的對映call_id_map,可以用dict完成

2. 等待請求,包括多執行緒的併發處理能力

3. 得到一個請求後,將其資料包反序列化,得到Call ID

4. 透過在call_id_map中查詢,得到相應的函式指標

5. 將a和rb反序列化後,在本地呼叫add函式,得到結果

6. 將結果序列化後透過網路返回給Client

注意:

  • Call ID可以是字串,也可以是整數ID,對映其實是一個雜湊表
  • 序列化與反序列化可以自己寫,當然也可以使用Protobuf或者FlatBuffers之類的。
  • 網路傳輸庫可以自己實現socket,也可以使用asio,ZeroMQ,Netty之類。

RPC使用例項

我們來簡單模擬一下RPC的呼叫過程,例項介紹:客戶端需要呼叫遠端計算機上的計算方法,這裡以加法為例(當然可以非常複雜的大型計算)

客戶端的呼叫時無需關心遠端服務端的add方法是怎麼實現的,我們只需要在客戶端對遠端呼叫的add方法的過程進行封裝:

service:

由於是簡單的例子,就不做完整的錯誤處理了

package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "strconv"
)

func main() {
    //http://127:0:0:1:8000/add?a=1&b=2
    //Call ID 使用request.URL.PATH
    http.HandleFunc("/add",
        func(writer http.ResponseWriter, request *http.Request) {
            //解析引數
            err := request.ParseForm()
            if err != nil {
                panic("解碼失敗")
            }
            fmt.Println("path:", request.URL.Path)
            //取出引數,做型別轉換
            a, err := strconv.Atoi(request.Form["a"][0])
            if err != nil {
                panic("轉換失敗")
            }
            b, err := strconv.Atoi(request.Form["b"][0])
            if err != nil {
                panic(err)
            }
            //返回的資料格式:json{"data":3}
            //使用json編碼,即序列化
            writer.Header().Set("Content-Type", "application/json")
            //序列化
            jData, err := json.Marshal(map[string]int{
                "data": a + b,
            })
            if err != nil {
                panic(err)
            }
            _, err = writer.Write(jData)
            if err != nil {
                panic("寫入失敗")
            }
        })

    //監聽埠
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        panic("監聽失敗")
    }
}

啟動服務端

client:

這裡同樣不做完整的錯誤處理了
這裡我們使用第三方包:github.com/kirinlabs/HttpRequest
使用以下命令獲取:

go get github.com/kirinlabs/HttpRequest

當然你也可以使用其他相關的方法來連線我們的service

package main

import (
    "encoding/json"
    "fmt"

    "github.com/kirinlabs/HttpRequest"
)

//解析結構
type ResponseData struct {
    data int `json:"data"`
}

//對add進行封裝
func add(a, b int) int {
    //生成一個例項
    req := HttpRequest.NewRequest()
    res, err := req.Get(fmt.Sprintf("http://127.0.0.1:8080/add?a=%d&b=%d", a, b))
    if err != nil {
        panic("連線失敗")
    }
    body, err := res.Body()
    if err != nil {
        panic(err)
    }

    var resData ResponseData
    err = json.Unmarshal(body, &resData)
    if err != nil {
        panic("解碼失敗")
    }
    return resData.data
}

func main() {
  fmt.Println(add(1, 4))
}

列印結果:

{"data":5}

Process finished with the exit code 0

當然我們也可以直接在瀏覽器裡訪問:

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章