C-- 使用 protobuf 實現序列化與反序列化

一、protobuf 簡介:


1.1 protobuf 的定義:

protobuf 是用來幹嘛的?

protobuf 是一種用於 對結構數據進行序列化的工具,從而實現 數據存儲和交換。

(主要用於網絡通信中 收發兩端進行消息交互。所謂的 “結構數據” 是指類似於 struct 結構體的數據,可用於表示一個網絡消息。當結構體中存在函數指針類型時,直接對其存儲或傳輸相當於是“淺拷貝”,而對其序列化後則是“深拷貝”。)

序列化:將結構數據或者對象轉換成能夠用於存儲和傳輸的格式。

反序列化:在其他的計算環境中,將序列化後的數據還原爲數據結構和對象。

從 “序列化” 字面上的理解,似乎使用 C 語言中的 struct 結構體就可以實現序列化的功能:將結構數據填充到定義好的結構體中的對應字段即可,接收方再對結構體進行解析。

在單機的不同進程間通信時,使用 struct 結構體這種方法實現 “序列化” 和“反序列化”的功能問題不大,但是,在網絡編程中,即面向網絡中不同主機間的通信時,則不能使用 struct 結構體,原因在於:

(1)跨語言平臺,例如發送方是用 C 語言編寫的程序,接收方是用 Java 語言編寫的程序,不同語言的 struct 結構體定義方式不同,不能直接解析;

(2)struct 結構體存在 內存對齊 和 CPU 不兼容的問題。

因此,在網絡編程中,實現 “序列化” 和“反序列化”功能需要使用通用的組件,如 Json、XML、protobuf 等。

1.2 protobuf 的優缺點:

1.2.1 優點:

① 性能高效:

與 XML 相比,protobuf 更小(3 ~ 10 倍)、更快(20 ~ 100 倍)、更爲簡單。

② 語言無關、平臺無關:

protobuf 支持 Java、C++、Python 等多種語言,支持多個平臺。

③ 擴展性、兼容性強:

只需要使用 protobuf 對結構數據進行一次描述,即可從各種數據流中讀取結構數據,更新數據結構時不會破壞原有的程序。

Protobuf 與 XML、Json 的性能對比:

測試 10 萬次序列化:

測試 10 萬次反序列化:

1.2.2 缺點:

① 自解釋性較差,數據存儲格式爲二進制,需要通過 .proto 文件才能瞭解到內部的數據結構;

② 不適合用來對 基於文本的標記文檔(如 HTML) 建模。

1.3 protobuf 中的數據類型限定修飾符:

protobuf 2 中有三種數據類型限定修飾符:

required, optional, repeated

required 表示字段必選,optional 表示字段可選,repeated 表示一個數組類型。

其中, required 和 optional 已在 proto3 棄用了。

1.4 protobuf 中常用的數據類型:

bool,		布爾類型

double,		64位浮點數
float,		32位浮點數

int32,		32位整數
int64,		64位整數
uint64,		64位無符號整數
sint32,		32位整數,處理負數效率更高
sint64,		64位整數,處理負數效率更高

string,		只能處理ASCII字符
bytes,		用於處理多字節的語言字符
enum,		枚舉類型

二、protobuf 的使用流程:


下載 protobuf 壓縮包後,解壓、配置、編譯、安裝,即可使用 protoc 命令 查看 Linux 中是否安裝成功:

[root@linux] protoc --version
libprotoc 3.15.8

使用 protobuf 時,需要先根據應用需求編寫 .proto 文件 定義消息體格式,例如:

syntax = "proto3";
package tutorial;

option optimize_for = LITE_RUNTIME;

message Person {
	int32 id = 1;
	repeated string name = 2;
}

其中,syntax 關鍵字表示使用的 protobuf 的版本,如不指定則默認使用 "proto2";package 關鍵字 表示 “包”,生成目標語言文件後對應 C++ 中的 namespace 命名空間,用於防止不同的消息類型間的命名衝突。

