Protobuf动态解析、自描述消息(java版)

Protocol Buffers是结构化数据格式标准,提供序列化和反序列方法,用于存储和交换。语言中立,平台无关、可扩展。目前官方提供了C++、Java、Python API,也有其他语言的开源api(比如php)。可通过 .proto文件生成对应语言的类代码。

如果已知protobuf内容对应的是哪个类对象,则可以直接使用反序列化方法搞定(Xxx.parseFrom(inputStream)由二进制转换,TextFormat.merge(string, xxxBuilder)由文本转换)。

而我们经常遇到的情况是,拿到一个被protobuf序列化的二进制内容,但不知道它的类型,无法获得对应的类对象。这种多见于需要处理各种各样未知的ProtoBuf对象的系统。ProtoBuf提供了动态解析机制来解决这个问题,它要求提供二进制内容的基础上,再提供对应类的Descriptor对象,在解析时通过DynamicMessage类的成员方法来获得对象结果。

最后问题就是Descriptor对象从哪里来?这是通过protoc --descriptor_set_out=$outputpath 命令生成descriptor文件,进而得到的。接下来,我们看一些例子(使用proto3)

maven:


        com.google.protobuf
        protobuf-java
        3.0.2
        
    

1、使用protobuf的动态解析机制,来处理消息:

所谓动态解析,就是消费者不根据proto文件编译生成的类来反序列化消息,而是通过proto文件生成的descriptor来构造动态消息类,然后反序列化(解析)消息。代码如下:

1)cinema1.proto proto文件:

syntax = "proto3";
option java_package="com.lanjingling.cinema";

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:\>protoc.exe --java_out=./ ./cinema1.proto

//生成descriptor,这里通过命令行延时如何生成的,下面的java代码里每次会通过命令生成
D:\>protoc.exe --descriptor_set_out=D://cinema1.description D://cinema1.proto --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 {
		
		System.out.println("------init msg------");
		byte[] byteArray = initMsg();

		
		System.out.println("------generate descriptor------");
		// 生成descriptor文件
		String protocCMD = "protoc --descriptor_set_out=D://cinema1.description D://cinema1.proto --proto_path=D://";
        Process process = Runtime.getRuntime().exec(protocCMD);
        process.waitFor();
        int exitValue = process.exitValue();
        if (exitValue != 0) {
            System.out.println("protoc execute failed");
            return;
        }

        System.out.println("------customer msg------");
		Descriptors.Descriptor pbDescritpor = null;
		DescriptorProtos.FileDescriptorSet descriptorSet = DescriptorProtos.FileDescriptorSet
				.parseFrom(new FileInputStream("D://cinema1.description"));
		for (DescriptorProtos.FileDescriptorProto fdp : descriptorSet.getFileList()) {
			Descriptors.FileDescriptor fileDescriptor = Descriptors.FileDescriptor.buildFrom(fdp, new Descriptors.FileDescriptor[] {});
			for (Descriptors.Descriptor descriptor : fileDescriptor.getMessageTypes()) {
				String className = fdp.getOptions().getJavaPackage() + "."
						+ fdp.getOptions().getJavaOuterClassname() + "$"
						+ descriptor.getName();
				System.out.println(descriptor.getFullName() + " -> "+ className);

				if (descriptor.getName().equals("Ticket")) {
					System.out.println("Movie descriptor found");
					pbDescritpor = descriptor;
					break;
				}
			}
		}

		if (pbDescritpor == null) {
			System.out.println("No matched descriptor");
			return;
		}
		DynamicMessage.Builder pbBuilder = DynamicMessage.newBuilder(pbDescritpor);
		Message pbMessage = pbBuilder.mergeFrom(byteArray).build();

        //DynamicMessage parseFrom = DynamicMessage.parseFrom(pbDescritpor, byteArray);

		System.out.println(pbMessage);
	}
	
	private static byte[] initMsg() {
		Cinema1.Movie.Builder movieBuilder = Cinema1.Movie.newBuilder();
		movieBuilder.setName("The Shining");
		movieBuilder.setType(Cinema1.MovieType.ADULT);
		movieBuilder.setReleaseTimeStamp(327859200);
		Movie movie = movieBuilder.build();
		// byte[] byteArray = movie.toByteArray();

		Cinema1.Movie.Builder movieBuilder1 = Cinema1.Movie.newBuilder();
		movieBuilder1.setName("The Shining1");
		movieBuilder1.setType(Cinema1.MovieType.CHILDREN);
		movieBuilder1.setReleaseTimeStamp(327859201);
		Movie movie1 = movieBuilder1.build();

		Cinema1.Customer.Builder customerBuilder = Cinema1.Customer.newBuilder();
		customerBuilder.setName("echo");
		customerBuilder.setGender(Cinema1.Gender.MAN);
		customerBuilder.setBirthdayTimeStamp(1231232333);

		Cinema1.Ticket.Builder ticketBuilder = Cinema1.Ticket.newBuilder();
		ticketBuilder.setId(1);
		ticketBuilder.addMovie(movie);
		ticketBuilder.addMovie(movie1);
		ticketBuilder.setCustomer(customerBuilder.build());
		Ticket ticket = ticketBuilder.build();
		System.out.println(ticket.toString());
		byte[] byteArray = ticket.toByteArray();
		
		return byteArray;
	}
}

