protobuf学习

protobuf的安装

  • Windos版安装
    下载地址:https://github.com/protocolbuffers/protobuf/releases 选择合适的版本下载
    将下载的压缩包解压,把解压后文件的bin目录的路径配置到系统环境比变量Path中
    在cmd 中执行 protoc --version 成功就说明安装成功了
  • linux版安装
  1. 安装依赖库
    Ubuntu
    sudo apt-get install autoconf automake libtool curl make g++ unzip -y
    CentOs
    sudo yum install autoconf automake libtool curl make gcc-c++ unzip
  2. 下载安装包
    也是到github protobuf的仓库找到合适的包,然后执行下面的命令
    wget https://github.com/protocolbuffers/protobuf/releases/download/v21.11/protobuf-all-
    21.11.zip
  3. 解压安装包
    解压后进入protobuf目录,之后如果安装全语言的就执行 ./autogen.sh 否则忽略
    执行 ./configure 或者 ./configure --prefix=/usr/local/protobuf
    前者是默认安装路径 后者是修改安装⽬录,统⼀安装在/usr/local/protobuf下
    下面依次执行
    make
    make check
    sudo make install
    如果在执行./configure是选择第二种方式那么还需要在/etc/profile 中添加⼀些内容
    sudo vim /etc/profile
#(动态库搜索路径) 程序加载运⾏期间查找动态链接库时指定除了系统默认路径之外的其他路径
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/protobuf/lib/
#(静态库搜索路径) 程序编译期间查找动态链接库时指定查找共享库的路径
export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/protobuf/lib/
#执⾏程序搜索路径
export PATH=$PATH:/usr/local/protobuf/bin/
#c程序头⽂件搜索路径
export C_INCLUDE_PATH=$C_INCLUDE_PATH:/usr/local/protobuf/include/
#c++程序头⽂件搜索路径
export CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/usr/local/protobuf/include/
#pkg-config 路径
export PKG_CONFIG_PATH=/usr/local/protobuf/lib/pkgconfig/

最后⼀步,重新执⾏ /etc/profile ⽂件
source /etc/profile
protoc --version 查看版本,有显⽰说明安装成功

protobuf 使用

步骤一:创建.proto文件

在一个目录下编写.proto文件

syntax="proto3";  // 首行指定 proto3 语法  否则默认使用proto2语法
package contacts;  // package 是⼀个可选的声明符,能表⽰ .proto ⽂件的命名空间,在项⽬中要有唯⼀性。它的作⽤是为
                    //了避免我们定义的消息出现冲突。

            
message PeopleInfo{   // 定义消息 就是我们要定义的结构化对象,要序列化反序列化的属性内容定义在其中
    string name=1;   //所以 ProtoBuf 就是以 message 的⽅式来⽀持我们定制协议字段,后期帮助我们形成类和⽅法来使⽤
    int32 age=2;
}
  • 定义消息字段
    在 message 中我们可以定义其属性字段,字段定义格式为:字段类型 字段名 = 字段唯⼀编号;
  1. 字段类型分为:标量数据类型 和 特殊类型(包括枚举、其他消息类型等)
  2. 字段唯⼀编号:⽤来标识字段,⼀旦开始使⽤就不能够再改变。
  • 消息字段标量数据类型与C++中对应的类型
.proto Type Notes C++ Type
double double
float float
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
bool bool
string 包含 UTF-8 和 ASCII 编码的字符串,⻓度不能超过2^32 string
bytes 可包含任意的字节序列但⻓度不能超过 2^32 string

变⻓编码是指:经过protobuf 编码后,原本4字节或8字节的数可能会被变为其他字节数
字段唯一编号的范围
1 ~ 536,870,911 (2^29 - 1) ,其中 19000 ~ 19999 不可⽤,为预留标识号,如果设置了这些编号,编译会报警,1~15只需一个字节进行编码,16 ~ 2047 内的数字需要两个字节进⾏编码。编码后的字节不仅只包含了编号,还包含了字段类型,所以为了提高传输效率应尽量使用小的编号

步骤2:编译 contacts.proto ⽂件,⽣成 C++ ⽂件

protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto
protoc       # 是 Protocol Buffer 提供的命令⾏编译⼯具。
--proto_path # 指定 被编译的.proto⽂件所在⽬录,可多次指定。
#可简写成 -I IMPORT_PATH 。如不指定该参数,则在当前⽬录进⾏
#搜索。当某个.proto ⽂件 import 其他.proto ⽂件时,或需要编译的 .proto ⽂件不在当前⽬录下,这时就要⽤-I来指定搜索⽬录。

--cpp_out=   # 指编译后的⽂件为 C++ ⽂件
OUT_DIR           #编译后⽣成⽂件的⽬标路径。
path/to/file.proto        # 要编译的.proto⽂件。
在当前路径编译 contacts.proto
protoc  --cpp_out=.   contacts.proto
在其他路径编译这个contacts.proto
protoc -I  /root/data/protobuf/fast_start --cpp_out= /root/data/protobuf/fast_start  contacts.proto

编译成功 生成相应的.h .cc文件

protobuf学习_第1张图片
对于编译⽣成的 C++ 代码,包含了以下内容 :
• 对于每个 message ,都会⽣成⼀个对应的消息类。
• 在消息类中,编译器为每个字段提供了获取和设置⽅法,以及⼀下其他能够操作字段的⽅法。
• 编辑器会针对于每个 .proto ⽂件⽣成 .h 和 .cc ⽂件,分别⽤来存放类的声明与类的实现。

  • 每个字段都有设置和获取的⽅法, getter 的名称与⼩写字段完全相同,setter ⽅法以 set_ 开头。

  • 每个字段都有⼀个 clear_ ⽅法,可以将字段重新设置回 empty 状态。

在消息类的⽗类MessageLite 中,提供了读写消息实例的⽅法,包括序列化⽅法和反序列化⽅法。

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

步骤3 引入编译生成的头文件测试序列化和反序列化

#include
#include
#include"contacts.pb.h"

using namespace std;


int main()
{
    contacts::PeopleInfo posttest;
    posttest.set_age(18);
    posttest.set_name("张三");
    string str;
    if(!posttest.SerializeToString(&str))
    {
        cout<<"Serialize fail"<<endl;
        return -1;
    }
    cout<<"Serialize success\tstr:"<<str<<endl;

    contacts::PeopleInfo recvtest;
    if(!recvtest.ParseFromString(str))
    {
        cout<<"Parse fail"<<endl;
        return -1;
    }
   cout<<"Parse success"<<endl
       <<"age:"<<recvtest.age()<<endl
       <<"name:"<<recvtest.name()<<endl;
    return 0;
}

protobuf学习_第2张图片

proto3 语法

  • 一个.proto文件可以定义多个message ,message可以看成是自定义类型,成为其他message的属性
  • message可以嵌套定义,而且隔离字段编号
  • .proto 可以使用 import 导入其他.proto文件,如果其他.proto文件定义的message使用了命名空间,在导入后只需使用name.message引入想使用的message既可以,没用命名空间隔离就直接使用message
  • message中嵌套message 字段也是通过mutable 返回那个字段的地址来设置
  • singular :消息中可以包含该字段零次或⼀次(不超过⼀次)。 proto3 语法中,字段默认使⽤该规则。也就是非数组类型
  • repeated :消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了⼀个数组 例如 repeated Phone phone 加入的方式是
 contacts::PeopleInfo_Phone* peo_phone = people->add_phone();
 然后设置 peo_phone
 支持迭代器遍历
  for (auto& phone : request.phone()) 
  • enum类型
    在.proto文件中我们可以定义枚举类型
enum PhoneType {
 MP = 0; // 移动电话
 TEL = 1; // 固定电话
}

注意事项

  1. 0 值常量必须存在,且要作为第⼀个元素。这是为了与 proto2 的语义兼容:第⼀个元素作为默认值,且值为 0
  2. 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)
  3. 枚举的常量值在 32 位整数的范围内。但因负值⽆效因⽽不建议使⽤(与编码规则有关)
    还要注意将两个 ‘具有相同枚举值名称’ 的枚举类型放在单个 .proto ⽂件下测试时,编译后会报错:某某某常量已经被定义!所以这⾥要注意:
    • 同级(同层)的枚举类型,各个枚举类型中的常量不能重名。
    • 单个 .proto ⽂件下,最外层枚举类型和嵌套枚举类型,不算同级。
    • 多个 .proto ⽂件下,若⼀个⽂件引⼊了其他⽂件,且每个⽂件都未声明 package,每个 proto ⽂件中的枚举类型都在最外层,算同级。
    • 多个 .proto ⽂件下,若⼀个⽂件引⼊了其他⽂件,且每个⽂件都声明了 package,不算同级。
    对应数据中没有主动设置时 枚举属性值默认是0值

