gRPC 是由 Google 所開源的一項 RPC(Remote Procedure Call) 專案。

由於 Google 內部使用了相當多的 Microservices, 也因此 Google 內部十分仰賴以 RPC 技術作為資料傳遞、處理的骨幹,他們內部也使用了一套稱為 Stubby 的 RPC 技術框架,可以視為 gRPC 的前身,不過隨著 SPDY, HTTP/2 及 QUIC 等技術的出現,加上多年使用 Stubby 的經驗以及為了改近 Stubby 的不足,促使 Google 決定打造一套新世代的 RPC 框架,最後造就 gRPC 問世。(有興趣可以進一步閱讀 gRPC Blog

總的來說, gRPC 很適合應用於內部環境很多微服務(Microservices)的情況,甚至是各個內部系統以不同程式語言開發的情況( gRPC 支援多種程式語言)。而且 gRPC 支援串流(streaming)形式的資料傳輸,因此也十分適合行動裝置與瀏覽器用戶端等需要串流的情境,例如聲音辨識、即時翻譯等等。

接著,開始介紹如何在 Go 中使用 gRPC 吧!

(本文建議開始之前先學過 Protocol Buffers 會比較輕鬆)

本文環境

  • macOS 10.15
  • go 1.13.5
  • Protocol Buffers 3.11.3

前言

gRPC 可以想像成一種事先定義好的語言(實際上是擴充 Protocol Buffers 的語法),裡面只有幾個簡單的語法,只需要寫好服務(service)以及該服務提供的功能,最後再描述每項功能接受什麼格式的參數以及回應的格式,例如以下官方的範例:

syntax = "proto3";

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}

由於上述範例十分容易理解,在此就不多做解釋。透過上述的語法定義 gRPC 服務之後,就能夠透過 Protocol Buffers 編譯 gRPC 支援的程式語言的程式碼,開發者只要實作程式碼內的功能流程與邏輯即可。

安裝 Protocol Buffers & gRPC

如前言所示, gRPC 擴充 Protocol Buffers 的語法,因此使用 gRPC 時必須先安裝 Protocol Buffers, 可以先至 protobuf 的 Github 下載 執行檔( e.g. protoc-3.11.3-osx-x86_64.zip ) 進行安裝,安裝方式詳見該檔案夾內的 readme.txt ,在此不多贅述。

接著安裝 gRPC .

首先建立個資料夾作為 GOPATH ,並切換進該資料夾:

$ mkdir -p myGOPATH; cd myGOPATH

再來設定 GOPATH 環境變數,並建立我們專案資料夾 repo 後,切換進該資料夾:

$ GOPATH=$(pwd)
$ mkdir -p src/github.com/my/repo; cd src/github.com/my/repo

接下來就能夠透過 go get 指令 gRPC 囉:

$ go get -u google.golang.org/grpc
$ go get -u github.com/golang/protobuf/protoc-gen-go

安裝完成後,將在 src 的同層資料夾發現 bin 資料夾,裡面有 1 個可執行檔 protoc-gen-go

$ ls ../../../../bin/
protoc-gen-go

我們必須將這個 bin 資料夾的路徑加入 PATH 環境變數中,否則將造成編譯 gRPC 時出現以下錯誤:

protoc-gen-go: program not found or is not executable
Please specify a program using absolute path or make sure the program is available in your PATH system variable
--go_out: protoc-gen-go: Plugin failed with status code 1.

使用以下指令加入到 PATH 中:

$ PATH=$PATH:$(realpath ../../../../bin/)

至此安裝就告一段落囉!

Hello gRPC

體驗透過 gRPC 產生程式碼之前,我們必須先定義 gRPC 服務,在此先用官方範例作為示範,我們在 repo 資料夾內新增資料夾 hello

$ mkdir hello; cd hello

接著在 hello 資料夾內新增檔案 hello.proto 並放入以下內容:

syntax = "proto3";

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}

然後用以下指令將上述檔案編譯成 go 的程式碼:

$ protoc -I . hello.proto --go_out=plugins=grpc:.

成功後將會出現一個檔案 hello.pb.go ,打開它之後可以搜尋字串 UnimplementedHelloServiceServer ,這個 struct 就是我們要實作的 gRPC service 的 server .