4)代码整体流程如下:

  1. 生产者:通过proto文件编译产生的类构造一个消息(byteArray数组);
  2. 消费者: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/wrappers.proto等,我们直接引入即可。

1)selfmd.proto 文件:

syntax = "proto3";

option java_package="com.lanjingling.selfmd";

import "google/protobuf/descriptor.proto";

message SelfDescribingMessage {
  // Set of FileDescriptorProtos which describe the type and its dependencies.
  google.protobuf.FileDescriptorSet 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)cinema1.proto proto文件:

同上

3)编译cinema1.proto 文件:

同上,只需编译成java源文件即可,为了发送消息使用。(消费端不需要该java源文件,也不需要再生成descriptor了)

4)java代码:

public class Test2 {

	public static void main(String[] args) throws Exception {
        
		Ticket initMsg = initMsg();
		System.out.println(initMsg);
		
		//product
		System.out.println("--------------protuct--------------");
		DescriptorProtos.FileDescriptorSet descriptorSet = DescriptorProtos.FileDescriptorSet
				.parseFrom(new FileInputStream("D://cinema1.description"));
		
        Builder selfmdBuilder = Selfmd.SelfDescribingMessage.newBuilder();
        selfmdBuilder.setDescriptorSet(descriptorSet);
        selfmdBuilder.setMsgName(Ticket.getDescriptor().getFullName());
        selfmdBuilder.setMessage(initMsg.toByteString());
        SelfDescribingMessage build = selfmdBuilder.build();
        byte[] byteArray = build.toByteArray();
        
        //customer
        System.out.println("--------------customer--------------");
        SelfDescribingMessage parseFrom = Selfmd.SelfDescribingMessage.parseFrom(byteArray);
        FileDescriptorSet descriptorSet2 = parseFrom.getDescriptorSet();
        ByteString message = parseFrom.getMessage();
        String msgName = parseFrom.getMsgName();
        
        Descriptors.Descriptor pbDescritpor = null;
        for (DescriptorProtos.FileDescriptorProto fdp : descriptorSet2
				.getFileList()) {
			Descriptors.FileDescriptor fileDescriptor = Descriptors.FileDescriptor
					.buildFrom(fdp, new Descriptors.FileDescriptor[] {});
			for (Descriptors.Descriptor descriptor : fileDescriptor
					.getMessageTypes()) {
				if (descriptor.getName().equals(msgName)) {
					System.out.println("descriptor found");
					pbDescritpor = descriptor;
					break;
				}
			}
		}
        
        if (pbDescritpor == null) {
			System.out.println("No matched descriptor");
			return;
		}
        DynamicMessage dmsg = DynamicMessage.parseFrom(pbDescritpor, message);
        
        System.out.println(dmsg);
	}
	
	public static Ticket initMsg() {
		//同上
		
		Ticket ticket = ticketBuilder.build();
		
		return ticket;
	}
}

5)代码整理流程:

  1. 初始化一个消息对象Ticket;
  2. 生产者:1)将cinema1.proto的descriptor内容放到selfmd中;2)将cinema1.proto中的消息名放到selfmd中;3)将cinema1.proto消息内容放到selfmd中;
  3. 消费者:获取selfmd.proto消息,根据三个字段分别解析descriptor、msg_name构造DynamicMessage 类,然后解析cinema1消息;

注:这里没有找到通过protobuf的api方式生成descriptor文件内容的,网上都是通过protobuf提供的命令行工具来生成

6)优点:

  • 优点:实现了消费者和生产者解耦;
  • 缺点:消息量增大、解析速度不如直接通过pb对象快;

 

你可能感兴趣的:(java)