Protobuf快速入门

Protobuf入门

Protobuf介绍

Protobuf (Protocol Buffers) 是谷歌开发的一款无关平台,无关语言,可扩展,轻量级高效的序列化结构的数据格式,用于将自定义数据结构序列化成字节流,和将字节流反序列化为数据结构。所以很适合做数据存储和为不同语言,不同应用之间互相通信的数据交换格式,只要实现相同的协议格式,即后缀为proto文件被编译成不同的语言版本,加入各自的项目中,这样不同的语言可以解析其它语言通Protobuf序列化的数据。目前官方提供c++,java,go等语言支持。

Protobuf特点

  • 语言无关、平台无关:即Protobuf支持java、C++、Python等多种语言。
  • 高效:即比XML更小、更快、更简单。
  • 扩展性、兼容性好:可以更新数据结构,而不影响和破坏原有的旧程序。
  • 使用特定:Protobuf是需要依赖通过编译生成的头文件和源文件来使用。

工作流程:

Protobuf快速入门_第1张图片

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

什么是序列化和反序列化

  • 序列化过程:是指把一个对象变成二进制内容。
  • 反序列化过程:把一个二进制内容变回对象。

Protobuf入门实战

实现目标如下:

• 对⼀个联系⼈的信息使⽤PB进⾏序列化,并将结果打印出来。
• 对序列化后的内容使⽤PB进⾏反序列,解析出联系⼈信息并打印出来。
• 联系⼈包含以下信息:姓名、年龄。

步骤如下:

  • 编写.proto文件,定义message
  • 编译.proto文件,查看生成的代码
  • 实现对一个联系人的序列化和反序列化操作

编写.proto文件

创建contacts.proto文件

Protocol Buffers语⾔版本3,简称proto3,是.proto⽂件最新的语法版本。proto3简化了Protocol Buffers语⾔,既易于使⽤,⼜可以在更⼴泛的编程语⾔中使⽤。它允许你使⽤Java,C++,Python等多种语⾔⽣成protocol buffer代码。在.proto⽂件中,要使⽤syntax = “proto3”; 来指定⽂件语法为proto3,并且必须写在除去注释内容的第⼀⾏。如果没有指定,编译器会使⽤proto2语法。

syntax="proto3"; //消息类型命名规范:使⽤驼峰命名法,⾸字⺟⼤写。
package contacts;

//定义联系人消息
message PeopleInfo{
    string name=1;
    string age=2;
}

Protobuf标量数据

protoType 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

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

字段的唯一编号

字段编号的范围为1536,870,911(2^29-1),其中1900019999不可⽤。

19000~19999不可⽤是因为:在Protobuf协议的实现中,对这些数进⾏了预留。如果⾮要在.proto⽂件中使⽤这些预留标识号,例如将name字段的编号设置为19000,编译时就会报警:

Protobuf快速入门_第2张图片

编译.proto文件

protoc [--proto_path=IMPORT_PATH] --cpp_out=DST_DIR path/to/file.proto
protoc 是 Protocol Buffer 提供的命令⾏编译⼯具。

--proto_path 指定 被编译的.proto⽂件所在⽬录,可多次指定。可简写成 -IIMPORT_PATH 。如不指定该参数,则在当前⽬录进⾏搜索。当某个.proto ⽂件 import 其他.proto ⽂件时,或需要编译的 .proto ⽂件不在当前⽬录下,这时就要⽤-I来指定搜索⽬录。

--cpp_out= 指编译后的⽂件为 C++ ⽂件。

DST_DIR 编译后⽣成⽂件的⽬标路径。

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

执行命令:protoc --cpp_out=. ./contacts.proto

Protobuf快速入门_第3张图片

编译contacts.proto⽂件后会⽣成什么

编译contacts.proto⽂件后,会⽣成所选择语⾔的代码,这里选择的是C++,所以编译后⽣成了两个⽂件: contacts.pb.h contacts.pb.cc。对于编译⽣成的C++代码,包含了以下内容:
• 对于每个message,都会⽣成⼀个对应的消息类。

• 在消息类中,编译器为每个字段提供了获取和设置⽅法,以及⼀下其他能够操作字段的⽅法。

• 编辑器会针对于每个.proto ⽂件⽣成 .h 和 .cc ⽂件,分别⽤来存放类的声明与类的实现。

Protobuf快速入门_第4张图片

该类是继承于Message类。且对于每一个成员,都有set方法和get方法获取和设置对应的成员属性。

Protobuf快速入门_第5张图片

而序列化和反序列化的方法,则是定义在其父类中:MessageLite

