冰冰学习笔记:简单了解protobuf

欢迎各位大佬光临本文章!!!

还请各位大佬提出宝贵的意见,如发现文章错误请联系冰冰,冰冰一定会虚心接受,及时改正。

本系列文章为冰冰学习编程的学习笔记,如果对您也有帮助,还请各位大佬、帅哥、美女点点支持,您的每一分关心都是我坚持的动力。

我的博客地址:bingbing~bang的博客_CSDN博客https://blog.csdn.net/bingbing_bang?type=blog

我的gitee:冰冰棒 (BingbingSuperEffort) - Gitee.comhttps://gitee.com/BingbingSuperEffort


目录

前言

1.初识protobuf

1.1protobuf简介

1.2快速使用protobuf

2.protobuf的语法介绍 

2.1 字段规则与消息类型的定义和使用

2.2 protoc命令选项

2.3 enum类型

2.4 Any类型

2.5 oneof类型

2.6 map类型

2.7 默认值

3.消息更新

3.1 更新规则

3.2 删除规则

3.3 未知字段       

4.option选项

5.实战使用:网络通信录


前言

        序列化与反序列化是我们在通讯传输和文件保存时常用的手段。尤其是在网络传输协议中,字符串与各种格式之间的转换也需要这种手段。protobuf,json,xml都是常用的集成库,json我们已经了解过了,而protobuf作为Google公司的产品,其具备的优点更是数不胜数。下面我们一起了解一下protobuf。

1.初识protobuf

1.1protobuf简介

        protobuf是Google公司内部的混合语言数据标准,是一种轻便高效的结构化数据存储格式,可以用于序列化与反序列化。他与语言无关,平台无关,可用于即时通讯、数据存储等领域。相比于xml和json,protobuf更加轻便,体量更小,解析速度更快。

        protobuf最常用的就是序列化与反序列化操作,那什么是序列化呢?序列化就是将对象转换为字节序列的过程,反序列化就是把字节序列恢复为对象的过程。在网络通信中,我们往往需要将传输的报文进行序列化操作,然后在发送给远端机器,远端接收到后,在通过反序列化操作将内容进行解析,这才真正得到发送的内容。

        protobuf作为一种轻量化的序列化工具,他的扩展性以及兼容性更加灵活,我们可以更新数据结构而无需担心影响和破坏原有的旧程序。protobuf最重要的特点就是需要依赖通过编译生成的头文件和源文件来使用。

1.2快速使用protobuf

        首先在使用protobuf之前我们需要先安装protobuf,大家可自行搜索查找安装流程,这里不再赘述。

        我们需要创建一个后缀为.proto的文件,在该文件中进行protobuf的编写逻辑。文件命名应该使用全小写字母命名,多个字母之间使用下划线 “_” 进行连接。例如first_pro.proto。

(1)指定语法

        进入文件后,我们需要指定protobuf的语法,我们常用的语法为proto3。proto3 简化了 Protocol Buffers 语⾔,既易于使⽤,⼜可以在更⼴泛的编程语⾔中使⽤。它允许你使⽤ Java,C++,Python 等多种语⾔⽣成 protocol buffer 代码。因此我们需要在首行添加如下代码:

syntax = "proto3";

  (2)package声明符

        package 是⼀个可选的声明符,能表示 .proto ⽂件的命名空间,在项⽬中要有唯⼀性。它的作⽤是为 了避免我们定义的消息出现冲突。在编译完成后,相当于C++中的命名空间。

 (3)定义消息及消息字段

         消息(message): 要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。实际上就是编译生成后的class。.proto文件的定义消息的格式如下:

syntax = "proto3";
package conntacts;

//定义信息
message PeopleInfo
{
    string name = 1;//姓名,=1 表示字段编号
    int32 age = 2;//年龄
}

        命名规范则采用驼峰法命名,首字母大写。

        name和age是我们在message中定义的属性字段,字段定义格式为:字段类型  字段名称 = 字段唯一编号;

        字段名称命名规范:全⼩写字⺟,多个字⺟之间⽤ _ 连接。

        字段类型分为:标量数据类型和特殊类型(包括枚举、其他消息类型等)。

        字段唯⼀编号:⽤来标识字段,⼀旦开始使⽤就不能够再改变。

        实际上,属性字段就对应的C++语言的成员变量,只不过后面不再是定义的默认值,而是定义的编号,用来在生成的.h文件中进行标识这些成员。