實作 UnimplementedHelloServiceServer

定義與編譯 gRPC 之後,最後就是實作的部分,我們回到 hello 的上層資料夾中,並新增 cmd/server/ 資料夾,在該資料夾中撰寫 main.go 實作 gRPC server, 以下是 main.go 的程式碼範例:

package main

import (
    "context"
    pb "github.com/my/repo/hello"
    "google.golang.org/grpc"
    "log"
    "net"
)

type service struct {
    pb.UnimplementedHelloServiceServer
}

func (s *service) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
    log.Printf("Received: %v", in.GetGreeting())
    return &pb.HelloResponse{Reply: "Hello, " + in.GetGreeting()}, nil
}

func main() {
    addr := "127.0.0.1:9999"
    lis, err := net.Listen("tcp", addr)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    log.Println("Server listening on", addr)
    gRPCServer := grpc.NewServer()
    pb.RegisterHelloServiceServer(gRPCServer, &service{})
    if err := gRPCServer.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

首先,可以看到 11 - 13 行的 service 結構嵌了 pb.UnimplementedHelloServiceServer 結構,接著 15 - 18 行則是實作 hello.proto 中定義的 SayHello ,為求簡便此處直接將 "Hello, " + in.GetGreeting() ( GetGreeting() 被定義於 hello.pb.go 中,有興趣的話可以閱讀該檔案試試)放進 pb.HelloResponse 中的 Reply 作為回應,如此便實作完成 SayHello

接著 22 行則是將 gRPC server 的監聽地址設為 127.0.0.1:9999 ,在 29 行將 gRPC server 以及我們的 service 註冊到 gRPC server 中,最後在 30 行將 gRPC server 給運行起來。

以上就完成了 gRPC server 的實作。

執行 main.go 成功的話,會顯示以下畫面:

$ go run cmd/server/main.go
2020/02/05 22:17:36 Server listening on 127.0.0.1:9999

實作 gRPC Client

實作完 gRPC server 之後,接著試試實作 gRPC client 並呼叫 SayHello 看看!

我們回到 cmd 資料夾中,並新增 client/ 資料夾,在該資料夾中撰寫 main.go 實作 gRPC client, 以下是 main.go 的程式碼範例:

package main

import (
    "context"
    "fmt"
    pb "github.com/my/repo/hello"
    "google.golang.org/grpc"
    "log"
    "time"
)

func main() {
    addr := "127.0.0.1:9999"
    conn, err := grpc.Dial(addr, grpc.WithInsecure(), grpc.WithBlock())
    if err != nil {
        log.Fatalf("Can not connect to gRPC server: %v", err)
    }
    defer conn.Close()

    c := pb.NewHelloServiceClient(conn)
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    r, err := c.SayHello(ctx, &pb.HelloRequest{Greeting: "Moto"})
    if err != nil {
        log.Fatalf("Could not get nonce: %v", err)
    }
    fmt.Println("Response:", r.GetReply())
}

首先, 14 行先建立 1 個 gRPC 連線至 127.0.0.1:9999Dial 會負責建立起 client 與 server 間的 gRPC channel 以進行溝通。 grpc.WithInsecure() 則是設定使用不安全的連線設定,這是由於本範例為求簡便所以停用安全連線,否則 client 與 server 之間需要使用安全連線(e.g. TLS/SSL) , grpc.WithBlock() 則讓 client 在能夠連線到 server 之前先 block 住。

20 行則是建立 gRPC 的 client ,讓我們能夠在 23 行呼叫 SayHello

21 行則是利用 context package 所提供的 WithTimeout 函式,讓我們能夠在呼叫 SayHello 時如果超過 1 秒就視為超時(timeout) 。

23 行如果呼叫成功,會得到 1 個 pb.HelloResponse 的結構,該結構中有定義 GetReply() 方法,能夠取得來自 gRPC server 的回應。

以上就是最簡單的 gRPC client 實作,執行成功後會出現以下畫面:

$ go run cmd/client/main.go
Response: Hello, Moto

以上, Happy Coding!

References

https://grpc.io/

https://godoc.org/google.golang.org/grpc