protobuf数据格式
参考:
https://github.com/protocolbuffers/protobuf/releases(环境安装-下载资源)
https://blog.csdn.net/m0_45971439/article/details/129351032(win下安装protobuf)
https://zhuanlan.zhihu.com/p/451390348(数据格式-字段类型)
https://www.cnblogs.com/miaochuanjie/p/17434551.html
一、protobuf的定义
protobuf是一种用于序列化结构数据的工具,实现数据的存储与交换,与编程语言和开发平台无关。
序列化:将结构数据或者对象转换成能够用于存储和传输的格式。
反序列化:在其他的计算环境中,将序列化后的数据还原为结构数据和对象。
定义数据的结构,然后使用protoc编译器生成源代码,在各种数据流中使用各种语言进行编写和读取结构数据。甚至可以更新数据结构,而不破坏由旧数据结构编译的已部署程序。
二、protobuf的优缺点
2.1、优点
- 性能高效:与XML相比,protobuf更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。
- 语言无关、平台无关:protobuf支持Java、C++、Python 等多种语言,支持多个平台。
- 扩展性、兼容性强:只需要使用protobuf对结构数据进行一次描述,即可从各种数据流中读取结构数据,更新数据结构时不会破坏原有的程序。
2.2、缺点
- 不适合用来对基于文本的标记文档(如 HTML)建模。
- 自解释性较差,数据存储格式为二进制,需要通过proto文件才能了解到内部的数据结构。
三、protobuf的使用流程
3.1、protobuf在Linux下的安装过程
$ sudo apt-get install autoconf automake libtool curl make g++ unzip $ git clone https://github.com/google/protobuf.git $ cd protobuf $ git submodule update --init --recursive $ ./autogen.sh $ ./configure $ make $ make check $ sudo make install $ sudo ldconfig
3.2、定义proto文件
message Person { string name = 1; int32 id = 2; string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { string number = 1; PhoneType type = 2; } repeated PhoneNumber phone = 4; }
message是消息体,包含了多个fields(数据项),每一个fields都是key-value类型。
3.3、protoc编译器
使用proto文件定义好结构数据后,可以使用protoc编译器生成结构数据的源代码,这些源代码提供了读写结构数据的接口,从而能够构造、初始化、读取、序列化、反序列化结构数据。使用以下命令生成相应的接口代码:
// $SRC_DIR: .proto所在的源目录
// --cpp_out: 生成C++代码
// $DST_DIR: 生成代码的目标目录
// xxx.proto: 要针对哪个proto文件生成接口代码
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/xxx.proto
编译完成后将会生成一个xxx.pb.h和xxx.pb.cpp文件,会提供类似SerializeToOstream()、set_name()、name()等方法。
3.4、调用接口进行序列化、反序列化
/* 下面的代码即为protoc编译器生成的原结构数据的接口, 提供了构造函数、初始化、序列化、反序列化和读取数据的方法, 因此可以调用这些接口进行序列化与反序列化。 */ // 构造函数 Person person; // 初始化 person.set_name("John Doe"); person.set_id(1234); person.set_email("jdoe@example.com"); fstream output("myfile", ios::out | ios::binary); // 序列化结构数据到文件中 person.SerializeToOstream(&output); fstream input("myfile", ios::in | ios::binary); Person person; // 从文件中反序列化出结构数据 person.ParseFromIstream(&input); // 读取结构数据 cout << "Name: " << person.name() << endl; cout << "E-mail: " << person.email() << endl;
四、protobuf的应用场景
- 压缩效率高:服务器间的海量数据传输与通信,可以节省磁盘和带宽,protobuf适合处理大数据集中的单个小消息,但并不适合处理单个的大消息。
- 解析速度快:可以提高服务器的吞吐能力。
五、protobuf与json和XML的对比
- XML、JSON、protobuf都具有数据结构化和数据序列化的能力。
- XML、JSON更注重数据结构化,关注可读性和语义表达能力;protobuf 更注重数据序列化,关注效率、空间、速度,可读性较差,语义表达能力不足。
- protobuf的应用场景更为明确,XML、JSON的应用场景更为丰富。
参考:
https://www.iteye.com/blog/maoyidao-1236916
https://www.jianshu.com/p/cae40f8faf1e
https://www.jianshu.com/p/a24c88c0526a
protobuf语法详解
一、包(package)
为.proto文件添加package声明符,可以防止不同 .proto项目间消息类型的命名发生冲突。
package foo.bar; message Open { ... } message Foo { ... foo.bar.Open open = 1; ... }
protobuf包类型的解析和C++类似,都是由内而外进行解析。对于C++,产生的类会被包装在C++的命名空间中,如上例中的Open会被封装在 foo::bar空间中。
二、选项(option)
option会影响特定环境下的处理方式,但是不会改变整个文件声明的含义。
option optimize_for = CODE_SIZE;
1
三、消息类型(message)
message用于定义结构数据,可以包含多种类型字段(field),每个字段声明以分号结尾。message经过protoc编译后会生成对应的class类,field则会生成对应的方法。
syntax = "proto3"; // 表示使用的protobuf版本是proto3 message SearchRequest { string query = 1; int32 page_number = 2; int32 result_per_page = 3; } 1234567
3.1、常规消息类型
3.1.1、字段修饰符
在proto3中,去掉了required和optional,对于原始数据类型字段不再提供 hasxxx()方法,只有单个字段或者重复字段:
- 单个字段:表示字段可以出现0次或者1次。
- repeated:表示字段可以重复任意次。
3.1.2、字段类型
3.1.2.1、标量类型
protobuf标量数据类型与各平台的数据类型对应如下表:
3.1.2.2、枚举类型
protobuf中的enum类型和C++中的枚举类型相似,表示字段取值的集合。
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; }
枚举类型中第一个元素的值必须从0开始,而且proto3中删除了default标记,默认值为第一个元素。
当枚举类型是在某一个消息内部定义,但是希望在另一个消息中使用时,需要采用MessageType.EnumType的语法格式。
3.1.2.3、Any类型
protobuf中的Any类型与C++中的泛型概念类似,可以定义为任意的类型。在序列化的时候可以通过PackFrom()方法将任意的数据类型打包为Any类型,反序列化的时候通过UnpackTo()把Any类型还原为原始类型。
// 要使用Any类型必须导入该proto文件 import "google/protobuf/any.proto"; message ErrorStatus { string message = 1; repeated google.protobuf.Any details = 2; } // 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 ... } }
3.1.2.4、oneof类型
protobuf中的oneof类似与C++中的联合体类型相似,所有的字段共享内存,最多只能同时设置一个字段,设置oneof的任何字段会自动清除所有其他字段,可以使用case()或WhichOneof()方法检查oneof中使用的是哪个字段。
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
【oneof特性】:
- 设置oneof会自动清除其它oneof字段的值:
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());
- oneof不能声明为repeated类型。
- 注意不要出现内存崩溃问题:
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // Will delete sub_message
sub_message->set_... // Crashes here
- 可以在oneof内部添加和删除field,但是删除和添加oneof要小心
3.1.2.5、map类型
protobuf中的map类似与STL中的关联型容器相似,map是key-value类型,key可以是int或者string,value可以是自定义message。
map<key_type, value_type> map_field = N;
// 与上述定义等价
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
【map特性】:
- map不能定义为repeated类型。
- 当为.proto文件产生生成文本格式的时候,map会按照key 的顺序排序,数值化的key会按照数值排序。
- 从序列化中解析时,如果有重复的key,只会使用第一个key。
3.1.3、默认值说明
- string类型,默认值是空字符串。
- bytes类型,默认值是空bytes。
- bool类型,默认值是false。
- 数字类型,默认值是0。
- 枚举类型,默认值是第一个枚举值,即0。
- repeated修饰的属性,默认值是空。
3.1.4、标识号
在消息类型中,每一个字段都有一个唯一的标识符(Tag),不应该随意改动。
[1-15]内的标识号在编码时只占用一个字节,包含标识符和字段类型,[16-2047]之间的标识符占用2个字节。建议为频繁出现的字段使用[1-15]间的标识符。
如果考虑到以后可能扩展元素,可以预留一些标识符或者字段。注意不能在一个reserved声明中混合使用字段名和标识符。
message Foo { reserved 2, 15, 9 to 11; reserved "foo", "bar"; }
最小的标识符可以从1开始,最大到2^29 - 1,或536,870,911。不可以使用[19000-19999]之间的标识符, Protobuf协议实现中预留了这些标识符。在.proto文件中使用这些预留标识号,编译时就会报错。
3.2、多个消息类型
一个.proto文件中可以定义多个消息类型:
syntax = "proto3"; // SearchRequest 搜索请求 message SearchRequest { string query = 1; // 查询字符串 int32 page_number = 2; // 页码 int32 result_per_page = 3; // 每页条数 } // SearchResponse 搜索响应 message SearchResponse { ... }
3.3、嵌套消息类型
在protobuf中message之前可以嵌套使用:
message SearchResponse { message Result { string url = 1; string title = 2; repeated string snippets = 3; } repeated Result results = 1; }
内部声明的消息message名称只可在内部直接使用,在外部使用需要添加父级message名称(Parent.Type):
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; } } }
3.4、更新消息类型
如果一个已有的消息类型已无法满足新的需求,比如需要添加一个额外的字段,但是同时旧版本写的代码仍然可用。在更新消息类型需要遵循以下规则:
- 不要更改任何已有字段的标识号。
- int32、uint32、int64、uint64,和bool是全部兼容的,这意味着可以将这些类型中的一个转换为另外一个,而不会破坏向前、 向后的兼容性。
- sint32和sint64是互相兼容的,但是它们与其他整数类型不兼容。
- string和bytes是兼容的,只要bytes是有效的UTF-8编码。
- 嵌套消息与bytes是兼容的,只要bytes包含该消息的一个编码过的版本。
- fixed32与sfixed32是兼容的,fixed64与sfixed64是兼容的。
四、RPC服务(service)
如果想要将消息类型用在远程方法调用(RPC)系统中,可以在.proto文件中定义一个RPC服务接口。
service UserService { // 包含方法名、方法参数和返回值, // 接收SearchRequest并返回一个SearchResponse rpc GetUser(Request) returns (Response); }
gRPC在使用protobuf时非常有效,如果使用特殊的protobuf插件可以直接从.proto文件中产生相关的RPC代码。
五、其他
5.1 导入proto文件(import)
如果希望在当前proto文件中引用其他的proto文件中的内容,可以使用import:
import "other_project/other_protos.proto";
参考:
https://developers.google.cn/protocol-buffers/docs/proto3#oneof
https://segmentfault.com/a/1190000007917576#item-1-10
https://www.jianshu.com/p/e06ba6249edc
protobuf语法风格
一、代码风格
- 每一行的代码长度不要超过80。
- 使用两个空格进行缩进。
二、文件格式
文件命名应该采用蛇形命名法(即用下划线连接),如:lower_snake_case.proto。所有文件应以下列方式排列:
- License header (if applicable)
- File overview
- Syntax
- Package
- Imports (sorted)
- File options
- Everything else
三、包
包名应该是小写的,并且应该对应于目录层次结构。例如,如果一个文件位于my/Package/*中,那么包名应该是*my.Package。
四、消息类型和字段
消息名使用驼峰命名法,例如:SongServerRequest,字段名和扩展名使用小写的下划线分隔式,例如:song_name。
message SongServerRequest { required string song_name = 1; } 123 const string& song_name() { ... } void set_song_name(const string& x) { ... }
如果字段名包含数字,则该数字应出现在字母之后,而不是下划线之后。例如:song_name1。
五、repeated字段
repeated字段使用复数命名:
repeated string keys = 1;
repeated MyMessage accounts = 17;
六、枚举类型
枚举名使用使用驼峰命名法,成员使用大写的下划线分隔式:
enum Foo { FOO_UNSPECIFIED = 0; FOO_FIRST_VALUE = 1; FOO_SECOND_VALUE = 2; }
七、服务
服务名称和任何RPC方法名称均使用驼峰命名法:
service FooService {
rpc GetSomething(FooRequest) returns (FooResponse);
}
参考:
https://developers.google.cn/protocol-buffers/docs/style
protobuf使用实例
一、描述proto文件
proto文件名称为addressbook.proto。
syntax = "proto3"; import "google/protobuf/any.proto"; // package类似于namespace,可以避免命名冲突 package AddressBookInfo; // message类似于class message Person { string name = 1; int32 id = 2; string email = 3; // 枚举类型 enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { string number = 1; // proto3中enum没有default选项,把第一个值作为default PhoneType type = 2; } // repeated表示message或者filed可以重复多次 repeated PhoneNumber phones = 4; } message Address { string address = 1; } message AddressBook { string owner = 1; repeated Person person_infos = 2; /* ** oneof类似于union类型,某一个时刻只能设置一个field,所有的field共享同一段内存。 ** 设置oneof字段将自动清除oneof的所有其他字段,即只能同时设置(set_)一个,不然就会core dump。 ** 可以在oneof内部添加和删除field,但是删除和添加oneof要小心。 ** oneof中数据成员的编号建议承上启下,尽量不要随意编号。 */ oneof PayType { string type_ali = 3; string type_wx = 4; } /* ** map是key-value类型,key可以是int或者string,value可以是自定义message。 ** Any用来实现泛型,可以表示任意类型。 */ map<string, google.protobuf.Any> owner_address = 5; }
二、编译proto文件
protoc -I=. --go_out=. --go-grpc_out=. service.proto
linux 安装 protoc
- 环境 #54~20.04.1-Ubuntu SMP
- protoc 安装
下载地址:https://github.com/protocolbuffers/protobuf/releases 找到自己需要的版本
这里下载版本:https://github.com/protocolbuffers/protobuf/archive/refs/tags/v3.9.2.tar.gz
cd /tmp wget https://github.com/protocolbuffers/protobuf/archive/refs/tags/v3.9.2.tar.gz tar -zxvf v3.9.2.tar.gz cd /protobuf-3.9. sudo ./autogen.sh sudo ./configure sudo make sudo make install 确认是否安装 protoc --version 出现异常 protoc: error while loading shared libraries: libprotoc.so.20: cannot open shared object file: No such file or directory 查阅资料,解决办法 export LD_LIBRARY_PATH=/usr/local/lib 再次测试,命令正常 将 export LD_LIBRARY_PATH=/usr/local/lib 加到 /etc/profile 文件中,以免下次使用失效 将 protoc 命令放到 GOBIN 目录 which protoc cp /usr/local/bin/protoc $GOBIN/ 安装 protoc-gen-go go install github.com/golang/protobuf/protoc-gen-go@v1.4.2 安装 protoc-gen-grpc-gateway go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway@v1.15 安装 protoc-gen-swagger go install github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger@v1.15 安装 protoc-gen-gofast go install github.com/gogo/protobuf/protoc-gen-gofast@latest