Go Protobuf(比xml小3-10倍, 快20-100倍)

youmen發表於2021-04-11

簡介

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

*/

相關文章