如何在C#中使用Google.Protobuf工具
阅读原文时间:2021年11月25日阅读:1

  protobuf是一个语言无关、平台无关的序列化协议,由谷歌开源提供。再加上其高性能、存储占用更小等特点,在云原生的应用中越来越广泛。

在C#中主要有两种方法来使用protobuf协议,nuget包分别为Google.Protobuf和protobuf-net,其中Google.Protobuf由谷歌官方提供。本文简要记录和展示Google.Protobuf的使用方法和特点。

项目资料及文档

准备工作

  需要用到的nuget有如下两个:Google.Protobuf、Google.Protobuf.Tools,其中Google.Protobuf是主类库,运行时要用到。Google.Protobuf.Tools提供了命令行工具,用于根据.proto文件转为目标语言的类型,仅开发时使用,运行时不需要。

  本次Demo使用的.proto文件内容如下:

syntax = "proto3";
option cc_enable_arenas = true;

package Tccc.Demo.Protobuf;

message ErrorLog {
string LogID = 1;
string Context = 2;
string Stack = 3;
}

  首先需要根据.proto文件生成目标类型,操作如下:

./google.protobuf.tools\3.19.1\tools\windows_x64\protoc.exe --csharp_out=./generatedCode ./proto/ErrorLog.proto

  其中--csharp_out选项是生成C#语言的目标类型,运行protoc.exe -h 查看帮助信息,可以看到还支持一下几种选项:

--proto_path=PATH
--cpp_out=OUT_DIR Generate C++ header and source.
--csharp_out=OUT_DIR Generate C# source file.
--java_out=OUT_DIR Generate Java source file.
--js_out=OUT_DIR Generate JavaScript source.
--kotlin_out=OUT_DIR Generate Kotlin file.
--objc_out=OUT_DIR Generate Objective-C header and source.
--php_out=OUT_DIR Generate PHP source file.
--python_out=OUT_DIR Generate Python source file.
--ruby_out=OUT_DIR Generate Ruby source file.

  运行上述命令,会根据指定的ErrorLog.proto文件生成ErrorLog.cs文件,文件中就是C#类型ErrorLog。生成的代码中会给此类型增加方法void WriteTo(CodedOutputStream output)和只读属性Parser,接下来进行序列化和反序列化的关键。

生成的ErrorLog类的完整代码:

// // Generated by the protocol buffer compiler. DO NOT EDIT! // source: ProtoFiles/ErrorLog.proto //
#pragma warning disable 1591, 0612, 3021
#region Designer generated code

