二、protocol buffer二进制协议
2018-12-16 本文已影响0人
小manong
一、是什么
- 是一种灵活,高效,自动化的机制,用于序列化结构化数据 - 想想XML,但更小,更快,更简单。您可以定义数据的结构化时间,然后可以使用特殊生成的源代码轻松地在各种数据流中使用各种语言编写和读取结构化数据。您甚至可以更新数据结构,而不会破坏根据“旧”格式编译的已部署程序
- 特点:
- 在谷歌内部长期使用,产品成熟度高;
- 跨语言、支持多种语言,包括 C++、Java 和 Python
- 编码后的消息更小,更加有利于存储和传输
- 编解码的性能非常高
- 支持不同协议版本的前向兼容
- 支持定义可选和必选字段
二、如何工作
通过在.proto文件中定义protocol buffer消息类型来指定您希望如何构建序列化信息。每个protocol buffer消息都是一个小的逻辑信息记录,包含一系列名称 - 值对。以下.proto是定义包含有关人员信息的消息的文件的一个非常基本的示例:
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
- 消息格式很简单 - 每种消息类型都有一个或多个唯一编号的字段,每个字段都有一个名称和一个值类型,其中值类型可以是数字(整数或浮点数),布尔值,字符串,原始字节,甚至(如上例所示)其他protocol buffer消息类型,允许分层次地构建数据。同时可以指定可选字段,必填字段和重复字段。
- 一旦定义了消息,就可以在.proto文件上运行应用程序语言的protocol buffer编译器来生成数据访问类。并为每个字段(如name()和set_name())提供了简单的访问器,以及将整个结构序列化/解析为原始字节的方法,同时提供了向后兼容性,在消息格式中添加新字段,而不会破坏向后兼容性; 旧的二进制文件在解析时只是忽略新字段。
三、proto3
- Protocol Buffers语言版本是3(proto3),Proto3简化了协议缓冲区语言,既易于使用,又可以在更广泛的编程语言中使用。但是Proto3和Proto2不完全兼容。因此如果现在使用最好使用Proto3
1、定义消息类型
syntax = "proto3";//明确使用版本proto3,不指定的话默认是proto2
//SearchRequest 定义了三个属性,每一个属性都有名称和类型
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
2、指定字段类型
- 在上面的示例中,所有字段都是标量类型:两个整数(
page_number
和result_per_page
)和一个字符串(query
)。但是,您还可以为字段指定复合类型,包括枚举和其他消息类型
(1)标量值类型(基本数据类型)
image.png
image.png
(2)复杂数据结构
- repeated 关键字,标识该字段可以重复任意次数,等价与数组和列表
- enum表示枚举
- map类型:map<key_type,value_type> map_field=N;
3、分配字段编号
- 消息定义中的每个字段都有唯一的编号。这些字段编号用于以消息二进制格式标识字段,并且在使用消息类型后不应更改。请注意,1到15范围内的字段编号需要一个字节进行编码,包括字段编号和字段类型,16到2047范围内的字段编号占用两个字节。因此,您应该为非常频繁出现的消息元素保留数字1到15。请记住为将来可能添加的常用元素留出一些空间。
- 可以指定的最小字段数为1,最大字段数为2 29 - 1或536,870,911。但是不能使用不能使用数字19000到19999,因为这些数字是被Protocol Buffers保留的
4、指定字段规则
- 消息字段可以是以下之一:
单数:格式良好的消息可以包含该字段中的零个或一个(但不超过一个)。
复数:此字段可以在格式良好的消息中重复任意次数(包括零)。将保留重复值的顺序。
5、添加更多消息类型
- 可以在单个.proto文件中定义多种消息类型。如果要定义多个相关消息,这很有用 - 例如,如果要定义与SearchResponse消息类型对应的回复消息格式,可以将其添加到相同的消息.proto:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
6、添加注释
- 采用// 和 /* ... */ 符号进行添加注释,例如
/* 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.
}
7、导入其他的proto文件
- 对于一些公共的数据结构,可以单独放在一个proto文件里面,然后通过导入的方式导入到其他的文件中,同时导入也支持级联。
import "/other.proto"
8、编写proto3文件规范
(1)消息(使用驼峰命名)和字段名称(使用下划线)
message SongServerRequest {
required string song_name = 1;
}
(2)枚举(名称使用驼峰,字段使用大写的下划线)
enum Foo {
FIRST_VALUE = 0;
SECOND_VALUE = 1;
}
(3)RPC 服务(服务名称和任何RPC方法名称全部使用驼峰命名)
service FooService {
rpc GetSomething(FooRequest) returns (FooResponse);
}
四、java语言实战Proto3
- 需求:创建一个非常简单的“地址簿”应用程序,可以在文件中读取和写入人员的联系人详细信息。地址簿中的每个人都有姓名,ID,电子邮件地址和联系电话号码。(这里使用Proto3序列化和检索这样的结构化数据)
1、定义协议格式(addressbook.proto)
syntax = "proto3";
//1、proto3包名,这有助于防止不同项目之间的命名冲突
package tutorial;
//2、申明java包名
option java_package = "com.example.tutorial";
//3、申明产生的外部java类名,如果不申明将使用驼峰形式产生(AddressBook)
option java_outer_classname = "AddressBookProtos";
message Person {
//4、required 必须提供该字段的值,否则该消息将被视为“未初始化”。
//尝试构建一个未初始化的消息将抛出一个RuntimeException。
//解析未初始化的消息将抛出一个IOException。除此之外,必填字段的行为与可选字段完全相同。
required string name = 1;
required int32 id = 2;
//5、optional:该字段可能已设置,也可能未设置。如果未设置可选字段值,则使用默认值。
//对于简单类型,您可以指定自己的默认值,就像我们type在示例中为电话号码所做的那样。
//否则,使用系统默认值:数字类型为0,字符串为空字符串,bools为false。对于嵌入式消息,
//默认值始终是消息的“默认实例”或“原型”,其中没有设置任何字段。调用访问器以获取尚未显
//式设置的可选(或必需)字段的值始终返回该字段的默认值。
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
//6、repeated:该字段可以重复任意次数(包括零)。重复值的顺序将保
//留在协议缓冲区中。将重复字段视为动态大小的数组。
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
- 注意点:
(1)应该非常小心地将字段标记为required。谷歌的一些工程师得出的结论是,使用required弊大于利; 他们更喜欢只使用optional和repeated。但是,这种观点并不普遍。
(2)可以在Protocol Buffer Language Guide中中找到需要的所有格式形式
2、编译Proto3文件
参考网址进行:https://my.oschina.net/u/573325/blog/1617416
(1)proto文件(由于插件的原因上面所讲的optional 和required 属性描述全部会报错误,因此全部删除)
syntax = "proto3";
option java_package = "com.qiu.proto";
option java_outer_classname = "AddressBookProtos";
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 phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
(2)生成java文件解析
- 每一个meeage都会生成一个Builder类用于生成message类实例,例如message Person会生成一个Person类和Person.builder类。
- message中的每一个属性都有自己的set和get方法。
- enum关键字会直接生成对应的一个java枚举类
3、测试
maven添加如下的依赖,用于序列化操作
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java-util</artifactId>
<version>3.5.1</version>
</dependency>
测试类
public class ProtoFileTest {
public static void main(String[] args) throws InvalidProtocolBufferException {
//1、构建person类的build
AddressBookProtos.Person.Builder person= AddressBookProtos.Person.newBuilder();
person.setEmail("2222@a");
person.setId(1);
person.setName("test");
//2、构建phonenumer内部类的build
AddressBookProtos.Person.PhoneNumber.Builder phoneNumer= AddressBookProtos.Person.PhoneNumber.newBuilder();
phoneNumer.setNumber("123456789");
phoneNumer.setType(AddressBookProtos.Person.PhoneType.MOBILE);
AddressBookProtos.Person.PhoneNumber.Builder phoneNumer2= AddressBookProtos.Person.PhoneNumber.newBuilder();
phoneNumer2.setNumber("987654321");
phoneNumer2.setType(AddressBookProtos.Person.PhoneType.MOBILE);
//person类添加phonenumbers列表
person.addPhones(phoneNumer);
person.addPhones(phoneNumer2);
//创建person类
AddressBookProtos.Person build = person.build();
byte[] bytes = build.toByteArray();
AddressBookProtos.Person addressBook1 = AddressBookProtos.Person.parseFrom(bytes);
List<AddressBookProtos.Person.PhoneNumber> phonesList = person.getPhonesList();
phonesList.forEach(p->{
System.out.println("type:"+p.getType()+" number:"+p.getNumber());
});
//进行person json序列化
String personJsonString = JsonFormat.printer().print(person);
System.out.println("person序列化:"+personJsonString);
//3、构建AddressBook通讯录
AddressBookProtos.AddressBook.Builder addressBook= AddressBookProtos.AddressBook.newBuilder();
addressBook.addPeople(person);
//进行地址序列化
String addressJsonStr = JsonFormat.printer().print(addressBook);
System.out.println("通讯录序列化:"+addressJsonStr);
}
}
测试结果:
type:MOBILE number:123456789
type:MOBILE number:987654321
person序列化:{
"name": "test",
"id": 1,
"email": "2222@a",
"phones": [{
"number": "123456789"
}, {
"number": "987654321"
}]
}
通讯录序列化:{
"people": [{
"name": "test",
"id": 1,
"email": "2222@a",
"phones": [{
"number": "123456789"
}, {
"number": "987654321"
}]
}]
}
4、总结
(1)使用步骤总结
- IDL 文件定义(*.proto), 包含数据结构定义
- 各种语言的代码生成(含数据结构定义、以及序列化和反序列化接口)
- 使用 Protocol Buffers 的 API 进行序列化和反序列化
(2)使用protoBuffer原生api总结
- 构建对象(构造器模式):
AddressBookProtos.Person person1= AddressBookProtos.Person.newBuilder()
.setId(1).setName("test")
.build();
- 序列化,转为字节数组或者输出流
toByteArray()
toByteString()
writeTo()
....
- 反序列化,转为原始对象或者类(使用parseFrom方法)
AddressBookProtos.Person build = person.build();
byte[] bytes = build.toByteArray();
AddressBookProtos.Person addressBook1 = AddressBookProtos.Person.parseFrom(bytes);
(3)使用第三方工具进行序列化和反序列化(比如netty)