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地址下載編譯器解壓縮後執行下面的命令即可
./configuremake && 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 mainimport ( "io/ioutil" "log" proto "github.com/golang/protobuf/proto")func main() { fname := "address.out" p := Person{ Id: 1234, Name: "Mike", Email: "mike@example.com", 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除了高效的序列化操作外,自動產生不同語言的代碼的確為寫程式提供了非常方便且高效的開發體驗。