protobuf实现原理 protobuf优缺点

文章目录

  • 简介
  • proto3 与 proto2 的区别
  • 定义数据结构
    • 字段类型
    • 字段编号
    • 字段规则
    • 添加更多消息类型
    • 添加注释
    • 保留字段
    • 默认值
    • 定义枚举
  • 编译.proto 文件
    • protoc编译
    • cmake编译
      • protobuf_generate_cpp
      • execute_process
  • 示例1:定义proto
  • 实例2:proto文件读写
  • 参考资料

简介

Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件。他们用于 RPC 系统和持续数据存储系统。

Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化、或者说序列化。它很适合做数据存储或RPC数据交换格式。可以用于即时通讯、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式

Google的Protobuf了,相比于它的前辈xml、json,它的体量更小,解析速度更快,所以在 IM 这种通信应用中,非常适合将 Protobuf 作为数据传输格式。

protobuf的核心内容包括:

  • 定义消息:消息的结构体,以message标识。
  • 定义接口:接口路径和参数,以service标识。

通过protobuf提供的机制,服务端与服务端之间只需要关注接口方法名(service)和参数(message)即可通信,而不需关注繁琐的链路协议和字段解析,极大降低了服务端的设计开发成本。

查看版本

protoc --version //查看版本

  • 1

proto3 与 proto2 的区别

proto3 比 proto2 支持更多语言但 更简洁。去掉了一些复杂的语法和特性,更强调约定而弱化语法

  • 在第一行非空白非注释行,必须写:syntax = “proto3”;

  • 字段规则移除了 “required”,并把 “optional” 改名为 “singular”;

  • proto3 repeated标量数值类型默认packed,而proto2默认不开启

    在 proto2 中,需要明确使用 [packed=true] 来为字段指定比较紧凑的 packed 编码方式

  • 语言增加 Go、Ruby、JavaNano 支持;

  • proto2可以选填default,而proto3只能使用系统默认的

    在 proto2 中,可以使用 default 选项为某一字段指定默认值。在 proto3 中,字段的默认值只能根据字段类型由系统决定。也就是说,默认值全部是约定好的,而不再提供指定默认值的语法

  • proto3必须有一个零值,以便我们可以使用 0 作为数字默认值。零值需要是第一个元素,以便与proto2语义兼容,其中第一个枚举值始终是默认值。proto2则没有这项要求。

  • roto3在3.5版本之前会丢弃未知字段。但在 3.5 版本中,重新引入了未知字段的保留以匹配 proto2 行为。在 3.5 及更高版本中,未知字段在解析过程中保留并包含在序列化输出中

  • proto3移除了proto2的扩展,新增了Any(仍在开发中)和JSON映射

定义数据结构

syntax = "proto3"; message Person { string name = 1; int32 id = 2; string email = 3; }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

字段类型

Protobuf定义了一套基本数据类型

proto文件消息类型 C++ 类型 说明
double double 双精度浮点型
float float 单精度浮点型
int32 int32 使用可变长编码方式,负数时不够高效,应该使用sint32
int64 int64 使用可变长编码方式,负数时不够高效,应该使用sint32
unit32 unit32 使用可变长编码方式
unit64 unit64 使用可变长编码方式
sint32 int32 使用可变长编码方式,有符号的整型值,负数编码时比通常的int32高效
sint64 sint64 使用可变长编码方式,有符号的整型值,负数编码时比通常的int64
fixed32 unit32 总是4个字节,如果数值总是比2^28大的话,这个类型会比uint32高效
fixed64 unit64 总是8个字节,如果数值总是比2^56大的话,这个类型会比uint64高效
sfixed32 int32 总是4个字节
sfixed64 int64 总是8个字节
bool bool 布尔类型
string string 一个字符串必须是utf-8编码或者7-bit的ascii编码的文本
bytes string 可能包含任意顺序的字节数据

字段编号

消息定义中的每个字段都有一个唯一的编号。这些字段编号用于以二进制格式标识您的字段,一旦您的消息类型被使用,就不应该被更改。

Tag的取值范围最小是1,最大是229229-1,但但 19000~19999 是 protobuf 预留的,用户不能使用。

