golang實現RPC的幾種方式

原文鏈接:https://studygolang.com/articles/14336

什麼是RPC

遠程過程調用(Remote Procedure Call,縮寫爲 RPC)是一個計算機通信協議。 該協議允許運行於一臺計算機的程序調用另一臺計算機的子程序,而程序員無需額外地爲這個交互作用編程。 如果涉及的軟件採用面向對象編程,那麼遠程過程調用亦可稱作遠程調用或遠程方法調用。維基百科:遠程過程調用

用通俗易懂的語言描述就是:RPC允許跨機器、跨語言調用計算機程序方法。打個比方,我用go語言寫了個獲取用戶信息的方法getUserInfo,並把go程序部署在阿里雲服務器上面,現在我有一個部署在騰訊雲上面的php項目,需要調用golang的getUserInfo方法獲取用戶信息,php跨機器調用go方法的過程就是RPC調用。

golang中如何實現RPC

在golang中實現RPC非常簡單,有封裝好的官方庫和一些第三方庫提供支持。Go RPC可以利用tcp或http來傳遞數據,可以對要傳遞的數據使用多種類型的編解碼方式。golang官方的net/rpc庫使用encoding/gob進行編解碼,支持tcphttp數據傳輸方式,由於其他語言不支持gob編解碼方式,所以使用net/rpc庫實現的RPC方法沒辦法進行跨語言調用。

golang官方還提供了net/rpc/jsonrpc庫實現RPC方法,JSON RPC採用JSON進行數據編解碼,因而支持跨語言調用。但目前的jsonrpc庫是基於tcp協議實現的,暫時不支持使用http進行數據傳輸。

除了golang官方提供的rpc庫,還有許多第三方庫爲在golang中實現RPC提供支持,大部分第三方rpc庫的實現都是使用protobuf進行數據編解碼,根據protobuf聲明文件自動生成rpc方法定義與服務註冊代碼,在golang中可以很方便的進行rpc服務調用。

net/rpc庫

下面的例子演示一下如何使用golang官方的net/rpc庫實現RPC方法,使用http作爲RPC的載體,通過net/http包監聽客戶端連接請求。

$GOPATH/src/test/rpc/rpc_server.go

package main

import (
    "errors"
    "fmt"
    "log"
    "net"
    "net/http"
    "net/rpc"
    "os"
)

// 算數運算結構體
type Arith struct {
}

// 算數運算請求結構體
type ArithRequest struct {
    A int
    B int
}

// 算數運算響應結構體
type ArithResponse struct {
    Pro int // 乘積
    Quo int // 商
    Rem int // 餘數
}

// 乘法運算方法
func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error {
    res.Pro = req.A * req.B
    return nil
}

// 除法運算方法
func (this *Arith) Divide(req ArithRequest, res *ArithResponse) error {
    if req.B == 0 {
        return errors.New("divide by zero")
    }
    res.Quo = req.A / req.B
    res.Rem = req.A % req.B
    return nil
}

func main() {
    rpc.Register(new(Arith)) // 註冊rpc服務
    rpc.HandleHTTP()         // 採用http協議作爲rpc載體

    lis, err := net.Listen("tcp", "127.0.0.1:8095")
    if err != nil {
        log.Fatalln("fatal error: ", err)
    }

    fmt.Fprintf(os.Stdout, "%s", "start connection")

    http.Serve(lis, nil)
}

上述服務端程序運行後,將會監聽本地的8095端口,我們可以實現一個客戶端程序,連接服務端並實現RPC方法調用。

$GOPATH/src/test/rpc/rpc_client.go

package main

import (
    "fmt"
    "log"
    "net/rpc"
)

// 算數運算請求結構體
type ArithRequest struct {
    A int
    B int
}

// 算數運算響應結構體
type ArithResponse struct {
    Pro int // 乘積
    Quo int // 商
    Rem int // 餘數
}

func main() {
    conn, err := rpc.DialHTTP("tcp", "127.0.0.1:8095")
    if err != nil {
        log.Fatalln("dailing error: ", err)
    }

    req := ArithRequest{9, 2}
    var res ArithResponse

    err = conn.Call("Arith.Multiply", req, &res) // 乘法運算
    if err != nil {
        log.Fatalln("arith error: ", err)
    }
    fmt.Printf("%d * %d = %d\n", req.A, req.B, res.Pro)

    err = conn.Call("Arith.Divide", req, &res)
    if err != nil {
        log.Fatalln("arith error: ", err)
    }
    fmt.Printf("%d / %d, quo is %d, rem is %d\n", req.A, req.B, res.Quo, res.Rem)
}

