ProtoBuf全称:protocol buffers,直译过来是:“协议缓冲区”,是一种与语言无关、与平台无关的可扩展机制,用于序列化结构化数据。
ProtoBuf和json或者xml,从一定意义上来说他们的作用是一样的。
有 Protocol buffer 这种轻便的序列化反序列化工具,Json 为什么还会大量使用?
提问:google protobuf和gRPC的关系?
链接:
注意: Protobuf 有两个大版本,proto2 和 proto3,同比 python 的 2.x 和 3.x 版本,如果是新接触的话,同样建议直接入手 proto3 版本。proto3 相对 proto2 而言,简言之就是支持更多的语言(Ruby、C#等)、删除了一些复杂的语法和特性、引入了更多的约定等。
下载地址: https://github.com/protocolbuffers/ProtoBuf/releases
注意,不同的电脑系统安装包是不一样的:
注意:我们需要将下载得到的可执行文件protoc所在的 bin 目录加到我们电脑的环境变量中。
可以通过protoc --version
#查看protoc的版本
sudo apt update; sudo apt upgrade
sudo apt install libprotobuf-dev protobuf-compiler
编译安装
sudo wget https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protobuf-all-3.19.4.tar.gz
sudo tar -zxvf protobuf-all-3.19.4.tar.gz
sudo apt-get install build-essential
sudo apt install libtool autoconf make
sudo apt install make-guile
cd protobuf-3.19.4
sudo ./autogen.sh
sudo ./configure
sudo make
sudo make check
sudo make install
sudo ldconfig # refresh shared library cache
make install 注意权限问题,最好使用 sudo make install。安装成功之后,使用 which protoc 就可以查看 protoc 已经安装成功了。ProtoBuf 默认安装的路径在 /usr/local,当然我们可以在配置的时候改变安装路径,使用如下命令:
./configure --prefix=/usr
安装成功后,我们执行protoc --version
查看我们的 Protocol Buffers 的版本
protoc --version
protoc就是protobuf的编译器,它把proto文件编译成不同的语言
protobuf里最基本的类型就是message。一般来讲,设计协议是在fileName.proto文件中, 其中fileName是自己定义, 在通过protoc转换成为对应的代码。
每一个messgae都会有一个或者多个字段(field),其中字段包含如下元素
使用(首字母大写)作为消息名,例如SongServerRequest。字段名称使用下划线分隔名称,例如song_name。
message SongServerRequest {
required string song_name = 1;
}
c++访问器:
const string& song_name() { ... }
void set_song_name(const string& x) { ... }
可以在单个.proto中定义多种消息类型。比如下面:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
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.
}
保留字段
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
.proto类型 | 说明 | C++ | Java | Python | Go |
---|---|---|---|---|---|
double | float64 | ||||
float | float32 | ||||
int32 | 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,请改用sint32。 | int32 | int32 | ||
int64 | 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,请改用sint64。 | int64 | |||
uint32 | 使用可变长度编码。 | uint32 | |||
uint64 | 使用可变长度编码。 | uint64 | |||
sint32 | 使用可变长度编码。符号整型值。这些比常规int32s编码负数更有效。 | int32 | |||
sint64 | 使用可变长度编码。符号整型值。这些比常规int64s编码负数更有效。 | int64 | |||
fixed32 | 总是四字节。如果值通常大于228,则比uint 32更有效 | uint32 | |||
fixed64 | 总是八字节。如果值通常大于256,则比uint64更有效 | uint64 | |||
sfixed32 | 总是四字节。 | int32 | |||
sfixed64 | 总是八字节。 | int64 | |||
bool | bool | ||||
string | 字符串必须始终包含UTF - 8编码或7位ASCII文本 | string | |||
bytes | 可以包含任意字节序列 | string |
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
枚举类型名使用CamelCase (带首字母大写),值名使用CAPITALS_WITH_UNDERSCORES:
enum Foo {
FIRST_VALUE = 0;
SECOND_VALUE = 1;
}
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
如果要在RPC(Remote Procedure Call,远程过程调用)系统中使用消息类型,可以在.proto文件中定义RPC服务接口,协议缓冲区编译器将根据所选语言生成服务接口代码和存根。
service FooService {
rpc GetSomething(FooRequest) returns (FooResponse);
}
使用import引用另外一个文件的pb
syntax = "proto3";
import "google/protobuf/wrappers.proto";
package ecommerce;
message Order {
string id = 1;
repeated string items = 2;
string description = 3;
float price = 4;
google.protobuf.StringValue destination = 5;
}
默认情况下,只能使用直接导入的.proto文件中的定义。但是,有时可能需要将.proto文件移动到新位置。不用直接移动.proto文件并在一次更改中更新所有import调用,现在可以在旧位置放置一个伪.proto文件,使用import public概念将所有导入转发到新位置。任何导入包含import public语句的proto的人都可以传递地依赖import public依赖项。例如:
// new.proto
// All definitions are moved here
// new.proto
// All definitions are moved here
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
协议编译器使用-I/–proto_path路径标志在协议编译器命令行上指定的一组目录中搜索导入的文件。如果没有给定标志,它将在调用编译器的目录中查找。通常,您应该将–proto_path标志设置为项目的根目录,并对所有导入使用完全限定名。
在.proto上运行协议缓冲区编译器时,编译器用您指定的编程语言生成代码,您需要使用您在文件中描述的消息类型,包括获取和设置字段值,将消息序列化为输出流,以及从输入流解析消息。
对于C++,编译器会从每个 .proto文件生成一个 .h and .cc文件, 文件中描述的每种消息类型都有一个类。
对于Java,编译器为每个消息类型生成一个Java文件,其中包括每个类的定义,以及用于创建消息类实例的特殊构建器类。
Python有点不同,Python编译器生成一个模块,其中包含中每种消息类型的静态描述符,然后与元类一起使用,在运行时创建必要的Python数据访问类。
对于Go, 编译器生成一个.pb.go文件,其中包含文件中每种消息类型的类型。
对于Ruby, 编译器生成一个.rb文件带有包含消息类型的Ruby模块。
对于Objective-C, 编译器为每个.proto文件中生成一个pbobjc.h和pbobjc.m文件,文件中描述的每种消息类型都有一个类。
对于C#, 编译器为每个.proto文件生成一个.cs文件,文件中描述的每种消息类型都有一个类。
解析消息时,如果编码消息不包含特定的单数元素,则解析对象中的相应字段将设置为该字段的默认值。这些默认值是特定于类型的:
对于字符串,默认值为空字符串。
对于字节,默认值为空字节。
对于布尔,默认值为false。
对于数字类型,默认值为零。
对于枚举,默认值是第一个定义的枚举值,必须为0。
对于消息字段,该字段未设置。它的确切值取决于语言。有关详细信息,请参见生成的代码指南。
重复字段的默认值为空(通常是相应语言的空列表)。
请注意,对于标量消息字段,一旦消息被解析,就无法判断字段是显式设置为默认值(例如,布尔值是否设置为false )还是根本没有设置:定义消息类型时应该记住这一点。例如,如果不希望默认情况下也发生某些行为,不要有一个布尔值在设置为false时打开该行为。另请注意,如果标量消息字段设置为默认值,则不会在线路上序列化该值。
您可以使用其他消息类型作为字段类型
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
可以在其他消息类型中定义和使用消息类型,如下例:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果您想在其父消息类型之外重用此消息类型,则需要先指定它的父类型,如下所示:
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
您可以随心所欲地嵌套:
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}
如果现有的消息类型不满足你的所有需求——例如,你希望消息格式有一个额外的字段——但是你仍然希望使用用旧格式创建的代码,别担心!在不破坏任何现有代码的情况下更新消息类型非常简单。请记住以下规则:
未知字段是格式良好的协议缓冲区序列化数据,表示解析器无法识别的字段。例如,当旧二进制文件用新字段解析新二进制文件发送的数据时,这些新字段将成为旧二进制文件中的未知字段。
最初,proto3 消息在解析过程中总是丢弃未知字段,但在 3.5 版本中,我们重新引入了保留未知字段以匹配 proto2 行为。在 3.5 及更高版本中,未知字段在解析期间保留并包含在序列化输出中。
Any消息类型允许您将消息作为嵌入类型,而不需要它们 .proto定义。Any包含任意序列化的消息(字节),以及一个URL,该URL充当该消息的全局唯一标识符并解析为该消息的类型。要使用Any类型,你需要导入google/protobuf/any.proto.
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
给定消息类型的默认类型 URL 是type.googleapis.com/packagename.messagename。
不同的语言实现将支持运行时库助手以Any类型安全的方式打包和解包值——例如,在 Java 中,Any类型将具有特殊的pack()和unpack()访问器,而在 C++ 中则有PackFrom()和UnpackTo()方法:
// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const Any& detail : status.details()) {
if (detail.Is<NetworkErrorDetails>()) {
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
... processing network_error ...
}
}
如果您有一条包含多个字段的消息,并且最多同时设置一个字段,可以强制执行此行为并使用 oneof 功能节省内存。
oneof 字段与常规字段一样,除了一个 oneof 共享内存中的所有字段外,最多可以同时设置一个字段。设置 oneof 的任何成员会自动清除所有其他成员。您可以使用case()或WhichOneof()方法检查Oneof 中的哪个值被设置(如果有的话),具体取决于您选择的语言。
使用 Oneof
怎么用呢?.proto使用oneof关键字后跟 oneof 名称
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
注意:
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // 将删除 sub_message
sub_message->set_... // 这里崩溃了
SampleMessage msg1;
msg1.set_name("name");
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK(msg2.has_name());
语法:
map<key_type, value_type> map_field = N;
举个例子:
syntax = "proto3";
message Product
{
string name = 1; // 商品名
// 定义一个k/v类型,key是string类型,value也是string类型
map<string, string> attrs = 2; // 商品属性,键值对
}
注意:
map语法序列化后等同于如下内容,故而即使是不支持map语法的protocol buffers实现也是可以处理你的数据。
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
可以向.proto文件中添加可选的package明符,以防止协议消息类型之间的名称冲突。
package foo.bar;
message Open { ... }
怎么用?
message Foo {
...
foo.bar.Open open = 1;
...
}
proto3支持JSON中的规范编码,从而在系统之间共享数据更加容易。下表中按类型对编码进行了描述。
如果JSON编码数据中缺少了某个值,或者该值为null,则在解析为protocol buffer时,它将被解释为适当的默认值。如果字段在protocol buffer中具有默认值,则默认情况下会在JSON编码的数据中将其省略以节省空间。具体实现可以提供在 JSON编码中可选的默认值。
proto3 | json | json实例 | 说明 |
---|---|---|---|
message | object | {“fooBar”: v, “g”: null, …} | |
enum |
要生成Java,Python,C ++,Go,Ruby,Objective-C或C#代码,你需要使用.proto文件中定义的消息类型,需要在.proto上运行protocol buffers编译器。如果尚未安装编译器,请下载软件包并按照README中的说明进行操作。对于Go,你还需要为编译器安装一个特殊的代码生成器插件:你可以在GitHub上的golang / protobuf仓库中找到此代码和安装说明。
protocol buffers编译器的调用如下:
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
IMPORT_PATH
指定解析导入指令时查找.proto文件的目录。如果省略,则使用当前目录。可以通过多次传递–proto_path选项来指定多个导入目录。将按顺序搜索它们。消息拷贝函数
void CopyFrom(const Message& from)
实例:
message Param {
optional string name = 1;
optional ParamType type = 2;
optional string type_name = 3;
oneof oneof_value {
bool bool_value = 4;
int64 int_value = 5;
double double_value = 6;
string string_value = 7;
}
optional bytes proto_desc = 8;
}
class Parameter {
public:
/**
* @brief copy constructor
*/
explicit Parameter(const Parameter& parameter);
private:
Param param_;
}
Parameter::Parameter(const Parameter& parameter) {
param_.CopyFrom(parameter.param_);
}
创建一个.proto文件:addressbook.proto,内容如下
syntax = "proto3";
package IM;
message Account {
//账号
uint64 ID = 1;
//名字
string name = 2;
//密码
string password = 3;
}
message User {
Account user = 1;
}
编译.proto文件,生成C++语言的定义及操作文件
protoc addressbook.proto --cpp_out=./
生成的文件:addressbook.pb.h addressbook.proto
让我们看看编译器为.proto文件创建了哪些类和函数。如果你查看addressbook.pb.h,你会发现你在addressbook.proto中指定的每条消息都有一个类。仔细查看Person类,您可以看到编译器已经为每个字段生成了访问器。例如,对于name, id, email和phones字段,并且生成了相应的方法:
编写程序main.cpp
#include
#include
#include "addressbook.pb.h"
using namespace std;
int main(int argc, char** argv)
{
IM::Account account1;
account1.set_id(1);
account1.set_name("windsun");
account1.set_password("123456");
string serializeToStr;
account1.SerializeToString(&serializeToStr);
cout <<"序列化后的字节:"<< serializeToStr << endl;
IM::Account account2;
if(!account2.ParseFromString(serializeToStr))
{
cerr << "failed to parse student." << endl;
return -1;
}
cout << "反序列化:" << endl;
cout << account2.id() << endl;
cout << account2.name() << endl;
cout << account2.password() << endl;
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
编译
g++ main.cpp Account.pb.cc -o main -lprotobuf -std=c++11 -lpthread
运行
protobuf提供了一种textformat的序列化格式,类似json格式,清晰易读。比如一棵行为树节点描述文件:
数据定义为:
message BehaviorNodeConf
{
required int32 type = 1;
// 条件需要的参数
repeated int32 args = 2;
// 包含多个子节点
repeated BehaviorNodeConf node = 3;
};
message BehaviorTreeConf
{
// 行为树类型: AI, ON_HIT, ON_HITTED ...
required int32 type = 1;
// 行为树节点
required BehaviorNodeConf node = 2;
};
配置文件为:
type: 5
node:
{
type: 1
node:
{
type: 101
args: 2
}
node:
{
type: 1
node:
{
type: 1001
args: 0
args: 100
}
node:
{
type: 1001
args: 1
args: -100
}
}
}
以下两行代码即可解析这个配置文件:
BehaviorTreeConf conf;
google::protobuf::TextFormat::ParseFromString(fileContent, &conf);
https://www.kaifaxueyuan.com/basic/protobuf3/protocol-buffer-cpp-writing-a-message.html