概述
一个语言中立,平台无关的、可扩展的用来进行数据结构和序列化的协议栈,相比xml它更小、更快、更简单,一次完成数据结构定义可以使用工具生成多种语言代码,实现轻松的从各种数据流中写入和读取结构化数据.
下述定一个消息体,两个必须的字段name,和id,一个可选字段email.
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
}
目前支持Java、Python、Object C、C++中生成代码,Proto3版本可以使用Go,Buby和C#.
下述代码使用生成的Java代码将一个消息对象进行序列化输出.
Person john =Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.build();
output = newFileOutputStream(args[0]);
john.writeTo(output);
下面的代码表示从一个流中读取消息字节并反序列化为消息对象.
Person john;
fstreaminput(argv[1],
ios::in | ios::binary);
john.ParseFromIstream(&input);
id = john.id();
name =john.name();
email =john.email();
协议目标
.很容易就引入新的字段而不需要改动服务,
.协议格式自我描述性
.自动序列化和反序列化,避免手工操作
.可用于RPC调用、和数据持久存储
.支持生成客户端RPC STUB类[占坑代码,需要实现的被拿到Server端执行的代码]
可参考文档
官方教程, https://developers.google.com/protocol-buffers/docs/tutorials
协议编码, https://developers.google.com/protocol-buffers/docs/encoding
参考文献, https://developers.google.com/protocol-buffers/docs/reference/overview
语法[proto2]
定义消息类型
protobuf IDL[接口定义语言]以.proto文件结尾,当然只要内容是一个合法的protobuf语法格式就可以.
#下面定义一个请求消息[SearchRequest],带有必须的字段query,和两个可选的字段page_number,result_per_page.
messageRequest{
requiredstring query = 1;
optional int32 page_number = 2;
optional int32 result_per_page =3;
}
消息SearchRequest定义了三个字段,每个字段都有一个名称和类型,一个必须在字段修饰符,和一个必须的编号[编号在序列化和反序列化是标识字段顺序]
指定字段类型
可以使用标量类型也可以指定复合类型如枚举和其它消息类型.
指定字段标签
消息格式中的每个字段都必须有一个唯一的数字类型的标签,上面的1,2,3.用于在二进制格式中识别消息字段.
1-15使用1个字节编码,16-2027使用2字节编码,
上面的required,optionel都属于字段的标签,
标记的范围1-(2^29-1)且 19000 -19999为协议保留标记.
消息体中的每一个字段都有一个唯一的编号标记,用于在消息的二进制格式中识别字段,一旦消息类型被使用就不应该修改.
指定字段规则
消息字段必须指定一个规则,标识该字段的行为可以是以下之一
.required,必须的,格式良好的消息有一个required字段
.optional,可选的,0个或者1个
.repeated,可重复的,出现0次或者多次,顺序被保留
repeated字段应该使用[packed=true],获得更有效的编码.
repeatedint32 samples = 4 [packed=true];
添加更多的消息类型
messageSearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page =3;
}
messageSearchResponse {
...
}
一个IDL文件中可以出现多个messge类型,
使用注释
使用// /** doc **/格式进行注释
保留字段
在定义消息时可定义保留字段,保留字段可被子消息实现并使用.保留字段可以定义保留的属性和保留的标签.
messageFoo {
reserved 2, 15, 9 to 11;
reserved "foo","bar";
}
生成代码
>protoc.exe --java_out ./ test.proto
数据类型对照
下面是支持的字段类型和对应关系.
可选字段和默认值
被标记为optional的字段可以指定默认值,如果一个optional字段没有被设置则使用默认值,
optional int32result_per_page = 3 [default = 10];
内部定义了一些特殊的默认值,
string ->””,Boolean->false,number->0,emums ->第一个值,
枚举
do`tlike this.
使用其它消息类型
消息类型可以是一个已经被定义的消息,类似于面向对象语言的引用关系.
messageSearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets =3;
}
导入IDL文件
可以使用import语法导入一个已经存在的.proto文件到当前.proto文件中来使用,
import"myproject/other_protos.proto";
默认只能使用由import直接导入的元素,如果存在间接的import关系可以使用import public 语法,
import public "new.proto";
import "other.proto";
嵌套类型
允许定义像内部类一样的语法,protobuf message允许嵌套定义,
message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
更新消息字段
慎重,允许更新单不能破坏现有消息规则!
.不改变任何现有字段的标记
.添加的任何新字段都应该是optinal或者repeated修饰的,设置合理的默认值,
.被删除的字段的标记号不应该被再次使用
.非 required字段可以被转为扩展字段,反过来也是
.int32, uint32, int64, uint64, and bool是兼容的
.sint32 and sint64弧线兼容,与其它整数类型不兼容
.utf-8格式的字符串和字节是兼容的
…….
扩展
允许实现类似于继承的语意,使用extensions定义保留的字段标记,通过继承让子消息来实现
message Foo {
// ...
extensions 100 to 199;
}
extend Foo {
optional int32 bar = 126;
}
生成的代码对扩展字段有专门的处理代码,以和标准字段进行区分.
嵌套扩展
扩展允许定义嵌套的行为
messageBaz {
extend Foo {
optional int32 bar = 126;
}
...
}
选择扩展的标签
一定要避免扩展消息不能使用原消息中已经使用的字段标签,为此可使用语法extensions指定扩展标记的范围.
messageFoo {
extensions 1000 to max;
}
max is 2^29 - 1, or 536,870,911.
One of
指定一组属性其中只能有一个有效,当设置其中一个时,其它的属性被擦除,oneof中不能使用required,optional和repeated进行修饰.
messageSampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message =9;
}
}
和optional修饰的字段一样也有对饮的get和set方法.
Maps
类型允许是Map类型,使用以下语法:
map<key_type,value_type> map_field = N;
key必须是整数或者String类型,value可以是除了map之外的其它类型,map不支持repeated,optional,required修饰,
map<string,Project> projects = 3;
.map不支持扩展,
.不能被repeated,optional,required修饰
.不保证顺序
向后兼容
map可以被模拟
messageMapFieldEntry {
optional key_type key = 1;
optional value_type value =2;
}
repeatedMapFieldEntry map_field = N;
Package
允许使用包概念来区分命名空间,
package foo.bar;
message Open {... }
messageFoo {
...
required foo.bar.Open open =1;
...
}
Java中的包生成规则,package默认被用作为Java包,除非在proto文件中使用了optionjava_package语意.
建议使用package来区分命名空间!
定义服务
Protobuf本身是一个序列化协议,允许将序列化后的数据通过RPC方式进行传输.Protobuf提供了定义RPC服务的接口,Protobuf可帮助生成接口文件和客户端STUB类.
serviceSearchService {
rpc Search (SearchRequest)returns (SearchResponse);
}
Stub转发所有的调用到一个RpcChannel, RpcChannel是一个抽象的接口,可自定义实现。
服务端类应该实现Service接口.
grpc,google提供的一种开源RPC系统,
其它可选操作
可通过一些可选的配置,来影响整体的处理方式,
.java_package,生成的javal类所在的包
.java_outer_classname,输出的Java类名,如果没有则使用proto的文件名作为类名,
.optimize_for,代码优化[SPEED,CODE_SIZE,LITE_RUNTIME]
.java_generic_services,是否生成服务接口[true,false]
.packed,使用该字段描述没有任何缺点,可能是用更紧凑的编码方式.
.deprecated,过时的,true.
生成代码
protoc指令用于将IDL文件转为对应开发语言的实现
protoc--proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR--python_out=DST_DIR path/to/file.proto
.--java_out,生成的java代码的存储路径
技巧
使用流式程序
Protobuf的格式是固有的,不能确定消息的开始和结束,在流式写入过程中需要记录消息的开始和结束,最简单的写入消息的时候先写入消息的大小.读取消息时先读取消息长度在获取消息体.
大数集
Protobuf不是为大数据的场景设计的,消息大与1M可以考虑使用其它的方案,分而治之将大数据集中的小数据使用Protobuf进行编码.
自描述
定义的属性应该自我描述.表述其用意.
Java开发
Why Use Protocol Buffers?
Java序列化已知的性能问题和兼容性,序列化后的大小,Protobuf可以表现出很好性能.
定义一个消息对象
packagecom.xiaofen;
option java_package="com.xiaofen.bean";
option java_outer_classname="MessageProtos";
message Message{
required string id=1;
optional string msg=2;
optional User from=3;
optional User to=4;
}
message User{
required string id=1;
optional stringname=2;
}
生成Java Class
protoc--java_out=./ message.proto
序列化一个消息
@Test public void testSerialize() { MessageProtos.User from = MessageProtos.User.newBuilder().setId("1001").setName("client").build(); MessageProtos.User to = MessageProtos.User.newBuilder().setId("1000").setName("server").build(); MessageProtos.Message message = MessageProtos.Message.newBuilder().setId("0").setFrom(from).setTo(to).setMsg("hai server i`m client").build(); OutputStream outputStream = null; try { Path path = Paths.get("./message.stream"); if (!path.toFile().exists()) { path.toFile().createNewFile(); } outputStream = Files.newOutputStream(Paths.get("./message.stream"), StandardOpenOption.WRITE); message.writeTo(outputStream); outputStream.flush(); } catch (IOException e) { e.printStackTrace(); } finally { if (outputStream != null) { try { outputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } }
从序列化文件读取消息
@Test public void testUSerialize() { { InputStream inputStream = null; try { inputStream = Files.newInputStream(Paths.get("./message.stream"), StandardOpenOption.READ); MessageProtos.Message message = MessageProtos.Message.parseFrom(inputStream); System.out.println(message); } catch (IOException e) { e.printStackTrace(); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
其它方法
@Test
public void testUseFull() {
MessageProtos.Message message =MessageProtos.Message.newBuilder().setMsg("msg").setId("1").build();
//是否所有必须的字段已经被设置
System.out.println(message.isInitialized());
//toString
System.out.println(message.toString());
//序列化为字节数组
System.out.println(message.toByteArray());
//序列化到输出流
//System.out.println(message.writeTo(null));
//....
}
还可以做什么?
序列化框架和RPC框架是分布式系统的基础,在掌握Hessian、Avro、Protobuf、Protostuff等序列化[编码]技术后还需要掌握高性能IO框架,这在设计高性能应用程序中是必不可少的.