标量数据类型与C++类型对应表:

.protoc TypeNotesC++ Type
doubledouble
floatfloat
int32使用变长编码[1]。负数的编码效率较低⸺若字段可能为负值,应使用 sint32 代替int32
int64使用变长编码[1]。负数的编码效率较低⸺若字段可 能为负值,应使用 sint64 代替int64
uint32使用变⻓编码[1]。uint32
uint64使用变长编码[1]。uint64
sint32使用变长编码[1]。符号整型。负值的编码效率高于常规的 int32 类型int32
sint64使用变长编码[1]。符号整型。负值的编码效率高于 常规的 int64 类型。int64
fixed32定长4 字节。若值常大于2^28 则会⽐ uint32 更高 效。uint32
fixed64定长8 字节。若值常大于2^56 则会⽐ uint64 更高效uint64
sfixed32定长4 字节int32
sfixed64定长8 字节。int64
boolbool
string包含 UTF-8 和 ASCII 编码的字符串,⻓度不能超过 2^32string
bytes可包含任意的字节序列但⻓度不能超过 2^32。string

(4)编译命令

protoc -I path --cpp_out=DST_DIR path/to/file.proto

1.protoc 为编译命令

2.-I 指定被编译的.proto文件所在目录,可多次指定,当前指定为path

3.--cpp_out:表示编译生成C++文件

4.= 后面加生成文件的路径

        执行编译命令后,会在指定的文件目录中出现.h和.cc的C++文件,而我们在.proto文件中定义的message将会生成对应的类,而类中的操作方法则定义在生成的.h文件和.cc文件中。

        例如上文中定义的message,在生成的.h文件中就有如下方法:

        而对于序列化和反序列化的代码,则位于MessageLite类中,该类为message的父类。

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

(5)序列化与反序列化的使用

        下面的代码我们使用先前创建的message并且创建一个对象设置对应的信息,然后调用SerializeToString函数将其序列化为字符串。随后在将字符串内容调用ParseFromString函数反序列化出来。

#include<iostream>
#include"contacts.pb.h"
#include<string>
using namespace std;
int main()
{
    string peostr;
    {
        conntacts::PeopleInfo people;
        people.set_name("大兵");
        people.set_age(24);
        if(!people.SerializeToString(&peostr))
        {
            cout<<"序列化失败"<<endl;
            return 1;
        }
        cout<<"序列化成功"<<endl;
        cout<<"序列化结果:"<<peostr<<endl;
    }
    {
        conntacts::PeopleInfo people;
        if(!people.ParseFromString(peostr))
        {
            cout<<"反序列化失败"<<endl;
            return 1;
        }
        cout<<"反序列化成功"<<endl;
        cout<<"反序列化结果:"<<endl
            <<"name:"<<people.name()<<endl
            <<"age:"<<people.age()<<endl;
    }
    return 0;
}

(6)编译链接库

        正如C++引入其他库一样,在使用.proto文件生成的.h里面的函数时,我们也需要连接protobuf提供的第三方库protobuf。这里要注意,在连接protobuf库时,一定要增加-std=c++11,因为protobuf中使用了部分c++11的语法。

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

总结:protobuf的使用流程

  • 编写 .proto ⽂件,⽬的是为了定义结构对象(message)及属性内容
  • 使⽤ protoc 编译器编译 .proto ⽂件,⽣成⼀系列接⼝代码,存放在新⽣成头⽂件和源⽂件中
  • 依赖⽣成的接⼝,将编译⽣成的头⽂件包含进我们的代码中,实现对 .proto ⽂件中定义的字段进⾏ 设置和获取,和对 message 对象进⾏序列化和反序列化。

2.protobuf的语法介绍 

2.1字段规则与消息类型的定义和使用

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

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

        注:Proto3语法支持嵌套定义message,支持多个message定义在同一个文件,不同message中的字段编号并不冲突。

当我们想引入其他定义的.proto文件时,需要使用import引入。

import  “phone.proto”    //引入其他的 .proto文件

syntax = "proto3";
package contacts2;
import "google/protobuf/any.proto";//引入其他文件