虽然 编号的定义范围比较大,但不同 编号也会对 protobuf 编码带来一些影响:

  • 1 ~ 15:单字节编码
  • 16 ~ 2047:双字节编码

使用频率高的变量最好设置为1~15,这样可以减少编码后的数据大小,但由于编号一旦指定不能修改,所以为了以后扩展,也记得为未来保留一些 1~15 的 编号

字段规则

  • singular: 可以有零个或其中一个字段(但不超过一个)。

  • repeated: 该字段可以重复任意次数(包括零次)。重复值的顺序将被保留。

在proto 3中,可扩展的repeated字段为数字类型的默认编码。

在proto2中,规则为:

  • required:必须有一个
  • optional:0或者1个
  • repeated:任意数量(包括0)

添加更多消息类型

可以在单个.proto中定义多种消息类型。如果您要定义多个相关消息,这很有用——例如,如果您想定义与搜索响应消息类型相对应的回复消息格式,可以将其添加到该.proto中:

message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; } message SearchResponse { ... }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

添加注释

proto 添加注释,使用 C/C++风格的 // 或者 /* … */ 语法.

保留字段

如果通过完全删除某个字段或对其进行注释来更新消息类型,将来的用户可以在对该类型进行自己的更新时重用该字段编号。如果他们以后加载旧版本的相同.proto文件,这可能会导致严重的问题 ,包括数据损坏、隐私漏洞等。

可以把它的变量名或 字段编号 用 reserved 标注,这样,当这个 Tag 或者变量名字被重新使用的时候,编译器会报错

