C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++ProtoBuf使用

C/C++ProtoBuf使用小结

作者:函数指针

ProtoBuf全称:protocol buffers,直译过来是:“协议缓冲区”,是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化数据,这篇文章主要介绍了C/C++ProtoBuf使用,需要的朋友可以参考下

一.什么是ProtoBuf

1.序列化和反序列化概念

序列化:把对象转变为字节序列的过程,称为系列化。

反序列化:把字节序列的内容恢复为对象的过程,称为反序列化。

2.什么情况下需要序列化和反序列化

存储数据:将内存中的对象状态保存在文件中或存储在数据库中时。

网络传输:网络传输数据时,无法直接传输对象,需要在传输前序列化,传输完成后反 序列化成对象,就像学习Socket编程中发送与接收时。

3.如何实现序列化

xml,json,protoBuf这三种工具都可以实现序列化和反序列化

4.ProtoBuf是什么

ProtoBuf是让数据结构序列化的方法,具有以下特点:

  • 语言无关、平台无关:即 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台。
  • 高效:即比 XML 更小、更快、更为简单。
  • 扩展性、兼容性好:你可以更新数据结构,而不影响和破坏原有的旧程序。

二.ProtoBuf简单语法知识

1.文件规范

创建一个ProtoBuf文件,后缀一定以.proto结尾,文件命名全小写字母,多个字母之间以_为分隔符,添加注释的方法和C/C++的一毛一样。

2.ProtoBuf语法分类

ProtoBuf有对个版本,在这里我们使用最新的版本,protobuf3的语法,简称proto3,它是最新的ProtoBuf语法版本。proto3 简化了 ProtoBuf 语言,既易于使用,又可以在更广泛的编程语言中使用。它允许你使用 Java,C++,Python等多种语言生成 protocol buffer 代码。

3.ProtoBuf3语法

syntax

我们在创建一个文件时,一定要在文件开头致命你所使用的语法,如果没有指定,默认使用Protobuf2的语法来使用,

比如:

syntax = "proto3";

syntax为指定语法,一定要有,就像函数中一定要指明函数返回类型一样。

package(声明符)

package 是一个可选的声明符,能表示 .proto 文件的命名空间,在项目中要有唯一性。它的作用是为了避免我们定义的消息出现冲突,就像c++中的namespace一样,指定一个域,防止冲突。

比如:

package person;

message(定义消息)

消息(message):要定义的结构化对象,可以给这个结构化对象中定义对用的属性内容,就像C//C++中的结构体一样,只能够定义对象。

这里强调下为什么要定义对象:

网络传输中,需要传输双方定制协议,定制协议就是定制结构体或者结构化数据,比如:Tcp,Udp。而且将数据存储在数据库时,需将数据统一为对象组织起来,在进行存储。

所以ProtoBuf就是以message的方式支持我们定义协议字段的。比如:

syntax="proto3";
package=package;
message people
{
        ;      
}

定义字段消息

在 message 中我们可以定义其属性字段,字段定义格式为:字段类型 字段名 = 字段唯一编号,比如:

syntax="proto3";
package=package;
message people
{
       int32 age=1;     
}

int32为字段类型 age为字段名 1为字段唯一编号

下面是protobuf创建类型及所对应的C/C++类型:

注:[1] 变长编码是指:经过protobuf 编码后,原本4字节或8字节的数可能会被变为其他字节数。

比如:

syntax="proto3"
package sss
message person
{
    string name=1;
    int32 age=2;
    string sex=3;
}

注意:字段唯一编号范围:

2^0~2^29-1,其中,19000~19999不可用,在 Protobuf 协议的实现中,对这些数进行了预留。如果非要在.proto文件中使用这些预留标识号,例如将 name 字段的编号设置为19000,编译时就会报警。值得一提的是,范围为 1 ~ 15 的字段编号需要一个字节进行编码, 16 ~ 2047 内的数字需要两个字节进行编码。编码后的字节不仅只包含了编号,还包含了字段类型。所以 1 ~ 15 要用来标记出现非常频繁的字段,要为将来有可能添加的、频繁出现的字段预留一些出来。

通过上面的学习,我们来实现一个简单的test.proto文件,

syntax="proto3";
package sss;
message person
{
    string name=1;
    int32 age=2;
    string sex=3;
}

编辑protobuf命令如下:proto --cpp_out. [文件名]

我们使用的是C++语言,所以编译之后生成两个文件:test.pb.h和test.pb.cc。

