【C++】protobuf序列化与反序列化

介绍

protobuf是一种将结构数据进行序列化的方法。protobuf序列化是将数据序列化为二进制,比其他序列化方式(如JSON)更加高效。

protobuf序列化规则非常简单,它序列化只存储字段编号、数据类型、字段值

(字段编号 + 数据类型) + 字段值


protobuf使用

protobuf的使用流程分为3步:

1,编写.proto文件

.proto是协议文件,用来定义要传输的信息字段。如下编写contacts.proto

syntax = "proto3"; // 版本,使用proto3版本

package contacts; // 包名(C++命名空间)
message People { // message消息的定义

string name = 1; // 字段1(1是字段编)

int32 age = 2; // 字段2(2是字段编号)

}

// 字段编号用于唯一识别字段,传输数据时根据编号识别字段

以上是基本的proto语法,具体的其他语法后面会详细介绍。

2,用protoc编译生成C++代码

当proto文件编写好后,下面需要用protobuf提供的编译器编译proto文件,使其生成C++代码。编译格式如下:

protoc --proto_path=IMPORT_PATH --cpp_out=OUT_DIR path/to/file.proto

protoc // 是protobuf提供的命令行编译工具

--proto_path // 指定被编译的.proto问件所在木录,可多次指定。可简写成 -I。若不指定改参数,则在当前目录进行搜索

--cpp_out= // 指编译后的文件为 C++ 文件

OUT_DIR // 编译后生成文件的目标路径

path/to/file.proto // 要编译的.proto文件

编译 contacts.proto 文件命令如下:

protoc --cpp_out=. contacts.proto

3,引入编译的C++文件

编译 contacts.proto 后,会生成所选择语言的代码,我们选择的是C++,所以编译后生成了两个文件:contacts.pb.h 和 contacts.pb.cc。其中,每个 message,都会生成一个对应的消息类。在消息类中,编译器为每个字段提供了获取和设置方法,以及能够操作字段的方法。其中,序列化和反序列化方法也包含在其中。序列化与反序列化相关的接口如下:

class MessageLite {

public:

// 序列化

// 将序列化后数据写入文件流

bool SerializeToOstream(ostream* output) const;

// 把消息序列化成二进制,写入指定的内存缓冲区(数组)

bool SerializeToArray(void *data, int size) const;

// 把消息序列化成二进制,存入 C++ 字符串string

bool SerializeToString(string* output) const;

// 反序列化,与上面序列化接口对应

bool ParseFromIstream(istream* input); // 从流中读取数据,再反序列化

bool ParseFromArray(const void* data, int size);

bool ParseFromString(const string& data);

};

编译C++代码时,需要链接protubuf的库,且至少是C++11语法。例如:

g++ main.cc contacts.pb.cc -o TestProtoBuf -std=c++11 -lprotobuf


proto语法详解

proto语法这里主要围绕proto3展开。

1,字段规则

message消息的字段可以用 singular 和 repeated 两个字段规则修饰。singular表示消息中可以包含该字段零次或⼀次(不超过⼀次)。proto3语法中,字段默认使用该规则。repeated表示消息中可以包含该字段任意多次,可以将其看作一个数组。

syntax = "proto3";

package contacts;

message People {

string name = 1;

int32 age = 2;

repeated string phone_numbers = 3; // phone_numbers可以看作一个数组

}

2,消息类型的定义与使用

在单个.proto文件中可以定义多个消息体,且支持定义嵌套类型的消息(任意多层)。每个消息体中的字段编号可以重复。除此外,还可以使用 import 导入其他的proto文件

// 嵌套写法

syntax = "proto3";

package contacts;

message Peoplefo {

string name = 1;

int32 age = 2;

message Phone {

string number = 1;

}

}

// 非嵌套写法

syntax = "proto3";

package contacts;

message Phone {

string number = 1;

}

message Peoplefo {

string name = 1;

int32 age = 2;

}

嵌套信息在信息的作用域内,访问时需要使用域的解引用操作符("::"),非嵌套信息在全局中,可直接访问。后面样例会说明。

contacts.proto文件:
syntax = "proto3";

package contacts;

message Peoplefo {

string name = 1;

int32 age = 2;

message Phone {

string number = 1;

}

repeated Phone phone = 3;

}

phone.proto文件:
syntax = "proto3";

package phone;

message Phone {

string number = 1;

}

contacts.proto中的Peoplefo 使用 Phone 消息:
syntax = "proto3";

package contacts;

import "phone.proto";

message Peoplefo {

string name = 1;

int32 age = 2;

// 引入的文件声明了package,使用消息时,需要用 "命名空间.消息类型" 格式

repeated phone.Phone phone = 3;

}

3,enum类型

enum表示枚举类型,且枚举必须定义在 message内部或文件顶层,枚举值与编号默认情况下是相同的,格式如下:

enum 枚举名 {

枚举项 = 编号;

...
}

proto3语法有强制要求,第一个枚举项的编号必须 = 0,枚举编号必须是整数、非负、唯一,同级作用域下的枚举,不能包含相同枚举值名称。

enum PhoneType {

MP = 0;

TEL = 1;

}

enum PhoneTypeCopy {

MP = 0; // 编译后报错:MP 已经定义

}

4,Any类型

