【protobuf】3. 基础知识

快速上⼿

在快速上⼿中,会编写第⼀版本的通讯录 1.0。在通讯录 1.0 版本中,将实现:
• 对⼀个联系⼈的信息使⽤ PB 进⾏序列化,并将结果打印出来。
• 对序列化后的内容使⽤ PB 进⾏反序列,解析出联系⼈信息并打印出来。
• 联系⼈包含以下信息: 姓名、年龄。
通过通讯录 1.0能了解使⽤ ProtoBuf 初步要掌握的内容,以及体验到 ProtoBuf 的完整使⽤流程。

步骤1:创建 .proto ⽂件

⽂件规范

• 创建 .proto ⽂件时,⽂件命名应该使⽤全⼩写字⺟命名,多个字⺟之间⽤ _ 连接。 例如:lower_snake_case.proto 。
• 书写 .proto ⽂件代码时,应使⽤ 2 个空格的缩进。
我们为通讯录 1.0 新建⽂件: contacts.proto

添加注释

向⽂件添加注释,可使⽤ // 或者 /* … */

指定 proto3 语法

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

syntax = "proto3";

package 声明符

package 是⼀个可选的声明符,能表⽰ .proto ⽂件的命名空间,在项⽬中要有唯⼀性。它的作⽤是为了避免我们定义的消息出现冲突。
在通讯录 1.0 的 contacts.proto ⽂件中,可以声明其命名空间,内容如下:

syntax = "proto3";
package contacts;

在这里插入图片描述

定义消息(message)

消息(message): 要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。
这⾥再提⼀下为什么要定义消息?
在⽹络传输中,我们需要为传输双⽅定制协议。定制协议说⽩了就是定义结构体或者结构化数据,⽐如,tcp,udp 报⽂就是结构化的。
再⽐如将数据持久化存储到数据库时,会将⼀系列元数据统⼀⽤对象组织起来,再进⾏存储。
所以 ProtoBuf 就是以 message 的⽅式来⽀持我们定制协议字段,后期帮助我们形成类和⽅法来使⽤。
在通讯录 1.0 中我们就需要为 联系⼈ 定义⼀个 message。
.proto ⽂件中定义⼀个消息类型的格式为:

message 消息类型名{
}
// 消息类型命名规范:使⽤驼峰命名法,⾸字⺟⼤写。

为 contacts.proto(通讯录 1.0)新增联系⼈message,内容如下:

syntax = "proto3";
package contacts;
// 定义联系⼈消息
message PeopleInfo {
}

在这里插入图片描述

定义消息字段

在 message 中我们可以定义其属性字段,字段定义格式为:字段类型 字段名 = 字段唯⼀编号;
• 字段名称命名规范:全⼩写字⺟,多个字⺟之间⽤ _ 连接。
• 字段类型分为:标量数据类型 和 特殊类型(包括枚举、其他消息类型等)。
• 字段唯⼀编号:⽤来标识字段,⼀旦开始使⽤就不能够再改变。

该表格展⽰了定义于消息体中的标量数据类型,以及编译 .proto ⽂件之后⾃动⽣成的类中与之对应的字段类型。在这⾥展⽰了与 C++ 语⾔对应的类型。

.proto 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^32 。string
bytes可包含任意的字节序列但⻓度不能超过 2^32 。string

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

更新 contacts.proto (通讯录 1.0),新增姓名、年龄字段

syntax = "proto3";
package contacts;
message PeopleInfo {
 string name = 1; 
 int32 age = 2; 
}

在这⾥还要特别讲解⼀下字段唯⼀编号的范围:

1 ~ 536,870,911 (2^29 - 1) ,其中 19000 ~ 19999 不可⽤。

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

 // 消息中定义了如下编号,代码会告警:
// Field numbers 19,000 through 19,999 are reserved for the protobuf implementation
string name = 19000;

值得⼀提的是,范围为 1 ~ 15 的字段编号需要⼀个字节进⾏编码, 16 ~ 2047 内的数字需要两个字节进⾏编码。编码后的字节不仅只包含了编号,还包含了字段类型。所以 1 ~ 15 要⽤来标记出现⾮常频繁的字段,要为将来有可能添加的、频繁出现的字段预留⼀些出来。

步骤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 ⽂件后会⽣成什么

编译 contacts.proto ⽂件后,会⽣成所选择语⾔的代码,我们选择的是C++,所以编译后⽣成了两个⽂件: contacts.pb.h contacts.pb.cc。
对于编译⽣成的 C++ 代码,包含了以下内容 :
• 对于每个 message ,都会⽣成⼀个对应的消息类。
• 在消息类中,编译器为每个字段提供了获取和设置⽅法,以及⼀下其他能够操作字段的⽅法。
• 编辑器会针对于每个 .proto ⽂件⽣成 .h 和 .cc ⽂件,分别⽤来存放类的声明与类的实现。

contacts.pb.h 部分代码展⽰

