在Golang中實現RPC

什麼是RPC

遠程過程調用(Remote Procedure Call,縮寫爲 RPC)是一個計算機通訊協議。遠程調用是由於被調用方法的具體實現不在程序運行本地,而是在遠程服務器上。須要將對象名、函數名、參數等傳遞給遠程服務器,服務器將處理結果返回給客戶端。RPC 的消息能夠經過 TCP、UDP 或者 HTTP等傳輸。html

在Golang中實現RPC的方式大致有三種,分別來看。git

net/rpc

Golang官方的net/rpc包使用encoding/gob進行編解碼,支持tcp或http數據傳輸方式。可是因爲gob編碼是Golang獨有的因此它只支持Golang開發的服務器與客戶端之間的交互。github

RPC採用客戶機/服務器模式。先看一下server例子:golang

package main

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

type Listener int

type Reply struct {
	Data string
}

func (l *Listener) GetLine(line []byte, reply *Reply) error {
	rv := string(line)
	fmt.Printf("Receive: %v\n", rv)
	*reply = Reply{rv}
	return nil
}

func main() {
	addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:12345")
	if err != nil {
		log.Fatal(err)
	}

	inbound, err := net.ListenTCP("tcp", addy)
	if err != nil {
		log.Fatal(err)
	}

	listener := new(Listener)
	rpc.Register(listener)
	rpc.Accept(inbound)
}
複製代碼

在這裏例子中給Listener添加了GetLine方法,這個方法的返回值必須是error類型,第一個參數是客戶端傳來的內容,第二個參數是響應的內容:須要是一個指針,因此定義了一個叫作Reply的結構體,只有一個Data成員用於存儲響應內容。json

在main函數中,首先用net.ResolveTCPAddrnet.ListenTCP建立一個TCP鏈接,監聽全部地址的12345端口。最後用rpc.Register註冊監聽對象,接受上面的tcp鏈接的請求。bash

而後是客戶端:服務器

package main

import (
	"bufio"
	"log"
	"net/rpc"
	"os"
)

type Reply struct {
    Data string
}

func main() {
	client, err := rpc.Dial("tcp", "localhost:12345")
	if err != nil {
		log.Fatal(err)
	}

	in := bufio.NewReader(os.Stdin)
	for {
		line, _, err := in.ReadLine()
		if err != nil {
			log.Fatal(err)
		}
		var reply Reply
		err = client.Call("Listener.GetLine", line, &reply)
		if err != nil {
			log.Fatal(err)
		}
		log.Printf("Reply: %v, Data: %v", reply, reply.Data)
	}
}
複製代碼

客戶端用rpc.Dial建立鏈接到服務端的主機和端口,而後是一個永久的for循環,ReadLine方法會接收終端輸入,若是寫了一些內容回車,就會執行client.Call,開始過程調用,調用成功後,reply就被寫入數據,能夠拿到reply.Data了(其實就是輸入什麼,收到什麼)。體驗一下:併發

❯ go run simple_server.go
Receive: hi
Receive: haha

❯ go run simple_client.go
hi
2019/07/14 18:19:14 Reply: {hi}, Data: hi
haha
2019/07/14 18:19:15 Reply: {haha}, Data: haha
複製代碼

net/rpc/jsonrpc

使用net/rpc實現的RPC只能使用Golang語言編寫的服務端/客戶端之間交互,因此Go語言標準庫經過net/rpc/jsonrpc這個包支持跨語言的RPC。要實現上面同樣的效果,代碼主要是改了main的rpc.Accept部分就能夠了:框架

import "net/rpc/jsonrpc"

func main() {
    addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:12345")
    if err != nil {
        log.Fatal(err)
    }

    inbound, err := net.ListenTCP("tcp", addy)
    if err != nil {
        log.Fatal(err)
    }

    listener := new(Listener)
    rpc.Register(listener)
    for {
        conn, err := inbound.Accept()
        if err != nil {
            continue
        }
        jsonrpc.ServeConn(conn)
    }
}
複製代碼