net/rpc/jsonrpc庫

上面的例子我們演示了使用net/rpc實現RPC的過程,但是沒辦法在其他語言中調用上面例子實現的RPC方法。所以接下來的例子我們演示一下使用net/rpc/jsonrpc庫實現RPC方法,此方式實現的RPC方法支持跨語言調用。

$GOPATH/src/test/rpc/jsonrpc_server.go

package main

import (
    "errors"
    "fmt"
    "log"
    "net"
    "net/rpc"
    "net/rpc/jsonrpc"
    "os"
)

// 算數運算結構體
type Arith struct {
}

// 算數運算請求結構體
type ArithRequest struct {
    A int
    B int
}

// 算數運算響應結構體
type ArithResponse struct {
    Pro int // 乘積
    Quo int // 商
    Rem int // 餘數
}

// 乘法運算方法
func (this *Arith) Multiply(req ArithRequest, res *ArithResponse) error {
    res.Pro = req.A * req.B
    return nil
}

// 除法運算方法
func (this *Arith) Divide(req ArithRequest, res *ArithResponse) error {
    if req.B == 0 {
        return errors.New("divide by zero")
    }
    res.Quo = req.A / req.B
    res.Rem = req.A % req.B
    return nil
}

func main() {
    rpc.Register(new(Arith)) // 註冊rpc服務

    lis, err := net.Listen("tcp", "127.0.0.1:8096")
    if err != nil {
        log.Fatalln("fatal error: ", err)
    }

    fmt.Fprintf(os.Stdout, "%s", "start connection")

    for {
        conn, err := lis.Accept() // 接收客戶端連接請求
        if err != nil {
            continue
        }

        go func(conn net.Conn) { // 併發處理客戶端請求
            fmt.Fprintf(os.Stdout, "%s", "new client in coming\n")
            jsonrpc.ServeConn(conn)
        }(conn)
    }
}

上述服務端程序啓動後,將會監聽本地的8096端口,並處理客戶端的tcp連接請求。我們可以用golang實現一個客戶端程序連接上述服務端並進行RPC調用。

$GOPATH/src/test/rpc/jsonrpc_client.go

package main

import (
    "fmt"
    "log"
    "net/rpc/jsonrpc"
)

// 算數運算請求結構體
type ArithRequest struct {
    A int
    B int
}

// 算數運算響應結構體
type ArithResponse struct {
    Pro int // 乘積
    Quo int // 商
    Rem int // 餘數
}

func main() {
    conn, err := jsonrpc.Dial("tcp", "127.0.0.1:8096")
    if err != nil {
        log.Fatalln("dailing error: ", err)
    }

    req := ArithRequest{9, 2}
    var res ArithResponse

    err = conn.Call("Arith.Multiply", req, &res) // 乘法運算
    if err != nil {
        log.Fatalln("arith error: ", err)
    }
    fmt.Printf("%d * %d = %d\n", req.A, req.B, res.Pro)

    err = conn.Call("Arith.Divide", req, &res)
    if err != nil {
        log.Fatalln("arith error: ", err)
    }
    fmt.Printf("%d / %d, quo is %d, rem is %d\n", req.A, req.B, res.Quo, res.Rem)
}

protorpc庫

爲了實現跨語言調用,在golang中實現RPC方法的時候我們應該選擇一種跨語言的數據編解碼方式,比如JSON,上述的jsonrpc可以滿足此要求,但是也存在一些缺點,比如不支持http傳輸,數據編解碼性能不高等。於是呢,一些第三方rpc庫都選擇採用protobuf進行數據編解碼,並提供一些服務註冊代碼自動生成功能。下面的例子我們使用protobuf來定義RPC方法及其請求響應參數,並使用第三方的protorpc庫來生成RPC服務註冊代碼。

首先,需要安裝protobufprotoc可執行命令,可以參考此篇文章:protobuf快速上手指南

然後,我們編寫一個proto文件,定義要實現的RPC方法及其相關參數。

$GOPATH/src/test/rpc/pb/arith.proto

syntax = "proto3";
package pb;

// 算術運算請求結構
message ArithRequest {
    int32 a = 1;
    int32 b = 2;
}

// 算術運算響應結構
message ArithResponse {
    int32 pro = 1;  // 乘積
    int32 quo = 2;  // 商
    int32 rem = 3;  // 餘數
}