message PeopleInfo
{
    string name = 1; //姓名,=1 表示字段编号
    int32 age = 2;   //年龄
    message Phone
    {
        string number=1;
    }
    repeated Phone phione=3;//电话  实际为一个数组
   
}

2.2 protoc命令选项

protoc -h :查看所有选项

protoc –decode=contacts2.Contacts contacts.proto < contacts.bin

        查看二进制文件的内容,将其转为我们认识的字符

        contacts2.Contacts:contacs2命名空间下的Contacts结构体的输出内容

        contacts.proto:该结构体存储在contacts.proto文件中

        contacts.bin:输出的二进制内容的文件

2.3 enum类型

        枚举类型,使⽤驼峰命名法,⾸字⺟⼤写。里面的常量值为全大写多个字⺟之间⽤ _ 连接。

enum PhoneType {
 MP = 0; // 移动电话
 TEL = 1; // 固定电话
}

定义特点:

  1. (1)0 值常量必须存在,且要作为第一个元素。这是为了与 proto2 的语义兼容:第⼀个元素作为默认值,且值为 0
  2. (2)枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)
  3. (3)枚举的常量值在 32 位整数的范围内。但因负值无效因而不建议使用(与编码规则有关)。
  4. (4)将两个 ‘具有相同枚举值名称’ 的枚举类型放在单个 .proto 文件下测试时,编译后会报错
  5. (5)同级(同层)的枚举类型,各个枚举类型中的常量不能重名
  6. (6)单个 .proto ⽂件下,最外层枚举类型和嵌套枚举类型,不算同级
  7. (7)多个 .proto ⽂件下,若一个文件引入了其他文件,且每个文件都未声明 package,每个 proto 文件中的枚举类型都在最外层,算同级
  8. (8)多个 .proto ⽂件下,若⼀个文件引入了其他文件,且每个文件都声明了 package,不算同级。

2.4 Any类型

        字段还可以声明为 Any 类型,可以理解为泛型类型。使用时可以在 Any 中存储任意消息类型。Any 类型的字段也可用 repeated 来修饰。

        Any 类型是 google 已经帮我们定义好的类型,在安装 ProtoBuf 时,其中的 include 目录下查找所有google 已经定义好的 .proto 文件。

        引入:import "google/protobuf/any.proto";  使用

  1. 使用PackFrom() 方法可以将任意消息类型转为 Any 类型
  2. 使用 UnpackTo() 方法可以将 Any 类型转回之前设置的任意消息类型
  3. 使用Is()方法可以用来判断存放的消息类型是否为typename T
message Address{
    string home_address =1;
    string unit_address =2;
}

message PeopleInfo
{
    string name = 1; //姓名,=1 表示字段编号
    int32 age = 2;   //年龄
    message Phone
    {
        string number=1;
        enum PhoneType
        {
            MP=0;//移动电话
            TEL=1;//固定电话
        }
        PhoneType type =2;
    }
    repeated Phone phione=3;//电话
    google.protobuf.Any data = 4;//添加any类型
}

        在输入处进行any类型的绑定

        contacts2::Address address;//定义Address类
        cout<<"请输入联系人家庭地址:";
        string homeadd;
        getline(cin,homeadd);
        address.set_home_address(homeadd);
        cout<<"请输入联系人单位地址:";
        string unitadd;
        getline(cin,unitadd);
        address.set_unit_address(unitadd);
        google::protobuf::Any* data = pcont->mutable_data();
        data->PackFrom(address);//将Any类型绑定为Address类型

        在读取处进行数据读取

        if(people.has_data()&&people.data().Is<contacts2::Address>())
        {
            contacts2::Address addr;
            people.data().UnpackTo(&addr);
            if(!addr.home_address().empty())
            {
                cout<<"家庭地址:"<<addr.home_address()<<endl;
            }
            if(!addr.unit_address().empty())
            {
                cout<<"单位地址:"<<addr.unit_address()<<endl;
            }
        }

        has_data()方法为检测是否绑定了data类型,Is<Type>()方法为检测绑定的类型是否为Type。

2.5 oneof类型

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

        我们不能使用repeated,只会保留最后设置的内容,并且字段编号不能与其他字段重复。

//.proto文件中message新增oneof修饰的字段
    oneof other_contact{
     string qq=5;
     string wechat=6;
     
    }