客戶端部分也只是改動rpc.Dial部分:異步

func main() {
    client, err := jsonrpc.Dial("tcp", "localhost:12345") // 只改動這一行
    if err != nil {
        log.Fatal(err)
    }

    in := bufio.NewReader(os.Stdin)
    for {
        line, _, err := in.ReadLine()
        if err != nil {
            log.Fatal(err)
        }
        var reply Reply
        err = client.Call("Listener.GetLine", line, &reply)
        if err != nil {
            log.Fatal(err)
        }
        log.Printf("Reply: %v, Data: %v", reply, reply.Data)
    }
}
複製代碼

json-rpc是基於TCP協議實現的,目前它還不支持HTTP方式。運行效果和上面的同樣:

❯ go run simple_jsonrpc_server.go
Receive: hi
Receive: haha

❯ go run simple_jsonrpc_client.go
hi
2019/07/14 20:22:02 Reply: {hi}, Data: hi
haha
2019/07/14 20:22:03 Reply: {haha}, Data: haha
複製代碼

請求的json數據對象在內部對應兩個結構體:客戶端是clientRequest,服務端是serverRequest。大抵是這樣

type serverRequest struct {
	Method string           `json:"method"`
	Params *json.RawMessage `json:"params"`
	Id     *json.RawMessage `json:"id"`
}

type clientRequest struct {
	Method string         `json:"method"`
	Params [1]interface{} `json:"params"`
	Id     uint64         `json:"id"`
}
複製代碼

因此咱們能夠基於這個格式用其餘語言拼消息。簡單一點,在命令行試試:

echo -n "hihi" |base64  # 參數須要用base64編碼
aGloaQ==

~/strconv.code/rpc master*
❯ echo -e '{"method": "Listener.GetLine","params": ["aGloaQ=="], "id": 0}' | nc localhost 12345
{"id":0,"result":{"Data":"hihi"},"error":null}
複製代碼

看到了吧,能夠拿到對應的結果。其中id不是必須的,可是能夠基於id在併發高或者異步調用中對應某一次調用。

gRPC

jsonrpc雖然能夠支持跨語言可是不支持HTTP傳輸,並且性能不高,因此在實際生產環境中都不會用標準庫裏面的方式,而是選擇Thrift、gRPC等方案。

gRPC是Google開源的高性能、通用的開源RPC框架,其主要面向移動應用開發並基於HTTP/2協議標準而設計,基於ProtoBuf序列化協議開發,支持Python、Golang、Java等衆多開發語言。

ProtoBuf協議

Protobuf是Protocol Buffers的簡稱,它是Google公司開發的一種數據描述語言,相似於XML、JSON等數據描述語言,它很是輕便高效,很適合作數據存儲或 RPC 數據交換格式。因爲它一次定義,可生成多種語言的代碼,很是適合用於通信協議、數據存儲等領域的語言無關、平臺無關、可擴展的序列化結構數據格式。首先安裝它和Go語言的生成工具:

❯ brew install protobuf
❯ protoc --version
libprotoc 3.7.1
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
複製代碼

而後按照最新的proto3協議寫一個描述文件

syntax = "proto3";

package simple;

// 請求
message SimpleRequest {
    string data = 1;
}

// 響應
message SimpleResponse {
    string data = 1;
}

// rpc方法
service Simple {
    rpc  GetLine (SimpleRequest) returns (SimpleResponse);
}
複製代碼

其中描述了請求SimpleRequest(只有一個字符串參數data)、響應SimpleResponse(只有一個字符串參數data)和rpc方法。Simple服務只有一個GetLine方法,請求是SimpleRequest,響應SimpleResponse。而後基於.proto文件生成數據操做代碼:

❯ mkdir src/simple
❯ protoc --go_out=plugins=grpc:src/simple simple.proto
❯ ll src/simple
total 8.0K
-rw-r--r-- 1 xiaoxi staff 7.0K Jul 14 21:43 simple.pb.go
複製代碼