using pb = global::Google.Protobuf;
using pbc = global::Google.Protobuf.Collections;
using pbr = global::Google.Protobuf.Reflection;
using scg = global::System.Collections.Generic;
namespace Tccc.Demo.Protobuf {

///

Holder for reflection information generated from ProtoFiles/ErrorLog.proto
public static partial class ErrorLogReflection {

#region Descriptor  
/// <summary>File descriptor for ProtoFiles/ErrorLog.proto</summary>  
public static pbr::FileDescriptor Descriptor {  
  get { return descriptor; }  
}  
private static pbr::FileDescriptor descriptor;

static ErrorLogReflection() {  
  byte\[\] descriptorData = global::System.Convert.FromBase64String(  
      string.Concat(  
        "ChlQcm90b0ZpbGVzL0Vycm9yTG9nLnByb3RvEhJUY2NjLkRlbW8uUHJvdG9i",  
        "dWYiOQoIRXJyb3JMb2cSDQoFTG9nSUQYASABKAkSDwoHQ29udGV4dBgCIAEo",  
        "CRINCgVTdGFjaxgDIAEoCUID+AEBYgZwcm90bzM="));  
  descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,  
      new pbr::FileDescriptor\[\] { },  
      new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo\[\] {  
        new pbr::GeneratedClrTypeInfo(typeof(global::Tccc.Demo.Protobuf.ErrorLog), global::Tccc.Demo.Protobuf.ErrorLog.Parser, new\[\]{ "LogID", "Context", "Stack" }, null, null, null, null)  
      }));  
}  
#endregion

}
#region Messages
public sealed partial class ErrorLog : pb::IMessage
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
, pb::IBufferMessage
#endif
{
private static readonly pb::MessageParser _parser = new pb::MessageParser(() => new ErrorLog());
private pb::UnknownFieldSet _unknownFields;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
public static pb::MessageParser Parser { get { return _parser; } }

\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
public static pbr::MessageDescriptor Descriptor {  
  get { return global::Tccc.Demo.Protobuf.ErrorLogReflection.Descriptor.MessageTypes\[0\]; }  
}

\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
pbr::MessageDescriptor pb::IMessage.Descriptor {  
  get { return Descriptor; }  
}

\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
public ErrorLog() {  
  OnConstruction();  
}

partial void OnConstruction();

\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
public ErrorLog(ErrorLog other) : this() {  
  logID\_ = other.logID\_;  
  context\_ = other.context\_;  
  stack\_ = other.stack\_;  
  \_unknownFields = pb::UnknownFieldSet.Clone(other.\_unknownFields);  
}

\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
public ErrorLog Clone() {  
  return new ErrorLog(this);  
}

/// <summary>Field number for the "LogID" field.</summary>  
public const int LogIDFieldNumber = 1;  
private string logID\_ = "";  
\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
public string LogID {  
  get { return logID\_; }  
  set {  
    logID\_ = pb::ProtoPreconditions.CheckNotNull(value, "value");  
  }  
}

/// <summary>Field number for the "Context" field.</summary>  
public const int ContextFieldNumber = 2;  
private string context\_ = "";  
\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
public string Context {  
  get { return context\_; }  
  set {  
    context\_ = pb::ProtoPreconditions.CheckNotNull(value, "value");  
  }  
}

/// <summary>Field number for the "Stack" field.</summary>  
public const int StackFieldNumber = 3;  
private string stack\_ = "";  
\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
public string Stack {  
  get { return stack\_; }  
  set {  
    stack\_ = pb::ProtoPreconditions.CheckNotNull(value, "value");  
  }  
}

\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
public override bool Equals(object other) {  
  return Equals(other as ErrorLog);  
}

\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
public bool Equals(ErrorLog other) {  
  if (ReferenceEquals(other, null)) {  
    return false;  
  }  
  if (ReferenceEquals(other, this)) {  
    return true;  
  }  
  if (LogID != other.LogID) return false;  
  if (Context != other.Context) return false;  
  if (Stack != other.Stack) return false;  
  return Equals(\_unknownFields, other.\_unknownFields);  
}

\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
public override int GetHashCode() {  
  int hash = 1;  
  if (LogID.Length != 0) hash ^= LogID.GetHashCode();  
  if (Context.Length != 0) hash ^= Context.GetHashCode();  
  if (Stack.Length != 0) hash ^= Stack.GetHashCode();  
  if (\_unknownFields != null) {  
    hash ^= \_unknownFields.GetHashCode();  
  }  
  return hash;  
}

\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
public override string ToString() {  
  return pb::JsonFormatter.ToDiagnosticString(this);  
}

\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
public void WriteTo(pb::CodedOutputStream output) {  
#if !GOOGLE\_PROTOBUF\_REFSTRUCT\_COMPATIBILITY\_MODE  
  output.WriteRawMessage(this);  
#else  
  if (LogID.Length != 0) {  
    output.WriteRawTag(10);  
    output.WriteString(LogID);  
  }  
  if (Context.Length != 0) {  
    output.WriteRawTag(18);  
    output.WriteString(Context);  
  }  
  if (Stack.Length != 0) {  
    output.WriteRawTag(26);  
    output.WriteString(Stack);  
  }  
  if (\_unknownFields != null) {  
    \_unknownFields.WriteTo(output);  
  }  
#endif  
}

#if !GOOGLE\_PROTOBUF\_REFSTRUCT\_COMPATIBILITY\_MODE  
\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
void pb::IBufferMessage.InternalWriteTo(ref pb::WriteContext output) {  
  if (LogID.Length != 0) {  
    output.WriteRawTag(10);  
    output.WriteString(LogID);  
  }  
  if (Context.Length != 0) {  
    output.WriteRawTag(18);  
    output.WriteString(Context);  
  }  
  if (Stack.Length != 0) {  
    output.WriteRawTag(26);  
    output.WriteString(Stack);  
  }  
  if (\_unknownFields != null) {  
    \_unknownFields.WriteTo(ref output);  
  }  
}  
#endif

\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
public int CalculateSize() {  
  int size = 0;  
  if (LogID.Length != 0) {  
    size += 1 + pb::CodedOutputStream.ComputeStringSize(LogID);  
  }  
  if (Context.Length != 0) {  
    size += 1 + pb::CodedOutputStream.ComputeStringSize(Context);  
  }  
  if (Stack.Length != 0) {  
    size += 1 + pb::CodedOutputStream.ComputeStringSize(Stack);  
  }  
  if (\_unknownFields != null) {  
    size += \_unknownFields.CalculateSize();  
  }  
  return size;  
}

\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
public void MergeFrom(ErrorLog other) {  
  if (other == null) {  
    return;  
  }  
  if (other.LogID.Length != 0) {  
    LogID = other.LogID;  
  }  
  if (other.Context.Length != 0) {  
    Context = other.Context;  
  }  
  if (other.Stack.Length != 0) {  
    Stack = other.Stack;  
  }  
  \_unknownFields = pb::UnknownFieldSet.MergeFrom(\_unknownFields, other.\_unknownFields);  
}

\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
public void MergeFrom(pb::CodedInputStream input) {  
#if !GOOGLE\_PROTOBUF\_REFSTRUCT\_COMPATIBILITY\_MODE  
  input.ReadRawMessage(this);  
#else  
  uint tag;  
  while ((tag = input.ReadTag()) != 0) {  
    switch(tag) {  
      default:  
        \_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(\_unknownFields, input);  
        break;  
      case 10: {  
        LogID = input.ReadString();  
        break;  
      }  
      case 18: {  
        Context = input.ReadString();  
        break;  
      }  
      case 26: {  
        Stack = input.ReadString();  
        break;  
      }  
    }  
  }  
#endif  
}

#if !GOOGLE\_PROTOBUF\_REFSTRUCT\_COMPATIBILITY\_MODE  
\[global::System.Diagnostics.DebuggerNonUserCodeAttribute\]  
\[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)\]  
void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {  
  uint tag;  
  while ((tag = input.ReadTag()) != 0) {  
    switch(tag) {  
      default:  
        \_unknownFields = pb::UnknownFieldSet.MergeFieldFrom(\_unknownFields, ref input);  
        break;  
      case 10: {  
        LogID = input.ReadString();  
        break;  
      }  
      case 18: {  
        Context = input.ReadString();  
        break;  
      }  
      case 26: {  
        Stack = input.ReadString();  
        break;  
      }  
    }  
  }  
}  
#endif

}

#endregion

}

#endregion Designer generated code

序列化操作

    public static byte\[\] Serialize(ErrorLog log)  
    {  
        using (MemoryStream output = new MemoryStream())  
        {  
            log.WriteTo(output);  
            return output.ToArray();  
        }  
    }

反序列化操作

        ErrorLog desErrorLog= ErrorLog.Parser.ParseFrom(data);

使用特点和理解

  • protoc.exe是支持生成多语言类型,这对于跨语言的混合编程比较方便。
  • 根据上述使用步骤可以看到,必须先使用工具protoc生成目标类型,才能调用序列化和反序列化方法,这有些不符合.net平台的编码习惯。
  • 一堆自动生成的C#类在可维护性方面欠佳,当需要调整属性字段时,还要通过工具重新生成,较为麻烦。