1. 程式人生 > >Go 中的 gRPC 入門詳解

Go 中的 gRPC 入門詳解

[TOC] ## Go GRPC 入門 ### 1,安裝包 **grpc** golang-grpc 包提供了 gRPC 相關的程式碼庫,通過這個庫我們可以建立 gRPC 服務或客戶端,首先需要安裝他。 ```shell go get -u google.golang.org/grpc ``` **協議外掛** 要玩 gRPC,自然離不開 proto 檔案,需要安裝兩個包,用於支援 protobuf 檔案的處理。 ``` go get -u github.com/golang/protobuf go get -u github.com/golang/protobuf/protoc-gen-go ``` 注:`GOPATH/bin` 下有個 `protoc-gen-go.exe` 檔案,然而這個只是 protoc 的外掛,他本身不是 protoc 工具。。。 **Protocol Buffers** Protocol Buffers 是一個與程式語言無關、與平臺無關的可拓展機制,用於序列化結構資料,是一種資料交換格式,gRPC 使用 protoc 作為協議處理工具。 學習 Go 的 gRPC 時,有個坑,很多文章裡面都沒有說到要安裝這個,執行命令提示不存在 protoc 命令。 首先到 [https://github.com/protocolbuffers/protobuf/releases](https://github.com/protocolbuffers/protobuf/releases) 下載 相應的包,例如筆者下載的是 `protoc-3.15.6-win64.zip`。 解壓後,複製裡面的 `bin\protoc.exe` 檔案,複製到 `GOPATH\bin` 命令,跟 `protoc-gen-go.exe` 放一起。 **測試** 以上都妥當後,我們在一個新的目錄,建立一個 test.proto 檔案,其內容示例如下如下: 注:`protoc-3.15.6-win64\include\google\protobuf` 目錄也有很多示例。 ```protobuf syntax = "proto3"; // 包名 package test; // 指定輸出 go 語言的原始碼到哪個目錄以及檔名稱 // 最終在 test.proto 目錄生成 test.pb.go // 也可以只填寫 "./" option go_package = "./;test"; // 如果要輸出其它語言的話 // option csharp_package="MyTest"; service Tester{ rpc MyTest(Request) returns (Response){} } // 函式引數 message Request{ string jsonStr = 1; } // 函式返回值 message Response{ string backJson = 1; } ``` 然後在 proto 所在目錄,執行命令將 proto 轉換為相應的程式語言檔案。 ```shell protoc --go_out=plugins=grpc:. *.proto ``` 會發現在當前目錄輸出了 `test.pb.go` 檔案。 ### 2,gRPC 服務端 建立一個 go 程式,把 test.pb.go 複製放到在 main.go 目錄,在 main.go 引入 grpc: ```go import ( "context" "fmt" "google.golang.org/grpc" // test.pb.go 預設包名是 package 為 main,不需要在這裡引入 "google.golang.org/grpc/reflection" "log" "net" ) ``` 在 `test.pb.go` 中,生成了兩個個 `Tester` 的介面,我們來看一下這兩個介面的定義: ```go type TesterServer interface { MyTest(context.Context, *Request) (*Response, error) } type TesterClient interface { MyTest(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) } ``` 要實現 proto 中的服務,則需要我們實現 `TesterServer` 介面,要編寫 客戶端,則需要實現 `TesterClient` 。 這裡我們先實現 Server。 ```go // 用於實現 Tester 服務 type MyGrpcServer struct{} func (myserver *MyGrpcServer) MyTest(context context.Context, request *Request) (*Response, error) { fmt.Println("收到一個 grpc 請求,請求引數:", request) response := Response{BackJson: `{"Code":666}`} return &response, nil } ``` 接著我們建立 gRPC 服務。 ```go func main() { // 建立 Tcp 連線 listener, err := net.Listen("tcp", ":8028") if err != nil { log.Fatalf("監聽失敗: %v", err) } // 建立gRPC服務 grpcServer := grpc.NewServer() // Tester 註冊服務實現者 // 此函式在 test.pb.go 中,自動生成 RegisterTesterServer(grpcServer, &MyGrpcServer{}) // 在 gRPC 服務上註冊反射服務 // func Register(s *grpc.Server) reflection.Register(grpcServer) err = grpcServer.Serve(listener) if err != nil { log.Fatalf("failed to serve: %v", err) } } ``` ### 3,gRPC 客戶端 建立一個新的 go 專案,把 test.pb.go 複製放到 main.go 同級目錄,main.go 的程式碼: ```go package main import ( "bufio" "context" "google.golang.org/grpc" "log" "os" ) func main() { conn, err := grpc.Dial("127.0.0.1:8028", grpc.WithInsecure()) if err != nil { log.Fatal("連線 gPRC 服務失敗,", err) } defer conn.Close() // 建立 gRPC 客戶端 grpcClient := NewTesterClient(conn) // 建立請求引數 request := Request{ JsonStr: `{"Code":666}`, } reader := bufio.NewReader(os.Stdin) for { // 傳送請求,呼叫 MyTest 介面 response, err := grpcClient.MyTest(context.Background(), &request) if err != nil { log.Fatal("傳送請求失敗,原因是:", err) } log.Println(response) reader.ReadLine() } } ``` ### 4,編譯執行 由於建立的時候,test.pb.go 使用的包名是 main,所以在編譯時,需要把多個 go 檔案一起編譯: ```shell go build .\main.go .\test.pb.go ``` 然後分別啟動 server 和 client,在 client 每按下一次回車鍵,便傳送一次 gRPC 訊息。 ![gRPC請求和響應](https://img2020.cnblogs.com/blog/1315495/202103/1315495-20210328105712012-1639814131.png) 到這裡,我們學習了一個完整的 gRPC 從建立協議到建立服務和客戶端的過程,下面將接著學習一些相關的知識,瞭解一些細節。 ### 5,其它 `proto.Marshal` 可以對請求的引數進行序列化,如: ```go // 建立請求引數 request := Request{ JsonStr: `{"Code":666}`, } out,err:= proto.Marshal(&request) if err != nil { log.Fatalln("Failed to encode address book:", err) } if err := ioutil.WriteFile("E:/log.txt", out, 0644); err != nil { log.Fatalln("Failed to write address book:", err) } ``` 而 `proto.Unmarshal` 則可以反序列化。 我們還可以自定義如何序列化反序列化訊息,程式碼示例: ```go b, err := MarshalOptions{Deterministic: true}.Marshal(m) ``` 感興趣的讀者可訪問 [https://pkg.go.dev/google.golang.org/protobuf/proto#MarshalOptions](https://pkg.go.dev/google.golang.org/protobuf/proto#MarshalOptions) ## GRPC ### Protobuf buffer Protobuf buffer 是一種資料格式,而 Protobuf 是 gRPC 協議,這裡需要區分一下。 protobuf buffer 是 Google 用於序列化結構話資料的開源機制,要定義一個 protobuf buffer,需要使用 message 定義。 ```protobuf message Person { string name = 1; int32 id = 2; bool has_ponycopter = 3; } ``` 開源看到,每個欄位都有一個 數字,` = 1` 這個不是賦值,而是編號。一個 message 中,每個欄位都有唯一的編號,這些數字用於標識二進位制格式的欄位(資料傳輸時會被壓縮等),當編號範圍是 1-15 時,儲存編號需要一個位元組,也就是說 message 中的欄位儘量不超過 15 個,1-15 編號用來定義頻繁出現的訊息元素。當然,也可以使用`16-2047` 之間的數字作為編號,此時儲存編號需要兩個位元組。 詳細的說可以參考官方文件: [https://developers.google.com/protocol-buffers/docs/overview](https://developers.google.com/protocol-buffers/docs/overview) #### 欄位型別 欄位型別就不詳細列表了,讀者可以參考官方文件,這裡列一下常用的資料型別: double、float、int32、int64、bool、string、bytes、列舉。 由於 gRPC 需要考慮相容 C 語言、C#、Java、Go 語言等,所以 gRPC 中的型別不等同於程式語言中的相關型別。這些型別都是 gRPC 中定義的,並且如果要轉換為程式語言中的型別,需要一些轉換機制,而這有時會十分麻煩。 #### 欄位規則 每個欄位都可以指定一個規則,在定義欄位型別的開頭使用規則標識。 有以下三種規則: - `required`:格式正確的訊息必須恰好具有此欄位之一,即必填欄位。 - `optional`:格式正確的訊息可以包含零個或一個此欄位(但不能超過一個,即值是可選的。 - `repeated`:在格式正確的訊息中,此欄位可以重複任意次(包括零次),重複值的順序將保留,表示該欄位可以包含0~N個元素。 由於歷史原因,`repeated`標量數字型別的欄位編碼效率不高。新程式碼應使用特殊選項`[packed=true]`來獲得更有效的編碼。例如: ```protobuf repeated int32 samples = 4 [packed=true]; ``` 在可選欄位中 optional 中,我們可以為其設定一個預設值,當傳遞訊息時如果沒有填寫此欄位,則使用其預設值: ```protobuf optional int32 result_per_page = 3 [default = 10]; ``` ### Protobuf 接下來將介紹 gRPC 的協議格式(protobuf),下面是官方文件的一個示例: ```protobuf syntax = "proto3"; package tutorial; import "google/protobuf/timestamp.proto"; ``` syntax 指明協議的版本; package 指明該 .proto 的名稱; import 關鍵字可以在當前 .proto 中引入其它 .proto 檔案,gRPC 基本資料型別中不包含時間格式,可以引入 `timestamp.proto`。 不同程式語言引入包/庫的方式是不同的,C++ 和 C# 都是使用名稱空間區分程式碼位置;Java 以目錄、公共類嚴格區別包名;go 則是以一個 .go 檔案任意設定 package 名稱。 前面提到了 protoc,可以將協議檔案轉為為具體的程式碼。 為了相容各種程式語言,我們協議設定 `_package`,這樣可以支援生成不同語言程式碼時設定包/庫名稱。 例如 : ``` option go_package = "Test"; // ... option csharp_package = "MyGrpc.Protos"; // 生成名稱空間 namespace MyGrpc.Protos{} option java_paclage = "MyJava.Protos"; // ... ``` ### gRPC 四種服務方法 protobuf 中除了可以定義 message,也可以定義流式介面。 gRPC使您可以定義四種服務方法: - 一元 RPC,客戶端向伺服器傳送單個請求並獲得單個響應,就像普通的函式呼叫一樣。前面我們提到的都是一元 gRPC。 ```protobuf rpc SayHello(HelloRequest) returns (HelloResponse); ``` - 伺服器流式RPC,客戶端在其中向伺服器傳送請求,並獲取流以讀取回一系列訊息。客戶端從返回的流中讀取,直到沒有更多訊息為止。gRPC保證在單個RPC呼叫中對訊息進行排序。 客戶端 -> 服務端 -> 返回流 -> 客戶端 -> 接收流 ```protobuf rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse); ``` - 客戶端流式RPC,客戶端在其中編寫訊息序列,然後再次使用提供的流將其傳送到伺服器。客戶端寫完訊息後,它將等待伺服器讀取訊息並返回其響應。gRPC再次保證了在單個RPC呼叫中的訊息順序。 客戶端 -> 傳送流 -> 服務端 -> 接收流 -> ```proto rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse); ``` - 雙向流式RPC,雙方都使用讀寫流傳送一系列訊息。這兩個流獨立執行,因此客戶端和伺服器可以按照自己喜歡的順序進行讀寫:例如,伺服器可以在寫響應之前等待接收所有客戶端訊息,或者可以先讀取訊息再寫入訊息,或讀寫的其他組合。每個流中的訊息順序都會保留。 ```proto rpc BidiHello(stream HelloRequest) returns (stream HelloResponse); ``` #### 編譯 proto 前面我們用 protoc 來編譯 .proto 檔案為 go 語言,為了支援編譯為 go,需要安裝 `protoc-gen-go` 外掛,C# 可以安裝 `protoc-gen-zsharp` 外掛。 需要注意的是,轉換 .proto 為程式語言,不一定要安裝 protoc。 例如 C# 只需要把 .proto 檔案放到專案中,通過包管理器安裝一個庫,就會自動轉換為相應的程式碼。 迴歸正題,聊一下 protoc 編譯 .proto 檔案的命令。 protoc 常用的引數如下: ``` --proto_path=. #指定proto檔案的路徑,填寫 . 表示就在當前目錄下 --go_out=. #表示編譯後的檔案存放路徑;如果編譯的是 csharp,則 --csharp_out --go_opt={xxx.proto}={xxx.proto的路徑} # 示例:--go_opt=Mprotos/bar.proto=example.com/project/protos/foo ``` 最簡單的編譯命令: ```shell protoc --go_out=. *.proto ``` `--{xxx}_out` 指令是必須的,因為要輸出具體的程式語言程式碼。 這個輸出檔案的路徑是執行命令的路徑,如果我們不在 .proto 檔案目錄下執行命令,則輸出的程式碼便不是相同位置了。為了解決這個問題,我們可以使用: ``` --go_opt=paths=source_relative ``` 這樣在別的地方執行命令,生成的程式碼會跟 .proto 檔案放在相同的