c/c++序列化数据之protobuf

Protocol Buffers(protobuf):

是一种由google开发的高效、跨语言、跨平台的序列化框架。它的核心功能是定义结构化数并将其序列化(序列化是指将数据对象转换为以字节流以便传输或存储:所谓序列化就通俗来说就是把内存的一段数据转换为二进制并存储或者通过网络传输,而读取磁盘或另一端接收到后可以在内存中重建这段数据,即protobuf就是编解码,可以把程序中的一些对象用pb序列化,然后存储到本地文件,过一会在读取文件,然后恢复出那些对象),同时支持多种多种语言。它被广泛应用于需要高性能数据传输和存储的场景。

protobuf的优缺点

优点:

  1. 序列化后体积相比json和xml小,适合网络传输
  2. 序列化反序列化速度快,快于json的处理速度
  3. 支持跨平台多语言

缺点:

  1. 相比于json、xml应用不够广泛(json和xml也是一种序列化的方式,但它们不需要提前定义idl,且具备可读性)
  2. 序列化结果为二进制格式导致可读性差
  3. 缺乏自描述(protobuf的序列化结果不包含字段名或结构描述,仅记录字段编号和值。 反序列化时,必须依赖与之对应的.proto定义文件即数据结构描述。如果没有.proto文件, 数据内容就难以还原)

protobuf的安装

ubuntu:sudo apt install protobuf-compiler

centos:sudo yum install protobuf-compiler

windows:https://github.com/protocolbuffers/protobuf/releases

                下载.zip文件后解压并配置环境变量即可

protobuf基本的使用流程梳理

编写.proto文件

        .proto 是 Protobuf 的核心数据结构描述文件,类似于定义类的头文件。
        文件中使用 message 关键字定义数据结构(类似 C++ 的 class)。

示例:persion.proto

// person.proto
syntax = "proto3";
package = test;

message Person {
  string name = 1;
  int32 age = 2;
}

使用 protoc 编译生成代码

        用 protoc 编译器将 .proto 文件编译为对应语言的源文件, .pb.h 和 .pb.cc(C++ 中使用)。

protoc --cpp_out=生成文件的目标路径 person.proto

在 C++ 中包含上一步生成的.pb.h头文件并使用命名空间(如果在.proto中声明了package)

#include "person.pb.h"

using namespace test; 

在cpp文件中按需执行操作

Person person;
person.set_name("Tom");
person.set_age(20);

std::string name = person.name();
int age = person.age();

std::string output;
person.SerializeToString(&output);

Person new_person;
new_person.ParseFromString(output);

protobuf数据结构的基本定义(.proto文件格式解析)

指定protobuf的版本

syntax = “proto3”;
syntax = “proto2”;

指定优化场景

option = optimize_for = SPEED
默认的优化场景:优化生成代码以提高性能即运行速度,者通常会生成较大的代码文件,因为它内联了更多的逻辑
option = optimize_for = CODESIZE
优化生成的代码以最小化文件大小
option = optimize_for = LITE_RUNTIME
生成的代码会依赖一个较小的protocol buffers运行库,而不是完整的标准库lite_runtime,因此降低链接库的大小,但这个lite_runtime库去掉了许多高级功能

 package选项

package 命名空间;
package tengxun::myprotobuf;

在程序中使用时,先include生成的对应pb.h文件,再使用设置的命名空间
#include “......pb.h”
using namespace tengxun::myptotobuf;

 protobuf的消息类型message:相当于是一个类

一个message是一组字段(field)的集合,每个字段都有以下元素:

message Test {
    string name = 1;
}

①字段类型:比如字符串、整数等

②字段名

③字段编号:唯一标识字段的数字