Protobuf快速入门_第6张图片

其接口主要如下:

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成员函数,因为序列化不会改变类对象的内容,⽽是将序列化的结果保存到函数⼊参指定的地址中
• 详细messageAPI可以参⻅完整列表。 Protocol Buffers Documentation (protobuf.dev)

实现序列化和反序列化

  • 对⼀个联系⼈的信息使⽤protobuf进⾏序列化,并将结果打印出来。
  • 然后将二进制数据使用protobuf进行反序列,并获取其内容。
  • 创建文件use_contacts.cpp
#include 
#include "contacts.pb.h"
using namespace std;

int main()
{

    string people_str;
    { 
        // .proto⽂件声明的package,通过protoc编译后,会为编译⽣成的C++代码声明同名的命名空间
        // 其范围是在.proto ⽂件中定义的内容
        contacts::PeopleInfo people;
        people.set_age("20");
        people.set_name("张三");
        // 调⽤序列化⽅法,将序列化后的⼆进制序列存⼊string中
        if (!people.SerializeToString(&people_str))
        {
            cout << "序列化联系⼈失败." << endl;
        }
        // 打印序列化结果
        cout << "序列化后的 people_str: " << people_str << endl;
    }
    {
        contacts::PeopleInfo people;
        // 调⽤反序列化⽅法,读取string中存放的⼆进制序列,并反序列化出对象
        if (!people.ParseFromString(people_str))
        {
            cout << "反序列化出联系⼈失败." << endl;
        }
        // 打印结果
        cout << "Parse age: " << people.age() << endl;
        cout << "Parse name: " << people.name() << endl;
    }

    return 0;
}

编译:g++ -o contacts use_contacts.cpp contacts.pb.cc -lprotobuf -std=c++11

Protobuf快速入门_第7张图片

注意:

  • 由于ProtoBuf是把对象序列化成了⼆进制序列,这⾥⽤string来作为接收⼆进制序列的容器。所以在终端打印的时候会有换⾏等⼀些乱码显⽰。
  • 所以相对于xml和JSON来说,因为被编码成⼆进制,破解成本增⼤,ProtoBuf编码是相对安全的。

protobuf语法详解

项目目标:升级的版本,最终将会升级如下内容:
• 不再打印联系⼈的序列化结果,⽽是将通讯录序列化后并写⼊⽂件中。
• 从⽂件中将通讯录解析出来,并进⾏打印。
• 新增联系⼈属性,共包括:姓名、年龄、电话信息、地址、其他联系⽅式、备注。

字段规则

  • singular:消息中可以包含该字段零次或⼀次(不超过⼀次)。proto3语法中,字段默认使⽤该
    规则。
  • repeated:消息中可以包含该字段任意多次(包括零次),其中重复值的顺序会被保留。可以理解为定义了⼀个数组。

比如下面的写法表示:在一个PeopleInfo信息里面,可以包含多个phone_num

//定义联系人消息
message PeopleInfo{
    string name=1;
    string age=2;
    repeated string phone_num=3;
}

消息类型的定义和使用

下面实现,在目前的contacts.proto文件中加入phone属性,并单独设置为一个message。

则一共有三种写法,包括嵌套,非嵌套写法,分开住两个文件中定义

嵌套写法

/--------------嵌套写法---------------/
message PeopleInfo{
    string name=1;
    string age=2;
    message Phone{
        string number=1;
    }
    repeated Phone phone=3;
}
//通讯录定义
message Contacts{
    repeated PeopleInfo people=1;
}

Protobuf快速入门_第8张图片

当为嵌套定义时,Phone在头文件中的类名就变为了PeopleInfo_Phone。也是使用下划线将内部与外部名连接起来。

非嵌套写法

message Phone{
    string number=1;
}

//定义联系人消息
message PeopleInfo{
    string name=1;
    string age=2;
    repeated Phone phone=3;
}

message Contacts{
    repeated PeopleInfo people=1;
}

Protobuf快速入门_第9张图片

分为两个文件

Protobuf快速入门_第10张图片

如果contacts2.proto文件中并没有定义package contacts2,那么文件1在使用message phone时,也不需要带上contacts2.。

Protobuf快速入门_第11张图片

实现通讯录v0.2

实现write.cc:向通讯录中写入内容

