寫給go開發者的gRPC教程-protobuf基礎

語言: CN / TW / HK

theme: juejin

本篇為【寫給go開發者的gRPC教程】系列第二篇


gRPC是谷歌開源的一款高效能、支援多種開發語言的服務框架,對於一個rpc我們關注如下幾方面:

序列化協議gRPC使用protobuf,首先使用protobuf定義服務,然後使用這個檔案來生成客戶端和服務端的程式碼。因為pb是跨語言的,因此即使服務端和客戶端語言並不一致也是可以互相序列化和反序列化的

網路傳輸層。gRPC使用http2.0協議,http2.0相比於HTTP 1.x ,大幅度的提升了 web 效能。

image-20220328002124007-8398087.png

Protobuf IDL

所謂序列化通俗來說就是把記憶體的一段資料轉化成二進位制並存儲或者通過網路傳輸,而讀取磁碟或另一端收到後可以在記憶體中重建這段資料

1、protobuf協議是跨語言跨平臺的序列化協議。

2、protobuf本身並不是和gRPC繫結的。它也可以被用於非RPC場景,如儲存等

jsonxml都是一種序列化的方式,只是他們不需要提前預定義idl,且具備可讀性,當然他們傳輸的體積也因此較大,可以說是各有優劣

所以先來介紹下protobuf的idl怎麼寫。protobuf最新版本為proto3,在這裡你可以看到詳細的文件說明:https://protobuf.dev/programming-guides/proto3/

定義訊息型別

protobuf裡最基本的型別就是message,每一個messgae都會有一個或者多個欄位(field),其中欄位包含如下元素

protobuf型別格式.drawio.png

  • 型別:型別不僅可以是標量型別(intstring等),也可以是複合型別(enum等),也可以是其他message
  • 欄位名:欄位名比較推薦的是使用下劃線/分隔名稱
  • 欄位編號:一個messgae內每一個欄位編號都必須唯一的,在編碼後其實傳遞的是這個編號而不是欄位名
  • 欄位規則:訊息欄位可以是以下欄位之一

  • singular:格式正確的訊息可以有零個或一個欄位(但不能超過一個)。使用 proto3 語法時,如果未為給定欄位指定其他欄位規則,則這是預設欄位規則

  • optional:與 singular 相同,不過您可以檢查該值是否明確設定

  • repeated:在格式正確的訊息中,此欄位型別可以重複零次或多次。系統會保留重複值的順序

  • map:這是一個成對的鍵值對欄位

  • 保留欄位:為了避免再次使用到已移除的欄位可以設定保留欄位。如果任何未來使用者嘗試使用這些欄位識別符號,編譯器就會報錯

標量值類

標量型別會涉及到不同語言和編碼方式,後續有機會深入講

| .proto Type | Go Type | Notes | | ----------- | ------- | ------------------------------------------------------------ | | double | float64 | | | float | float32 | | | int32 | int32 | 使用可變長度的編碼。對負數的編碼效率低下 - 如果您的欄位可能包含負值,請改用 sint32。 | | int64 | int64 | 使用可變長度的編碼。對負數的編碼效率低下 - 如果欄位可能有負值,請改用 sint64。 | | uint32 | uint32 | 使用可變長度的編碼。 | | uint64 | uint64 | 使用可變長度的編碼。 | | sint32 | int32 | 使用可變長度的編碼。有符號整數值。與常規 int32 相比,這些函式可以更高效地對負數進行編碼。 | | sint64 | int64 | 使用可變長度的編碼。有符號整數值。與常規 int64 相比,這些函式可以更高效地對負數進行編碼。 | | fixed32 | uint32 | 始終為 4 個位元組。如果值通常大於 2^28,則比 uint32 更高效。 | | fixed64 | uint64 | 始終為 8 個位元組。如果值通常大於 2^56,則比 uint64 更高效。 | | sfixed32 | int32 | 始終為 4 個位元組。 | | sfixed64 | int64 | 始終為 8 個位元組。 | | bool | bool | | | string | string | 字串必須始終包含 UTF-8 編碼或 7 位 ASCII 文字,並且長度不得超過 232。 | | bytes | []byte | 可以包含任意長度的 2^32 位元組。 |

複合型別

陣列

```protobuf message SearchResponse { repeated Result results = 1; }

message Result { string url = 1; string title = 2; repeated string snippets = 3; } ```

列舉

protobuf message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; enum Corpus { UNIVERSAL = 0; WEB = 1; IMAGES = 2; LOCAL = 3; NEWS = 4; PRODUCTS = 5; VIDEO = 6; } Corpus corpus = 4; }

服務

定義的method僅能有一個入參和出引數。如果需要傳遞多個引數需要定義成message

protobuf service SearchService { rpc Search(SearchRequest) returns (SearchResponse); }

使用其他訊息型別

使用import引用另外一個檔案的pb

```protobuf syntax = "proto3";

import "google/protobuf/wrappers.proto";

package ecommerce;

message Order { string id = 1; repeated string items = 2; string description = 3; float price = 4; google.protobuf.StringValue destination = 5; } ```