对于编译生成的 C++ 代码,包含了以下内容 :

  • 对于每个 message ,都会生成一个对应的消息类。
  • 在消息类中,编译器为每个字段提供了获取和设置方法,以及一下其他能够操作字段的方法。
  • 编辑器会针对于每个 .proto 文件生成.h 和 .cc 文件,分别用来存放类的声明与类的实现。

代码过长,就不展示了,感兴趣的自己做下!!!

编译命令行格式

protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto

protoc:是protobuf提供的命令行编辑工具。

--proto_path: 指定被编辑的所在目录,可多次指定,可简写为-I。如不指定,则在当前目录下进行搜索。

--cpp_out=: 指定编译后的文件为c++文件。

DST_DIR; 文件路径

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

上述的例子中,每个字段都有设置和获取的方法,getter 的名称与小写字段完全相同,setter 方法以 set_ 开头,每个字段都有一个clear_的方法,可以将字段设置为empty状态。比如:

#include <iostream>
#include "test.ph.h"
using namespace sss;
using namespace std;
int main()
{
    person p1;
    p1.set_name("张三");
    p1.set_age(11);
    p1.set_sex("nan");
    return 0;
}

在消息类的父类MessageLift中,提供了读写消息实例的方法,包括序列化方法和反序列化方法。

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成员函数,因为序列化不会改变类对象的内容, 而是将序列化的结果保存到函数入参指定的地址中。

更多详细API函数参考:message.h | Protocol Buffers Documentation (protobuf.dev)

序列化和反序列化的使用:看结果

#include "contact.pb.h"
using namespace std;
int main()
{
    string people_str;
    contact::PersonInit p1;
    p1.set_age(100);
    p1.set_name("张三");
    p1.set_sex("boy");
    if (!p1.SerializeToString(&people_str)) 
    {
        cout << "序列化联系人失败." << endl;
    }
    // 打印序列化结果
    cout << "序列化后的 people_str: " << people_str << endl;
    if(!p1.SerializePartialToString(&people_str))
    {
        cout<<"反序列化失败:"<<endl;
    }
    cout<<"反序列化成功:"<<endl<<people_str<<endl;
    return 0;
}

这就是简单的Protobuf的应用。

接下来是ProtoBuf的语法详解。

三.ProtoBuf语法详解

在这部分,我们使用一个简单的项目推进的方式来讲解语法内容,通过一个通讯录的实现,完成我们从最基础的内容到网络版本的知识。

1.字段规则

singular :消息中可以包含该字段零次或一次(不超过一次)。 proto3 语法中,字段默认使用该规则,可以不用设置。

repeated :消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了一个数组。

比如:

syntax="proto3";
package contacts2;
message person
{
    string name=1;
    int32 age=2;
    repeated string phone_num=3;
}

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

在单个.proto文件中,可以定义多个message,也可以嵌套定义,每个message中的字段编号可以重复,并且,消息字段也可以作为字段类型来使用,比如:

syntax="proto3";
package contacts2;
// message Phone{
//     string phone_num=1;
// }
message person
{
    string name=1;
    int32 age=2;
    //第一种,单独写法
    //repeated Phone=3;
    //第二种,嵌套写法
    message Phone{
        string phone_num=1;
    }
    repeated Phone=3;
}

也可以将其他.proto文件中定义的消息导入并使用,但一定要加import 比如:

syntax="proto3";
package contacts2;
import "Photo.proto";//将其他文件中的消息字段导入
message person
{
    string name=1;
    int32 age=2;
    //第三种
    repeated Phone.Phone phone=3;
}
syntax="proto3";
package Phone;
message Phone{
    string phone_num=1;
}

注:在 proto3 文件中可以导入 proto2 消息类型并使用它们,反之亦然

编译

我们可以将以上的内容稍加修改,变为我们的第一版通讯录:

syntax="proto3";
package contacts2;
//import "Photo.proto";//将其他文件中的消息字段导入
message peopleInfo
{
    string name=1;
    int32 age=2;
    message Phone{
        string phone_num=1;
    }
    repeated Phone=3;
}
message Contact{
    repeated PeopleInfo contact=1;
}

然后编译,编译后生成的 contacts.pb.h contacts.pb.cc 会将老版本的代码覆盖掉,由于代码过长,就不展示啦!!!

上述的代码中:

我们可以对编译好的代码试用一下:

#include <iostream>
#include <fstream>
#include "contact.pb.h"
using namespace std;
using namespace contacts;
void AddPeopleInfo(PeopleInfo *people_info_ptr)
{
    cout << "-------------新增联系人-------------" << endl;
    cout << "请输入联系人姓名: ";
    string name;
    getline(cin, name);
    people_info_ptr->set_name(name);//输入用set_函数
    cout << "请输入联系人年龄: ";
    int age;
    cin >> age;
    people_info_ptr->set_age(age);
    cin.ignore(256, '\n');
    for (int i = 1;; i++)
    {
        cout << "请输入联系人电话" << i << "(只输入回车完成电话新增): ";
        string number;
        getline(cin, number);
        if (number.empty())
        {
            break;
        }
        //number需要添加才可以用
        PeopleInfo_Phone *phone = people_info_ptr->add_phone();
        phone->set_number(number);
    }
    cout << "-----------添加联系人成功-----------" << endl;
}
int main()
{
    Contacts contacts;
    // 先读取已存在的 contacts
    fstream input("text.bin",ios::in | ios::binary);
    if (!input)
    {
        cout << ": File not found. Creating a new file." << endl;
    }
    else if (!contacts.ParseFromIstream(&input))//序列化
    {
        cerr << "Failed to parse contacts." << endl;
        input.close();
        return -1;
    }
    // 新增一个联系人
    AddPeopleInfo(contacts.add_contacts());
    //向磁盘文件写入新的 contacts
    fstream output("test.bin",ios::out | ios::trunc | ios::binary);
    if (!contacts.SerializeToOstream(&output))
    {
        cerr << "Failed to write contacts." << endl;
        input.close();
        output.close();
        return -1;
    }
    input.close();
    //output.close();
    return 0;
}

3:enum类型的使用

语法支持我们定义枚举类型来使用,我们可以定义一个enum类型的使用,

syntax="proto3";
package Phone;
message{
    enum PhoneType{
        MP=0;//移动电话
        TEL=1;//固定电话
    }
}

要注意枚举类型的定义有以下几种规则:
1. 0 值常量必须存在,且要作为第一个元素。这是为了与 proto2 的语义兼容:第一个元素作为默认值,且值为 0。
2. 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
3. 枚举的常量值在 32 位整数的范围内。但因负值无效因而不建议使用(与编码规则有关)。

定义时注意:
将两个 ‘具有相同枚举值名称’ 的枚举类型放在单个 .proto 文件下测试时,编译后会报错:某某某常量已经被定义!所以这里要注意:
• 同级(同层)的枚举类型,各个枚举类型中的常量不能重名。
• 单个 .proto 文件下,最外层枚举类型和嵌套枚举类型,不算同级。
• 多个 .proto 文件下,若一个文件引入了其他文件,且每个文件都未声明 package,每个 proto 文件中的枚举类型都在最外层,算同级。
• 多个 .proto 文件下,若一个文件引入了其他文件,且每个文件都声明了 package,不算同级。

下面我们来使用一下:

message Address
{
    string home_address=1;
    string unit_address=2;
}
message PeopleInfo
{
    string name=1;
    int32 age=2;
    message Phone
    {
        string number=1;
        enum PhoneType 
        {
            MP = 0; // 移动电话
            TEL = 1; // 固定电话
        }
        PhoneType type=2;
    }
    //多组电话
    repeated Phone phone=3;
}

编译生成后的文件:对于在.proto文件中定义的枚举类型,编译生成的代码中会含有与之对应的枚举类型、校验枚举值是否有效的方法 _IsValid、以及获取枚举值名称的方法 _Name。对于使用了枚举类型的字段,包含设置和获取字段的方法,已经清空字段的方法clear_;

4.any类型

字段还可以声明为 Any 类型,可以理解为泛型类型。使用时可以在 Any 中存储任意消息类型。Any 类型的字段也用 repeated 来修饰,Any 类型是 google 已经帮我们定义好的类型,在安装 ProtoBuf 时,其中的 include 目录下查找所有google 已经定义好的 .proto 文件,在此目录下查找:/usr/local/protobuf/include/google/protobuf/

我们可以来使用一下:

message Address
{
    string home_address=1;
    string unit_address=2;
}
message PeopleInfo
{
    string name=1;
    int32 age=2;
    message Phone
    {
        string number=1;
        enum PhoneType 
        {
            MP = 0; // 移动电话
            TEL = 1; // 固定电话
        }
        PhoneType type=2;
    }
    //多组电话
    repeated Phone phone=3;
    google.protobuf.Any data = 4;
    oneof other_contact 
    {
        // repeated string qq = 5;  // 不能使用 repeated
        string qq = 5;
        string wechat = 6;
    }
    //备注信息
    map<string,string> remake=7;    
}