執行命令完成,就在src/simple下生成了一個叫作simple.pb.go的文件,它支持gRPC。放在src/simple目錄下是爲了讓它做爲一個包(package)。

體驗 gRPC

首先須要安裝 gRPC

❯ go get -u google.golang.org/grpc
複製代碼

而後就能夠基於src/simple這個包寫代碼了

package main

import (
	"fmt"
	"log"
	"net"

	pb "./src/simple"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
)

type Listener int

func (l *Listener) GetLine(ctx context.Context, in *pb.SimpleRequest) (*pb.SimpleResponse, error) {
	rv := in.Data
	fmt.Printf("Receive: %v\n", rv)
	return &pb.SimpleResponse{Data: rv}, nil
}

func main() {
	addy, err := net.ResolveTCPAddr("tcp", "0.0.0.0:12345")
	if err != nil {
		log.Fatal(err)
	}

	inbound, err := net.ListenTCP("tcp", addy)
	if err != nil {
		log.Fatal(err)
	}

	s := grpc.NewServer()
	listener := new(Listener)
    pb.RegisterSimpleServer(s, listener)
    s.Serve(inbound)
}
複製代碼

其中pb "./src/simple"表示把當前目錄下的src/simple做爲一個包,給它取了個別名pb,內容就來自前面建立的simple.pb.go。

GetLine方法要從新定義,它的第一個參數是context.Context,第二個是*pb.SimpleRequest(在proto文件中定義的請求),返回的結果是(*pb.SimpleResponse, error),pb.SimpleResponse在proto文件中定義的響應。另外要注意,雖然在proto文件中SimpleRequest和SimpleResponse的成員data是小寫開頭的,可是使用時要首字母大寫(Data)。

再看客戶端:

package main

import (
	"bufio"
	"log"
	"os"

	pb "./src/simple"
    "golang.org/x/net/context"
    "google.golang.org/grpc"
)

func main() {
	conn, err := grpc.Dial("localhost:12345", grpc.WithInsecure())
	if err != nil {
		log.Fatal(err)
	}

	c := pb.NewSimpleClient(conn)

	in := bufio.NewReader(os.Stdin)
	for {
		line, _, err := in.ReadLine()
		if err != nil {
			log.Fatal(err)
		}
		reply, err := c.GetLine(context.Background(), &pb.SimpleRequest{Data: string(line)})
		if err != nil {
			log.Fatal(err)
		}
		log.Printf("Reply: %v, Data: %v", reply, reply.Data)
	}
}
複製代碼

首先用grpc.Dial("localhost:12345", grpc.WithInsecure())建立鏈接,而後用pb.NewSimpleClient建立simpleClient對象。爲何叫SimpleClient呢?其實格式是XXXClient, XXX是前面在proto文件中定義的service Simple中的Simple

rpc調用時要這樣寫:reply, err := c.GetLine(context.Background(), &pb.SimpleRequest{Data: string(line)}),GetLine就是proto文件中定義的方法(rpc GetLine (SimpleRequest) returns (SimpleResponse)),第一個參數context.Background(),第二個參數是請求,因爲line是[]byte類型的,因此須要用string轉換成字符串。響應reply是SimpleResponse對象,能夠從reply.Data中得到返回的實際結果:

❯ go run grpc_server.go
Receive: hi
Receive: Haha
Receive: vvv

❯ go run grpc_client.go
hi
2019/07/15 07:57:48 Reply: data:"hi" , Data: hi
Haha
2019/07/15 07:57:51 Reply: data:"Haha" , Data: Haha
vvv
2019/07/15 07:57:53 Reply: data:"vvv" , Data: vvv
複製代碼

代碼地址

原文地址: strconv.com/posts/rpc/

完整代碼能夠在這個地址找到。

延伸閱讀

  1. books.studygolang.com/NPWG_zh/Tex…
  2. golang.org/pkg/net/rpc…
  3. developers.google.com/protocol-bu…
  4. github.com/grpc/grpc-g…
相關文章
相關標籤/搜索