Golang gRPC 教學 - part 1
Posted on Feb 4, 2020 in Go 程式設計 - 高階 by Amo Chen ‐ 4 min read
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:9999
, Dial
會負責建立起 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