// rpc方法
service ArithService {
    rpc multiply (ArithRequest) returns (ArithResponse);    // 乘法運算方法
    rpc divide (ArithRequest) returns (ArithResponse);      // 除法運算方法
}

接下來我們需要根據上述定義的arith.proto文件生成RPC服務代碼。
要先安裝protorpc庫:go get github.com/chai2010/protorpc
然後使用protoc工具生成代碼:protoc --go_out=plugin=protorpc=. arith.proto
執行protoc命令後,在與arith.proto文件同級的目錄下生成了一個arith.pb.go文件,裏面包含了RPC方法定義和服務註冊的代碼。

基於生成的arith.pb.go代碼我們來實現一個rpc服務端

$GOPATH/src/test/rpc/protorpc_server.go

package main

import (
    "errors"
    "test/rpc/pb"
)

// 算術運算結構體
type Arith struct {
}

// 乘法運算方法
func (this *Arith) Multiply(req *pb.ArithRequest, res *pb.ArithResponse) error {
    res.Pro = req.GetA() * req.GetB()
    return nil
}

// 除法運算方法
func (this *Arith) Divide(req *pb.ArithRequest, res *pb.ArithResponse) error {
    if req.GetB() == 0 {
        return errors.New("divide by zero")
    }
    res.Quo = req.GetA() / req.GetB()
    res.Rem = req.GetA() % req.GetB()
    return nil
}

func main() {
    pb.ListenAndServeArithService("tcp", "127.0.0.1:8097", new(Arith))
}

運行上述程序,將會監聽本地的8097端口並接收客戶端的tcp連接。

基於ariti.pb.go再來實現一個客戶端程序。

$GOPATH/src/test/protorpc_client.go

package main

import (
    "fmt"
    "log"
    "test/rpc/pb"
)

func main() {
    conn, err := pb.DialArithService("tcp", "127.0.0.1:8097")
    if err != nil {
        log.Fatalln("dailing error: ", err)
    }
    defer conn.Close()

    req := &pb.ArithRequest{9, 2}

    res, err := conn.Multiply(req)
    if err != nil {
        log.Fatalln("arith error: ", err)
    }
    fmt.Printf("%d * %d = %d\n", req.GetA(), req.GetB(), res.GetPro())

    res, err = conn.Divide(req)
    if err != nil {
        log.Fatalln("arith error ", err)
    }
    fmt.Printf("%d / %d, quo is %d, rem is %d\n", req.A, req.B, res.Quo, res.Rem)
}

如何跨語言調用golang的RPC方法

上面的三個例子,我們分別使用net/rpcnet/rpc/jsonrpcprotorpc實現了golang中的RPC服務端,並給出了對應的golang客戶端RPC調用示例,因爲JSON和protobuf是支持多語言的,所以使用jsonrpcprotorpc實現的RPC方法我們是可以在其他語言中進行調用的。下面給出一個php客戶端程序,通過socket連接調用jsonrpc實現的服務端RPC方法。

$PHPROOT/jsonrpc.php

<?php

class JsonRPC {

    private $conn;

    function __construct($host, $port) {
        $this->conn = fsockopen($host, $port, $errno, $errstr, 3);
        if (!$this->conn) {
            return false;
        }
    }

    public function Call($method, $params) {
        if (!$this->conn) {
            return false;
        }
        $err = fwrite($this->conn, json_encode(array(
                '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;
        }
        return json_decode($line,true);
    }
}

$client = new JsonRPC("127.0.0.1", 8096);
$args = array('A'=>9, 'B'=>2);
$r = $client->Call("Arith.Multiply", $args);
printf("%d * %d = %d\n", $args['A'], $args['B'], $r['result']['Pro']);
$r = $client->Call("Arith.Divide", array('A'=>9, 'B'=>2));
printf("%d / %d, Quo is %d, Rem is %d\n", $args['A'], $args['B'], $r['result']['Quo'], $r['result']['Rem']);

其他RPC庫

除了上面提到的三種在golang實現RPC的方式外,還有一些其他的rpc庫提供了類似的功能,比較出名的有google開源的grpc,但是grpc的初次安裝比較麻煩,這裏就不做進一步介紹了,有興趣的可以自己瞭解。

參考資料

本文來自:簡書

感謝作者:豆瓣奶茶

查看原文:golang實現RPC的幾種方式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章