有个需求需要能动态的解析线上的proto数据,因为是对外的应用,不能随时重启,所以需要支持新的数据格式
《游戏系统设计十三》搞清楚游戏通信协议之protobuf的方方面面_游戏通讯协议-CSDN博客
之前做过一篇文章,常规的使用细节,总结流程如下

文件上传通过springboot的请求支持
数据在emqx上,通过订阅新的topic可以获得数据
proto文件的运行时解析从来没做过,也是这次的难点
先看下整个Message的数据结构

方案1:通过静态编译的方式,然后jvm 虚拟机加载生成的class文件,进行解析
方案2:通过获取proto的desc描述文件,获取proto的元数据,可以动态更新。
方案对比:
方案1更接近原生的使用方式,不利的方式在于jvm的热更新存在的问题
方案2可以自己管理,非常规方式,主要是灵活
对比下来,方案2效率略低,更灵活,能更好的满足需求
Descriptor: 对一个message定义的描述,它包含该message定义的名字、所有字段、内嵌message、内嵌enum、关联的FileDescriptor等。可以使用字段名或字段号查找FieldDescriptor。
Descriptor:简单理解为元数据

通过编辑器打开的desc文件,整个文件很小,有一些特殊的字符
相当于将proto进行了格式转换,可以在运行时进行使用
- public static String createDesc(String protoFile) throws IOException, InterruptedException {
- Path path = Paths.get(protoFile);
- String dirName = path.getParent().toString();
- String fileName = path.getFileName().toString();
- String fileNameNoExt = fileName.split("\\.")[0];
- String descriptorFile = dirName + File.separator + fileNameNoExt + ".description";
-
- // 生成descriptor文件
- String protocCMD = String.format("protoc --descriptor_set_out=%s %s --include_imports --proto_path %s", descriptorFile, protoFile, dirName);
- // protoc --descriptor_set_out=E:\work\bsm.description E:\\work\bsm.proto --include_imports --proto_path E:\work
- log.error(protocCMD);
-
- Process process = Runtime.getRuntime().exec(protocCMD);
- process.waitFor();
- int exitValue = process.exitValue();
- if (exitValue != 0) {
- log.error("protoc execute failed");
- }
- return descriptorFile;
- }
这里很简单通过调用protoc文件生成描述文件
descriptorFile:生成的文件名
protoFile:需要生成描述文件的proto
dirName:include文件所在目录
ProtoBuf提供了动态解析机制来解决这个问题,它要求提供二进制内容的基础上,再提供对应类的Descriptor对象,在解析时通过DynamicMessage类的成员方法来获得对象结果。
- public DynamicMessage.Builder getBuilder(String descriptorFile, String targetClassName) throws Descriptors.DescriptorValidationException, IOException {
-
- Descriptors.Descriptor pbDescritpor = null;
- DescriptorProtos.FileDescriptorSet descriptorSet = DescriptorProtos.FileDescriptorSet.parseFrom(new FileInputStream(descriptorFile));
-
- List<Descriptors.FileDescriptor> dependencyFileDescriptorList = new ArrayList<>();
- for (int i = 0; i < descriptorSet.getFileCount() - 1; i++) {
- dependencyFileDescriptorList.add(Descriptors.FileDescriptor.buildFrom(descriptorSet.getFile(i), dependencyFileDescriptorList.toArray(new Descriptors.FileDescriptor[i])));
- }
-
- for (DescriptorProtos.FileDescriptorProto fdp : descriptorSet.getFileList()) {
- Descriptors.FileDescriptor fileDescriptor = Descriptors.FileDescriptor.buildFrom(fdp, dependencyFileDescriptorList.toArray(new Descriptors.FileDescriptor[0]));
-
- for (Descriptors.Descriptor descriptor : fileDescriptor.getMessageTypes()) {
- String className = fdp.getOptions().getJavaPackage() + "."
- + fdp.getOptions().getJavaOuterClassname() + "$"
- + descriptor.getName();
- log.error(descriptor.getFullName() + " -> " + className);
-
- if (descriptor.getName().equals(targetClassName)) {
- log.error("descriptorFile {} targetClassName {} descriptor found", descriptorFile, targetClassName);
- pbDescritpor = descriptor;
- break;
- }
- }
-
- if (pbDescritpor != null) {
- break;
- }
- }
-
- if (pbDescritpor == null) {
- log.error("descriptorFile {} targetClassName {} No matched descriptor", descriptorFile, targetClassName);
- return null;
- }
-
- return DynamicMessage.newBuilder(pbDescritpor);
- }
这里解释下参数,
targetClass:因为proto中可能定义多个Message,这里输入你要转换的那个Message就好
descriptorFile:上一步生成为文件地址
- msgBuilder = getBuilder(descriptorFilePath, targetClass);
- DynamicMessage pbMessage = msgBuilder.mergeFrom(data).build();
- String j = JsonFormat.printer().print(pbMessage);
这里解析数据,并且转换为json
这种方式非常规方式,作为动态解析还行,如果能使用静态文件最好使用静态文件