在默认路径 /usr/local/protobuf/下面有protobuf的一些配置文件
有bin、include、lib
在include下面就有已经写好的各种头文件包括message序列化反序列化的头文件 message.h message_lite.h

  • == Any==类型
    是已经为我们定义好的一种message类型 相关的.proto文件就在上面所说的include 目录下。Any 类型可以理解为泛型类型。使⽤时可以在 Any 中存储任意消息类型。Any 类型的字段也可⽤ repeated 来修饰
    在any.pb.h中可以查看Any类型主要的方法,Any类型就是用来转换其他任意message的一种类型所以有以下的方法
class PROTOBUF_EXPORT Any final : public ::PROTOBUF_NAMESPACE_ID::Message {
 bool PackFrom(const ::PROTOBUF_NAMESPACE_ID::Message& message) {
 ...
 }
 bool UnpackTo(::PROTOBUF_NAMESPACE_ID::Message* message) const {
 ...
 }
 template<typename T> bool Is() const {
 return _impl_._any_metadata_.Is<T>();
 }
};
// 对应any类型,还有has_name 方法在解析时用来判断之前是否有设置过这个any类型
// bool类型返回值,来判断执行是否成功
 //使⽤ PackFrom() ⽅法可以将任意消息类型转为 Any 类型。
 //使⽤ UnpackTo() ⽅法可以将 Any 类型转回之前设置的任意消息类型。
 //使⽤ Is() ⽅法可以⽤来判断存放的消息类型是否为 typename T。
 //message中都有一个mutable_name()方法,对message中这个字段申请号空间返回相应类型地址让我们设置值
  • oneof类型
    如果消息中有很多可选字段, 并且将来同时只有⼀个字段会被设置, 那么就可以使⽤ oneof 加强这
    个⾏为,也能有节约内存的效果,对oneof类型里面的字段设置值,最终只会取最后一次设置的值,oneof类型不能是repeated
 oneof other_contact{  // oneof 中的字段编号与PeopleInfo 中的字段编号是处于同一级别
        string qq=5;
        string weixin=6;
    } 
oneof 类型对应生成的.h中常用的方法有
qq字段就有以下常用类型  weixin 相应的也有这些方法
 bool has_qq() const;
  const std::string& qq() const;
   std::string* mutable_qq();
    template <typename ArgT0 = const std::string&, typename... ArgT>
  void set_qq(ArgT0&& arg0, ArgT... args);

 inline PeopleInfo::OtherContactCase PeopleInfo::other_contact_case() const {
  return PeopleInfo::OtherContactCase(_impl_._oneof_case_[0]);
} //这个是返回设置了oneof中对应字段的字段编号
  • map类型
    语法⽀持创建⼀个关联映射字段,也就是可以使⽤ map 类型去声明字段类型
map<string,string>remark=7;  
key_type 是除了 float 和 bytes 类型以外的任意标量类型。 value_type 可以是任意类型
map 字段不可以⽤ repeated 修饰
 map 中存⼊的元素是⽆序的
