多平台统一管理软件接口,如何实现多平台统一管理软件接口
446
2022-10-26
使用CSharp编写Google Protobuf插件
什么是 Google Protocol Buffer?
Google Protocol Buffer( 简称 Protobuf) 是 Google 公司内部的混合语言数据标准,目前已经正在使用的有超过 48,162 种报文格式定义和超过 12,183 个 .proto 文件。他们用于 RPC 系统和持续数据存储系统。
Protocol Buffers 是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC 数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 多种语言的API,包括C++、 C# 、GO、 JAVA、 PYTHON
如果你并不了解Protobuf能做什么,建议结合google搜索关键字,看一下入门级别的文章,或者看一下官方文档中的Developer Guide,或者中文的开发指南 .官方的文档中有各种语言相关的示例,可以结合代码看一下实际的用法。
很多人说为什么不用json(或者xml), 答案很简单,Protobuf更小,更简洁,而且序列化和反序列化更快!
谷歌最新开源的gRpc框架就是默认使用Protobuf作为数据传输格式和服务描述文件。对于gRpc 就不做详细介绍了,有兴趣的可以看一下官网。
言归正传,在实际使用Protobuf过程中,我发现Protobuf不但可以编写描述消息(Message)的内容,同时可以表述其他方法(类似Rpc中的方法),主要是gRpc中看到的。同时在Protobuf 代码生成工具的包中,有一个这样的目录,一致以来都没搞明白是做什么用的,如下图:
在目录中存在大量已经定义好的proto文件,其实这些文件是Protobuf的描述文件,类似元数据。用本身的语法描述本身,同时通过这些文件生成对应的语言的元数据类等代码,比如在C#版本的Google.Protobuf中就能看到上述描述文件生成的类,如下图所示
而这些描述文件中最重要的文件 就是descriptor.proto 这个文件,这个文件是整个proto语法的描述类,描述了实际Protobuf各层次语法的结构,来一起看一下这个文件的一些代码, 上面这个代码描述了proto文件定义的语法定义,如前面两个字段意思是可选的name,可选的package字段,中间是描述可多个message_type(Message),service(Rpc Service) ,enum_type(枚举)等定义,然后一层层分解下去。 基本上就可以了解Protobuf语法的全貌和扩展点了
message FileDescriptorProto { optional string name = 1; // file name, relative to root of source tree optional string package = 2; // e.g. "foo", "foo.bar", etc. // Names of files imported by this file. repeated string dependency = 3; // Indexes of the public imported files in the dependency list above. repeated int32 public_dependency = 10; // Indexes of the weak imported files in the dependency list. // For Google-internal migration only. Do not use. repeated int32 weak_dependency = 11; // All top-level definitions in this file. repeated DescriptorProto message_type = 4; repeated EnumDescriptorProto enum_type = 5; repeated ServiceDescriptorProto service = 6; repeated FieldDescriptorProto extension = 7; optional FileOptions options = 8; // This field contains optional information about the original source code. // You may safely remove this entire field without harming runtime // functionality of the descriptors -- the information is needed only by // development tools. optional SourceCodeInfo source_code_info = 9; // The syntax of the proto file. // The supported values are "proto2" and "proto3". optional string syntax = 12; }
同时在compiler目录下 还有一个plugin的目录,其中的plugin.proto文件很耐人寻味,先来看下这个文件中的内容
syntax = "proto3";package google.protobuf.compiler;option java_package = "com.google.protobuf.compiler";option java_outer_classname = "PluginProtos";option csharp_namespace = "Google.Protobuf.Compiler";option go_package = "plugin_go";import "google/protobuf/descriptor.proto";message CodeGeneratorRequest { repeated string file_to_generate = 1; string parameter = 2; repeated FileDescriptorProto proto_file = 15; }message CodeGeneratorResponse { string error = 1; message File { string name = 1; string insertion_point = 2; string content = 15; } repeated File file = 15; }
删除了非必要的注释后,我们可以看到这个文件里面其实只定义了两个类型,一个是代码生成请求,一个是代码生成响应,而在CodeGeneratorRequest中又有之前我们在descriptor.proto中看到的FileDescriptorProto 这个类的信息,用大腿都可以想到这里应该就是代码生成插件获取元数据的入口了,那么怎么做呢?
从gRpc 的代码生成示例中 我们可以看到 其实Protobuf是支持自定义生成代码插件的,如下所示:
%PROTOC% -I../../protos --csharp_out Greeter ../../protos/helloworld.proto --grpc_out Greeter --plugin=protoc-gen-grpc=%PLUGIN%
按理我们可以实现自己的插件来生成我们需要的任意格式,包括各种代码,甚至是文档。但是这个资料却非常少,几乎没有多少相关的文章,后来终于找到一片关于plugin的文章core part is the interface code to read a request from the stdin, traverse the AST and write the response on the stdout.
原来插件的接口代码其实是从标准输入中读取流,然后再把你要生成的内容输出到标准输出中。这些终于知道怎么用了。。
撩起袖子开始干,通过protoc命令行生成plugin.proto的代码
protoc-I../../protos --csharp_out test ../../protos/plugin.proto
新建一个控制台项目,把代码copy 到项目中,并在Program.cs代码中添加测试的代码
using Google.Protobuf;using Google.Protobuf.Compiler;using System;namespace DotBPE.ProtobufPlugin{ class Program { static void Main(string[] args) { Console.OutputEncoding = System.Text.Encoding.UTF8; var response = new CodeGeneratorResponse(); try { CodeGeneratorRequest request; using (var inStream = Console.OpenStandardInput()) { request = CodeGeneratorRequest.Parser.ParseFrom(inStream); } ParseCode(request, response); } catch (Exception e) { response.Error += e.ToString(); } using (var output = Console.OpenStandardOutput()) { response.WriteTo(output); output.Flush(); } } private static void ParseCode(CodeGeneratorRequest request, CodeGeneratorResponse response) { DotbpeGen.Generate(request,response); } } }
哈哈 开始编译,然而编译不通过!,坑爹啊! 原来C#版本中 Google.Protobuf已经生成好的类 都是internal访问权限,不能从外部引用。。。但是Google.Protobuf是开源的。。而且我需要用的类 我也可以通过protoc命令自己生成到同一个项目中,或者设置成public访问权限。。方便起见,我直接copy了Google.Protobuf的源码到我们的项目中,这次再次编译 ,代码就完美运行了,接下来的工作 不过是填充DotbpeGen.Generate 的代码了,这不过是体力活。
至于CodeGeneratorRequest和CodeGeneratorResponse 到底有什么方法,其实看proto文件就能知道。以下是我自己在项目中使用的生成代码类 供大家参考
using Google.Protobuf.Compiler;using Google.Protobuf.Reflection;using System;using System.Collections.Generic;using System.IO;using System.Linq;using System.Text;namespace DotBPE.ProtobufPlugin{ public class DotbpeGen
{ public static void Generate(CodeGeneratorRequest request, CodeGeneratorResponse response) { foreach (var protofile in request.ProtoFile)
{ try{
GenerateByProtoFile(protofile, response);
} catch(Exception ex){ using (Stream stream = File.Create("./error.txt"))
{ byte[] err = Encoding.UTF8.GetBytes(ex.Message+ex.StackTrace);
stream.Write(err,0,err.Length);
}
response.Error += ex.Message;
}
}
} private static void GenerateSourceInfo(FileDescriptorProto protofile, CodeGeneratorResponse response) { bool genericDoc;
protofile.Options.CustomOptions.TryGetBool(DotBPEOptions.GENERIC_MARKDOWN_DOC,out genericDoc); if (!genericDoc)
{ return;
}
StringBuilder sb = new StringBuilder(); foreach (var location in protofile.SourceCodeInfo.Location)
{ string path = String.Join(",", location.Path); string span = String.Join(",", location.Span); string leadingDetachedComments = String.Join("\r", location.LeadingDetachedComments); string trailingComments = String.Join("\r", location.TrailingComments);
sb.AppendLine("{\"Path\",\""+path+"\",\"Span\",\""+span+"\",\"LeadingComments\",\""+ location.LeadingComments + "\",\"LeadingDetachedComments\",\""+ leadingDetachedComments + "\",\"TrailingComments\",\""+ trailingComments + "\"}");
} var nfile = new CodeGeneratorResponse.Types.File
{
Name = GetFileName(protofile.Name) + "SI.txt",
Content = sb.ToString()
};
response.File.Add(nfile);
} private static void GenerateByProtoFile(FileDescriptorProto protofile, CodeGeneratorResponse response) {
GenerateSourceInfo(protofile, response);
GenerateServer(protofile, response);
GenerateClient(protofile, response);
} private static void GenerateServer(FileDescriptorProto protofile, CodeGeneratorResponse response) { bool genericServer;
protofile.Options.CustomOptions.TryGetBool(DotBPEOptions.DISABLE_GENERIC_SERVICES_SERVER, out genericServer); if (genericServer)
{ return;
} if (protofile.Service == null || protofile.Service.Count <= 0) return; //生成文件头
StringBuilder sb = new StringBuilder();
sb.AppendLine("// Generated by the protocol buffer compiler. DO NOT EDIT!");
sb.AppendLine($"// source: {protofile.Name}"); //还可以生成注释
sb.AppendLine("#region Designer generated code");
sb.AppendLine("");
sb.AppendLine("using System; ");
sb.AppendLine("using System.Threading.Tasks; ");
sb.AppendLine("using DotBPE.Rpc; ");
sb.AppendLine("using DotBPE.Protocol.Amp; ");
sb.AppendLine("using Google.Protobuf; ");
sb.AppendLine(""); string ns = GetFileNamespace(protofile);
sb.AppendLine("namespace " + ns + " {"); //生成代码
foreach (ServiceDescriptorProto t in protofile.Service)
{
t.Options.CustomOptions.TryGetBool(DotBPEOptions.DISABLE_GENERIC_SERVICES_SERVER, out genericServer); if (genericServer)
{ continue;
}
sb.AppendLine("");
sb.AppendLine("//start for class Abstract"+t.Name);
GenerateServiceForServer(t, sb);
sb.AppendLine("//end for class Abstract"+t.Name);
}
sb.AppendLine("}\n");
sb.AppendLine("#endregion\n"); var nfile = new CodeGeneratorResponse.Types.File
{
Name = GetFileName(protofile.Name) + "Server.cs",
Content = sb.ToString()
};
response.File.Add(nfile);
} private static void GenerateClient(FileDescriptorProto protofile, CodeGeneratorResponse response) { bool genericClient;
protofile.Options.CustomOptions.TryGetBool(DotBPEOptions.DISABLE_GENERIC_SERVICES_CLIENT, out genericClient); if (genericClient)
{ return;
} if (protofile.Service == null || protofile.Service.Count <= 0) return; //生成文件头
StringBuilder sb = new StringBuilder();
sb.AppendLine("// Generated by the protocol buffer compiler. DO NOT EDIT!");
sb.AppendLine($"// source: {protofile.Name}"); //还可以生成注释
sb.AppendLine("#region Designer generated code");
sb.AppendLine("");
sb.AppendLine("using System; ");
sb.AppendLine("using System.Threading.Tasks; ");
sb.AppendLine("using DotBPE.Rpc; ");
sb.AppendLine("using DotBPE.Protocol.Amp; ");
sb.AppendLine("using DotBPE.Rpc.Exceptions; ");
sb.AppendLine("using Google.Protobuf; ");
sb.AppendLine(""); string ns = GetFileNamespace(protofile);
sb.AppendLine("namespace " + ns + " {"); //生成代码
foreach (ServiceDescriptorProto t in protofile.Service)
{
t.Options.CustomOptions.TryGetBool(DotBPEOptions.DISABLE_GENERIC_SERVICES_CLIENT, out genericClient); if (genericClient)
{ continue;
}
sb.AppendLine("");
sb.AppendLine("//start for class "+t.Name+"Client");
GenerateServiceForClient(t, sb);
sb.AppendLine("//end for class "+t.Name+"Client");
}
sb.AppendLine("}");
sb.AppendLine("#endregion"); //生成文件
var nfile = new CodeGeneratorResponse.Types.File
{
Name = GetFileName(protofile.Name) + "Client.cs",
Content = sb.ToString()
};
response.File.Add(nfile);
} private static void GenerateServiceForClient(ServiceDescriptorProto service, StringBuilder sb) { int serviceId; bool hasServiceId = service.Options.CustomOptions.TryGetInt32(DotBPEOptions.SERVICE_ID, out serviceId); if (!hasServiceId || serviceId <= 0)
{ throw new Exception("Service=" + service.Name + " ServiceId NOT_FOUND");
} if (serviceId >= ushort.MaxValue)
{ throw new Exception("Service=" + service.Name + "ServiceId too large");
}
sb.AppendFormat("public sealed class {0}Client : AmpInvokeClient \n",service.Name);
sb.AppendLine("{"); //构造函数
sb.AppendLine($"public {service.Name}Client(IRpcClient
然后我们编写一个proto文件测试以下
//benchmark.protosyntax = "proto3";package dotbpe; option csharp_namespace = "DotBPE.IntegrationTesting";import public "dotbpe_option.proto"; option optimize_for = SPEED;//Benchmark测试服务service BenchmarkTest{ option (service_id)= 50000 ;//设定服务ID //测试发送Echo消息 rpc Echo (BenchmarkMessage) returns (BenchmarkMessage){ option (message_id)= 1 ;//设定消息ID };//Echo尾部的注释 // 测试发送退出消息 rpc Quit (Void) returns (Void){ option (message_id)= 10000 ;//设定消息ID };//Quit尾部的注释}//我是void消息message Void { }//我是BenchmarkMessage消息message BenchmarkMessage { //字段前的注释 string field1 = 1; //字段后的注释 //字段前的注释 多行 //字段前的字数多行 int32 field2 = 2; //字段后的注释 /** * 字段前注释特殊格式 * 字段前注释特殊格式多行 */ int32 field3 = 3; string field4 = 4; repeated fixed64 field5 = 5; string field9 = 9; string field18 = 18; bool field80 = 80; bool field81 = 81; int32 field280 = 280 ; int32 field6 = 6; int64 field22 = 22 ; bool field59 = 59 ; string field7 = 7; int32 field16 = 16 ; int32 field130 = 130 ; bool field12 = 12 ; bool field17 = 17; bool field13 = 13; bool field14 = 14; int32 field104 = 104 ; int32 field100 = 100 ; int32 field101 = 101 ; string field102 = 102; string field103 = 103; int32 field29 = 29 ; bool field30 = 30 ; int32 field60 = 60 ; int32 field271 = 271 ; int32 field272 = 272; int32 field150 = 150; int32 field23 = 23; bool field24 = 24; int32 field25 = 25 ; bool field78 = 78 ; int32 field67 = 67; int32 field68 = 68 ; int32 field128 = 128 ; string field129 = 129 ; int32 field131 = 131 ; }
// dotbpe_option.proto// [START declaration]syntax = "proto3";package dotbpe;// [END declaration]// [START csharp_declaration]option csharp_namespace = "DotBPE.ProtoBuf";// [END csharp_declaration]import "google/protobuf/descriptor.proto";//扩展服务extend google.protobuf.ServiceOptions { int32 service_id = 51001; bool disable_generic_service_client = 51003; //是否生成客户端代码 bool disable_generic_service_server = 51004; //是否生成服务端代码} extend google.protobuf.MethodOptions { int32 message_id = 51002; } extend google.protobuf.FileOptions { bool disable_generic_services_client = 51003; //是否生成客户端代码 bool disable_generic_services_server = 51004; //是否生成服务端代码 bool generic_markdown_doc = 51005; //是否生成文档}
上面的dotbpe_option.proto 我们proto文件进行了自定义的扩展,添加一些自己需要的额外信息,其实所有扩展都是对descriptor.proto中消息的扩展。
然后我们通过命令来生成一下,这里有个特殊的约定,一定要注意当我们设置
protoc-gen-dotbpe=../../tool/ampplugin/dotbpe_amp.exe 插件的名称protoc-gen-dotbpe时,那么输出的目录一定要写成--dotbpe_out ,两个名字一点要匹配哦
set -excd $(dirname $0)/../../test/IntegrationTesting/ PROTOC=protoc PLUGIN=protoc-gen-dotbpe=../../tool/ampplugin/dotbpe_amp.exe IntegrationTesting_DIR=./DotBPE.IntegrationTesting/$PROTOC -I=./protos --csharp_out=$IntegrationTesting_DIR --dotbpe_out=$IntegrationTesting_DIR \ ./protos/benchmark.proto --plugin=$PLUGIN
差不多就结束了,相关的代码可以在SourceCodeInfo, 这个消息体里有如下字段
optional string leading_comments = 3; optional string trailing_comments = 4; repeated string leading_detached_comments = 6;
这是非常有意思的定义,意思是可以在运行时获取到proto文件中的注释。这可以帮助我们生成 文档或者代码注释,但是读取逻辑比较复杂,其内部有一个通过Path和Span来定位元素的逻辑。因为在实际的情况中,一般都是要获取Service和Message上的注释,那么就来专门讨论一下如何获取这两个类型的注释吧。
下面是 SourceCodeInfo.Location 中我们需要用到Path示例
* [4, m] - Message的注释 * [4, m, 2, f] - Message 中 字段(field)的注释 * [6, s] - Service的注释 * [6, s, 2, r] - Service中Rpc方法的注释
where:
m - proto文件中Message的索引(就是第几个定义的Message), 从0开始f - Message中Field字段的索引(就是第几个字段), 从0开始s - proto文件中Service的索引, 从0开始r - Service中Rpc方法的索引, 从0开始
like this:
// [4, 0] 就是这里的注释 message MyMessage { // [4, 0, 2, 0] 在这里 int32 field1 = 1; // [4, 0, 2, 0] 也在这里}// [4, 0] 就是这里的注释 // [6, 0] 在这里!service MyService { // [6, 0, 2, 0] 在这里! rpc (MyMessage) returns (MyMessage); }
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~