Any是万能容器,可以包裹任意一个其他消息对象,实现动态类型嵌套,类似 C++ 里的 "基类指针或泛型容器"。

Any类型是在安装protobuf时,就已经定义好的,在include目录下的goole里的protobuf中可以看到。

syntax = "proto3";

import "google/protobuf/any.proto"; // 引入 any.proto 文件

// 使用Any存储任意消息

message AnyMessage {

google.protobuf.Any data = 1;

}

5,oneof类型

oneof是protobuf中定义互斥字段的关键字,核心作用是在一组字段里,最多只能同时设置一个字段,设置其中一个会自动清空同组内的其他所有字段。oneof定义的规则如下:

  • oneof 后面跟组名(用于代码访问)
  • 大括号内定义多个同级别互斥字段
  • 字段编号不能重复(和普通 message 规则一致)
  • 同一时间只能赋值一个字段,给 oneof 组内任意一个字段赋值,会自动清空组内其他字段。
  • oneof 内部不能定义 repeated 字段。
  • 同一个 oneof 组里的所有字段,共享同一块内存空间。

syntax = "proto3";

message UserInfo {

string name = 1;

// 定义 oneof 组:contact 是组名(自定义)

oneof contact {

string phone = 2; // 字段1

string email = 3; // 字段2

int32 wechat = 4; // 字段3

}

}

oneof内的字段可看成信息作用域中的字段,只不过该字段被oneof进行了修饰限制,使用时跟上面name字段的基本运用方法还是一样的。

6,保留字段reserved

reserved保留字段是为了防止误用老的字段、防止协议兼容出问题的一种方式。来看以下情况

message User {

int32 id = 1;

string name = 2;

string phone = 3; // 后来不用了,想删掉

}

这里若直接删掉 phone = 3,未来别人可能会复用 3 这个编号给新字段:string email = 3; 这里就会出现严重问题,旧数据里的 3 是 phone,新代码里的 3 是 email,导致数据错乱。

reserved把废弃的字段名 + 字段编号 保护起来,不让别人用:

message User {

int32 id = 1;

string name = 2;

reserved 3,4; // 保留 3 这个编号,不许用

reserved "phone"; // 保留 "phone" 这个名字,不许用

}

这样再写 编号3、4 或者 名称phone,编译器编译时会直接报错。

7,选项option

option选项是 protobuf 的核心配置机制,用来定义 protobuf 协议行为规则,简单说,它不定义数据结构本身,只控制数据结构如何被处理和使用。该选项分为文件级、消息级、字段级等,文件级是在整个 proto 文件中设置,消息级是在message中进行设置,字段级是在字段中进行设置。

文件级:
syntax = "proto3";

option cc_generic_services = true;

.....

消息级:

message TestMsg {

option no_standard_descriptor = true;

int32 id = 1;

}

字段级:

string old_name = 1 deprecated = true;

对于C++而言,常用的有 optimize_for 和 allow_alias 两个选项。

optimize_for:该选项为文件选项,可以设置 protoc 编译器的优化级别,分别为 SPEED、CODE_SIZE、LITE_RUNTIME。受该选项影响,设置不同的优化级别,编译 .proto 文件后生成的代码内容不同。

SPEED:protoc编译器将生成的代码是高度优化的,代码运行效率高,序列化/反序列化速度最快,但是由此生成的代码,编译后会占用更多的空间,代码体积大,功能全面。SPEED是默认选项。

CODE_SIZE:proto编译器将生成最少的类,与 SPEED 相反,它的代码运行效率较低,但会占用更少的空间,精简代码。这种方式适合资源受限的设备。

LITE_RUNTIME:生成的代码执行效率高,同时生成代码编译后的所占用的空间也是非常少。这是以牺牲 protobuf 提供的反射功能为代价的, 其次编译连接时必须链接protobuf-lite,不能链接完整 protobuf。

allow_alias:该选项为枚举选项,用来定义别名,它允许同一个枚举值,对应多个不同的名字,也就是取别名。别名值相同,名字不同,与 C++ 里别名(引用)是完全等价的。

enum Test {

A = 1;

B = 1; // 报错:值重复

}

enum Test {

option allow_alias = true; // 开启别名

A = 1;

B = 1; // 合法,A 和 B 是别名

}


protobuf实战------实现一个通讯录:protobuf通讯录

相关推荐
夕除2 小时前
shizhan--10
java·开发语言
Zhang~Ling2 小时前
C++ 红黑树封装:myset和mymap的底层实现
开发语言·数据结构·c++·算法
啦啦啦啦啦zzzz2 小时前
数据结构:堆排序
数据结构·c++·
原来是猿2 小时前
为什么 C++ 需要区分左值和右值?
开发语言·c++
xier_ran2 小时前
【infra之路】PagedAttention
java·开发语言
SilentSamsara3 小时前
NumPy 进阶:广播机制、ufunc 与向量化计算的工程实践
开发语言·python·青少年编程·性能优化·numpy
珊瑚里的鱼3 小时前
C++的强制类型转换
android·开发语言·c++
编程探索者小陈3 小时前
接口自动化三件套:JSON Schema 校验 + logging 日志 + Allure 测试报告
开发语言·python
星恒随风3 小时前
C++ 类和对象入门(二):默认成员函数、构造函数和析构函数详解
开发语言·c++·笔记·学习