上述的代码中,对于 Any 类型字段:
设置和获取:获取方法的方法名称与小写字段名称完全相同。设置方法可以使用 mutable_ 方
法,返回值为Any类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行
修改;之前说过,我们可以在 Any 字段中存储任意消息类型,这就要涉及到任意消息类型 和 Any 类型的互转。这部分代码就在 Google为我们写好的头文件 any.pb.h 中。使用方法:

方法使用:

Address address;
cout << "请输入联系人家庭地址: ";
string home_address;
getline(cin, home_address);
address.set_home_address(home_address);
cout << "请输入联系人单位地址: ";
string unit_address;
getline(cin, unit_address);
address.set_unit_address(unit_address);
google::protobuf::Any * data = people_info_ptr->mutable_data();
data->PackFrom(address);
if (people.has_data() && people.data().Is<Address>()) 
{
    Address address;
    people.data().UnpackTo(&address);
    if (!address.home_address().empty()) 
    {
        cout << "家庭地址:" << address.home_address() << endl;
    }
    if (!address.unit_address().empty()) 
    {
    cout << "单位地址:" << address.unit_address() << endl;
    }
}

5.oneof 类型

如果消息中有很多可选字段, 并且将来同时只有一个字段会被设置, 那么就可以使用 oneof 加强这个行为,也能有节约内存的效果。

oneof other_contact 
 {
    // repeated string qq = 5;  // 不能使用 repeated
    string qq = 5;
    string wechat = 6;
 }

注意:

    std::cout << "请选择要添加的其他联系方式(1、qq     2、wechat):";
    int other_contact;
    std::cin>>other_contact;
    std::cin.ignore(256, '\n');
    if(other_contact==1)
    {
        std::cout<<"输入QQ: ";
        std::string qq;
        getline(std::cin, qq);
        people->set_qq(qq);
    }
    else if(other_contact==2)
    {
        std::cout<<"输入wechat: ";
        std::string wechat;
        getline(std::cin, wechat);
        people->set_wechat(wechat);
    }
    else std::cout<<"输入错误!"<<std::endl;
    switch (people.other_contact_case())
    {
        case PeopleInfo::OtherContactCase::kQq:
            cout << "qq号: " << people.qq() << endl;
            break;
        case PeopleInfo::OtherContactCase::kWeixin:
            cout << "微信号: " << people.weixin() << endl;
            break;
        case PeopleInfo::OtherContactCase::OTHER_CONTACT_NOT_SET:
            break;
     }

6.map类型

语法支持创建一个关联映射字段,也就是可以使用 map 类型去声明字段类型,格式为:
map<key_type, value_type> map_field = N;
要注意的是:
• key_type 是除了 float 和 bytes 类型以外的任意标量类型。 value_type 可以是任意类型。
• map 字段不可以用 repeated 修饰。
• map 中存入的元素是无序的。

使用:

map<string, string> remark = 7; // 备注

清空map: clear_ 方法
• 设置和获取:获取方法的方法名称与小写字段名称完全相同。设置方法为 mutable_ 方法,返回
值为Map类型的指针,这类方法会为我们开辟好空间,可以直接对这块空间的内容进行修改。

7.默认值

反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:

8.更新消息

更新规则·

保留字段:reserved

  • 如果通过 删除 或 注释掉 字段来更新消息类型,未来的用户在添加新字段时,有可能会使用以前已经存在,但已经被删除或注释掉的字段编号。将来使用该 .proto 的旧版本时的程序会引发很多问题:数据损坏、隐私错误等等。
  •         确保不会发生这种情况的一种方法是:使用 reserved 将指定字段的编号或名称设置为保留项 当我们再使用这些编号或名称时,protocol buffer 的编译器将会警告这些编号或名称不可用。

未知字段

未知字段:解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表示方式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。
本来,proto3 在解析消息时总是会丢弃未知字段,但在 3.5 版本中重新引入了对未知字段的保留机制。所以在 3.5 或更高版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。

9.UnknownFieldSet 类介绍

  • UnknownFieldSet 包含在分析消息时遇到但未由其类型定义的所有字段。
  • 若要将 UnknownFieldSet 附加到任何消息,请调用 Reflection::GetUnknownFields()。
  • 类定义在 unknown_field_set.h 中。

10.UnknownField 类介绍

  • 表示未知字段集中的一个字段。
  •  类定义在 unknown_field_set.h 中。

