protobuf 指南

簡(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.

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末举农,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖掀潮,帶你破解...
    沈念sama閱讀 218,386評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異寻歧,居然都是意外死亡耘戚,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門本涕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來业汰,“玉大人,你說我怎么就攤上這事菩颖⊙幔” “怎么了?”我有些...
    開封第一講書人閱讀 164,704評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵晦闰,是天一觀的道長放祟。 經(jīng)常有香客問我,道長呻右,這世上最難降的妖魔是什么跪妥? 我笑而不...
    開封第一講書人閱讀 58,702評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮声滥,結(jié)果婚禮上眉撵,老公的妹妹穿的比我還像新娘。我一直安慰自己落塑,他們只是感情好纽疟,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,716評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著芜赌,像睡著了一般仰挣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上缠沈,一...
    開封第一講書人閱讀 51,573評(píng)論 1 305
  • 那天膘壶,我揣著相機(jī)與錄音,去河邊找鬼洲愤。 笑死颓芭,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的柬赐。 我是一名探鬼主播亡问,決...
    沈念sama閱讀 40,314評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了州藕?” 一聲冷哼從身側(cè)響起束世,我...
    開封第一講書人閱讀 39,230評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎床玻,沒想到半個(gè)月后毁涉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡锈死,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,873評(píng)論 3 336
  • 正文 我和宋清朗相戀三年贫堰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片待牵。...
    茶點(diǎn)故事閱讀 39,991評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡其屏,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缨该,到底是詐尸還是另有隱情偎行,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評(píng)論 5 346
  • 正文 年R本政府宣布压彭,位于F島的核電站睦优,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏壮不。R本人自食惡果不足惜汗盘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,329評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望询一。 院中可真熱鬧隐孽,春花似錦、人聲如沸健蕊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽缩功。三九已至晴及,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間嫡锌,已是汗流浹背虑稼。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留势木,地道東北人蛛倦。 一個(gè)月前我還...
    沈念sama閱讀 48,158評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像啦桌,于是被迫代替她去往敵國和親溯壶。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,941評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容