Golang與RPC

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 調用流程

在這裏插入圖片描述

  1. 服務消費者(Client )通過本地調用的方式調用服務。
  2. 客戶端存根(Client Stub)接收到調用請求後負責將方法、入參等信息序列化(組裝)成能夠進行網絡傳輸的消息體。
  3. 客戶端存根(Client Stub)找到遠程的服務地址,並且將消息通過網絡發送給服務端。
  4. 服務端存根(Server Stub)收到消息後進行解碼(反序列化操作),服務端存根(Server Stub)根據解碼結果調用本地的服務進行相關處理
  5. 服務端(Server)本地服務業務處理。
  6. 處理結果返回給服務端存根(Server Stub)。
  7. 服務端存根(Server Stub)序列化結果,
  8. 服務端存根(Server Stub)將結果通過網絡發送至消費方。
  9. 客戶端存根(Client Stub)接收到消息,並進行解碼(反序列化)。
  10. 服務消費方得到最終結果。

通過上面的操作簡單分析之後,我們可以將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 , TCPJSONPRC

  • Golang PRC 的函數必須是特定的格式寫法才能被遠程方法,不然就訪問不到了,golang RPC 對外暴露服務的標準如下

    func (t *T) MethodName(argType T1, replyType *T2) error
    

    簡單說明如下:

    1. 方法的類型是能導出的
    2. 方法是能導出的
    3. 方法的只有兩個參數,這兩個參數必須是能導出的或者是內建類型
      1. 參數 T1表示調用方提供的參數
      2. 參數T2 表示要放回調用方的結果
      3. 參數T1和T2 必須能被golang 的encoding/gob 包 編碼和解碼
    4. 方法的第二個參數必須是指針類型的
    5. 方法的返回值必須是 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章