簡(jiǎn)介
Protocol Buffers 是 google 出品的一種數(shù)據(jù)交換格式, 縮寫為 protobuf.
主要介紹 proto3 版本和 Golang 下的使用.
安裝
protobuf 分為編譯器和運(yùn)行時(shí)兩部分. 編譯器直接使用預(yù)編譯的二進(jìn)制文件即可,
可以從 releases 上下載.
protobuf 運(yùn)行時(shí)就是不同語言對(duì)應(yīng)的庫, 以 Golang 為例:
go get github.com/golang/protobuf/protoc-gen-go
語言定義
protobuf 現(xiàn)在有兩個(gè)版本, proto2 和 proto3. 本著學(xué)新不學(xué)舊的原則, 這里只介紹 proto3.
既然是一種數(shù)據(jù)交換格式, 必然是要學(xué)習(xí)它的語法的, 就像學(xué)習(xí) JSON 一樣, 你總得知道如何定義.
默認(rèn)的文件是 .proto
.
下面是一個(gè)簡(jiǎn)單的例子, 來自于官方文檔.
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
首行定義了語法版本, 即使用 proto3 版本. 然后定義了一個(gè)名為 SearchRequest 的 message, 以及它包含的字段(鍵值對(duì)).
message 的結(jié)構(gòu)非常類似于各種語言中的 struct, dict 或者 map.
每個(gè)字段包括三個(gè)部分, 類型, 字段名和字段編號(hào). 前兩個(gè)部分非常易懂, 主要解釋一下字段編號(hào).
在同一個(gè) message 中字段編號(hào)應(yīng)該是唯一的, 用于在 message 的二進(jìn)制格式(message binary format)中標(biāo)識(shí)字段.
因此數(shù)字的大小決定了編碼的長度, 1-15 的數(shù)字只占用一個(gè)字節(jié).
注釋語法是 //
和 /* ... */
.
特殊指令
使用 reserved
注明已被廢棄的字段編號(hào)和字段名稱.
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
使用 repeated
可以指定重復(fù)字段, 即數(shù)組.
message Test4 {
repeated int32 d = 4 [packed=true];
}
使用 enum
定義枚舉類型. 每個(gè)枚舉定義都必須包含一個(gè)映射值為 0 的常量作為第一個(gè)元素.
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;
}
定義枚舉時(shí), 可以使用 allow_alias
選項(xiàng)允許枚舉值的別名.
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1; // RUNNING 是 STARTED 的別名
}
類型也可以嵌套使用.
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
使用 import
可以導(dǎo)入外部定義.
import "myproject/other_protos.proto";
使用 import public
可以傳遞導(dǎo)入依賴, 通常用于被導(dǎo)入的 proto 文件需要更改的情況下.
import public "new.proto";
protocol 編譯器搜索的位置是命令行參數(shù)中的 -I/--proto_path
flag. 如果沒有提供, 則搜索編譯器被調(diào)用時(shí)所在的目錄.
Any
類型可以讓你可以在沒有它們的 proto 定義時(shí), 將 messages 用作內(nèi)嵌的類型. 一個(gè) Any
包括任意的二進(jìn)制序列化的
message, 就像 bytes 類型那樣, 以及用作該類型的全局唯一的標(biāo)識(shí)符 URL.
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
默認(rèn)的類型 URL 是 type.googleapis.com/packagename.messagename
.
如果你有一個(gè) message 包括許多字段, 但同時(shí)最多只有一個(gè)字段會(huì)被設(shè)置, 可以使用 Oneof
特性來節(jié)省內(nèi)存.
Oneof 字段和普通的字段沒有區(qū)別, 除了所有這些 Oneof 字段共享內(nèi)存, 且同時(shí)只能由一個(gè)字段被設(shè)置.
你可以使用特殊的 case()
或 WhichOneof()
方法檢查哪個(gè)字段被設(shè)置了, 具體方法名稱取決于實(shí)現(xiàn)的語言.
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
可以在 Oneof 中添加任何類型的字段, 但不能使用 repeated
.
使用 map
可以創(chuàng)建關(guān)聯(lián)映射.
map<key_type, value_type> map_field = N;
map<string, Project> projects = 3;
key_type 可以是 integral 或 string 類型, 即 scalar 類型中除了 floating point types 和 bytes 以外的類型.
value_type 可以是除 map 以外的任何類型.
使用 package
可以設(shè)置命名空間, 防止 message 類型沖突.
package foo.bar;
message Open { ... }
在另一個(gè) proto 文件中使用.
message Foo {
...
foo.bar.Open open = 1;
...
}
package 說明符會(huì)影響不同語言生成代碼的方式:
- Python: 忽略 package 指令
- Golang: package 用作 Go 包名, 除了你使用
option go_package
顯式聲明包名
定義服務(wù)
如果你想要和 RPC 系統(tǒng)集成使用, 你可以定義 RPC 服務(wù)接口, 編譯器會(huì)根據(jù)選擇的語言, 生成對(duì)應(yīng)的服務(wù)接口代碼和存根.
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
最常見的是和 gRPC 一起使用.
JSON 支持
proto3 支持 JSON 中的規(guī)范編碼, 具體的類型轉(zhuǎn)換關(guān)系, 查看官方文檔.
選項(xiàng)
有很多選項(xiàng)可以調(diào)節(jié)特性的行為, 完整的選項(xiàng)列表定義在 google/protobuf/descriptor.proto
.
生成代碼
要生成代碼, 需要使用下面的命令, 對(duì)于 Golang 需要安裝額外的插件.
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
基礎(chǔ)類型
標(biāo)量值類型和各種語言間的轉(zhuǎn)換可以參考 官方文檔.
包括以下類型:
double
float
int32
int64
unit32
unit64
sint32 編碼負(fù)數(shù)比 int32 更高效
sint64
fixed32 固定四字節(jié). 如果數(shù)字通常大于 2^28, 比 uint32 更高效
fixed64
sfixed32 負(fù)數(shù)優(yōu)化版本的 fixed32
sfixed64
bool
string
bytes
更新 message
官方指南翻譯
- 不要更改任何現(xiàn)有字段的字段編號(hào).
- 如果添加新的字段, 仍然可以使用新生成的代碼解析舊的 message 格式. 但應(yīng)該自行處理新加字段的默認(rèn)值.
使用新代碼創(chuàng)建的 message 也可以被舊代碼解析, 那些新字段會(huì)被忽略. - 字段可以被刪除, 只要不要復(fù)用那些字段編號(hào). 要重命名字段, 可以給舊字段添加
OBSOLETE_
前綴,
或者將字段編號(hào)設(shè)置為reserved
. - int32, uint32, int64, uint64, bool 是相互兼容的類型.
- sint32 和 sint64 是相互兼容的, 但不兼容其他 int 類型.
- string 和 bytes 可以是兼容的, 只要 bytes 是有效的 UTF-8.
- 嵌入的 message 可以和 bytes 兼容, 如果 bytes 包含 message 的編碼后的版本.
- fixed32 和 sfixed32 兼容, fixed64 和 sfixed64 兼容.
- enum 在 wire 格式上兼容 int32, uint32, int64, uint64(如果值不合適會(huì)被截?cái)?. 當(dāng) message 被反序列化時(shí),
客戶端代碼可以用不同的方式對(duì)待它們. 比如, 未識(shí)別的 proto3 的 enum 類型將會(huì)保存在 message 中, 但它如何
表示是語言特定的. int 字段總是會(huì)保留它的值. - 將一個(gè)單個(gè)值更改為新
oneof
的成員是安全的且二進(jìn)制兼容的. 移動(dòng)多個(gè)字段到一個(gè)新的oneof
可能是安全的,
如果你確保沒有 code 被多次設(shè)置. 移動(dòng)任何字段到一個(gè)已存在的oneof
是不安全的.
Golang 下使用
定義 protobuf 文件 hello.proto
的內(nèi)容為:
syntax = "proto3";
import "google/protobuf/any.proto";
package hello;
option go_package = "hello";
message HelloReq {
string name = 1;
}
message HelloResp {
int32 code = 1;
string greet = 2;
google.protobuf.Any details = 3;
}
service HelloService {
rpc Greet(HelloReq) returns (HelloResp);
}
初始化項(xiàng)目, 并生成文件:
go mod init tzh.com/app
go get github.com/golang/protobuf/protoc-gen-go
mkdir hello
# 假設(shè) protoc3 已經(jīng)解壓好了
.\protoc3\bin\protoc.exe --proto_path=. --go_out=./hello hello.proto
main.go 如下:
package main
import (
"crypto/rand"
"fmt"
"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes/any"
hello "tzh.com/app/hello"
)
func main() {
req := &hello.HelloReq{
Name: "hello",
}
details := make([]byte, 10)
rand.Read(details)
resp := &hello.HelloResp{
Code: 1,
Greet: "hello name",
Details: &any.Any{Value: details},
}
fmt.Println(req.String())
fmt.Println(resp.String())
// 序列化
data, _ := proto.Marshal(req)
fmt.Println(data)
// 反序列化
newReq := &hello.HelloReq{}
proto.Unmarshal(data, newReq)
fmt.Println(newReq)
fmt.Println("text format", proto.MarshalTextString(req))
}
如果你去看生成的代碼, 會(huì)發(fā)現(xiàn)沒有 HelloService
相關(guān)的內(nèi)容, 這是因?yàn)闆]有使用 gRPC.