④字段的定义规则
singular(单值字段):表示消息中可以存在零个或一个该字段。这个规则在proto3中是默认的字段规则。如果字段没有被设置,它将使用该字段类型的默认值。上述示例中的name就是默认的singular
optional(可选字段):表示该字段是可选的,他可以在消息中存在,也可以缺失(这里的缺失是指在程序中可以不显示地为optional字段赋值,protobuf会自动为该字段填充一个默认值)。如果缺   失,字段将使用该类型的默认值。
repeated(重复字段):表示该字段可以重复零次或多次,即他是一个列表。消息中该字段的值可以出现任意次数,并且系统会保留重复值的顺序,对于这种类型的字段,protobuf会根据字段类型生 成一个列表(或数组) 

message Persion {
	repeated string phone_number = 1;
}
Persion persion;
persion.add_phone_numbers(“123”);
persion.add_phone_numbers(“456”);
for (int i = 0; i < perison.phone_numbers_size(); ++i) {
	std::cout << persion.phone_numbers(i) << std::endl;
}

 map(键值对字段):表示一个键值对字段,键和值的类型可以是基本类型,每个map字段都包含一个唯一的键与对应的值,键是唯一的,并且会自动去重。

message Persion{
	  map email_address = 4;
}
Persion persion;
persion.mutable_email_address()->insert({“a”, “b”});
persion.mutable_email_address()->insert({“c”,”d”});

protobuf中的枚举

enum MyTest {
	FIRST_VALUE = 0;
    SECOND_VALUE = 1;
}

①枚举类型名使用大驼峰命名,值名使用全大写,字母间以_分割

②每个枚举定义必须包括一个映射到0的常量作为第一个元素

③不能使用负数作为枚举值,编码效率低

④每个枚举值以分号(;)而不是逗号

⑤在枚举中可以为不同的枚举常量分配相同的值来定义别名。为此需要将allow_alias选项设置为       true

enum EnumAllowAlias {
	option allow_alias = true;
    UNKNOWN = 0;
    STARTING = 1;
    RUNNING = 1;
}

使用其他消息类型作为当前消息类型的字段

message Request {
	string url = 1;
    string title = 2;
    repeated string snippets = 3;
}
message SearchResponse {
	repeated Request requests = 1;  表示一个结果列表
}

 嵌套的消息类型

message SearchResponse {
	message Request {
		string url = 1;
        string title = 2;
        repeated string snippets = 3;
    }
    repeated Request requests = 1;
}

在外部引用嵌套类型

如果嵌套类型需要在其他消息中使用,需要通过父类型引用该嵌套类型。假如Request是SearchResponse内部定义的嵌套消息类型

message SomeOtherMessage {
	SearchResponse.Request request = 1;
}

any:允许将任意的消息类型嵌入到一个字段中(嵌入:不是直接的赋值,而是将一个任意的message消息类型的实例存储到另一个消息类型中的字段)

示例

error.proto

message NetworkErrorDetails {
	string error_code = 1;
    string error_message = 2;
}
message ErrorStatus {
	string message = 1;
    repeated google.protobuf.Any details = 2;
}

test.cpp

#include 
#include "error.pb.h"               // 包含 NetworkErrorDetails 和 ErrorStatus 的定义
#include  // Any 类型需要包含的头文件

int main() {
    // 创建 NetworkErrorDetails 对象并设置字段
    NetworkErrorDetails details;
    details.set_error_code("404");             // 设置错误码,例如 HTTP 404
    details.set_error_message("not found");    // 设置错误信息内容

    // 创建 ErrorStatus 对象用于封装多个错误详情
    ErrorStatus status;
    status.set_message("error occur");         // 设置顶层错误描述信息

    // 将 NetworkErrorDetails 打包进 Any 对象,并添加到 ErrorStatus 中的 repeated Any 列表中
    google::protobuf::Any* any_details = status.add_details(); // 获取 repeated Any 的新增元素指针
    any_details->PackFrom(details);                             // 使用 PackFrom 将实际类型打包到 Any 对象中

    // 遍历 ErrorStatus 中所有的 details(Any 类型),解包并识别具体类型
    for (const google::protobuf::Any& item : status.details()) {
        if (item.Is()) {
            // 如果能识别为 NetworkErrorDetails 类型,则解包
            NetworkErrorDetails network_error;
            item.UnpackTo(&network_error); // 将 Any 类型解包为原始类型 NetworkErrorDetails

            // 输出解包后的错误信息
            std::cout << "Error Code: " << network_error.error_code() << std::endl;
            std::cout << "Error Message: " << network_error.error_message() << std::endl;
        }
    }

    return 0;
}

