簡介
Protocol Buffers是什么?
protocol buffers 是一種靈活,高效,自動化機制的結構數據序列化方法-可類比 XML,但是比 XML 更小、更快、更為簡單。你可以定義數據的結構,然后使用特殊生成的源代碼輕松的在各種數據流中使用各種語言進行編寫和讀取結構數據。你甚至可以更新數據結構,而不破壞根據舊數據結構編譯而成並且已部署的程序。
1 . 使用protobuf實現節點間通信, 編碼報文以提高傳輸效率;
2 . protobuf全程Protocol Buffers, 是Google開發的一種數據描述語言;
3 . Protobuf是一種輕便高效的結構化數據存儲格式;
4 . Protobuf跟存儲格式,語言, 平台無關;
5 . protobuf可擴展可序列化;
6 . protobuf以二進制方式存儲, 占用內存空間小;
protobuf廣泛地應用於遠程過程調用(PRC)的二進制傳輸,使用protobuf的目的是為了獲得更高的性能。傳輸前使用protobuf編碼,接收方再進行解碼,可顯著地降低二進制傳輸數據的大小。另外,protobuf非常適合傳輸結構化數據,便於通信字段的擴展。
用途
1 . 可以輕松引入新字段, 中間服務器不需要檢查數據, 可以簡單解析他並傳遞數據而無需了解所有字段;
2 . 格式更具有自我描述性, 可以用各種語言處理(C++,Java等)
隨着系統發展, 他獲得了其他功能和用途:
3 . 自動生成的序列化和反序列化代碼避免了手動解析的需要;
**4 . 除了用於短期RPC(遠程過程調用)請求之外, 人們還開始使用 protocol buffers作為一種方便的自描述格式, **用於持久存儲數據(例如在 Bigtable中);
5 . 服務器RPC接口開始被聲明為協議文件的一部分, protocol編譯器生成存根類, 用戶可以使用服務器接口的實際實現來覆蓋這些類;
它是如何工作的?
你可以通過在 .proto 文件中定義 protocol buffer message 類型,來指定你想如何對序列化信息進行結構化。每一個 protocol buffer message 是一個信息的小邏輯記錄,包含了一系列的 name-value 對。這里有一個非常基礎的 .proto 文件樣例,它定義了一個包含 "person" 相關信息的 message:
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;
}
正如你所見,message 格式很簡單 - 每種 message 類型都有一個或多個具有唯一編號的字段,每個字段都有一個名稱和一個值類型,其中值類型可以是數字(整數或浮點數),布爾值,字符串,原始字節,甚至(如上例所示)其它 protocol buffer message 類型,這意味着允許你分層次地構建數據。你可以指定 optional 字段,required 字段和 repeated 字段。 你可以在 Protocol Buffer 語言指南 中找到有關編寫 .proto 文件的更多信息。
proto3 已舍棄 required 字段,optional 字段也無法顯示使用(因為缺省默認就設置為 optional)
一旦定義了 messages,就可以在 .proto 文件上運行 protocol buffer 編譯器來生成指定語言的數據訪問類。這些類為每個字段提供了簡單的訪問器(如 name()和 set_name()),以及將整個結構序列化為原始字節和解析原始字節的方法 - 例如,如果你選擇的語言是 C++,則運行編譯器上面的例子將生成一個名為 Person 的類。然后,你可以在應用程序中使用此類來填充,序列化和檢索 Person 的 messages。於是你可以寫一些這樣的代碼:
Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("jdoe@example.com");
fstream output("myfile", ios::out | ios::binary);
person.SerializeToOstream(&output);
之后,我們可以重新讀取解析 message
**
fstream input("myfile", ios::in | ios::binary);
Person person;
person.ParseFromIstream(&input);
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;
你可以在 message 格式中添加新字段,而不會破壞向后兼容性;舊的二進制文件在解析時只是忽略新字段。因此,如果你的通信協議使用 protocol buffers 作為其數據格式,則可以擴展協議而無需擔心破壞現有代碼。
為什么不適用XML?
對於序列化結構數據, protocol buffers 比XML更具優勢, Protocol buffers:
1 . 更簡單
2 . 小3 - 10倍
3 . 快20 - 100 倍
4 . 更加清晰明確
5 . 自動生成更易於以編程方式使用的數據訪問類;
**
例如:
假設你想要為具有姓名和電子郵件的人建模, 在xml中, 我們需要:
<person>
<name>John Doe</name>
<email>jdoe@example.com</email>
</person>
**
而相對應的Protocol Buffer Message 格式是:
# Textual representation of a protocol buffer.
# This is *not* the binary format used on the wire.
person {
name: "John Doe"
email: "jdoe@example.com"
}
當此消息編碼為protocol buffer 二進制格式 時(上面的文本格式只是為了調試和編輯的方便而用人類可讀的形式表示),它可能是 28 個字節長,需要大約 100-200 納秒來解析。如果刪除空格,XML版本至少為 69 個字節,並且需要大約 5,000-10,000 納秒才能解析。
此外,比起 XML,操作 protocol buffer 更為容易:
cout << "Name: " << person.name() << endl;
cout << "E-mail: " << person.email() << endl;
** 而使用XML, 必須執行如下操作:**
**
cout << "Name: "
<< person.getElementsByTagName("name")->item(0)->innerText()
<< endl;
cout << "E-mail: "
<< person.getElementsByTagName("email")->item(0)->innerText()
<< endl;
但是,protocol buffers 並不總是比 XML 更好的解決方案 - 例如,protocol buffers 不是使用標記(例如 HTML)對基於文本的文檔建模的好方法,因為你無法輕松地將結構與文本交錯。此外,XML 是人類可讀的和人類可編輯的;protocol buffers,至少它們的原生格式,並不具有這樣的特點。XML 在某種程度上也是自我描述的。只有擁有 message 定義(.proto文件)時,protocol buffer 才有意義;
准備使用的包
Protoc
protoc是protobuf文件(.proto)的編譯器,使用protoc工具可以將.proto文件轉換為各種編程語言對應的源碼,包含數據類型定義和調用接口等;
從 https://github.com/protocolbuffers/protobuf/releases 中下載最新的protobuf安裝包 protoc-3.15.6-win64.zip
解壓壓縮包后將bin目錄下的protoc.exe文件移動到$GOPATH/bin目錄下,注意$GOPATH/bin需要提前添加到環境變量Path目錄下;
$ protoc -help
$ protoc --version
libprotoc 3.15.6
Protobuf
protobuf對於Golang有兩個可選用的包分別是官方的goprotobuf和gogoprotobuf,gogoprotobuf是完全兼容Google Protobuf的,只是生成的代碼質量要比goprotbuf要高。
安裝
go get github.com/golang/protobuf/proto
go get github.com/gogo/protobuf/proto
Protoc-gen-go
protoc-gen-go 是 protobuf 編譯插件系列中的Go版本,protoc-gen-to 使用Golang編寫。
在Golang中使用protobuf需提前安裝 protoc-gen-to工具,用於將.proto文件轉換為Golang代碼。
go get -u github.com/golang/protobuf/protoc-gen-go
protoc-gen-go將自動安裝到$GOPATH/bin目錄下
protobuf會在.proto文件中定義需要處理的結構化數據,通過protoc工具可將.proto文件轉換為C、C++、Golang、Java、Python等多種語言的代碼,因此兼容性好且易於使用;
protoc --go_out=. *.proto
命令之后理論上會將當前目錄下的所有的.proto
文件生成.pb.go
文件,但實際測試發現報錯,不推薦使用;
Protoc-gen-gogo
gogoprotobuf有兩個插件可用分別是protoc-gen-gogo和protoc-gen-gofast,protoc-gen-gogo生成的文件和protoc-gen-go一樣性能略快,protoc-gen-gofast生成的Golang文件更為復雜,但性能卻高出5~7倍;
安裝
go get github.com/gogo/protobuf/protoc-gen-gogo
protoc *.proto --gogo_out=.
protoc-gen-gofast
安裝
go get github.com/gogo/protobuf/protoc-gen-gofast
protoc *.proto --gofast_out=.
# 執行后會將當前目錄下的所有.proto文件生成.pd.go文件
語法
Protobuf協議規定:使用Protobuf協議進行數據序列化和反序列化操作時,首先需要定義傳輸數據的格式,並命名以.proto為擴展名的消息定義文件;
使用message定義一個消息;
指定消息字段類型
分配標識符,在消息字段中每個字段都有唯一的一個標識符,最小標識號可以從1開始,最大到536870911。不可以使用[19000 ~ 19999]之間的標號;
指定字段規則,字段修飾符包括required、optional、repeated三種類型,注意required弊大於利;
使用
1 . 按照protobuf語法, 在.proto文件中定義數據結構, 同時使用protoc工具生成Golang代碼;
2 . 在項目代碼中引用生成的Golang代碼;
定義消息類型
syntax = "proto3";
package proto;
message User{
string name = 1;
bool male = 2;
repeated int32 balance = 3;
}
protoc *.proto --gogo_out=.
標量類型
Protobuf類型 | Golang類型 | 描述 |
---|---|---|
int32 | int32 | 變長編碼,對於負值效率較低。若域可能存在負值可使用sint64替代。 |
int64 | int64 | - |
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個字節。 |
float | float32 | - |
double | float64 | - |
bool | bool | 默認false |
bytes | []byte | 任意字節序列,長度不超過2^32,默認空數組。 |
string | string | UTF8編碼或7-bit ASCII編碼的文本,長度不超過2^32。 |
標量類型如果沒有被賦值則不會被序列化,解析時會賦予默認值
標量類型 | 默認值 |
---|---|
strings | 空字符串 |
bytes | 空序列 |
bools | false |
數值類型 | 0 |
**
文件
1 . 文件名使用小寫下划線的命名風格,例如lower_snake_case.proto;
2 . 每行不超過80個字符;
3 . 使用2個空格縮進;
包
包名應該和目錄結構對應,例如文件在my/package/目錄下,則包名為my.package;
消息
1 . 消息名使用首字母大寫駝峰風格(CamelCase),例如message PlayerRequest{...};
2 . 字段名使用小寫下划線風格,例如string user_id = 1;
3 . 枚舉類型中枚舉名使用首字母大寫駝峰風格,例如enum FooBar,枚舉值使用全大寫下划線分割的風格(CAPITALS_WITH_UNDERSCORES),例如FOO_DEFAULT = 1;
服務
RPC服務名和方法名均使用首字母大寫駝峰風格, 例如 service FooService{rpc GetSomething()};
案例
創建 .proto 文件
cat test1.proto
syntax = "proto3";
package pb;
message Player {
string user_id = 1;
string name = 2;
string icon = 3;
int32 point = 4;
int32 seat = 5;
int32 identity = 6;
int32 status = 7;
}
# 生成.pd.go文件
protoc test1.proto --gogo_out=.
創建測試代碼
package main
import (
"fmt"
"github.com/gogo/protobuf/proto"
"proto_demo1/pb"
)
func main() {
player := &pb.Player{
UserId: "1",
Name: "admin",
}
//序列化
buf, err := proto.Marshal(player)
if err != nil {
panic(err)
}
fmt.Printf("%v\n", buf) // [10 1 49 18 5 97 100 109 105 110]
fmt.Printf("%s\n", buf) // 1admin
//反序列化
obj := &pb.Player{}
err = proto.Unmarshal(buf, obj)
if err != nil {
panic(err)
}
fmt.Printf("%v\n", obj) // user_id:"1" name:"admin"
fmt.Printf("%v\n", obj.GetName()) // admin
}
/*
result
proto_demo1 % go run main.go
[10 1 49 18 5 97 100 109 105 110]
1admin
user_id:"1" name:"admin"
admin
*/