(syntax 單詞字面含義:句法,句法規則,語構)

然後使用 protobuf 編譯器(protoc 命令)將編寫好的 .proto 文件生成 目標語言文件(例如目標語言是 C++,則會生成 .cc 和 .h 文件),例如:

[root@linux] protoc -I=$SRC_DIR $SRC_DIR/xxx.proto --cpp_out=$DST_DIR

其中:

$SRC_DIR 表示 .proto 文件所在的源目錄;$DST_DIR 表示生成目標語言代碼的目標目錄;xxx.proto 表示要對哪個. proto 文件進行解析;--cpp_out 表示生成 C++ 代碼。

編譯完成後,將會在目標目錄中生成 xxx.pb.h 和 pb.cc, 文件,將其引入到我們的 C++ 工程中即可實現使用 protobuf 進行序列化:

在 C++ 源文件中包含 xxx.pb.h 頭文件,在 g++ 編譯時鏈接 xxx.pb.cc 源文件即可:

g++ main_test.cpp pb.cc, -o main_test -lprotobuf

三、C++ 使用 protobuf 實現序列化的示例:


在 protobuf 源碼中的 /examples 目錄下有官方提供的 protobuf 使用示例:addressbook.proto

參考官方示例實現 C++ 使用 protobuf 進行序列化和反序列化:

addressbook.proto :

syntax = "proto3";
package tutorial;

option optimize_for = LITE_RUNTIME;

message Person {
	string name = 1;
	int32 id = 2;
	string email = 3;

	enum PhoneType {
		MOBILE = 0;
		HOME = 1;
		WORK = 2;
	}
	
	message PhoneNumber {
		string number = 1;
		PhoneType type = 2;
	}

	repeated PhoneNumber phones = 4;
}

生成的 addressbook.pb.h 文件內容摘要:

namespace tutorial {
	class Person;
	class Person_PhoneNumber;
};

class Person_PhoneNumber : public MessageLite {
public:
	Person_PhoneNumber();
	virtual ~Person_PhoneNumber();
public:
	//string number = 1;
	void clear_number();
	const string& number() const;
	void set_number(const string& value);
	
	//int32 id = 2;
	void clear_id();
	int32 id() const;
	void set_id(int32 value);

	//string email = 3; 
	//...
};

add_person.cpp :

#include <iostream>
#include <fstream>
#include <string>
#include "pbs/addressbook.pb.h"
using namespace std;

void serialize_process() {
	cout << "serialize_process" << endl;
	tutorial::Person person;
	person.set_name("Obama");
	person.set_id(1234);
	person.set_email("1234@qq.com");

	tutorial::Person::PhoneNumber *phone1 = person.add_phones();
	phone1->set_number("110");
	phone1->set_type(tutorial::Person::MOBILE);

	tutorial::Person::PhoneNumber *phone2 = person.add_phones();
	phone2->set_number("119");
	phone2->set_type(tutorial::Person::HOME);

	fstream output("person_file", ios::out | ios::trunc | ios::binary);

	if( !person.SerializeToOstream(&output) ) {
		cout << "Fail to SerializeToOstream." << endl;
	}

	cout << "person.ByteSizeLong() : " << person.ByteSizLong() << endl;
}

void parse_process() {
	cout << "parse_process" << endl;
	tutorial::Person result;
	fstream input("person_file", ios::in | ios::binary);

	if(!result.ParseFromIstream(&input)) {
		cout << "Fail to ParseFromIstream." << endl;
	}

	cout << result.name() << endl;
	cout << result.id() << endl;
	cout << Buy and Sell Domain Names() << endl;
	for(int i = 0; i < result.phones_size(); ++i) {
		const tutorial::Person::PhoneNumber &person_phone = result.phones(i);

		switch(person_phone.type()) {
			case tutorial::Person::MOBILE :
				cout << "MOBILE phone : ";
				break;
			case tutorial::Person::HOME :
				cout << "HOME phone : ";
				break;
			case tutorial::Person::WORK :
				cout << "WORK phone : ";
				break;
			default:
				cout << "phone type err." << endl;
		}
		cout << person_phone.number() << endl;
	}
}