oneof:允许在同一个消息中定义多个字段,但一次只能设置一个字段。这个特性在内存管理中非常有用,因为它可以节省内存,因为只有一个字段会被存储,而其他字段会被清空,oneof字段类似于联合体Union,在任何时候只允许选择一个字段。

.proto
message SampleMessage {
	oneof test_oneof {
		string name = 1;
        int32 age = 2;
        bool is_active = 3;
    }
}

.cpp
SampleMessage message;
message.set_name(“name”);
if (message.has_name()) {
	std::cout << message.name();
}

protobuf常用方法(.cpp文件中使用)

message MyMesage {
    string name = 1;
}

默认的构造函数

protobuf自动为每个消息类型生成相应的构造函数和方法,用于构建和初始化消息。

MyMessage msg;

字段访问和操作(其中*是字段名)

 ①设置字段

set_*   msg.set_name(“john”);

 ②获取字段值:

get_*   std::string name = msg.get_name();

*()     std::string name = msg.name();

在c++中protobuf为每个字段生成了对应的访问器getter函数,即”字段名()”,字段是私有的,需要通过提供的这些访问器来访问这些字段

 ③检查字段是否设置:

has_*    bool is_set = msg.has_name();

 ④清除字段:

clear_*  msg.clear_name();

 ⑤访问消息变量的可变引用(指针),允许修改该字段的值

.mutable_*

.proto
message B {
	string description = 1;
}
message Msg {
    string name = 1;
    B b = 2;
}

.cpp
Msg msg;
B* b = msg.mutable_b();
b - > set_description(“name”);
msg.set_name(“name”);
std::cout << msg.b().description();

repeated字段操作

.proto
message MyMessage {
	string name = 1;
    int32 age = 2;
    repeated int32 numbers = 3;
}

.cpp
MyMessage msg;
①获取repeated字段的元素数量:msg.numbers_size();
②访问单个元素:msg.numbers(0);
③添加元素:msg.add_numbers(10);
④修改元素:msg.mutable_numbers(0) = 15;
⑤清除所有元素:msg.clear_numbers();

map字段操作

.proto
message Product {
	string name = 1;
    map attrs = 2;
}

.cpp
Product msg;
①设置键值对:msg.mutable_attrs()->insert({“key”, “value”});
②获取键值对:msg.attrs().at(“key”);
③检查键是否存在:msg.attrs().contains(“key”);
④清空所有键值对:msg.clear_attrs();

序列化和反序列化操作

①序列化为字节流:SerializeToString、SerializeToArray将消息对象序列化为字符串或者字节数组

②从字节流反序列化:ParseFromString、ParseFromArray方法将字节流反序列化为消息对象

// .proto
message MyMessage {
    string name = 1;
    int32 age = 2;
}

// .cpp
MyMessage msg;
msg.set_name("Alice");
msg.set_age(25);

// 序列化为 string
std::string output;
msg.SerializeToString(&output);

// 序列化为数组
int size = msg.ByteSizeLong();
char* buffer = new char[size];
msg.SerializeToArray(buffer, size);

// 从 string 反序列化
MyMessage from_str;
from_str.ParseFromString(output);

// 从 array 反序列化
MyMessage from_arr;
from_arr.ParseFromArray(buffer, size);

delete[] buffer;

    你可能感兴趣的:(c++,protobuf,序列化工具)