有常规的获取方法,设置方法使用mutable_name()、name_size()判断数量
支持迭代器遍历
  • 默认值
    反序列化消息时,如果被反序列化的⼆进制序列中不包含某个字段,反序列化对象中相应字段时,就
    会设置为该字段的默认值。不同的类型对应的默认值不同:
    • 对于字符串,默认值为空字符串。
    • 对于字节,默认值为空字节。
    • 对于布尔值,默认值为 false。
    • 对于数值类型,默认值为 0。
    • 对于枚举,默认值是第⼀个定义的枚举值, 必须为 0。
    • 对于消息字段,未设置该字段。它的取值是依赖于语⾔。
    • 对于设置了 repeated 的字段的默认值是空的( 通常是相应语⾔的⼀个空列表 )。
    • 对于 消息字段 、 oneof字段 和 any字段 ,C++ 和 Java 语⾔中都有 has_ ⽅法来检测当前字段
    是否被设置。标量数据类型没有has_ 方法

  • 更新消息
    如果现有的消息类型已经不再满⾜我们的需求,例如需要扩展⼀个字段,在不破坏任何现有代码的情况下更新消息类型⾮常简单。遵循如下规则即可:
    • 禁⽌修改任何已有字段的字段编号。
    • 若是移除⽼字段,要保证不再使⽤移除字段的字段编号。正确的做法是保留字段编号
    (reserved),以确保该编号将不能被重复使⽤。不建议直接删除或注释掉字段。
    • int32, uint32, int64, uint64 和 bool 是完全兼容的。可以从这些类型中的⼀个改为另⼀个,
    ⽽不破坏前后兼容性。若解析出来的数值与相应的类型不匹配,会采⽤与 C++ ⼀致的处理⽅案
    (例如,若将 64 位整数当做 32 位进⾏读取,它将被截断为 32 位)。
    • sint32 和 sint64 相互兼容但不与其他的整型兼容。
    • string 和 bytes 在合法 UTF-8 字节前提下也是兼容的。
    • bytes 包含消息编码版本的情况下,嵌套消息与 bytes 也是兼容的。
    • fixed32 与 sfixed32 兼容, fixed64 与 sfixed64兼容。
    • enum 与 int32,uint32, int64 和 uint64 兼容(注意若值不匹配会被截断)。但要注意当反序
    列化消息时会根据语⾔采⽤不同的处理⽅案:例如,未识别的 proto3 枚举类型会被保存在消息
    中,但是当消息反序列化时如何表⽰是依赖于编程语⾔的。整型字段总是会保持其的值。
    • oneof:
    ◦ 将⼀个单独的值更改为 新 oneof 类型成员之⼀是安全和⼆进制兼容的。
    ◦ 若确定没有代码⼀次性设置多个值那么将多个字段移⼊⼀个新 oneof 类型也是可⾏的。
    ◦ 将任何字段移⼊已存在的 oneof 类型是不安全的

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

message test
{
  reserved 10,11,20,50 to 100;  # 保留字段和名称,后续就不能在使用了
  reserved "field1","field2";
}

protobuf版通讯录

syntax="proto3";
package contacts2;



message PeopleInfo
{
    int32 age=1;
    string name=2;
    message Phone
    {
        string number=1;
    }
    repeated Phone phone=3;
}

message contacts
{
    repeated PeopleInfo people=1;
}

writefile.cc

syntax="proto3";
package contacts2;



message PeopleInfo
{
    int32 age=1;
    string name=2;
    message Phone
    {
        string number=1;
    }
    repeated Phone phone=3;
}

message contacts
{
    repeated PeopleInfo people=1;
}

read.cc

#include 
#include 
#include "contacts.pb.h"

using namespace std;

void printinfo(const contacts2::contacts &info)
{
    for (int i = 0; i < info.people_size(); ++i) // 数组类型字段,用 属性名_size() 获取数量
    {
        cout << "第" << i + 1 << "联系人信息:" << endl;
        const contacts2::PeopleInfo &people = info.people(i); // 取出数组元素i为值
        cout << "age:" << people.age() << endl;
        cout << "name:" << people.name() << endl;
        for (int j = 0; j < people.phone_size(); ++j)//也可以直接使用范围for 
        {
            const contacts2::PeopleInfo_Phone &phone = people.phone(j);
            cout << "phone:" << j + 1 << "\t" << phone.number() << endl;
        }
    }
}
int main()
{
    contacts2::contacts data;
    fstream input("contacts.bin", ios::in | ios::binary);
    if (!input)
    {
        cout << "open fail" << endl;
        return -1;
    }
    if (!data.ParseFromIstream(&input))
    {
        cout << "Parse fail" << endl;
        input.close();
        return -1;
    }
    printinfo(data);
    input.close();
    return 0;
}

–decode protoc的参数

表⽰从标准输⼊中读取给定类型的⼆进制消息,并将其以⽂本格式写⼊
标准输出。 消息类型必须在 .proto ⽂件或导⼊的⽂件中定义,我们可以选择从文件中读取二进制消息,这样就不用再写read.cc来解析文件后打印来看了

 protoc --decode=contacts2.contacts contacts.proto < contacts.bin
 # =后面是命名空间.message 后面给出.proto文件,从contacts.bin中读取
 #转换为文本格式输入到标准输出

protobuf学习_第3张图片

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