protobuf的官方C#版本额外扩展记录
问题的发端很简单,我们项目在开发中习惯性需要在导出proto脚本的同时,使用不同的id对message进行区分。
所以,我们通过extend google.protobuf.MessageOptions的方式,扩展一个msgid用于在message的设计阶段便设定消息的固定id。这样无论服务端使用java、go都可以自己生成。而C#之前使用protobuf-net的ProtoGen,所以当我们现在计划升级为protobuf官方版本时,之前的工具便不太好用了,所以抓瞎两天之后,终于梳理了官方的流程。
声明:本文截止发稿前,使用版本为protobuf官方的
3.12.0版本。
额外说明:文中的proto文件,我们使用的还是proto2语法。如果使用3.12.0版本的话,使用proto3也应是可行的。
定义extend
我们定义了一个options.proto文件,内容如下:
syntax = "proto2";
import "include/google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional int32 msgid = 10001;
}
之后使用protoc将options.proto导出,便可得到对应的OptionsExtensions:
public static partial class OptionsExtensions {
public static readonly pb::Extension<global::Google.Protobuf.Reflection.MessageOptions, int> Msgid =
new pb::Extension<global::Google.Protobuf.Reflection.MessageOptions, int>(10001, pb::FieldCodec.ForInt32(80008, 0));
}
其中,Msgid是我们在后续序列化过程中所需要使用的。
正常定义proto文件
定义并生成好了options.proto之后,正常定义需要使用的proto即可。
此处需要注意的,只有两点:
- 在需要定义msgid的message当中,使用句式
option (msgid) = XXXX;定义msgid; - 在proto文件头部引入options.proto。
例:
syntax = "proto2";
import "options.proto"; // 需要引入之前定义好的扩展proto
package minepkg;
message SCTest {
option (msgid) = 1001; // 针对扩展出的msgid需要使用 () 包起来
optional string name = 1;
}
调用--descriptor_set_out并使用FileDescriptorSet
首先使用protoc的--descriptor_set_out,将定义好的proto文件导出。此时导出的文件便是FileDescriptorSet。这是protobuf已经定义好的pb格式,因此需要使用时将文件加载进流,并反序列化为FileDescriptorSet。
因为我们额外定义了message的msgid,所以需要在反序列化FileDescriptorSet时进行一些额外设置,以协助其识别我们新增的消息配置。否则我们在具体message当中定义的msgid将作为UnknownFieldSet被加载。
此处需要注意,在protobuf官方的C#源码中,
DescriptorProto中的UnknownFieldSet是private类型,即便我们通过partial对DescriptorProto进行扩展,也依旧是无法读取到UnknownFieldSet的内部信息。因为官方对于UnknownFieldSet也只是为了兼容而设计,而不对无法解析的用户提供读取。
基于当前版本(3.12.0)版本的protobuf,需要如下操作:
CodedInputStream stream = new CodedInputStream(/*SOMETHING*/);
stream.ExtensionRegistry = new ExtensionRegistry();
stream.ExtensionRegistry.Add(OptionsExtensions.Msgid); // OptionsExtensions.Msgid 即options.proto文件生成的代码
FileDescriptorSet set = new FileDescriptorSet();
set.MergeFrom(stream);
上述代码中的/*SOMETHING*/指代的便是我们通过protoc --descriptor_set_out生成的文件。
至此,set中的数据便是我们期盼已久的所有我们定义的proto信息。
最终处理
当我们使用FileDescriptorSet获取到所有message类型,尤其是拥有msgid配置的message之后,便可以依据自身需求,生成对应的msgid代码(枚举或静态值之类)。
当然,生成proto的流程还是必不可少的,此时再调用protoc的--csharp_out就可大功告成。
总结
其实上述某些内容在protobuf的官方C#文档中是有说明的,但是关于FileDescriptorSet的说明却很难找到(毕竟英文文档,看了小半天我的脑子就已经疼了),特此记录。