protoc使用

protoc就是protobuf的編譯器,它把proto檔案編譯成不同的語言

📖 安裝

https://grpc.io/docs/protoc-installation/

  • Linux, using apt or apt-get, for example:

sh $ apt install -y protobuf-compiler $ protoc --version # Ensure compiler version is 3+

sh $ brew install protobuf $ protoc --version # Ensure compiler version is 3+

📖 使用

```shell $ protoc --help Usage: protoc [OPTION] PROTO_FILES

-IPATH, --proto_path=PATH 指定搜尋路徑 --plugin=EXECUTABLE:

....

--cpp_out=OUT_DIR Generate C++ header and source. --csharp_out=OUT_DIR Generate C# source file. --java_out=OUT_DIR Generate Java source file. --js_out=OUT_DIR Generate JavaScript source. --objc_out=OUT_DIR Generate Objective C header and source. --php_out=OUT_DIR Generate PHP source file. --python_out=OUT_DIR Generate Python source file. --ruby_out=OUT_DIR Generate Ruby source file

@ proto檔案的具體位置 ```

1.搜尋路徑引數

第一個比較重要的引數就是搜尋路徑引數,即上述展示的-IPATH, --proto_path=PATH。它表示的是我們要在哪個路徑下搜尋.proto檔案,這個引數既可以用-I指定,也可以使用--proto_path=指定。

如果不指定該引數,則預設在當前路徑下進行搜尋;另外,該引數也可以指定多次,這也意味著我們可以指定多個路徑進行搜尋。

2.語言外掛引數

語言引數即上述的--cpp_out=--python_out=等,protoc支援的語言長達13種,且都是比較常見的

執行help出現的語言引數,說明protoc本身已經內建該語言對應的編譯外掛,我們無需安裝

| Language | Generated Code | Source | | ------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ | | C++ (include C++ runtime and protoc) | C++ | src | | Java | Java | java | | Python | Python | python | | Objective-C | Objective-C | objectivec | | C# | C# | csharp | | Ruby | Ruby | ruby | | PHP | PHP | php |

下面的語言是由google維護,通過protoc的外掛機制來實現,所以倉庫單獨維護

3.proto檔案位置引數

proto檔案位置引數即上述的@<filename>引數,指定了我們proto檔案的具體位置,如proto1/greeter/greeter.proto

📖 語言外掛

✨ golang外掛

非內建的語言支援就得自己單獨安裝語言外掛,比如--go_out=對應的是protoc-gen-go,安裝命令如下:

```shell

最新版

$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

指定版本

$ go install google.golang.org/protobuf/cmd/[email protected] ```

可以使用下面的命令來生成程式碼

shell $ protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto

注意

protoc-gen-go要求pb檔案必須指定go包的路徑,即

protobuf option go_package = "liangwt/note/grpc/example/ecommerce";

--go_out

指定go程式碼生成的基本路徑

--go_opt:設定外掛引數

protoc-gen-go提供了 --go_opt 來為其指定引數,並可以設定多個

1、如果使用 paths=import , 生成的檔案會按go_package路徑來生成,當然是在--go_out目錄下,即

$go_out/$go_package/pb_filename.pb.go

2、如果使用 paths=source_relative , 就在當前pb檔案同路徑下生成程式碼。注意pb的目錄也被包含進去了。即

$go_out/$pb_filedir/$pb_filename.pb.go

✨ grpc go外掛

google.golang.org/protobuf中,protoc-gen-go純粹用來生成pb序列化相關的檔案,不再承載gRPC程式碼生成功能。

生成gRPC相關程式碼需要安裝grpc-go相關的外掛protoc-gen-go-grpc

shell $ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

執行code gen命令

bash $ protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ routeguide/route_guide.proto

--go-grpc_out

指定grpc go程式碼生成的基本路徑

命令會產生如下檔案

  • route_guide.pb.go, protoc-gen-go的產出物,包含所有型別的序列化和反序列化程式碼

  • route_guide_grpc.pb.go, protoc-gen-go-grpc的產出物,包含

  • 定義在 RouteGuide service中的用來給client呼叫的介面定義
  • 定義在 RouteGuide service中的用來給服務端實現的介面定義
--go-grpc_opt

protoc-gen-go類似,protoc-gen-go-grpc提供 --go-grpc_opt 來指定引數,並可以設定多個

github.com/golang/protobuf vs google.golang.org/protobuf

github.com/golang/protobuf雖然已經廢棄,但網上搜索時經常還能搜到,方便理解整理兩者區別。

程式碼差異

這兩個庫,google.golang.org/protobufgithub.com/golang/protobuf的升級版本,v1.4.0之後github.com/golang/protobuf僅是google.golang.org/protobuf的包裝

功能差異

google.golang.org/protobuf,純粹用來生成pb序列化相關的檔案,不再承載gRPC程式碼生成功能。生成gRPC相關程式碼需要安裝grpc-go相關的外掛protoc-gen-go-grpc

