赞
踩
protobuf也叫protocol buffer是google 的一种数据交换的格式,它独立于语言,独立于平台。google 提供了多种语言的实现:java、c#、c++、go 和 python 等,每一种实现都包含了相应语言的编译器以及库文件。
由于它是一种二进制的格式,比使用 xml 、json进行数据交换快许多。可以把它用于分布式应用之间的数据通信或者异构环境下的数据交换。作为一种效率和兼容性都很优秀的二进制数据传输格式,可以用于诸如网络传输、配置文件、数据存储等诸多领域。
序列化:将磁盘数据加载到内存 / 数据从客户端/服务器的一段发送给另一端。
反序列化:将内存数据存入磁盘,对数据包进行解析。
TLV:tag标签、len长度、value数值。(不是protobuf的格式,但是一般的格式)
github源代码下载地址:
https://github.com/protocolbuffers/protobuf/releases
- 以 protobuf 3.21.12 为例
- 自行下载源码包, 解压缩
$ tar zxvf protobuf-cpp-3.21.12.tar.gz
- 进入到解压目录
$ cd protobuf-3.21.12/
- 构建并安装
$ ./configure # 检查安装环境, 生成 makefile
$ make # 编译
$ sudo make install # 安装
$ protoc --version
protoc: error while loading shared libraries: libprotoc.so.32: cannot open shared object file: No such file or directory
$ sudo find /usr/local/ -name libprotoc.so
/usr/local/lib/libprotoc.so
此时存储动态库的路径就找到了。
使用 find 命令进行搜索的时候,如果从 /usr/local/ 目录中搜索不到需要的文件信息,可以从根目录 / 开始搜索。
其实在通过make install 安装的时候通过终端输出的日志信息也可以找到动态库对应的存储路径:
...................................
...................................
libtool: install: (cd /usr/local/lib && { ln -s -f libprotoc.so.32.0.12 libprotoc.so.32 || { rm -f libprotoc.so.32 && ln -s libprotoc.so.32.0.12 libprotoc.so.32; }; })
libtool: install: (cd /usr/local/lib && { ln -s -f libprotoc.so.32.0.12 libprotoc.so || { rm -f libprotoc.so && ln -s libprotoc.so.32.0.12 libprotoc.so; }; })
...................................
...................................
$ sudo vim /etc/ld.so.conf
打开文件后把目录添加到第二行,保存退出。
最后,需要更新配置,在终端执行如下命令:
$ sudo ldconfig
$ protoc --version
libprotoc 3.21.12
在学习protbuf之前,我们需要搞清楚使用protbuf进行数据的序列化主要有哪几个步骤:
确定数据格式,数据可简单可复杂,比如:
// 要序列化的数据
// 第一种: 单一数据类型
int number;
// 第二种: 复合数据类型
struct Person
{
int id;
string name;
string sex;
int age;
};
创建一个新的文件, 文件名随意指定, 文件后缀为 .proto
根据protobuf的语法, 编辑.proto文件
使用 protoc 命令将 .proto 文件转化为相应的 C++ 文件
源文件: xxx.pb.cc –> xxx对应的名字和 .proto文件名相同
头文件: xxx.pb.h –> xxx对应的名字和 .proto文件名相同
需要将生成的c++文件添加到项目中, 通过文件中提供的类 API 实现数据的序列化/反序列化
protobuf中的数据类型 和 C++ 数据类型对照表:
Protobuf 类型 | C++ 类型 | 备注 |
---|---|---|
double | double | 64位浮点数 |
float | float | 32位浮点数 |
int32 | int | 32位整数 |
int64 | long | 64位整数 |
uint32 | unsigned int | 32位无符号整数 |
uint64 | unsigned long | 64位无符号整数 |
sint32 | signed int | 32位整数,处理负数效率比int32更高 |
sint64 | signed long | 64位整数,处理负数效率比int64更高 |
fixed32 | unsigned int(32位) | 总是4个字节。如果数值总是比总是比228大的话,这个类型会比uint32高效。 |
fixed64 | unsigned long(64位) | 总是8个字节。如果数值总是比总是比256大的话,这个类型会比uint64高效。 |
sfixed32 | int (32位) | 总是4个字节 |
sfixed64 | long (64位) | 总是8个字节 |
bool | bool | 布尔类型 |
string | string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本 |
bytes | string | 处理多字节的语言字符、如中文, 建议protobuf中字符型类型使用 bytes |
enum | enum | 枚举 |
message | object of class | 自定义的消息类型 |
struct Person
{
int id;
string name;
string sex; // man woman
int age;
};
syntax="proto3";
message 名字
{
// 类中的成员, 格式
数据类型 成员名字 = 1;
数据类型 成员名字 = 2;
数据类型 成员名字 = 3;
......
......
}
// Person.proto
syntax = "proto3";
// 在该文件中对要序列化的结构体进行描述
message Person
{
int32 id = 1;
bytes name = 2;
bytes sex = 3;
int32 age = 4;
}
.proto文件编辑好之后就可以使用protoc工具将其转换为C++文件了。
$ protoc -I path .proto文件 --cpp_out=输出路径(存储生成的c++文件)
在 protoc 命令中,-I 参数后面可以跟随一个或多个路径,用于告诉编译器在哪些路径下查找导入的文件或依赖的文件,使用绝对路径或相对路径都是没问题的。
如果有多个路径,可以使用多个 -I 参数或在一个参数中使用冒号(:)分隔不同的路径。如果只有一个路径-I 参数可以省略。
例如,protoc -I path1 -I path2 或 protoc -I path1:path2 都表示告诉编译器在 path1 和 path2 路径下查找导入的文件或依赖的文件。
我们可以在.proto文件所在目录执行protoc命令,并生成到当前目录:
$ protoc ./Person.proto --cpp_out=.
- 或者使用 -I 参数
$ protoc -I ./ Person.proto --cpp_out=.
在使用Protocol Buffers(Protobuf)中,可以使用repeated关键字作为限定修饰符来表示一个字段可以有多个值,即重复出现的字段。repeated关键字可以用于以下数据类型:基本数据类型、枚举类型和自定义消息类型。
比如要序列化的数据中有一个数组,结构如下:
// 要序列化的数据
struct Person
{
int id;
string name[10];
string sex;
int age;
};
message Person
{
int32 id = 1;
repeated bytes name = 2;
bytes sex = 3;
int32 age = 4;
}
// 要序列化的数据 // 枚举 enum Color { Red = 5, // 可以不给初始值, 默认为0 Green, Yellow, Blue }; // 要序列化的数据 struct Person { int id; string name[10]; string sex; int age; // 枚举类型 Color color; };
enum 名字
{
元素名 = 0; // 枚举中第一个原素的值必须为0
元素名 = 数值;
}
枚举元素之间使用分号间隔 ;,并且需要注意一点proto3 中的第一个枚举值必须为 0,第一个元素以外的元素值可以随意指定。
上面例子中的数据在.proto文件中可以写成如下格式:
// 定义枚举类型 enum Color { Red = 0; Green = 3; // 第一个元素以外的元素值可以随意指定 Yellow = 6; Blue = 9; } // 在该文件中对要序列化的结构体进行描述 message Person { int32 id = 1; repeated bytes name = 2; bytes sex = 3; int32 age = 4; // 枚举类型 Color color = 5; }
在 Protocol Buffers 中,可以使用import语句在当前.ptoto中导入其它的.proto文件。这样就可以在一个.proto文件中引用并使用其它文件中定义的消息类型和枚举类型。
语法格式如下:
import "要使用的proto文件的名字";
假设现在我有一个proto文件Address.proto,里边记录了地址信息:
syntax = "proto3";
// 地址信息
message Address
{
bytes addr = 1;
bytes number = 2;
}
syntax = "proto3"; // 使用另外一个proto文件中的数类型, 需要导入这个文件 import "Address.proto"; // 在该文件中对要序列化的结构体进行描述 // 定义枚举类型 enum Color { Red = 0; Green = 3; // 第一个元素以外的元素值可以随意指定 Yellow = 6; Blue = 9; } // 在该文件中对要序列化的结构体进行描述 message Person { int32 id = 1; repeated bytes name = 2; bytes sex = 3; int32 age = 4; // 枚举类型 Color color = 5; // 添加地址信息, 使用的是外部proto文件中定义的数据类型 Address addr = 6; }
package类似于C++的命名空间。
在 Protobuf 中,可以使用package关键字来定义一个消息所属的包(package)。包是用于组织和命名消息类型的一种机制,类似于命名空间的概念。
在一个.proto文件中,可以通过在顶层使用package关键字来定义包:
syntax = "proto3";
package mypackage;
message MyMessage
{
// ...
}
在这个示例中,我们使用package关键字将MyMessage消息类型定义在名为mypackage的包中。包名作为一个标识符来命名,可以使用任何有效的标识符,按惯例使用小写字母和下划线。
使用包可以避免不同.proto文件中的消息类型名称冲突,同时也可以更好地组织和管理大型项目中的消息定义。可以将消息类型的名称定义在特定的包中,并使用限定名来引用这些类型。
下面有两个proto文件,分别给他们添加一个package:
proto文件 - Address.proto
syntax = "proto3";
// 添加命名空间 Dabing
package Dabing;
// 地址信息, 这个Address类属于命名空间: Dabing
message Address
{
bytes addr = 1;
bytes number = 2;
}
syntax = "proto3"; // 使用另外一个proto文件中的数类型, 需要导入这个文件 import "Address.proto"; // 指定命名空间 ErBing package ErBing; // 以下的类 Person 和枚举 Color 都属于命名空间 ErBing // 在该文件中对要序列化的结构体进行描述 // 定义枚举类型 enum Color { Red = 0; Green = 3; // 第一个元素以外的元素值可以随意指定 Yellow = 6; Blue = 9; } // 在该文件中对要序列化的结构体进行描述 message Person { int32 id = 1; repeated bytes name = 2; bytes sex = 3; int32 age = 4; // 枚举类型 Color color = 5; // 添加地址信息, 使用的是外部proto文件中定义的数据类型 // 如果这个外边类型属于某个命名空间, 语法格式: // 命名空间的名字.类名 变量名=编号; Dabing.Address addr = 6; }
通过protoc 命令对.proto文件的转换,得到的头文件中有一个类,这个类的名字和 .proto文件中message关键字后边指定的名字相同,.proto文件中message消息体的成员就是生成的类的私有成员。
那么如何访问生成的类的私有成员呢? 可以调用生成的类提供的公共成员函数,这些函数有如下规律:
序列化是指将数据结构或对象转换为可以在储存或传输中使用的二进制格式的过程。在计算机科学中,序列化通常用于将内存中的对象持久化存储到磁盘上,或者在分布式系统中进行数据传输和通信。
Protobuf 中为我们提供了相关的用于数据序列化的 API,如下所示:
// 头文件目录: google\protobuf\message_lite.h
// --- 将序列化的数据 数据保存到内存中
// 将类对象中的数据序列化为字符串, c++ 风格的字符串, 参数是一个传出参数
bool SerializeToString(std::string* output) const;
// 将类对象中的数据序列化为字符串, c 风格的字符串, 参数 data 是一个传出参数
bool SerializeToArray(void* data, int size) const;
// ------ 写磁盘文件, 只需要调用这个函数, 数据自动被写入到磁盘文件中
// -- 需要提供流对象/文件描述符关联一个磁盘文件
// 将数据序列化写入到磁盘文件中, c++ 风格
// ostream 子类 ofstream -> 写文件
bool SerializeToOstream(std::ostream* output) const;
// 将数据序列化写入到磁盘文件中, c 风格
bool SerializeToFileDescriptor(int file_descriptor) const;
反序列化是指将序列化后的二进制数据重新转换为原始的数据结构或对象的过程。通过反序列化,我们可以将之前序列化的数据重新还原为其原始的形式,以便进行数据的读取、操作和处理。
Protobuf 中为我们提供了相关的用于数据序列化的 API,如下所示:
- 哪个变量调用函数,参数就被传递到哪个对象中。
// 头文件目录: google\protobuf\message_lite.h
bool ParseFromString(const std::string& data) ;
bool ParseFromArray(const void* data, int size);
// istream -> 子类 ifstream -> 读操作
// wo ri
// w->写 o: ofstream , r->读 i: ifstream
bool ParseFromIstream(std::istream* input);
bool ParseFromFileDescriptor(int file_descriptor);
-I %{CurrentDocument:Path} --cpp_out=%{CurrentDocument:Path} %{CurrentDocument:FilePath}
-I $FileDir$ $FileName$ --cpp_out=$FileDir$
syntax = "proto3";
package Dabing;
message Address
{
int32 num = 1;
bytes addr = 2;
}
syntax = "proto3"; import "Address.proto"; package Erbing; enum Color { Red = 0; Green = 5; Yellow = 6; Blue = 9; } message Person { int32 id = 1; repeated bytes name = 2; // name为动态数组 bytes sex = 3; int32 age = 4; Dabing.Address addr = 5; Color color = 6; }
#ifndef SERVER_DDZ_MYTEST_H
#define SERVER_DDZ_MYTEST_H
class MyTest
{
public:
void test();
};
#endif //SERVER_DDZ_MYTEST_H
#include "MyTest.h" #include "Person.pb.h" //using namespace Dabing; //using namespace Erbing; void MyTest::test() { // 序列化 Erbing::Person p; p.set_id(10); p.set_age(32); p.set_sex("man"); p.add_name(); // 申请内存资源 p.set_name(0,"路飞"); // 设置数组的索引起始为0。 p.add_name("艾斯"); p.add_name("萨博"); p.mutable_addr()->set_addr("北京市长安区天安门"); //通过mutable可以获取addr的地址,进行读/写操作 p.mutable_addr()->set_num(1001); p.set_color(Erbing::Color::Blue); // 序列化对象 p, 最终得到一个字符串 std::string output; p.SerializeToString(&output); // 反序列化数据 Erbing::Person pp; pp.ParseFromString(output); std::cout << pp.id() << ", " << pp.sex() << ", " << pp.age() << std::endl; std::cout << pp.addr().addr() << ", " << pp.addr().num() << std::endl; int size = pp.name_size(); for(int i=0; i<size; ++i) { std::cout << pp.name(i) << std::endl; } std::cout << pp.color() << std::endl; }
#include "MyTest.h"
int main(int argc, char* argv[])
{
MyTest t;
t.test();
return 0;
}
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。