void AddPeopleInfo(contacts1::Contacts *contacts_ptr)
{
    for (int j = 0;; j++)
    {
        std::cout << "--------- 新增联系人(输入“退出”即可结束新增联系人) --------" << std::endl;
        std::cout << " 请输入联系人姓名: ";
        string name;
        getline(std::cin, name);
        if(name=="退出"){
            break;
        }
        contacts1::PeopleInfo *peopleinfo = contacts_ptr->add_people(); //添加一个people消息
        peopleinfo->set_name(name);
        std::cout << " 请输入联系人年龄: ";
        string age;
        getline(std::cin, age);
        peopleinfo->set_age(age);
        for (int i = 0;; i++)
        {
            std::cout << " 请输入联系人电话 " << i + 1 << "(只输入回车完成电话的新增): ";
            string number;
            getline(std::cin, number);
            if (number.empty())
            {
                break;
            }
            contacts1::Phone *phone = peopleinfo->add_phone();  //添加一个phone信息
            phone->set_number(number);
        }
    }

}

int main()
{
    contacts1::Contacts contacts;
    // 读取本地已经存在的联系人文件
    fstream input("contacts.bin", ios::in | ios::binary);
    if (!input)
    {
        std::cout << " contacts.bin not find,create new file! " << std::endl;
    }
    else if (!contacts.ParseFromIstream(&input))
    {
        std::cerr << " Failed to parse contacts.bin " << std::endl;
        input.close();
        return -1;
    }

    AddPeopleInfo(&contacts);
    // 向文件中覆盖写入新的内容
    fstream output("contacts.bin", ios::out | ios::binary | ios::trunc);
    if (!contacts.SerializeToOstream(&output))
    {
        std::cerr << " Failed to serialize ,write error! " << std::endl;
        output.close();
        input.close();
        return -1;
    }
    std::cout << " write success! " << std::endl;
    output.close();
    input.close();
    /*
        在程序结束时调⽤ ShutdownProtobufLibrary(),为了删除 Protocol Buffer 库分配的所
    有全局对象。对于⼤多数程序来说这是不必要的,因为该过程⽆论如何都要退出,并且操作系统将负责
    回收其所有内存。但是,如果你使⽤了内存泄漏检查程序,该程序需要释放每个最后对象,或者你正在
    编写可以由单个进程多次加载和卸载的库,那么你可能希望强制使⽤ Protocol Buffers 来清理所有
    内容。
    */
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

Protobuf快速入门_第12张图片

可以看到contacts.bin中以二进制的方式写入了内容:

Protobuf快速入门_第13张图片

hexdump程序

hexdump:是Linux下的⼀个⼆进制⽂件查看⼯具,它可以将⼆进制⽂件转换为ASCII、⼋进制、⼗进制、⼗六进制格式进⾏查看。

参数:

  • -C: 表⽰每个字节显⽰为16进制和相应的ASCII字符

Protobuf快速入门_第14张图片

实现read.cc:反序列化二进制文件的内容

void PrintContacts(contacts1::Contacts& contacts){
    for(int i=0;i<contacts.people_size();i++){
        contacts1::PeopleInfo peopleinfo=contacts.people(i);    //通过下标获取people
        std:cout<<"----------联系人"<<i+1<<"----------"<<std::endl;
        std::cout<<"联系人姓名: "<<peopleinfo.name()<<std::endl;
        std::cout<<"联系人年龄: "<<peopleinfo.age()<<std::endl;
        int j=1;
        for(;j<=peopleinfo.phone_size();j++){
            std::cout<<"电话: "<<peopleinfo.phone(j-1).number()<<std::endl;
        }
    }
}

int main(){
    contacts1::Contacts contacts;
    fstream input("contacts.bin",ios::binary | ios::in);
    if(!input){
        std::cout << " contacts.bin not find,create new file! " << std::endl;
    }
    else if(!contacts.ParseFromIstream(&input)){

        std::cerr<<" Failed to Parse contacts.bin !"<<std::endl;
        input.close();
        return -1;
    }
    PrintContacts(contacts);
    input.close();
    google::protobuf::ShutdownProtobufLibrary();
    return 0;
}

输出结果:

Protobuf快速入门_第15张图片

decode命令

我们可以⽤protoc -h命令来查看ProtoBuf为我们提供的所有命令option。

Protobuf快速入门_第16张图片

其中ProtoBuf提供⼀个命令选项 --decode ,表⽰从标准输⼊中读取给定类型的⼆进制消息,并将其以⽂本格式写⼊标准输出。消息类型必须在.proto⽂件或导⼊的⽂件中定义。

protoc --decode=MESSAGE_TYPE TYPE_PATH < STD_IN
可以指定解码一种消息类型,后面需要指明消息类型的路径 已经输入文件

protoc --decode=contacts.Contacts ./contacts.proto < contacts.bin

Protobuf快速入门_第17张图片

你可能感兴趣的:(中间件,开发语言,c++)