本篇為【寫給go開發者的gRPC教程】系列第一篇
第一篇:protobuf基礎 👈
第二篇:通信模式
第三篇:攔截器
第四篇:錯誤處理
本系列將持續更新,歡迎關注👏獲取實時通知
gRPC是谷歌開源的一款高性能、支持多種開發語言的服務框架,對於一個rpc我們關注如下幾方面:
序列化協議。gRPC使用protobuf,首先使用protobuf定義服務,然後使用這個文件來生成客户端和服務端的代碼。因為pb是跨語言的,因此即使服務端和客户端語言並不一致也是可以互相序列化和反序列化的
網絡傳輸層。gRPC使用http2.0協議,http2.0相比於HTTP 1.x ,大幅度的提升了 web 性能。
Protobuf IDL
所謂序列化通俗來説就是把內存的一段數據轉化成二進制並存儲或者通過網絡傳輸,而讀取磁盤或另一端收到後可以在內存中重建這段數據
1、protobuf協議是跨語言跨平台的序列化協議。
2、protobuf本身並不是和gRPC綁定的。它也可以被用於非RPC場景,如存儲等
json、 xml都是一種序列化的方式,只是他們不需要提前預定義idl,且具備可讀性,當然他們傳輸的體積也因此較大,可以説是各有優劣
所以先來介紹下protobuf的idl怎麼寫。protobuf最新版本為proto3,在這裏你可以看到詳細的文檔説明:https://protobuf.dev/programm...
定義消息類型
protobuf裏最基本的類型就是message,每一個messgae都會有一個或者多個字段(field),其中字段包含如下元素
- 類型:類型不僅可以是標量類型(
int、string等),也可以是複合類型(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 字節。 |
複合類型
數組
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
枚舉
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
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
使用其他消息類型
使用import引用另外一個文件的pb
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-i...
-
Linux, using
aptorapt-get, for example:$ apt install -y protobuf-compiler $ protoc --version # Ensure compiler version is 3+ -
MacOS, using Homebrew:
$ brew install protobuf $ protoc --version # Ensure compiler version is 3+
📖 使用
$ 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
@<filename> 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的插件機制來實現,所以倉庫單獨維護
- Dart
- Go
3.proto文件位置參數
proto文件位置參數即上述的@<filename>參數,指定了我們proto文件的具體位置,如proto1/greeter/greeter.proto。
📖 語言插件
✨ golang插件
非內置的語言支持就得自己單獨安裝語言插件,比如--go_out=對應的是protoc-gen-go,安裝命令如下:
# 最新版
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
# 指定版本
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.3.0
可以使用下面的命令來生成代碼
$ protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto
注意
protoc-gen-go要求pb文件必須指定go包的路徑,即
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
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
執行code gen命令
$ 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的產出物,包含- 定義在
RouteGuideservice中的用來給client調用的接口定義 - 定義在
RouteGuideservice中的用來給服務端實現的接口定義
- 定義在
--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/protobuf是github.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
$ 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
$ protoc --go_out=plugins=grpc,paths=import:. \
routeguide/route_guide.proto
--go_out的寫法是,參數之間用逗號隔開,最後加上冒號來指定代碼的生成位置,比如--go_out=plugins=grpc,paths=import:.
--go_out主要的兩個參數為plugins 和 paths,分別表示生成Go代碼所使用的插件,以及生成的Go代碼的位置。
plugins參數有不帶grpc和帶grpc兩種(應該還有其它的,目前知道的有這兩種),兩者的區別如下,帶grpc的會多一些跟gRPC相關的代碼,實現gRPC通信
paths參數有兩個選項,分別是 import 和 source_relative,默認為 import
import表示按照生成的Go代碼的包的全路徑去創建目錄層級source_relative表示按照 proto源文件的目錄層級去創建Go代碼的目錄層級,如果目錄已存在則不用創建。
總之,用google.golang.org/protobuf就對了!
Buf 工具
可以看到使用protoc的時候,當使用的插件逐漸變多,插件參數逐漸變多時,命令行執行並不是很方便和直觀。例如後面使用到了grpc-gateway+swagger插件時
$ 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模塊了
$ buf mod init
此時會在根目錄多出一個buf.yaml文件,內容為
# buf.yaml
version: v1
breaking:
use:
- FILE
lint:
use:
- DEFAULT
Lint pb文件
$ 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規則
# 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更加直觀
# 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
生成代碼
buf generate pb
buf generate 命令將會
- 搜索每一個
buf.yaml配置裏的所有protobuf文件 - 複製所有
protobuf文件到內存 - 編譯所有
protobuf文件 - 執行模版文件裏的每一個插件
添加依賴
在使用grpc-gateway時依賴了google.api.http,在不使用buf的場景,我們需要手動複製.proto到本地。
buf為我們提供了 Buf Schema Registry (BSR),除了可以使用其他人發佈的模塊,也可以把我們自己的模塊發佈到BSR
在模塊的文件裏聲明依賴項
# buf.yaml
version: v1
breaking:
use:
- FILE
lint:
use:
- DEFAULT
+deps:
+ - buf.build/googleapis/googleapis
然後執行
buf mod update
buf mod update 把你所有的 deps 更新到最新版。並且會生成 buf.lock 來固定版本
# Generated by buf. DO NOT EDIT.
version: v1
deps:
- remote: buf.build
owner: googleapis
repository: googleapis
commit: 75b4300737fb4efca0831636be94e517
此時執行buf generate pb 即使本地沒有依賴,也不會再報錯缺少依賴了
參考
- Buf 官方文檔
- Protocol Buffers Documentation