int main(int argc, char *argv[]) {
	serialize_process();
	parse_process();
	
	google::protobuf::ShutdownProtobufLibrary();	//刪除所有已分配的內存(Protobuf使用的堆內存)
	return 0;
}

輸出結果:

[serialize_process]
person.ByteSizeLong() : 39
[parse_process]
Obama
1234
1234@qq.com
MOBILE phone : 110
HOME phone : 119

3.1 protobuf 提供的序列化和反序列化的 API 接口函數:

class MessageLite {
public:
	//序列化:
	bool SerializeToOstream(ostream* output) const;
	bool SerializeToArray(void *data, int size) const;
	bool SerializeToString(string* output) const;
	
	//反序列化:
	bool ParseFromIstream(istream* input);
	bool ParseFromArray(const void* data, int size);
	bool ParseFromString(const string& data);
};

三種序列化的方法沒有本質上的區別,只是序列化後輸出的格式不同,可以供不同的應用場景使用。

序列化的 API 函數均爲 const 成員函數,因爲序列化不會改變類對象的內容, 而是將序列化的結果保存到函數入參指定的地址中。

3.2 .proto 文件中的 option 選項:

.proto 文件中的 option 選項用於配置 protobuf 編譯後生成目標語言文件中的代碼量,可設置爲 SPEED, CODE_SIZE, LITE_RUNTIME 三種。

默認 option 選項爲 SPEED,常用的選項爲 LITE_RUNTIME。

三者的區別在於:

① SPEED(默認值):表示生成的代碼運行效率高,但是由此生成的代碼編譯後會佔用更多的空間。② CODE_SIZE:與 SPEED 恰恰相反,代碼運行效率較低,但是由此生成的代碼編譯後會佔用更少的空間, 通常用於資源有限的平臺,如 Mobile。③ LITE_RUNTIME:生成的代碼執行效率高,同時生成代碼編譯後的所佔用的空間也非常少。這是以犧牲 Protobuf 提供的反射功能爲代價的。因此我們在 C++ 中鏈接 Protobuf 庫時僅需鏈接 libprotobuf-lite,而非 protobuf。

SPEED 和 LITE_RUNTIME 相比,在於調試級別上,例如 msg.SerializeToString(&str); 在 SPEED 模式下會利用反射機制打印出詳細字段和字段值,但是 LITE_RUNTIME 則僅僅打印字段值組成的字符串。

因此:可以在調試階段使用 SPEED 模式,而上線以後提升性能使用 LITE_RUNTIME 模式優化。

最直觀的區別是使用三種不同的 option 選項時,編譯後產生的 .pb.h 中自定義的類所繼承的 protobuf 類不同:

//1. SPEED模式:(自定義的類繼承自 Message 類)
// .proto 文件:
option optimize_for = SPEED;
// .pb.h 文件:
class Person : public ::PROTOBUF_NAMESPACE_ID::Message {};

//2. CODE_SIZE模式:(自定義的類繼承自 Message 類)
// .proto 文件:
option optimize_for = CODE_SIZE;
// .pb.h 文件:
class Person : public ::PROTOBUF_NAMESPACE_ID::Message {};

//3. LITE_RUNTIME模式:(自定義的類繼承自 MessageLite 類)
// .proto 文件:
option optimize_for = LITE_RUNTIME;
// .pb.h 文件:
class Person : public ::PROTOBUF_NAMESPACE_ID::MessageLite {};

四、protobuf 的編碼和存儲方式:


① protobuf 將消息裏的每個字段進行編碼後,再利用 TLV 或者 TV 的方式進行數據存儲;

② protobuf 對於不同類型的數據會使用不同的編碼和存儲方式;

③ protobuf 的編碼和存儲方式是其性能優越、數據體積小的原因。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/1Csoe8J9-sdnWbKVkh9wvg