github.com/golang/protobuf,可以同時生成pb和gRPC相關程式碼的

用法差異

google.golang.org/protobuf

shell $ protoc --go_out=. --go_opt=paths=source_relative \ --go-grpc_out=. --go-grpc_opt=paths=source_relative \ routeguide/route_guide.proto

github.com/golang/protobuf

shell $ protoc --go_out=plugins=grpc,paths=import:. \ routeguide/route_guide.proto

--go_out的寫法是,引數之間用逗號隔開,最後加上冒號來指定程式碼的生成位置,比如--go_out=plugins=grpc,paths=import:.

--go_out主要的兩個引數為pluginspaths,分別表示生成Go程式碼所使用的外掛,以及生成的Go程式碼的位置。

plugins引數有不帶grpc和帶grpc兩種(應該還有其它的,目前知道的有這兩種),兩者的區別如下,帶grpc的會多一些跟gRPC相關的程式碼,實現gRPC通訊

paths引數有兩個選項,分別是 importsource_relative,預設為 import

  • import表示按照生成的Go程式碼的包的全路徑去建立目錄層級
  • source_relative 表示按照 proto原始檔的目錄層級去建立Go程式碼的目錄層級,如果目錄已存在則不用建立。

總之,用google.golang.org/protobuf就對了!

Buf 工具

可以看到使用protoc的時候,當使用的外掛逐漸變多,外掛引數逐漸變多時,命令列執行並不是很方便和直觀。例如後面使用到了grpc-gateway+swagger外掛時

shell $ protoc -I ./pb \ --go_out ./ecommerce --go_opt paths=source_relative \ --go-grpc_out ./ecommerce --go-grpc_opt paths=source_relative \ --grpc-gateway_out ./ecommerce --grpc-gateway_opt paths=source_relative \ --openapiv2_out ./doc --openapiv2_opt logtostderr=true \ ./pb/ecommerce/v1/product.proto

其次依賴某些外部的protobuf檔案時,只能通過拷貝到本地的方式,也不夠方便

因此誕生了✨ Buf 這個專案,它除了能解決上述問題,還有額外的功能

  • 不相容破壞檢查
  • linter
  • 集中式的版本管理

初始化模組

在pb檔案的根目錄執行,為這個pb目錄建立一個buf的模組。此後便可以使用buf的各種命令來管理這個buf模組了

shell $ buf mod init

此時會在根目錄多出一個buf.yaml檔案,內容為

```yaml

buf.yaml

version: v1 breaking: use: - FILE lint: use: - DEFAULT ```

Lint pb檔案

shell $ buf lint ecommerce/v1/product.proto:10:9:Service name "ServiceOrderManagement" should be suffixed with "Service". ecommerce/v1/product.proto:11:18:RPC request type "getOrderReq" should be named "GetOrderRequest" or "ServiceOrderManagementGetOrderRequest".

調整lint規則

shell # buf.yaml version: v1 breaking: use: - FILE lint: use: - DEFAULT + except: + - PACKAGE_VERSION_SUFFIX + - FIELD_LOWER_SNAKE_CASE + - SERVICE_SUFFIX

生成程式碼

外掛:和使用protoc一樣,該裝的外掛一樣要裝

外掛模版

建立一個buf.gen.yaml ,它是buf生成程式碼的配置。上面的protoc同等功能的buf.gen.yaml可以寫成如下形式,相對protoc更加直觀

```yaml

buf.gen.yaml

version: v1 plugins: - plugin: go out: ecommerce opt: - paths=source_relative - plugin: go-grpc out: ecommerce opt: - paths=source_relative - name: grpc-gateway out: ecommerce opt: - paths=source_relative - generate_unbound_methods=true - name: openapiv2 out: doc opt: - logtostderr=true ```

生成程式碼

shell buf generate pb

buf generate 命令將會

  • 搜尋每一個buf.yaml配置裡的所有protobuf檔案
  • 複製所有protobuf檔案到記憶體
  • 編譯所有protobuf檔案
  • 執行模版檔案裡的每一個外掛

新增依賴

在使用grpc-gateway時依賴了google.api.http,在不使用buf的場景,我們需要手動複製.proto到本地。

buf為我們提供了 Buf Schema Registry (BSR),除了可以使用其他人釋出的模組,也可以把我們自己的模組釋出到BSR

在模組的檔案裡宣告依賴項

yaml # buf.yaml version: v1 breaking: use: - FILE lint: use: - DEFAULT +deps: + - buf.build/googleapis/googleapis

然後執行

shell buf mod update

buf mod update 把你所有的 deps 更新到最新版。並且會生成 buf.lock 來固定版本

```yaml

Generated by buf. DO NOT EDIT.

version: v1 deps: - remote: buf.build owner: googleapis repository: googleapis commit: 75b4300737fb4efca0831636be94e517 ```

此時執行buf generate pb 即使本地沒有依賴,也不會再報錯缺少依賴了

參考

掃碼_搜尋聯合傳播樣式-白色版.png