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

时间:2025-04-11 07:25:18

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)代码整体流程如下:

  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/等,我们直接引入即可。

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)代码整理流程:

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

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

6)优点:

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