message Foo { // 注意,同一个 reserved 语句不能同时包含变量名和 Tag reserved 2, 15, 9 to 11; reserved "foo", "bar"; }

  • 1
  • 2
  • 3
  • 4
  • 5

默认值

当解析 message 时,如果被编码的 message 里没有包含某些变量,那么根据类型不同,他们会有不同的默认值:

  • string:默认是空的字符串
  • byte:默认是空的bytes
  • bool:默认为false
  • numeric:默认为0
  • enums:定义在第一位的枚举值,也就是0
  • messages:根据生成的不同语言有不同的表现

收到数据后反序列化后,对于标准值类型的数据,比如bool,如果它的值是 false,那么我们无法判断这个值是对方设置的,还是对方压根就没给这个变量设置值。

定义枚举

在 protobuf 中,我们也可以定义枚举,并且使用该枚举类型,比如:

message SearchRequest { string query = 1; int32 page_number = 2; // Which page number do we want int32 result_per_page = 3; // Number of results to return per page enum Corpus { UNIVERSAL = 0; WEB = 1; IMAGES = 2; LOCAL = 3; NEWS = 4; PRODUCTS = 5; VIDEO = 6; } Corpus corpus = 4; }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

枚举定义在一个消息内部或消息外部都是可以的,如果枚举是 定义在 message 内部,而其他 message 又想使用,那么可以通过 MessageType.EnumType 的方式引用。定义枚举的时候,我们要保证第一个枚举值必须是0,枚举值不能重复,除非使用 option allow_alias = true 选项来开启别名。如:

enum EnumAllowingAlias { option allow_alias = true; UNKNOWN = 0; STARTED = 1; RUNNING = 1; }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

枚举值的范围是32-bit integer,但因为枚举值使用变长编码,所以不推荐使用负数作为枚举值,因为这会带来效率问题

编译.proto 文件

在.proto 文件中定义了数据结构,这些数据结构是面向开发者和业务程序的,并不面向存储和传输。当需要把这些数据进行存储或传输时,就需要将这些结构数据进行序列化、反序列化以及读写。ProtoBuf 提供相应的接口代码,可以通过 protoc这个编译器来生成相应的接口代码,命令如下:

protoc编译

protoc -I=$SRC_DIR--cpp_out=$DST_DIR$SRC_DIR/xxx.proto # $SRC_DIR: .proto 所在的源目录# --cpp_out: 生成 c++ 代码# $DST_DIR: 生成代码的目标目录# xxx.proto: 要针对哪个 proto 文件生成接口代码

  • 1
  • 2
  • 3
  • 4
  • 5

cmake编译

protobuf_generate_cpp

find_package(Protobuf REQUIRED) include_directories(${Protobuf_INCLUDE_DIRS}) # pb.cc文件路径 pb.h文件路径 protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS person.proto)

  • 1
  • 2
  • 3
  • 4
  • 5

有两个缺点:

  • 要求protobuf_generate_cpp命令和生成add_executable() 或 add_library() 的命令必须在同一个CMakeList中.
  • 无法设置源码的生成路径,只能默认在相应的build中生成

execute_process

以使用cmake中的execute_process命令调用protoc程序来自定义生成源码的路径

find_package(Protobuf REQUIRED) include_directories(${Protobuf_INCLUDE_DIRS}) execute_process(COMMAND ${PROTOBUF_PROTOC_EXECUTABLE} -I=${PROJECT_SOURCE_DIR}/proto/ --cpp_out=${PROJECT_SOURCE_DIR}/ ${PROJECT_SOURCE_DIR}/proto/xxx.proto) add_executable(file main.cpp ${PROJECT_SOURCE_DIR}/xxx.pb.cc file.cpp)

  • 1
  • 2
  • 3
  • 4

这种方法仍然存在缺点:每次执行cmake后,都会重新生成proto源码,导致make时会因为源码变动(内容未变,只是重新生成)而重新编译程序

示例1:定义proto

定义proto

syntax = "proto3"; // 声明是为了防止不同项目之间的命名冲突,编译生成的类将被放置在一个与 package 名相同的命名空间中。 package tutorial; message Student { // 字段编号:消息定义中的每个字段都有一个唯一的编号。这些字段编号用于以二进制格式标识您的字段,一旦您的消息类型被使用,就不应该被更改 uint64 id = 1; string name = 2; // singular修饰符修饰的字段可以是0次或者1次。但是当定制协议,用该修饰符修饰的字段都报错 // singular string email = 3; string email = 3; enum PhoneType { MOBILE = 0; //proto3版本中,首成员必须为0,成员不应有相同的值 HOME = 1; } message PhoneNumber { string number = 1; PhoneType type = 2; } // repeated: 该字段可以重复任意次数(包括零次)。重复值的顺序将被保留 repeated PhoneNumber phone = 4; }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

Protobuf API

看看读写类,编译器为每个字段生成读写函数

// optional uint64 id = 1; void clear_id(); static const int kIdFieldNumber = 1; ::google::protobuf::uint64 id() const; void set_id(::google::protobuf::uint64 value); // optional string name = 2; void clear_name(); static const int kNameFieldNumber = 2; const ::std::string& name() const; void set_name(const ::std::string& value); void set_name(const char* value); void set_name(const char* value, size_t size); ::std::string* mutable_name(); ::std::string* release_name(); void set_allocated_name(::std::string* name); // optional string email = 3; void clear_email(); static const int kEmailFieldNumber = 3; const ::std::string& email() const; void set_email(const ::std::string& value); void set_email(const char* value); void set_email(const char* value, size_t size); ::std::string* mutable_email(); ::std::string* release_email(); void set_allocated_email(::std::string* email); // repeated .tutorial.Student.PhoneNumber phone = 4; int phone_size() const; void clear_phone(); static const int kPhoneFieldNumber = 4; const ::tutorial::Student_PhoneNumber& phone(int index) const; ::tutorial::Student_PhoneNumber* mutable_phone(int index); ::tutorial::Student_PhoneNumber* add_phone(); ::google::protobuf::RepeatedPtrField< ::tutorial::Student_PhoneNumber >* mutable_phone(); const ::google::protobuf::RepeatedPtrField< ::tutorial::Student_PhoneNumber >& phone() const;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

基本函数:

  • set_*函数:设置字段值
  • clear_*函数:用来将字段重置到空状态

数值类型的字段 id 就只有基本读写函数,string类型的name和email有额外的函数:

  • mutable_*函数:数返回 string 的直接指针

重复的字段也有一些特殊的函数——如果你看一下重复字段 phone 的那些函数,就会发现你可以:

  • 得到重复字段的 _size(Person 关联了多少个电话号码)。

  • 通过索引(index)来获取一个指定的电话号码。

  • mutable_phone函数:通过指定的索引(index)来更新一个已经存在的电话号码。

  • add_phone函数:向消息(message)中添加另一个电话号码

标准消息函数

voidCopyFrom(constStudent&from);voidMergeFrom(constStudent&from);voidClear();bool IsInitialized()const;

  • 1
  • 2
  • 3
  • 4

序列化和反序列化

bool SerializeToString(string*output)const;//将消息序列化并储存在指定的string中。注意里面的内容是二进制的,而不是文本;我们只是使用string作为一个很方便的容器。bool ParseFromString(conststring&data);//从给定的string解析消息。bool SerializeToArray(void*data,intsize)const//将消息序列化至数组bool ParseFromArray(constvoid*data,intsize)//从数组解析消息bool SerializeToOstream(ostream*output)const;//将消息写入到给定的C++ ostream中。bool ParseFromIstream(istream*input);//从给定的C++ istream解析消息。

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

实例2:proto文件读写

下面演示一个简单例子,读写函数已经封装好了,大家可以自行调用!

pointpillars.conf:

pfe_file: "models/v1_v2_v3/pfe.trt" rpn_file: "models/v1_v2_v3/rpn.trt"

  • 1
  • 2

pointpillars_config.proto:

syntax = "proto3"; message PointPillarsConfig { string pfe_file = 1; string rpn_file = 2; }

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

main.cpp

bool SetProtoToASCIIFile(constgoogle::protobuf::Message &message,intfile_descriptor){using google::protobuf::TextFormat;using google::protobuf::io::FileOutputStream;using google::protobuf::io::ZeroCopyOutputStream;if(file_descriptor <0){std::cout <<"Invalid file descriptor.";returnfalse;}ZeroCopyOutputStream *output =new FileOutputStream(file_descriptor);bool success =TextFormat::Print(message,output);delete output;close(file_descriptor);returnsuccess;}bool GetProtoFromASCIIFile(conststd::string&file_name,google::protobuf::Message*message){using google::protobuf::TextFormat;using google::protobuf::io::FileInputStream;using google::protobuf::io::ZeroCopyInputStream;intfile_descriptor =open(file_name.c_str(),O_RDONLY);if(file_descriptor <0){std::cout <<"Failed to open file "<<file_name <<" in text mode.";// Failed to open;returnfalse;}ZeroCopyInputStream*input =new FileInputStream(file_descriptor);bool success =TextFormat::Parse(input,message);if(!success){std::cout <<"Failed to parse file "<<file_name <<" as text proto.";}delete input;close(file_descriptor);returnsuccess;}#include<iostream>#include<string>#include"pointpillars_config.pb.h"#include"file.h"intmain(intargc,char*argv[]){// 将此宏放在main函数中(使用 protobuf 库之前的某个位置), 以验证您链接的版本是否与您编译的头文件匹配。 如果检测到版本不匹配,该过程将中止GOOGLE_PROTOBUF_VERIFY_VERSION;PointPillarsConfig config;std::string config_file ="../config/pointpillars.conf";GetProtoFromFile(config_file,&config);std::cout <<config.pfe_file()<<std::endl;google::protobuf::ShutdownProtobufLibrary();}

protobuf实现原理 protobuf优缺点

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

CMakeLists.txt:

CMAKE_MINIMUM_REQUIRED(VERSION 3.10) project(file) find_package(Protobuf REQUIRED) include_directories( ${Protobuf_INCLUDE_DIRS} ${GLOB_INCLUDE_DIRS} ${CMAKE_CURRENT_BINARY_DIR} ) # protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS proto/pointpillars_config.proto) # add_executable(file main.cpp ${PROTO_SRCS} file.cpp) execute_process(COMMAND ${PROTOBUF_PROTOC_EXECUTABLE} -I=${PROJECT_SOURCE_DIR}/proto/ --cpp_out=${PROJECT_SOURCE_DIR}/ ${PROJECT_SOURCE_DIR}/proto/pointpillars_config.proto) add_executable(file main.cpp ${PROJECT_SOURCE_DIR}/pointpillars_config.pb.cc file.cpp) target_link_libraries(file ${Protobuf_LIBRARIES} )

protobuf实现原理 protobuf优缺点

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

上面是从文件读数据写入proto,SetProtoToASCIIFile为把proto数据写入文件的函数,大家可以自行调用

参考资料

官方文档:https://developers.google.com/protocol-buffers/docs/reference/overview