protobuf介绍
Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,缩写PB。protocol buffers 是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。通信时所传递的信息是通过Protobuf定义的message数据结构进行打包,然后编译成二进制的码流再进行传输或者存储。
Protocol buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
Protobuf 使用的时候必须写一个 IDL(Interface description language)文件,在里面定义好数据结构,只有预先定义了的数据结构,才能被序列化和反序列化。其中,序列化是将对象转换二进制数据,反序列化是将二进制数据转换成对象。
支持开发语言C++、Java、Python、Objective-C、C#、Ruby、Go、PHP、Dart、Javascript。本文主要使用C++介绍protobuf使用。
官方网站: https://github.com/protocolbuffers/protobuf
官方文档: https://protobuf.dev/
protobuf使用过程
- 创建 .proto 文件(IDL文件),定义需要处理的结构化数据。
- protoc 工具,将 .proto 文件转换为 C++、Golang、Java、Python 等多种语言的代码API
- 调用API实现序列化、反序列化以及读写
通讯录地址簿示例教程
第一步, 在 addressbook.proto 文件中定义数据结构
syntax = "proto2";
// xiujie.cn
// 包名(命名空间)
package tutorial;
// Person类定义
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
}
// 通讯录地址簿列表
message AddressBook {
repeated Person people = 1;
}
逐行解读 addressbook.proto
syntax
, protobuf 目前有两个版本proto2
和proto3
。如果不设置syntax
即默认为proto2
。syntax="proto3"
必须位于.proto
文中除去注释和空行的第一行。package
, 即包名声明符是可选的,用来防止不同的消息类型有命名冲突。C++中的命名空间。message
, 消息类型关键字,类似于C++/Java中的class
关键字。使用工具生成代码,每个message
都会生成一个名字与之对应的C++类。Person
类名称,name
,id
,email
,phones
是该类名称的4个字段,类型分包是string
,int32
,string
,class
。- 每个字段的最前面是修饰符。
proto2
版本有三种修饰符.分别为required
、optional
、repeated
。 required
: 字段属性为必填字段。若不设置,则会导致编解码异常,导致消息被丢弃。optional
: 字段属性为可选字段。发送方可以选择性根据需要进行设置;对于optional属性的字段,可以通过default关键字为字段设置默认值,即当发送方没有对该字段进行设置的时候,将使用默认值。如果没有对字段设置默认值,就会根据特定的类型给字段赋予特定的默认值。对于bool类型,默认值为false;对于string类型,默认值为空字符串;对于数值类型,默认值为0;对于枚举类型,默认值是枚举类型中的第一个值。repeated
: 字段属性为可重复字段,该字段可以包含[0,n]个元素,字段中的元素顺序被保留。- 字段名前是protobuf的数据类型包括
string
,int32
,double
等。protobuf数据类型与C++语言数据类型对应关系请参考 - 字段名
=
后面的数字称为标识符,每个字段都需要提供一个唯一的标识符。标识符用来在消息的二进制格式中识别各个字段,一旦使用就不能够再改变,标识符的取值范围为 [1, 2^29 - 1] 。1-15的标识符比其他较高标识符在二进制编码中少一个字节。尽量使用1-15定义字段序号。 enum
,枚举类型关键字。枚举常量的值必须在32位整数范围内,因为enum值是使用可编码方式存储的,对负数存储不够高效,因此不推荐在enum中使用负数。枚举类型可以定义在message内,也可以定义在message外。[default = HOME]
在optional
属性的字段,可以通过default
关键字为字段设置默认值。- 单行注释可以用
//
,多行注释使用/* */
。
第二步, 使用 protoc 工具,将 addressbook.proto 文件转换为 C++ 接口代码
下载安装protoc工具
官方下载: https://github.com/protocolbuffers/protobuf/releases/latest
国内加速下载:
windows
- https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-win32.zip
- https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-win64.zip
mac
- https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-osx-universal_binary.zip
- https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-osx-x86_64.zip
- https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-osx-aarch_64.zip
brew install protobuf
linux
- https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-x86_64.zip
- https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-x86_32.zip
- https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protoc-21.12-linux-aarch_64.zip
编译addressbook.proto生成C++接口代码
mkdir src protoc -I=./ --cpp_out=./src/ ./addressbook.proto
代码目录结构如下:
├── addressbook.proto └── src ├── addressbook.pb.cc └── addressbook.pb.h
第三步,调用API实现序列化、反序列化以及读写
下载protobuf源码生成编译库文件
# // 官方下载: https://github.com/protocolbuffers/protobuf/releases/latest
wget https://ghproxy.com/https://github.com/protocolbuffers/protobuf/releases/download/v21.12/protobuf-all-21.12.zip
unzip protobuf-all-21.12.zip
cd protobuf-all-21.12
./configure
make
sudo make install
sudo ldconfig # refresh shared library cache.
编写调用接口代码实现序列化、反序列化 main.cpp
// xiujie.cn
// c++ -std=c++0x addressbook.pb.cc main.cpp `pkg-config --cflags --libs protobuf`
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// 标准输入填充Person
void PromptForAddress(tutorial::Person* person) {
cout << "Enter person ID number: ";
int id;
cin >> id;
person->set_id(id);
cin.ignore(256, '\n');
cout << "Enter name: ";
getline(cin, *person->mutable_name());
cout << "Enter email address (blank for none): ";
string email;
getline(cin, email);
if (!email.empty()) {
person->set_email(email);
}
while (true) {
cout << "Enter a phone number (or leave blank to finish): ";
string number;
getline(cin, number);
if (number.empty()) {
break;
}
tutorial::Person::PhoneNumber* phone_number = person->add_phones();
phone_number->set_number(number);
cout << "Is this a mobile, home, or work phone? ";
string type;
getline(cin, type);
if (type == "mobile") {
phone_number->set_type(tutorial::Person::MOBILE);
} else if (type == "home") {
phone_number->set_type(tutorial::Person::HOME);
} else if (type == "work") {
phone_number->set_type(tutorial::Person::WORK);
} else {
cout << "Unknown phone type. Using default." << endl;
}
}
}
// 遍历 通讯录地址簿列表 打印每一项信息
void ListPeople(const tutorial::AddressBook& address_book) {
for (int i = 0; i < address_book.people_size(); i++) {
const tutorial::Person& person = address_book.people(i);
cout << "Person ID: " << person.id() << endl;
cout << " Name: " << person.name() << endl;
if (person.has_email()) {
cout << " E-mail address: " << person.email() << endl;
}
for (int j = 0; j < person.phones_size(); j++) {
const tutorial::Person::PhoneNumber& phone_number = person.phones(j);
switch (phone_number.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;
}
cout << phone_number.number() << endl;
}
}
}
// 主函数: 从存储文件中读取通讯录地址簿列表, 通过标准输入新增一条通讯录, 写入存储文件
int main(int argc, char* argv[]) {
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
return -1;
}
tutorial::AddressBook address_book; // 通讯录地址簿列表对象
{
// 从存储文件中读取通讯录地址簿列表
fstream input(argv[1], ios::in | ios::binary);
if (!input) {
cout << argv[1] << ": File not found. Creating a new file." << endl;
} else if (!address_book.ParseFromIstream(&input)) { // 从文件流反序列化到对象
cerr << "Failed to parse address book." << endl;
return -1;
}
}
// 遍历打印列表
ListPeople(address_book);
// 通过标准输入新增一条通讯录
PromptForAddress(address_book.add_people());
{
// 写入存储文件
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!address_book.SerializeToOstream(&output)) { // 从对象序列化到文件流
cerr << "Failed to write address book." << endl;
return -1;
}
}
// 可选的: 删除所有由 libprotobuf 分配的全局对象。
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
编译
c++ -std=c++0x addressbook.pb.cc main.cpp `pkg-config --cflags --libs protobuf`
运行
# 第一次运行
./a.out abc.db
abc.db: File not found. Creating a new file.
Enter person ID number: 1
Enter name: zhangsan
Enter email address (blank for none): zhangsan@163.com
Enter a phone number (or leave blank to finish): 123456
Is this a mobile, home, or work phone? mobile
Enter a phone number (or leave blank to finish): 654321
Is this a mobile, home, or work phone? work
Enter a phone number (or leave blank to finish):
# 第二次运行
./a.out abc.db
Person ID: 1
Name: zhangsan
E-mail address: zhangsan@163.com
Mobile phone #: 123456
Work phone #: 654321
Enter person ID number:
代码解析 addressbook.pb.h main.cpp
在 addressbook.pb.h 代码中 Person
类的 name
,id
,email
,phones
字段操作方法:
// optional string name = 1;
bool has_name() const;
void clear_name();
const std::string& name() const;
template <typename ArgT0 = const std::string&, typename... ArgT>
void set_name(ArgT0&& arg0, ArgT... args);
std::string* mutable_name();
PROTOBUF_NODISCARD std::string* release_name();
void set_allocated_name(std::string* name);
// optional int32 id = 2;
bool has_id() const;
void clear_id();
int32_t id() const;
void set_id(int32_t value);
// optional string email = 3;
bool has_email() const;
void clear_email();
const std::string& email() const;
template <typename ArgT0 = const std::string&, typename... ArgT>
void set_email(ArgT0&& arg0, ArgT... args);
std::string* mutable_email();
PROTOBUF_NODISCARD std::string* release_email();
void set_allocated_email(std::string* email);
// repeated .tutorial.Person.PhoneNumber phones = 4;
int phones_size() const;
void clear_phones();
::tutorial::Person_PhoneNumber* mutable_phones(int index);
::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::tutorial::Person_PhoneNumber >*
mutable_phones();
const ::tutorial::Person_PhoneNumber& phones(int index) const;
::tutorial::Person_PhoneNumber* add_phones();
const ::PROTOBUF_NAMESPACE_ID::RepeatedPtrField< ::tutorial::Person_PhoneNumber >&
phones() const;
Person
类的可选字段: 小写字段名()
方法获取字段值方法, has_
方法判断是否已设置值, set_
方法设置值, clear_
方法清理已设置值, mutable_
方法返回对象字段指针.
Person
类的重复字段: 小写字段名(索引序号)
方法获取指定索引字段值, _size
方法返回重复字段大小, add_
方法返回新增字段对象指针, mutable_
方法返回可用于更新指定索引字段指针.
Message
类是Person
类的基类.
Message
类一些方法:
std::string DebugString() const; // 调试打印类所有字段方法
std::string ShortDebugString() const; // 调试打印类所有字段方法
std::string Utf8DebugString() const; // 调试打印类所有字段方法
void PrintDebugString() const; // 调试打印类所有字段方法
bool SerializeToString(string* output) const;// 序列化到二进制字符串
bool ParseFromString(const string& data); // 反序列化二进制字符串
bool SerializeToOstream(ostream* output) const;// 序列化文件流
bool ParseFromIstream(istream* input); // 反序列化文件流
注意事项
proto定义类型和C++数据类型对照关系
proto类型 | 编码说明 | C++类型 |
---|---|---|
double | - | double |
float | - | float |
int32 | 可变长度编码.负数编码效率低.包含负数建议使用sint32 | int32 |
int64 | 可变长度编码.负数编码效率低.包含负数建议使用sint64 | int64 |
uint32 | 可变长度编码. | uint32 |
uint64 | 可变长度编码. | uint64 |
sint32 | 可变长度编码. 有符号.负数编码效率高. | int32 |
sint64 | 可变长度编码. 有符号.负数编码效率高. | int64 |
fixed32 | 固定4字节. 数值大于228,比uint32有效率. | uint32 |
fixed64 | 固定8字节.数值大于256,比uint64有效率. | uint64 |
sfixed32 | 固定4字节 | int32 |
sfixed64 | 固定8字节 | int64 |
bool | - | bool |
string | 必须是utf-8编码 | string |
bytes | 任何字节序列 | string |
proto定义类型缺省值
proto类型 | 缺省值 |
---|---|
string | 空字符串 |
bytes | 空 |
bool | false |
numeric types | 0 |
enums | 一个定义枚举值 |
为了达到前后消息类型兼容的目的,扩展Message消息类型的时候需要注意一下几点:
- 不要更改任何已有的字段的数值标识。
- 所添加的字段属性必须是optional 或者repeated类型,如果扩展required类型,会导致旧的消息解析异常
- 非required字段可以移除。要保证它们的标示在新的消息类型中不再使用。不建议移除可添加注释表示已废弃。移除可导致老旧代码编译出错。
- 一个非required的字段可以转换为一个扩展,反之亦然——只要它的类型和标识号保持不变。
- int32, uint32, int64, uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。如果解析出来的数字与对应的类型不相符,那么结果就像在C++中对它进行了强制类型转换一样(例如,如果把一个64位数字当作int32来 读取,那么它就会被截断为32位的数字)。
- sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。
- string和bytes是兼容的——只要bytes是有效的UTF-8编码。
- 嵌套消息与bytes是兼容的——只要bytes包含该消息的一个编码过的版本。
- fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
- 可以定义枚举类型,尽量不要使用枚举类型定义字段. 新增的枚举值,在老旧编译程序中不识别. 建议使用int32定义枚举对象,注释添加枚举类型.
文章评论