//写入:
        cout<<"请选择联系方式:1.qq 2.wechat";
        int other_contact;
        cin>>other_contact;
        cin.ignore(256,'\n');
        if(1==other_contact)
        {
            cout<<"请输入qq: ";
            string qq;
            getline(cin,qq);
            pcont->set_qq(qq);
        }
        else if(2==other_contact)
        {
             cout<<"请输入wechat: ";
            string wechat;
            getline(cin,wechat);
            pcont->set_wechat(wechat);
        }
        else{
            cout<<"选择错误"<<endl;
        }

//读取:
        switch (people.other_contact_case())
        {
        case contacts2::PeopleInfo::OtherContactCase::kQq:
            cout<<"qq号码:"<<people.qq()<<endl;
            break;
        case contacts2::PeopleInfo::OtherContactCase::kWechat:
            cout<<"wechat号码:"<<people.wechat()<<endl;
            break;
        
        default:
            break;
        }

2.6 map类型

语法支持创建一个关联映射字段,也就是可以使用 map 类型去声明字段类型,格式为:

map<key_type, value_type> map_field = N

  1. key_type 是除了 float 和 bytes 类型以外的任意标量类型。 value_type 可以是任意类型
  2. map 字段不可以用 repeated 修饰
  3. map 中存入的元素是无序的
//.proto文件中新增map
map<string,string> remake = 7;

//写入map类型的备注
for(int i=1;;i++)
{
   cout<<"请输入备注"<<i<<"标签:";
   string remark_key;
   getline(cin,remark_key);
   if(remark_key.empty())
   {
      break;
   }
   cout<<"请输入备注"<<i<<"内容:";
   string remark_value;
   getline(cin,remark_value);
   pcont->mutable_remake()->insert({remark_key,remark_value});
}

//读取类型为map的备注信息
if(!people.remake().empty())
{
   cout<<"备注信息:"<<endl;
}
for(auto it = people.remake().cbegin();it!=people.remake().cend();it++)
{
   cout<<"   "<<it->first<<": "<<it->second<<endl;
}

 

2.7 默认值

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

        对于标量数据类型,proto3语法下没有生成has_方法。

        对于消息字段、oneof字段和any字段 ,C++ 和 Java 语言中都有 has_ 方法来检测当前字段是否被设置。

        各类型的默认值设置:

类型默认值
字符串空字符串
字节空字节
布尔值false
数值类型0
枚举第一个被定义的默认值,必须为0
消息字段依赖于语言取值
repeated修饰后

3.消息更新

3.1 更新规则

        如果现有的消息类型已经不再满⾜我们的需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情况下更新消息类型非常简单。

        我们只需要保证更新的字段的名称和编号不要和老字段冲突即可。

        对于修改字段则具备以下规则:

  1. 禁止修改任何已有字段的字段编号。
  2. int32, uint32, int64, uint64 和 bool 是完全兼容的。可以从这些类型中的一个改为另一个, 而不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,会采用与 C++ 一致的处理方案 (例如,若将 64 位整数当做 32 位进行读取,它将被截断为 32 位)。
  3. sint32 和 sint64 相互兼容但不与其他的整型兼容。
  4. string 和 bytes 在合法 UTF-8 字节前提下也是兼容的。
  5. bytes 包含消息编码版本的情况下,嵌套消息与 bytes 也是兼容的。
  6. fixed32 与 sfixed32 兼容, fixed64 与 sfixed64兼容。

  7. enum 与 int32,uint32, int64 和 uint64 兼容(注意若值不匹配会被截断)。但要注意当反序列化消息时会根据语言采用不同的处理方案:例如,未识别的 proto3 枚举类型会被保存在消息中,但是当消息反序列化时如何表示是依赖于编程语言的。整型字段总是会保持其的值。

  8. oneof: 将一个单独的值更改为新oneof 类型成员之一是安全和二进制兼容的。 若确定没有代码一次性设置多个值那么将多个字段移入一个新 oneof 类型也是可行的。 将任何字段移入已存在的 oneof 类型是不安全的。

