RPC技術內部原理是通過兩種技術的組合來實現的:本地方法調用 和 網絡通信技術。
RPC簡介
RPC是Remote Procedure Call Protocol單詞首字母的縮寫,簡稱為:RPC,翻譯成中文叫遠程過程調用協議。所謂遠程過程調用,通俗的理解就是可以在本地程序中調用運行在另外一台服務器上的程序的功能方法。這種調用的過程跨越了物理服務器的限制,是在網絡中完成的,在調用遠端服務器上程序的過程中,本地程序等待返回調用結果,直到遠端程序執行完畢,將結果進行返回到本地,最終完成一次完整的調用。
需要強調的是:遠程過程調用指的是調用遠端服務器上的程序的方法整個過程。
RPC設計組成
RPC技術在架構設計上有四部分組成,分別是:客戶端、客戶端存根、服務端、服務端存根。
客戶端(client):服務調用發起方,也稱為服務消費者。
客戶端存根(Client Stub):該程序運行在客戶端所在的計算機機器上,主要用來存儲要調用的服務器的地址,另外,該程序還負責將客戶端請求遠端服務器程序的數據信息打包成數據包,通過網絡發送給服務端Stub程序;其次,還要接收服務端Stub程序發送的調用結果數據包,並解析返回給客戶端。
服務端(Server):遠端的計算機機器上運行的程序,其中有客戶端要調用的方法。
服務端存根(Server Stub):接收客戶Stub程序通過網絡發送的請求消息數據包,並調用服務端中真正的程序功能方法,完成功能調用;其次,將服務端執行調用的結果進行數據處理打包發送給客戶端Stub程序。
RPC原理及調用步驟
在上述圖中,通過1-10的步驟圖解的形式,說明了RPC每一步的調用過程。具體描述為:
1、客戶端想要發起一個遠程過程調用,首先通過調用本地客戶端Stub程序的方式調用想要使用的功能方法名;
2、客戶端Stub程序接收到了客戶端的功能調用請求,將客戶端請求調用的方法名,攜帶的參數等信息做序列化操作,並打包成數據包。
3、客戶端Stub查找到遠程服務器程序的IP地址,調用Socket通信協議,通過網絡發送給服務端。
4、服務端Stub程序接收到客戶端發送的數據包信息,並通過約定好的協議將數據進行反序列化,得到請求的方法名和請求參數等信息。
5、服務端Stub程序准備相關數據,調用本地Server對應的功能方法進行,並傳入相應的參數,進行業務處理。
6、服務端程序根據已有業務邏輯執行調用過程,待業務執行結束,將執行結果返回給服務端Stub程序。
7、服務端Stub程序將程序調用結果按照約定的協議進行序列化,並通過網絡發送回客戶端Stub程序。
8、客戶端Stub程序接收到服務端Stub發送的返回數據,對數據進行反序列化操作,並將調用返回的數據傳遞給客戶端請求發起者。
9、客戶端請求發起者得到調用結果,整個RPC調用過程結束。
RPC涉及到的相關技術
1、動態代理技術: 上文中我們提到的Client Stub和Sever Stub程序,在具體的編碼和開發實踐過程中,都是使用動態代理技術自動生成的一段程序。
2、序列化和反序列化: 在RPC調用的過程中,我們可以看到數據需要在一台機器上傳輸到另外一台機器上。在互聯網上,所有的數據都是以字節的形式進行傳輸的。而我們在編程的過程中,往往都是使用數據對象,因此想要在網絡上將數據對象和相關變量進行傳輸,就需要對數據對象做序列化和反序列化的操作。
序列化:把對象轉換為字節序列的過程稱為對象的序列化,也就是編碼的過程。
反序列化:把字節序列恢復為對象的過程稱為對象的反序列化,也就是解碼的過程。
RPC官方庫
在Go語言官方網站的pkg說明中,提供了官方支持的rpc包,具體鏈接如下:https://golang.org/pkg/net/rpc/。官方提供的rpc包完整的包名是:net/rpc。根據官方的解釋,rpc包主要是提供通過網絡訪問一個對象方法的功能。
net/rpc庫實現RPC調用編程
一、服務定義及暴露
在編程實現過程中,服務器端需要注冊結構體對象,然后通過對象所屬的方法暴露給調用者,從而提供服務,該方法稱之為輸出方法,此輸出方法可以被遠程調用。當然,在定義輸出方法時,能夠被遠程調用的方法需要遵循一定的規則。我們通過代碼進行講解:
func (t *T) MethodName(request T1,response *T2) error
上述代碼是go語言官方給出的對外暴露的服務方法的定義標准,其中包含了主要的幾條規則,分別是:
1、對外暴露的方法有且只能有兩個參數,這個兩個參數只能是輸出類型或內建類型,兩種類型中的一種。
2、方法的第二個參數必須是指針類型。
3、方法的返回類型為error。
4、方法的類型是可輸出的。
5、方法本身也是可輸出的。
type MathUtil struct { } func (this *MathUtil) CalculateCircleArea(req float32, resp *float32) error { *resp = math.Pi * req * req return nil }
二、注冊服務及監聽請求
//1、初始化指針數據類型
mathUtil := new(MathUtil)
//2、調用net/rpc包的功能將服務對象進行注冊
err := rpc.Register(mathUtil)
if err != nil {
panic(err.Error())
}
//3、通過該函數把mathUtil中提供的服務注冊到HTTP協議上,方便調用者可以利用http的方式進行數據傳遞
rpc.HandleHTTP()
//4、在特定的端口進行監聽
listen, err := net.Listen("tcp", ":8081")
if err != nil {
panic(err.Error())
}
go http.Serve(listen, nil)
三、客戶端調用
在服務端是通過Http的端口監聽方式等待連接的,因此在客戶端就需要通過http連接,首先與服務端實現連接。
client, err := rpc.DialHTTP("tcp", "localhost:8081") if err != nil { panic(err.Error()) } var req float32 req = 3 var resp *float32 err = client.Call("MathUtil.CalculateCircleArea", req, *resp) if err != nil { panic(err.Error()) } fmt.Println(*resp)
上述的調用方法核心在於client.Call方法的調用,該方法有三個參數,第一個參數表示要調用的遠端服務的方法名,第二個參數是調用時要傳入的參數,第三個參數是調用要接收的返回值。
上述的Call方法調用實現的方式是同步的調用,除此之外,還有一種異步的方式可以實現調用。異步調用代碼
var respSync *float32 //異步的調用方式 syncCall := client.Go("MathUtil.CalculateCircleArea", req, &respSync, nil) replayDone := <-syncCall.Done fmt.Println(replayDone) fmt.Println(*respSync)
多參數的請求調用參數傳遞
將參數定義在一個新的結構體中,存放在param包中:
type AddParma struct { Args1 float32 //第一個參數 Args2 float32 //第二個參數 }
在server.go文件中,實現兩數相加的功能,並實現服務注冊的邏輯:
func (mu *MathUtil) Add(param param.AddParma, resp *float32) error { *resp = param.Args1 + param.Args2 //實現兩數相加的功能 return nil } mathUtil := new(MathUtil) err := rpc.RegisterName("MathUtil", mathUtil) if err != nil { panic(err.Error()) } rpc.HandleHTTP() listen, err := net.Listen("tcp", ":8082") http.Serve(listen, nil)
RPC與Protobuf結合使用
需求:假設在一個系統中,有訂單模塊(Order),其他模塊想要實現RPC的遠程工程調用,根據訂單ID和時間戳可以獲取訂單信息。如果獲取成功就返回相應的訂單信息;如果查詢不到返回失敗信息。現在,我們來進行需求的編程實現。
傳輸數據格式定義
數據定義
根據需求,定義message.proto文件,詳細定義如下:
syntax = "proto3"; package message; // 訂單請求參數 message OrderRequest { string orderId = 1; int64 timeStamp = 2; } // 訂單信息 message OrderInfo { string OrderId = 1; string OrderName = 2; string OrderStatus = 3; }
編譯proto文件
通過proto編譯命令對.proto文件進行編譯,自動生成對應結構體的Go語言文件。編譯命令如下:
protoc ./message.proto --go_out=./
Protobufg格式數據與RPC結合
服務的定義:
進行RPC遠程過程調用,實現調用遠程服務器的方法,首先要有服務。在本案例中,定義提供訂單查詢功能的服務,取名為OrderService,同時提供訂單信息查詢方法供遠程調用。詳細的服務和方法定義如下:
type OrderService struct { } func (this *OrderService) GetOrderInfo(request message.OrderRequest, response *message.OrderInfo) error { orderMap := map[string]message.OrderInfo{ "201907300001": message.OrderInfo{OrderId: "201907300001", OrderName: "衣服", OrderStatus: "已付款"}, "201907310001": message.OrderInfo{OrderId: "201907310001", OrderName: "零食", OrderStatus: "已付款"}, "201907310002": message.OrderInfo{OrderId: "201907310002", OrderName: "食品", OrderStatus: "未付款"}, } current := time.Now().Unix() if (request.TimeStamp > current) { *response = message.OrderInfo{OrderId: "0", OrderName: "", OrderStatus: "訂單信息異常"} } else { result := orderMap[request.OrderId] if result.OrderId != "" { *response = orderMap[request.OrderId] } else { return errors.New("server error") } } return nil }
在服務的方法定義中,使用orderMap模擬初始訂單數據庫,方便案例查詢展示。GetOrderInfo方法有兩個參數,第一個是message.OrderRequest,作為調用者傳遞的參數,第二個是message.OrderInfo,作為調用返回的參數,通過此處的兩個參數,將上文通過.proto定義並自動生成的Go語言結構體數據結合起來。
服務的注冊和處理
服務定義好以后,需要將服務注冊到RPC框架,並開啟http請求監聽處理。這部分代碼與之前的RPC服務端實現邏輯一致,具體實現如下:
func main() { orderService := new(OrderService) rpc.Register(orderService) rpc.HandleHTTP() listen, err := net.Listen("tcp", ":8081") if err != nil { panic(err.Error()) } http.Serve(listen, nil) }
RPC客戶端調用實現
在客戶端,除了客戶端正常訪問遠程服務器的邏輯外,還需要准備客戶端需要傳遞的請求數據message.OrderInfo。具體實現如下:
func main() { client, err := rpc.DialHTTP("tcp", "localhost:8081") if err != nil { panic(err.Error()) } timeStamp := time.Now().Unix() request := message.OrderRequest{OrderId: "201907300001", TimeStamp: timeStamp} var response *message.OrderInfo err = client.Call("OrderService.GetOrderInfo", request, &response) if err != nil { panic(err.Error()) } fmt.Println(*response) }
運行結果