class PeopleInfo final : public ::PROTOBUF_NAMESPACE_ID::Message {
public:
 using ::PROTOBUF_NAMESPACE_ID::Message::CopyFrom;
 void CopyFrom(const PeopleInfo& from);
 using ::PROTOBUF_NAMESPACE_ID::Message::MergeFrom;
 void MergeFrom( const PeopleInfo& from) {
 PeopleInfo::MergeImpl(*this, from);
 }
 static ::PROTOBUF_NAMESPACE_ID::StringPiece FullMessageName() {
 return "PeopleInfo";
 }
 // string name = 1;
 void clear_name();
 const std::string& name() const;
 template <typename ArgT0 = const std::string&, typename... ArgT>
 void set_name(ArgT0&& arg0, ArgT... args);
 std::string* mutable_name();
 PROTOBUF_NODISCARD std::string* release_name();
 void set_allocated_name(std::string* name);
 // int32 age = 2;
 void clear_age();
 int32_t age() const;
 void set_age(int32_t value);
};

上述的例⼦中:
• 每个字段都有设置和获取的⽅法, getter 的名称与⼩写字段完全相同,setter ⽅法以 set_ 开头。
• 每个字段都有⼀个 clear_ ⽅法,可以将字段重新设置回 empty 状态。

contacts.pb.cc 中的代码就是对类声明⽅法的⼀些实现,在这⾥就不展开了。

到这⾥就存在一点疑惑了,那之前提到的序列化和反序列化⽅法在哪⾥呢?
在消息类的⽗类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成员函数,因为序列化不会改变类对象的内容, ⽽是将序列化的结果保存到函数⼊参指定的地址中。
• 详细 message API 可以参⻅ 完整列表

步骤3:序列化与反序列化的使⽤

创建⼀个测试⽂件 main.cc,⽅法中我们实现:
• 对⼀个联系⼈的信息使⽤ PB 进⾏序列化,并将结果打印出来。
• 对序列化后的内容使⽤ PB 进⾏反序列,解析出联系⼈信息并打印出来。
main.cc

#include <iostream> 
#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; 
 }
}

代码书写完成后,编译 main.cc,⽣成可执⾏程序 TestProtoBuf :

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

• -lprotobuf:必加,不然会有链接错误。
• -std=c++11:必加,使⽤C++11语法。
执⾏ TestProtoBuf ,可以看⻅ people 经过序列化和反序列化后的结果:
在这里插入图片描述
由于 ProtoBuf 是把联系⼈对象序列化成了⼆进制序列,这⾥⽤ string 来作为接收⼆进制序列的容器。
所以在终端打印的时候会有换⾏等⼀些乱码显⽰。
所以相对于 xml 和 JSON 来说,因为被编码成⼆进制,破解成本增⼤,ProtoBuf 编码是相对安全的。

⼩结 ProtoBuf 使⽤流程

  1. 编写 .proto ⽂件,⽬的是为了定义结构对象(message)及属性内容。
  2. 使⽤ protoc 编译器编译 .proto ⽂件,⽣成⼀系列接⼝代码,存放在新⽣成头⽂件和源⽂件中。
  3. 依赖⽣成的接⼝,将编译⽣成的头⽂件包含进我们的代码中,实现对 .proto ⽂件中定义的字段进⾏设置和获取,和对 message 对象进⾏序列化和反序列化。
    总的来说:ProtoBuf 是需要依赖通过编译⽣成的头⽂件和源⽂件来使⽤的。有了这种代码⽣成机制,开发⼈员再也不⽤吭哧吭哧地编写那些协议解析的代码了(⼲这种活是典型的吃⼒不讨好)。
hx@LAPTOP-H2EI4I6A:~/code/protobuf/fast_start$ protoc --cpp_out=. contacts.proto
hx@LAPTOP-H2EI4I6A:~/code/protobuf$ protoc -I fast_start/ --cpp_out=fast_start/ contacts.proto

两种编译方式:加 -I 选项就是指定文件的搜索路径(就是后面的contacts.proto文件路径) 默认从当前路径下查找

代码

contacts.proto 文件代码如下:

// 语法规定:首行(有效行:不包括注释)必须是语法指定行
/* */
// proto3 支持更多语言的使用 C#...
syntax = "proto3";
package contacts;

// 定义联系人message
message PeopleInfo {
    string name  = 1; // 姓名
    int32 age = 2;    // 年龄

    // 其中1,2表示的是序列编号
}

main.cc 文件代码如下:

#include <iostream>
#include "contacts.pb.h" // 联系人信息就包含在生成的头文件当中


int main()
{            
    std::string people_str; // 因为是to string 方法所以定义string来接收
    // 将序列化和反序列化用大括号隔开 这样定义两个people就不会冲突
    {
            // • 对⼀个联系⼈的信息使⽤ PB 进⾏序列化,并将结果打印出来。
            contacts::PeopleInfo people;
            people.set_name("张三");
            people.set_age(34);

        /* 这就是SerializeToString方法 按住ctrl跳转至该方法定义
            bool MessageLite::SerializeToString(std::string* output) const {
        output->clear();
        return AppendToString(output);
        }
        */    
            if(!people.SerializeToString(&people_str)) // 序列化方法定义在PeopleInfo的父类当中
            {
                std::cerr << "序列化联系人失败!" <<std::endl;
                return -1;
            }
            std::cout << "序列化成功,结果为" << people_str << std::endl;
    }
    

    {
            //• 对序列化后的内容使⽤ PB 进⾏反序列,解析出联系⼈信息并打印出来。
            contacts::PeopleInfo people;
            if(!people.ParseFromString(people_str))
            {
                std::cerr << "反序列化联系人失败!" << std::endl;
                return -2;
            }
            std::cout   << "反序列化成功!"<< std::endl
                        << "姓名:" << people.name() << std::endl
                        << "年龄:" << people.age() << std::endl;
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值