Protocol Buffers是结构化数据格式标准,提供序列化和反序列方法,用于存储和交换。语言中立,平台无关、可扩展。目前官方提供了C++、Java、Python API,也有其他语言的开源api(比如php)。可通过 .proto文件生成对应语言的类代码。
如果已知protobuf内容对应的是哪个类对象,则可以直接使用反序列化方法搞定((inputStream)由二进制转换,(string, xxxBuilder)由文本转换)。
而我们经常遇到的情况是,拿到一个被protobuf序列化的二进制内容,但不知道它的类型,无法获得对应的类对象。这种多见于需要处理各种各样未知的ProtoBuf对象的系统。ProtoBuf提供了动态解析机制来解决这个问题,它要求提供二进制内容的基础上,再提供对应类的Descriptor对象,在解析时通过DynamicMessage类的成员方法来获得对象结果。
最后问题就是Descriptor对象从哪里来?这是通过protoc --descriptor_set_out=$outputpath 命令生成descriptor文件,进而得到的。接下来,我们看一些例子(使用proto3)
maven:
<dependency>
<groupId></groupId>
<artifactId>protobuf-java</artifactId>
<version>3.0.2</version>
<!--<version>2.5.0</version> -->
</dependency>
1、使用protobuf的动态解析机制,来处理消息:
所谓动态解析,就是消费者不根据proto文件编译生成的类来反序列化消息,而是通过proto文件生成的descriptor来构造动态消息类,然后反序列化(解析)消息。代码如下:
1) proto文件:
syntax = "proto3";
option java_package="";
enum MovieType{
CHILDREN=0;
ADULT=1;
NORMAL=2;
OHTER=3;
}
enum Gender{
MAN=0;
WOMAN=1;
OTHER=2;
}
message Movie{
string name=1;
MovieType type=2;
int32 releaseTimeStamp=3;
string description=4;
string address=5;
}
message Customer{
string name=1;
Gender gender=2;
int32 birthdayTimeStamp=3;
}
message Ticket{
int32 id=1;
repeated Movie movie=2;
Customer customer=3;
}
2)编译proto文件:
//生成java源文件
D:\> --java_out=./ ./
//生成descriptor,这里通过命令行延时如何生成的,下面的java代码里每次会通过命令生成
D:\> --descriptor_set_out=D:// D:// --prot
o_path=D://
注:生成的descriptor文件也是一个二进制文件。
3)java代码:
public class Test1 {
public static void main(String[] args) throws Exception {
while (true) {
test();
break;
}
}
private static void test() throws IOException, InterruptedException,
FileNotFoundException, DescriptorValidationException,
InvalidProtocolBufferException {
("------init msg------");
byte[] byteArray = initMsg();
("------generate descriptor------");
// 生成descriptor文件
String protocCMD = "protoc --descriptor_set_out=D:// D:// --proto_path=D://";
Process process = ().exec(protocCMD);
();
int exitValue = ();
if (exitValue != 0) {
("protoc execute failed");
return;
}
("------customer msg------");
pbDescritpor = null;
descriptorSet =
.parseFrom(new FileInputStream("D://"));
for ( fdp : ()) {
fileDescriptor = (fdp, new [] {});
for ( descriptor : ()) {
String className = ().getJavaPackage() + "."
+ ().getJavaOuterClassname() + "$"
+ ();
(() + " -> "+ className);
if (().equals("Ticket")) {
("Movie descriptor found");
pbDescritpor = descriptor;
break;
}
}
}
if (pbDescritpor == null) {
("No matched descriptor");
return;
}
pbBuilder = (pbDescritpor);
Message pbMessage = (byteArray).build();
//DynamicMessage parseFrom = (pbDescritpor, byteArray);
(pbMessage);
}
private static byte[] initMsg() {
movieBuilder = ();
("The Shining");
();
(327859200);
Movie movie = ();
// byte[] byteArray = ();
movieBuilder1 = ();
("The Shining1");
();
(327859201);
Movie movie1 = ();
customerBuilder = ();
("echo");
();
(1231232333);
ticketBuilder = ();
(1);
(movie);
(movie1);
(());
Ticket ticket = ();
(());
byte[] byteArray = ();
return byteArray;
}
}
4)代码整体流程如下:
- 生产者:通过proto文件编译产生的类构造一个消息(byteArray数组);
- 消费者:1)根据proto文件生成descriptor文件;2)创建DynamicMessage类解析消息(byteArray数组);
5)优点:
生产者和消费者实现了解耦,消费者不再需要proto文件,而是使用proto文件生成的descriptor文件构造DynamicMessage类来解析生产者消息,一般我们可以把descriptor文件保存到某个地方,而不是每次都去生成。
2、自描述消息:
上面那种方式不太友好,消费者每次都需要重新生成或者从某个地方获取descriptor文件内容,google提供了一个更为合理的方式Self-describing Messages,即:将descriptor、消息名、消息内容放到一个wrapper message中,消费者收到消息后从wrapper message中先获取descriptor和name,然后构造DynamicMessage类,最后通过它来解析消息内容。
注:在protobuf库中,已经定义好了一些消息,如:descriptor/等,我们直接引入即可。
1) 文件:
syntax = "proto3";
option java_package="";
import "google/protobuf/";
message SelfDescribingMessage {
// Set of FileDescriptorProtos which describe the type and its dependencies.
descriptor_set = 1;
string msg_name=2;
// The message and its type, encoded as an Any message.
bytes message = 3;
}
用来定义通用的消息体,里面包含了:descriptor、msg_name、message
2) proto文件:
同上
3)编译 文件:
同上,只需编译成java源文件即可,为了发送消息使用。(消费端不需要该java源文件,也不需要再生成descriptor了)
4)java代码:
public class Test2 {
public static void main(String[] args) throws Exception {
Ticket initMsg = initMsg();
(initMsg);
//product
("--------------protuct--------------");
descriptorSet =
.parseFrom(new FileInputStream("D://"));
Builder selfmdBuilder = ();
(descriptorSet);
(().getFullName());
(());
SelfDescribingMessage build = ();
byte[] byteArray = ();
//customer
("--------------customer--------------");
SelfDescribingMessage parseFrom = (byteArray);
FileDescriptorSet descriptorSet2 = ();
ByteString message = ();
String msgName = ();
pbDescritpor = null;
for ( fdp : descriptorSet2
.getFileList()) {
fileDescriptor =
.buildFrom(fdp, new [] {});
for ( descriptor : fileDescriptor
.getMessageTypes()) {
if (().equals(msgName)) {
("descriptor found");
pbDescritpor = descriptor;
break;
}
}
}
if (pbDescritpor == null) {
("No matched descriptor");
return;
}
DynamicMessage dmsg = (pbDescritpor, message);
(dmsg);
}
public static Ticket initMsg() {
//同上
Ticket ticket = ();
return ticket;
}
}
5)代码整理流程:
- 初始化一个消息对象Ticket;
- 生产者:1)将的descriptor内容放到selfmd中;2)将中的消息名放到selfmd中;3)将消息内容放到selfmd中;
- 消费者:获取消息,根据三个字段分别解析descriptor、msg_name构造DynamicMessage 类,然后解析cinema1消息;
注:这里没有找到通过protobuf的api方式生成descriptor文件内容的,网上都是通过protobuf提供的命令行工具来生成;
6)优点:
- 优点:实现了消费者和生产者解耦;
- 缺点:消息量增大、解析速度不如直接通过pb对象快;