Protobuf是google開發的一種跨語言和平臺的序列化數據結構的方式,類似於XML但是更小更快而且更簡單,只需要定義一次結構體,通過生成的源代碼可以在不同的數據流和不同的語言平臺上去讀寫數據結構。
最新的protobuf3支持更多的語言使用,比如go 、 object-c等等。另外proto2與proto3並非完全兼容,官方仍舊提供proto2的支持。Google內部有超過40000多個數據結構是通過protobuf來進行的定義,這些數據結構的序列化操作不僅僅用在RPC接口,同時也用於持久化存儲數據。
protobuf3語法定義
Protobuf的定義存放在.proto文件中,記錄數據結構包含的字段和屬性,如下面所示定義了一個Person結構體。如果不包含首行版本聲明的話,編譯器將認爲是proto2版本
syntax = "proto3";
/* Person用於定義聯繫人相關信息
* 包含人員的基本信息和聯繫信息 */
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
數字標籤
每一個字段都有一個對應的數字標籤,用於在消息的二進制格式中識別每一個屬性。
該標籤是在此定義中獨一無二的,範圍爲1-536,870,911,另外19000-19999爲protobuf協議實施需要的,不要佔用。1-15佔用小於等於1個字節的存儲,因此爲優化使用,我們可以把經常使用或者重複型元素設置在該範圍。
每個字段都有缺省值,數值類型的爲0,字符串類型的爲空字符串,布爾類型爲false,枚舉類型爲第一個枚舉值,bytes類型爲空bytes。
重複域可以包含0到多個重複內容,可以看成爲動態數組。
保留域
如果刪除了某一個字段,protobuf允許重新使用該數值作爲新的屬性的標籤,但是爲了保證向後兼容,讀取舊的數據的時候不會出現問題,一般使用reserved來聲明該數值爲保留,不能被使用。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
客戶端生成文件
在不同的編程語言環境下,我們使用客戶端編譯器去編譯我們的protobuf文件到指定的編程語言對象,生成的是不同的文件。
比如C++對於每一個.proto文件生成的是一個.h文件和一個.cc文件,爲每一個消息類型生成一個類。對於go語言則生成的是.pb.go文件,裏面包含有消息的定義結構體以及常用的函數。python的話,編譯器爲每一個消息類型生成一個具有靜態描述符的模塊。使用metaclass去創建必要的python運行時數據訪問類。
枚舉類型
上面的代碼中包含了一個嵌入式的枚舉類型:
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
枚舉類型中第一個值必須是0值,因爲必須存在一個0值作爲枚舉類型的缺省值,並且這樣也兼容proto2版本。同時可以聲明allow_alias來設置具有相同值的枚舉屬性。
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
同時枚舉類型的刪除也需要加入到reserved保留字段中,就像上面定義的foo,bar一樣。
使用其他消息類型
對於相同文件中的屬性定義,我們可以直接使用,比如SearchResponse中包含了多個Result結構。
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
對於跨文件的定義,我們可以直接使用import的方式來使用
import public "new.proto";
import "other.proto";
嵌套定義
上面的例子也可以通過嵌套定義的方式實現,這裏直接將Result定義爲SearchResponse的內部的消息結構。
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
但是Result定義並非私有的,其他的結構定義的時候也可以使用。只是需要包含父級別的名稱,如下所示:
message OtherMessage {
SearchResponse.Result result = 1
}
Any類型
Any允許未定義具體類型的屬性聲明,如下爲一個簡單的使用實例。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
不同語言客戶端在使用的時候具體的實現不同,且接口一般提供pack或者unpack()實現數據的寫入和寫出。
oneof定義
Oneof定義用來代表在實現的時候,該組屬性中有且只能有一個被定義,不能出現多個。比如下面的定義中:
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
上述定義中只能出現name或者sub_message的出現,不能同時出現,同時Oneof不能出現repeated域。重複傳遞值到Oneof多個域僅僅最後的會生效,其他的將被忽略掉。
Maps類型
Map也可以在protobuf中直接使用,只是這裏面的key類型必須是整形或者string類型。value類型不能是其他的map類型。map類型不能是repeated類型。
map<string, Project> projects = 3;
服務定義
如果想要使用消息類型作爲一個RPC系統,可以定義一個RPC服務接口,編譯器將自動生成服務代碼依賴於選擇的語言。比如下面的定義了一個RPC服務接口用來傳遞搜索結果和返回搜索響應內容
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
最簡單直接的使用RPC系統是gRPC, 該系統也是跨平臺的開源RPC接口系統,可以直接生成相關的RPC代碼(使用編譯插件)
對比XML
相對於XML語言的序列化數據,Protobuf提供更簡單的定義方式,序列速度是XML的20-100倍,所佔的數據大小減少3-10倍,而且更容易集成到編程語言中使用。
安裝編譯器
前往官方github地址下載編譯器解壓縮後執行下面的命令即可
./configure
make && make install
執行下面的命令安裝go proto 插件
go get -u github.com/golang/protobuf/protoc-gen-go
使用go讀寫protobuf實例
這裏我們可以查看官網的protobuf-example,裏面包含了使用不同的語言去讀寫protobuf序列化數據的一些示例。
首先定義基本的數據結構文件address.proto,其中我們使用package main
僅僅作爲go語言中包的定義。Person定義人員信息,AddressBook定義人員聯繫方式集合,包含任意多的人員聯繫方式。
syntax = "proto3";
package main;
message Person {
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
// Our address book file is just one of these.
message AddressBook {
repeated Person people = 1;
}
編寫完成後就可以直接使用protoc命令來編譯.proto文件
protoc -I=. --go_out=. addressbook.proto
該操作將會在當前目錄生成一個addressbook.pb.go文件,如果打開該go文件可以看到,程序自動生成了預先定義的數據的struct結構體,該結構體支持一些基本的操作,比如獲取每個字段的GetName(),GetEmail等,以及用於重置清空數據的Reset函數,Person轉換後的結構體如下:
type Person struct {
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
Id int32 `protobuf:"varint,2,opt,name=id" json:"id,omitempty"`
Email string `protobuf:"bytes,3,opt,name=email" json:"email,omitempty"`
Phones []*Person_PhoneNumber `protobuf:"bytes,4,rep,name=phones" json:"phones,omitempty"`
}
其中不僅僅支持protobuf的序列化,同時支持json數據序列化操作。
我們編寫一個函數用於存儲序列化的數據到本地文件,並直接讀取文件獲取文件中的內容,以確保數據的確保存成功了。下面是針對該包操作的主函數內容。
package main
import (
"io/ioutil"
"log"
proto "github.com/golang/protobuf/proto"
)
func main() {
fname := "address.out"
p := Person{
Id: 1234,
Name: "Mike",
Email: "[email protected]",
Phones: []*Person_PhoneNumber{
{Number: "1234-2332", Type: Person_HOME},
},
}
book := &AddressBook{}
book.People = append(book.People, &p)
out, err := proto.Marshal(book)
if err != nil {
log.Fatalln("Failed to encode address book")
}
if err := ioutil.WriteFile(fname, out, 0644); err != nil {
log.Fatalln("Faile to write to file")
}
in, err := ioutil.ReadFile(fname)
if err != nil {
log.Fatalln("Error reading file")
}
readBook := &AddressBook{}
if err = proto.Unmarshal(in, readBook); err != nil {
log.Fatalln("Fail to parse file")
} else {
if len(readBook.People) > 0 {
log.Printf("First Person's Name in address is %s", readBook.People[0].GetName())
}
}
}
go build 生成二進制文件後,直接執行後就可以看到當前目錄上生成了一個address.out的存儲文件,存儲了之前定義的用戶聯繫信息。
輸出如下:
2018/04/21 18:41:03 First Person's Name in address is Mike
如果未來需要擴展或者修改字段定義,爲了保證向後兼容性,需要確保滿足以下的規則:
1. 不要改變已經存在的任何字段的tag數字編號
2. 可以刪除字段
3. 增加新的字段需要使用未被展示用的tag編號(這個編號甚至不能是之前用過且被刪除後的)
舊的數據中不包含的字段,在讀取時候,按照缺省值進行讀取操作。protobuf除了高效的序列化操作外,自動生成不同語言的代碼的確爲寫程序提供了非常方便且高效的開發體驗。