文章目錄
1. RPC 概述
RPC
是Remote Procedure Call Protocol 的簡寫,其中文意思是遠程過程調用協議
,就是通過網絡從遠程計算機程序上請求服務,而不需要了解底層網絡技術的協議.RPC將本地調用變爲遠程服務器上調用,這爲系統處理能力和吞吐量帶來了更大的提升,在OSI網絡通信模型中RPC跨越了傳輸層和應用層.
- 我們通俗的理解就是像調用本地函數一樣區調用
遠程
的函數,實現函數調用模式的網絡化.那麼這個遠程到底是多遠,既可以是物理上的遠程也可以是邏輯上的遠程.- 因爲PRC的這種跨越了物理服務器的限制,在 RPC 中可選的網絡傳輸方式有多種,可以選擇 TCP 協議、UDP 協議、HTTP 協議
- 在現在的分佈式系統中不同的節點之間比較常見的通信方式也是RPC
既然有
遠程過程調用
那麼就有本地過程調用,本地過程調用在不同的系統中叫法不在Windows系統中稱爲
LPC
在Linux系統中稱爲
IPC
進程間通信不論稱呼如何其本質都是 本機上不同的進程之間通信協作的調用方式
2. RPC 組成
我們簡單的看 RPC技術在構成上是由四部分組成的 客戶端 ,客戶端存根,服務端,服務端存根
- 客戶端(client) : 服務調用的發起方
- 客戶端存根(client Stub)
- 運行在客戶端機器上
- 存儲調用服務器地址
- 將客戶端請求的數據信息打包成數據包
- 通過網發送給服務端存根程序
- 接收服務端的發回的調用結果數據包,解析後給客戶端
- 服務端 : 服務提供者
- 服務端存根(server Stub) :
- 存在與服務端機器上
- 接收客戶端Stub程序發送來請求消息數據包
- 調用服務端中的程序方法
- 將結果打包成數據包發送給客戶端Stub程序
3. RPC 調用流程
- 服務消費者(Client )通過本地調用的方式調用服務。
- 客戶端存根(Client Stub)接收到調用請求後負責將方法、入參等信息序列化(組裝)成能夠進行網絡傳輸的消息體。
- 客戶端存根(Client Stub)找到遠程的服務地址,並且將消息通過網絡發送給服務端。
- 服務端存根(Server Stub)收到消息後進行解碼(反序列化操作),服務端存根(Server Stub)根據解碼結果調用本地的服務進行相關處理
- 服務端(Server)本地服務業務處理。
- 處理結果返回給服務端存根(Server Stub)。
- 服務端存根(Server Stub)序列化結果,
- 服務端存根(Server Stub)將結果通過網絡發送至消費方。
- 客戶端存根(Client Stub)接收到消息,並進行解碼(反序列化)。
- 服務消費方得到最終結果。
通過上面的操作簡單分析之後,我們可以將PRC調用看出一系列操作的集合,但是RPC涉及的幾個核心點我們可以看一下:
動態代理技術
: 客戶端存根(client Stub) 和 服務端存根(server Stub) 在具體實現中都是用動態代理技術自動生成的一段程序序列化
與反序列化
:爲啥要進行序列化和反序列化操作呢?
- RPC調用的過程我們可以看成是A機器上的程序調用B機器上的函數,那麼這個過程中需要進行數據的傳輸,我們知道所有的數據都是以字節的形式進行傳輸的,但是在具體編程過程中我們基本使用的是數據對象,因此想在網絡中進行數據對象和變量的傳輸,就需要將數據對象進行序列化和反序列化
序列化
: 將數據對象轉換成字節序列的過程,也就是編碼的過程反序列化
: 將字節序列恢復成數據對象的過程,也就是解碼的過程
4. Go語言實現PRC
Golang 中提供的標準包中實現了對PRC 的支持
Golang中提供的PRC標準包,只能支持使用Golang語言開發的RPC服務,也就是使用使用Golang 開發的PRC 服務端,只能使用Golang開發的PRC客戶端程序調用 ,爲啥爲這樣? 😂 因爲golang的自帶的RPC標準包採用的是 gob編碼
- gob 是Golang包自帶的一個數據結構序列化的編碼/解碼工具。編碼使用Encoder,解碼使用Decoder。一種典型的應用場景就是RPC(remote procedure calls)。
Golang 實現的PRC 可以支持三種方式請求
HTPP
,TCP
和JSONPRC
Golang PRC 的函數必須是特定的格式寫法才能被遠程方法,不然就訪問不到了,golang RPC 對外暴露服務的標準如下
func (t *T) MethodName(argType T1, replyType *T2) error
簡單說明如下:
- 方法的類型是能導出的
- 方法是能導出的
- 方法的只有兩個參數,這兩個參數必須是能導出的或者是內建類型
- 參數 T1表示調用方提供的參數
- 參數T2 表示要放回調用方的結果
- 參數T1和T2 必須能被golang 的
encoding/gob
包 編碼和解碼- 方法的第二個參數必須是指針類型的
- 方法的返回值必須是
error
類型的
4.1 HTTP PRC
我們看看golang 中的RPC的第一種實現方式方式通過HTTP傳輸
rpc 服務端代碼
rpc_server1.go
package main
import (
"log"
"net/http"
"net/rpc"
)
type Arguments struct {
A int
B int
}
type DemoRpc struct{}
func (d *DemoRpc) Add(req Arguments, resp *int) error {
*resp = req.A + req.B
return nil
}
func (d *DemoRpc) Minus(req Arguments, resp *int) error {
*resp = req.A - req.B
return nil
}
func main() {
// 註冊rpc服務
rpc.Register(new(DemoRpc))
// 將用於RPC消息的HTTP處理程序註冊到DefaultServer
rpc.HandleHTTP()
// 監聽8080端口
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err.Error())
}
}
rpc 客戶端代碼
rpc_client1.go
package main
import (
"fmt"
"log"
"net/rpc"
)
type Arguments struct {
A int
B int
}
func main() {
//DialHTTP連接到指定網絡地址的HTTP RPC服務器
//返回一個rpc客戶端
client, err := rpc.DialHTTP("tcp", ":8080")
if err != nil {
log.Fatal(err.Error())
}
arg := Arguments{99, 1}
var resp int
//調用指定的函數並等待其完成
err = client.Call("DemoRpc.Add", arg, &resp)
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("rpc DemoRpc Add %v\n", resp)
err = client.Call("DemoRpc.Minus", arg, &resp)
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("rpc DemoRpc Minus %v\n", resp)
//模擬一個錯誤的rpc調用
err = client.Call("DemoRpc.Nothing", arg, &resp)
if err != nil {
log.Fatal(" call err:", err.Error())
}
fmt.Printf("rpc DemoRpc Nothing %v\n", resp)
}
運行這兩個代碼文件,結果如下
rpc DemoRpc Add 100
rpc DemoRpc Minus 98
2019/12/14 13:49:25 call err:rpc: can't find method DemoRpc.Nothing
4.2 TCP RPC
rpc 服務端代碼
rpc_server2.go
package main
import (
"github.com/pkg/errors"
"log"
"net"
"net/rpc"
)
type Demo struct{}
type Params struct {
X int
Y int
}
// 暴露對外的服務
func (d *Demo) Add(p Params, result *int) error {
*result = p.X + p.Y
return nil
}
func (d *Demo) Minus(p Params, result *int) error {
*result = p.X - p.Y
return nil
}
func (d *Demo) Div(p Params, result *int) error {
if p.Y == 0 {
return errors.New("dividend is zero")
}
*result = p.X / p.Y
return nil
}
func main() {
//註冊一個自定義名稱的rpc服務
//和rpc.Register作用是一樣
rpc.RegisterName("DemoRpc", new(Demo))
// 開啓一個tcp服務,監聽8081端口
listen, err := net.Listen("tcp", ":8081")
if err != nil {
log.Fatal(err.Error())
}
for {
// 等待連接
conn, err := listen.Accept()
if err != nil {
log.Fatal(err.Error())
}
go rpc.ServeConn(conn)
}
}
rpc 客戶端代碼
rpc_client2.go
package main
import (
"fmt"
"log"
"net/rpc"
)
type Params struct {
X int
Y int
}
func main() {
// 連接到指定的rpc服務器
client, err := rpc.Dial("tcp", ":8081")
if err != nil {
log.Fatal(err.Error())
}
var result int
p := Params{99, 1}
err = client.Call("DemoRpc.Add", p, &result)
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("%d + %d = %d\n", p.X, p.Y, result)
err = client.Call("DemoRpc.Minus", p, &result)
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("%d - %d = %d\n", p.X, p.Y, result)
p.Y = 0
err = client.Call("DemoRpc.Div", p, &result)
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("%d / %d = %d\n", p.X, p.Y, result)
}
運行兩個代碼文件,結果如下
99 + 1 = 100
99 - 1 = 98
2019/12/14 14:23:29 dividend is zero
我們看到了http PRC 和tcp RPC 的客戶端處理特別相似,區別就在連接到服務端的方法一個是
DialHTTP
另一個是Dial
4.3 RPC 異步調用
這裏的異步調用主要是指的 RPC 的客戶端異步調用
我們對上面的代碼稍做修改即可
rpc 服務端代碼
rpc_server3.go
package main
import (
"fmt"
"log"
"net/http"
"net/rpc"
"time"
)
type ArgsDemo struct {
A int
B int
}
type DemoRpc3 struct{}
func (d *DemoRpc3) Add(req ArgsDemo, resp *int) error {
for i := 0; i < 5; i++ {
fmt.Println("sleep...", i)
time.Sleep(1 * time.Second)
}
*resp = req.A + req.B
fmt.Println("Add Do")
return nil
}
func (d *DemoRpc3) Minus(req ArgsDemo, resp *int) error {
*resp = req.A - req.B
return nil
}
func main() {
// 註冊rpc服務
rpc.Register(new(DemoRpc3))
// 將用於RPC消息的HTTP處理程序註冊到DefaultServer
rpc.HandleHTTP()
// 監聽8080端口
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal(err.Error())
}
}
rpc 客戶端代碼
rpc_client3.go
package main
import (
"fmt"
"log"
"net/rpc"
"time"
)
type ArgsDemo struct {
A int
B int
}
func main() {
//DialHTTP連接到指定網絡地址的HTTP RPC服務器
//返回一個rpc客戶端
client, err := rpc.DialHTTP("tcp", ":8080")
if err != nil {
log.Fatal(err.Error())
}
arg := ArgsDemo{9999, 8888}
var resp int
//異步調用指定的函數並等待其完成
call := client.Go("DemoRpc3.Add", arg, &resp, nil)
// 正常的同步調用
err = client.Call("DemoRpc3.Minus", arg, &resp)
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("rpc DemoRpc Minus %v\n", resp)
for {
select {
case <-call.Done:
if call.Error != nil {
log.Println(call.Error.Error())
return
}
fmt.Printf("rpc DemoRpc Add %v\n", resp)
return
default:
fmt.Println("wait...")
time.Sleep(1 * time.Second)
}
}
}
運行兩個代碼文件,結果如下
rpc DemoRpc Minus 1111
wait...
wait...
wait...
wait...
wait...
rpc DemoRpc Add 18887
5. json rpc
首先我們要明白 JSON-RPC,是一個無狀態且輕量級的遠程過程調用(RPC)傳送協議,其傳遞內容透過 JSON 爲主 並非是Goalng獨有的,其他的編程語言也能實現
我們前面都說了 golang 標準包中的RPC包採用的是gob的編碼,這就導致其他計算機編程語言想調用Golang寫的rpc 服務是行不通的,真是這樣的話那也太尷尬了 😭
但是不要慌,我們可以使用
jsonrpc
解決這個問題 Let me see see 🙈jsonrpc 其實也是Golang中的RPC實現,但是它採用的是
json
的編碼格式,用到的是net/rpc/jsonrpc
這個包
5.1 json rpc 服務端代碼
使用Golang實現
jsonrpc_server.go
package main
import (
"fmt"
"github.com/pkg/errors"
"log"
"net"
"net/rpc"
"net/rpc/jsonrpc"
)
type JsonDemo struct{}
type JsonParams struct {
X int
Y int
}
// 暴露對外的服務
func (d *JsonDemo) Add(p JsonParams, result *int) error {
*result = p.X + p.Y
return nil
}
func (d *JsonDemo) Minus(p JsonParams, result *int) error {
*result = p.X - p.Y
return nil
}
func (d *JsonDemo) Div(p JsonParams, result *int) error {
if p.Y == 0 {
return errors.New("dividend is zero")
}
*result = p.X / p.Y
return nil
}
func main() {
//註冊一個自定義名稱的rpc服務
rpc.RegisterName("JsonDemo", new(JsonDemo))
// 開啓一個tcp服務,監聽8081端口
listen, err := net.Listen("tcp", ":8081")
if err != nil {
log.Fatal(err.Error())
}
for {
// 等待連接
conn, err := listen.Accept()
if err != nil {
log.Fatal(err.Error())
} else {
fmt.Println(conn.RemoteAddr().String())
}
//在單個連接上運行JSON-RPC服務器
go jsonrpc.ServeConn(conn)
}
}
5.2 Golang json rpc 客戶端
jsonrpc_client.go
package main
import (
"fmt"
"log"
"net/rpc/jsonrpc"
)
type JsonParams struct {
X int
Y int
}
func main() {
// 連接到指定的json rpc服務器
client, err := jsonrpc.Dial("tcp", ":8081")
if err != nil {
log.Fatal(err.Error())
}
var result int
p := JsonParams{60, 40}
err = client.Call("JsonDemo.Add", p, &result)
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("%d + %d = %d\n", p.X, p.Y, result)
err = client.Call("JsonDemo.Minus", p, &result)
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("%d - %d = %d\n", p.X, p.Y, result)
p.Y = 0
err = client.Call("JsonDemo.Div", p, &result)
if err != nil {
log.Fatal(err.Error())
}
fmt.Printf("%d / %d = %d\n", p.X, p.Y, result)
}
運行RPC調用客戶端
go run jsonrpc_client.go
60 + 40 = 100
60 - 40 = 20
2019/12/14 15:58:44 dividend is zero
5.3 PHP json rpc客戶端
jsonrpc_client.php
<?php
class JsonRpc
{
// 定義一個私有變量
private $conn;
// 構造函數
public function __construct(string $host, string $port)
{
// 建立一個socket連接
$this->conn = fsockopen($host, $port);
if (!$this->conn) {
return false;
}
}
// 定義公有方法
public function CallRpc(string $method, array $params)
{
if (!$this->conn) {
return false;
}
// 發送json編碼的數據對象
$err = fwrite($this->conn, json_encode(
array(
"jsonrpc" => "2.0",
"method" => $method,
"params" => array($params),
"id" => 0
)) . "\n");
if ($err === false) {
return false;
}
// 設置流的超時時間
stream_set_timeout($this->conn, 0, 3000);
// 獲取響應結果
$line = fgets($this->conn);
if ($line === false) {
return NULL;
}
// json 解碼
return json_decode($line, true);
}
}
$host = "127.0.0.1";
$port = "8081";
// 新建一個對象
$client = new JsonRpc($host, $port);
$params = array(
"X" => 90,
"Y" => 80
);
// 調用方法
$result = $client->CallRpc("JsonDemo.Add", $params);
if (empty($result["error"])) {
printf("call JsonDemo.Add %d + %d = %s\n", $params["X"], $params["Y"], $result["result"]);
}
$result = $client->CallRpc("JsonDemo.Minus", $params);
if (empty($result["error"])) {
printf("call JsonDemo.Minus.Minus %d - %d = %s\n", $params["X"], $params["Y"], $result["result"]);
}
$params["Y"] = 0;
$result = $client->CallRpc("JsonDemo.Div", $params);
if (empty($result["error"])) {
printf("call JsonDemo.Div %d / %d = %s\n", $params["X"], $params["Y"], $result["result"]);
} else {
printf("call JsonDemo.Div error %s\n", $result["error"]);
}
運行RPC調用客戶端
php rpcjson_client.php
call JsonDemo.Add 90 + 80 = 170
call JsonDemo.Minus.Minus 90 - 80 = 10
call JsonDemo.Div error dividend is zero