使用:

        const Reflection *reflection = PeopleInfo::GetReflection();
        const UnknownFieldSet &unknowSet = reflection->GetUnknownFields(people);
        for (int j = 0; j < unknowSet.field_count(); j++)
        {
            const UnknownField &unknow_field = unknowSet.field(j);
            cout << "未知字段" << j + 1 << ":"
                 << " 字段编号: " << unknow_field.number()
                 << " 类型: " << unknow_field.type();
            switch (unknow_field.type())
            {
            case UnknownField::Type::TYPE_VARINT:
                cout << " 值: " << unknow_field.varint() << endl;
                break;
            case UnknownField::Type::TYPE_LENGTH_DELIMITED:
                cout << " 值: " << unknow_field.length_delimited() << endl;
                break;
            }
        }

11.前后兼容性

前后兼容的作用,当我们维护一个很大的分布式系统时,由于无法同时升级说有模块,为了在保证升级过程中,整个系统能够尽可能不受影响,就需要尽量保证通讯协议的“向后兼容”或“向前兼容”。

12.选项 option

proto 文件中可以声明许多选项,使用 option 标注。选项能影响 proto 编译器的某些处理方式。

选项的完整列表在google/protobuf/descriptor.proto中定义。文件分为字段级,消息级,文件级,但并没有所有的选项能作用于所有类型,常见举例

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

本地版本代码实现:

四:网络版本通讯录的实现

Protobuf 还常用于通讯协议、服务端数据交换场景。那么在这个示例中,我们将实现一个网络版本通讯录,模拟实现客户端与服务端的交互,通过 Protobuf 来实现各端之间的协议序列化。

1.环境搭建

Httplib 库:cpp-httplib 是个开源的库,是一个c++封装的http库,使用这个库可以在linux、
windows平台下完成http客户端、http服务端的搭建。使用起来非常方便,只需要包含头文件
httplib.h 即可。编译程序时,需要带上 -lpthread 选项。
镜像仓库:weixin_30886717 / cpp-httplib · GitCode

centos 下编写的注意事项:
如果使用 centOS 环境,yum源带的 g++ 最新版本是4.8.5,发布于2015年,年代久远。编译该项目会出现异常。将 gcc/g++ 升级为更高版本可解决问题。

实现:

// 自定义异常类
class ContactException
{
private:
    std::string message;
public:
    ContactException(std::string str = "A problem") : message{str} {}
    std::string what() const { return message; }
};
#include <iostream>
#include "contact.pb.h"
#include "ContactException.h"
void menu()
{
    std::cout << "-----------------------------------------------------" << std::endl
              << "--------------- 请选择对通讯录的操作 ----------------" << std::endl
              << "------------------ 1、新增联系人 --------------------" << std::endl
              << "------------------ 2、删除联系人 --------------------" << std::endl
              << "------------------ 3、查看联系人列表 ----------------" << std::endl
              << "------------------ 4、查看联系人详细信息 ------------" << std::endl
              << "------------------ 0、退出 --------------------------" << std::endl
              << "-----------------------------------------------------" << std::endl;
}
int main()
{
    enum OPERATE
    {
        QUIT = 0,
        ADD,
        DEL,
        FIND_ALL,
        FIND_ONE
    };
    while (true)
    {
        menu();
        std::cout << "---> 请选择:";
        int choose;
        std::cin >> choose;
        std::cin.ignore(256, '\n');
        try
        {
            switch (choose)
            {
            case OPERATE::ADD:
                contactsServer.addContact();
                break;
            case OPERATE::DEL:
                contactsServer.delContact();
                break;
            case OPERATE::FIND_ALL:
                contactsServer.findContacts();
                break;
            case OPERATE::FIND_ONE:
                contactsServer.findContact();
                break;
            case 0:
                std::cout << "---> 程序已退出" << std::endl;
                return 0;
            default:
                std::cout << "---> 无此选项,请重新选择!" << std::endl;
                break;
            }
        }
        catch (const ContactException &e)
        {
            std::cerr << "---> 操作通讯录时发现异常!!!" << std::endl
                      << "---> 异常信息:" << e.what() << std::endl;
        }
        catch (const std::exception &e)
        {
            std::cerr << "---> 操作通讯录时发现异常!!!" << std::endl
                      << "---> 异常信息:" << e.what() << std::endl;
        }
    }
}
class ContactsServer
{
public:
    void addContact();
    void delContact();
    void findContacts();
    void findContact();
private:
    void buildAddContactRequest(add_contact_req::AddContactRequest* req);
    void printFindOneContactResponse(find_one_contact_resp::FindOneContactResponse&
    resp);
    void printFindAllContactsResponse(find_all_contacts_resp::FindAllContactsResponse&
        resp);
};

五:总结:

到此这篇关于C/C++ProtoBuf使用的文章就介绍到这了,更多相关C++ProtoBuf使用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文