3.2 删除规则

         对于删除字段,则具备下列规则:

        不能直接删除字段。若是移除老字段,要保证不再使用移除字段的字段编号。正确的做法是保留字段编号 (reserved),以确保该编号将不能被重复使用。不建议直接删除或注释掉字段。

       reserved关键字,保留字段编号。使用reserved 将指定字段的编号或名称设置为保留项 。当我们再使用这些编号或名称时,protocol buffer 的编译器将会警告这些编号或名称不可用。

        reserved可以指定编号也可以指定名称。

reserved 2,10,100 to 200; //保留2,10,100到200的编号

reserved “age”;//保留“age”字段

3.3 未知字段       

        未知字段:解析结构良好的 protocol buffer 已序列化数据中的未识别字段的表示方式。例如,当旧程序解析带有新字段的数据时,这些新字段就会成为旧程序的未知字段。

        本来,proto3 在解析消息时总是会丢弃未知字段,但在 3.5 版本中重新引⼊了对未知字段的保留机制。所以在 3.5 或更高版本中,未知字段在反序列化时会被保留,同时也会包含在序列化的结果中。

        例如,我们定义了一个client和一个server两个通信端,并且原本约定的字段类型为名字和年龄,但是后面我们将server端的年龄字段保留,并且新增了生日字段,而client并没有更改,此时我们按照之前的通信方式,client接收到的年龄字段将会是默认值,而生日字段将是未知字段。

        如果我们想打印出来未知字段,就需要使用如下代码:

void PrintCon(c_contacts::PeopleContacts& con)
{
    for(int i=0;i<con.cont_size();i++)
    {
        cout<<"-------------联系人"<<i+1<<"--------------------"<<endl;
        const c_contacts::PeopleInfo& people = con.cont(i);
        cout<<"姓名:"<<people.name()<<endl;
        cout<<"年龄:"<<people.age()<<endl;
        for(int j=0;j<people.phione_size();j++)
        {
            const c_contacts::PeopleInfo_Phone& phone = people.phione(j);
            cout<<"电话"<<j+1<<":"<<phone.number();
            cout<<"  ("<<phone.PhoneType_Name(phone.type())<<")"<<endl;
        }
//获取未知字段
       const Reflection* reflection = PeopleInfo::GetReflection();
       const UnknownFieldSet& unknowSet = reflection->GetUnknownFields(people);

       for(int j=0;j<unknowSet.field_count();j++)
       {
        const UnknownField& fild = unknowSet.field(j);
        cout<<"未知字段"<<j+1<<":"<<"字段编号:"<<fild.number()<<"类型:"<<fild.type();
        switch(fild.type())
        {
            case UnknownField::Type::TYPE_VARINT:
                cout<<" 值:"<<fild.varint()<<endl;
                break;
            case UnknownField::Type::TYPE_LENGTH_DELIMITED:
                cout<<" 值:"<<fild.length_delimited()<<endl;
                break;
        }
       }
    }
}

4.option选项

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

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

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

        SPEED : protoc 编译器将⽣成的代码是⾼度优化的,代码运⾏效率⾼,但是由此⽣成的代码 编译后会占⽤更多的空间。 SPEED 是默认选项。

         CODE_SIZE : proto 编译器将⽣成最少的类,会占⽤更少的空间,是依赖基于反射的代码来 实现序列化、反序列化和各种其他操作。但和 SPEED 恰恰相反,它的代码运⾏效率较低。这种⽅式适合⽤在包含⼤量的.proto⽂件,但并不盲⽬追求速度的应⽤中。

         LITE_RUNTIME : ⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常 少。这是以牺牲Protocol Buffer提供的反射功能为代价的,仅仅提供 encoding+序列化功能, 所以我们在链接 BP 库时仅需链接libprotobuf-lite,⽽⾮libprotobuf。这种模式通常⽤于资源 有限的平台,例如移动⼿机平台中。

  option optimize_for = LITE_RUNTIME;

        allow_alias : 允许将相同的常量值分配给不同的枚举常量,⽤来定义别名。该选项为枚举选项。 举个例⼦:

enum PhoneType {
 option allow_alias = true;
 MP = 0;
 TEL = 1;
 LANDLINE = 1; // 若不加 option allow_alias = true; 这⼀⾏会编译报错
}

5.实战使用:网络通信录

代码仓库:https://gitee.com/BingbingSuperEffort/protobuf

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bingbing~bang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值