一、前言
RPC
,全稱Remote Procedure Call
,中文譯為遠程過程調用。通俗地講,使用RPC進行通信,調用遠程函數就像調用本地函數一樣,RPC底層會做好數據的序列化與傳輸,從而能使我們更輕松地創建分布式應用和服務。
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/programming-guides/proto3/
2.1、定義消息類型
protobuf
里最基本的類型就是message
,每一個messgae
都會有一個或者多個字段(field
),其中字段包含如下元素
-
類型:類型不僅可以是標量類型(
int
、string
等),也可以是復合類型(enum
等),也可以是其他message
-
字段名:字段名比較推薦的是使用下劃線/分隔名稱
-
字段編號:一個messgae內每一個字段編號都必須唯一的,在編碼后其實傳遞的是這個編號而不是字段名
-
字段規則:消息字段可以是以下字段之一
singular
:格式正確的消息可以有零個或一個字段(但不能超過一個)。使用 proto3 語法時,如果未為給定字段指定其他字段規則,則這是默認字段規則optional
:與singular
相同,不過您可以檢查該值是否明確設置repeated
:在格式正確的消息中,此字段類型可以重復零次或多次。系統會保留重復值的順序map
:這是一個成對的鍵值對字段
-
保留字段:為了避免再次使用到已移除的字段可以設定保留字段。如果任何未來用戶嘗試使用這些字段標識符,編譯器就會報錯
2.2、標量值類
標量類型會涉及到不同語言和編碼方式:
.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 字節。 |
2.3、復合類型
(1)數組
message SearchResponse {repeated Result results = 1;
}message Result {string url = 1;string title = 2;repeated string snippets = 3;
}
(2)枚舉
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;
}
(3)服務
定義的method僅能有一個
入參和出參數。如果需要傳遞多個參數需要定義成message
service SearchService {rpc Search(SearchRequest) returns (SearchResponse);
}
2.4、使用其他消息類型
使用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文件編譯成不同的語言
3.1、安裝
https://github.com/google/protobuf/releases
-
Windows 下載壓縮包解壓,并添加解壓路徑中的 bin 文件夾路徑到環境變量Path中,新開終端
protoc --version
驗證安裝。 -
Linux, using
apt
orapt-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+
3.2、使用
$ protoc --help
Usage: protoc [OPTION] PROTO_FILES-IPATH, --proto_path=PATH #指定搜索路徑--plugin=EXECUTABLE: # 指定要使用的插件可執行文件。通常,protocol會在PATH中搜索插件....--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
。
3.3、 語言插件
(1) 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=. --go_opt=paths=source_relative foo.proto bar/baz.proto
注意
protoc-gen-go
要求pb文件必須指定go包的路徑,即
option go_package = ".;streaming";
----proto_path
這個選項用于指定 protoc 編譯器在查找 .proto 文件時應該搜索的根目錄。當你在 .proto 文件中使用 import 語句導入其他 .proto 文件時,編譯器需要知道去哪里找到這些被導入的文件
–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
(2) 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
的產出物,包含- 定義在
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/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.yamlversion: v1breaking:use:- FILElint: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: goout: ecommerceopt:- paths=source_relative- plugin: go-grpcout: ecommerceopt:- paths=source_relative- name: grpc-gatewayout: ecommerceopt:- paths=source_relative- generate_unbound_methods=true- name: openapiv2out: docopt:- 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.yamlversion: v1breaking:use:- FILElint: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.buildowner: googleapisrepository: googleapiscommit: 75b4300737fb4efca0831636be94e517
此時執行buf generate pb
即使本地沒有依賴,也不會再報錯缺少依賴了
參考
- Buf 官方文檔
- Protocol Buffers Documentation
- https://segmentfault.com/a/1190000043353574