C#网络协议第三方库Protobuf的使用详解

 更新时间:2025年12月24日 10:47:36   作者:Thinbug  
文章介绍了使用二进制数据传输的必要性,并详细介绍了Protobuf(Protocol Buffers)的使用方法,通过将协议定义文件(.proto)转换为C#代码,可以方便地进行二进制数据的发送和解析

为什么要使用二进制数据

通常我们写一个简单的网络通讯软件可能使用的最多的是字符串类型,比较简单,例如发送格式为(head)19|Msg:Heart|100,x,y,z…,在接收端会解析收到的socket数据。

这样通常是完全可行的,但是随着数据量变大,网络吞吐量就变大,可能发送的字符串就不合适了,可能会数据量变大。

举例:

假如你要发送你的年收入和你的坐标,例如你的年收入是一亿两千万(123,456,789)(幸福死了)你的坐标是1.234567,如果通过字符串传输,你的收入就是9位,你的坐标可能你发小数因为精度问题还不准确通常使用二进制发送会大大节省。一个int32是4位,float类型也是4位,这样8位就够了。

看下面的例子:

        static void Main(string[] args)
        {
            Console.WriteLine("Hello, World!");

            int money = 123456789;
            float x = 1.234567f;
            byte[] moneybyte = BitConverter.GetBytes(money);
            byte[] xbyte = BitConverter.GetBytes(x);
            Console.WriteLine($"moneybyte: {moneybyte.Length},{money} :xbyte: {xbyte.Length},{x}" );

            int moneyget = BitConverter.ToInt32(moneybyte);
            float xget = BitConverter.ToSingle(xbyte);
            Console.WriteLine($"moneyget: {moneyget} :xget: {xget}");
		}

输出结果

Hello, World!
moneybyte: 4,123456789 :xbyte: 4,1.234567
moneyget: 123456789 :xget: 1.234567

我们看到对于数字32位占4个字节,这样如果是大量的数据就会很节省,甚至你可以使用int16,或者bool占用更小的字节。

对于大量密集的网络程序使用二进制数据进行发送很必要的。

初步思考如何方便的使用二进制或者封装

是不是有这样的疑问,如果要同步一个数据包含很多类型数据,如何拼接和解析呢,好像二进制没有字符串那么直观和好使用。

比如我要同步的数据是如下数据(通常我们把这种格式称作协议),需要发送结构和解析正确的匹配才能解析。

协议头|发送的大小|我的名字|18|123456789|1.234567|我的介绍|结束

对于二进制如果我们有这样的结构

public struct mydata
{
	public string name;
	public int age;
	public int money;
	public float x;
	public string readme;
	 
}

我们可以根据结构体内的属性进行二进制发送就可以了,接收方也有这样的数据结构也进行解析就可以了,这里要注意每个属性的顺序不能是错误的。

网上有一些把结构体或者类打包成二进制的方法,这里就不过多说明了。

使用Protobuf

protobuf就是专门为实现这个而生的,从名字就可以看出来。

Protobuf 的官方 C# 库是 Google.Protobuf,可以通过 NuGet 包管理器来方便的使用。

我们这里就来简单说一下如何使用:

安装

首先vs里创建一个c#控制台程序。

然后可以通过 NuGet安装

第一个协议

我们创建一个Person.proto文件

syntax = "proto3";

message Person {
  string name = 1;
  int32 age = 2;
  string email = 3;
}

我们需要把这个proto转成c#可以解析的c#程序

我们可以来到Protobuf库下载执行程序,这个程序可以把proto解析成c#文件。

我们下载好之后:输入指令

F:\Downloads\protoc-29.3-win64\bin>protoc --csharp_out=. Person.proto

F:\Downloads\protoc-29.3-win64\bin>

具体指令可以参考库里的文档

–csharp_out输出cs文件 .是当前路径

执行成功后会有一个Person.cs我们可以放入我们的项目,这样就很容易解析协议了。

生成的cs如下:

// <auto-generated>
//     Generated by the protocol buffer compiler.  DO NOT EDIT!
//     source: test.proto
// </auto-generated>
#pragma warning disable 1591, 0612, 3021, 8981
#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;
/// <summary>Holder for reflection information generated from test.proto</summary>
public static partial class TestReflection {

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

  static TestReflection() {
    byte[] descriptorData = global::System.Convert.FromBase64String(
        string.Concat(
          "Cgp0ZXN0LnByb3RvIjIKBlBlcnNvbhIMCgRuYW1lGAEgASgJEgsKA2FnZRgC",
          "IAEoBRINCgVlbWFpbBgDIAEoCWIGcHJvdG8z"));
    descriptor = pbr::FileDescriptor.FromGeneratedCode(descriptorData,
        new pbr::FileDescriptor[] { },
        new pbr::GeneratedClrTypeInfo(null, null, new pbr::GeneratedClrTypeInfo[] {
          new pbr::GeneratedClrTypeInfo(typeof(global::Person), global::Person.Parser, new[]{ "Name", "Age", "Email" }, null, null, null, null)
        }));
  }
  #endregion

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

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
  public static pbr::MessageDescriptor Descriptor {
    get { return global::TestReflection.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 Person() {
    OnConstruction();
  }

  partial void OnConstruction();

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
  public Person(Person other) : this() {
    name_ = other.name_;
    age_ = other.age_;
    email_ = other.email_;
    _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
  }

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

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

  /// <summary>Field number for the "age" field.</summary>
  public const int AgeFieldNumber = 2;
  private int age_;
  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
  public int Age {
    get { return age_; }
    set {
      age_ = value;
    }
  }

  /// <summary>Field number for the "email" field.</summary>
  public const int EmailFieldNumber = 3;
  private string email_ = "";
  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
  public string Email {
    get { return email_; }
    set {
      email_ = 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 Person);
  }

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
  public bool Equals(Person other) {
    if (ReferenceEquals(other, null)) {
      return false;
    }
    if (ReferenceEquals(other, this)) {
      return true;
    }
    if (Name != other.Name) return false;
    if (Age != other.Age) return false;
    if (Email != other.Email) 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 (Name.Length != 0) hash ^= Name.GetHashCode();
    if (Age != 0) hash ^= Age.GetHashCode();
    if (Email.Length != 0) hash ^= Email.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 (Name.Length != 0) {
      output.WriteRawTag(10);
      output.WriteString(Name);
    }
    if (Age != 0) {
      output.WriteRawTag(16);
      output.WriteInt32(Age);
    }
    if (Email.Length != 0) {
      output.WriteRawTag(26);
      output.WriteString(Email);
    }
    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 (Name.Length != 0) {
      output.WriteRawTag(10);
      output.WriteString(Name);
    }
    if (Age != 0) {
      output.WriteRawTag(16);
      output.WriteInt32(Age);
    }
    if (Email.Length != 0) {
      output.WriteRawTag(26);
      output.WriteString(Email);
    }
    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 (Name.Length != 0) {
      size += 1 + pb::CodedOutputStream.ComputeStringSize(Name);
    }
    if (Age != 0) {
      size += 1 + pb::CodedOutputStream.ComputeInt32Size(Age);
    }
    if (Email.Length != 0) {
      size += 1 + pb::CodedOutputStream.ComputeStringSize(Email);
    }
    if (_unknownFields != null) {
      size += _unknownFields.CalculateSize();
    }
    return size;
  }

  [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
  [global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
  public void MergeFrom(Person other) {
    if (other == null) {
      return;
    }
    if (other.Name.Length != 0) {
      Name = other.Name;
    }
    if (other.Age != 0) {
      Age = other.Age;
    }
    if (other.Email.Length != 0) {
      Email = other.Email;
    }
    _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) {
    if ((tag & 7) == 4) {
      // Abort on any end group tag.
      return;
    }
    switch(tag) {
        default:
          _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, input);
          break;
        case 10: {
          Name = input.ReadString();
          break;
        }
        case 16: {
          Age = input.ReadInt32();
          break;
        }
        case 26: {
          Email = 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) {
    if ((tag & 7) == 4) {
      // Abort on any end group tag.
      return;
    }
    switch(tag) {
        default:
          _unknownFields = pb::UnknownFieldSet.MergeFieldFrom(_unknownFields, ref input);
          break;
        case 10: {
          Name = input.ReadString();
          break;
        }
        case 16: {
          Age = input.ReadInt32();
          break;
        }
        case 26: {
          Email = input.ReadString();
          break;
        }
      }
    }
  }
  #endif

}

#endregion


#endregion Designer generated code

使用

我们开始使用

static void Main(string[] args)
{
    Console.WriteLine("Hello, World!");


    var person = new Person
    {
        Age = 18,
        Name = "",
        Email = ""
    };
    byte[] serializedData = person.ToByteArray();

    Console.WriteLine($"serialsize {serializedData.Length} :Serialized Data: "  + BitConverter.ToString(serializedData));

    // 从字节数组反序列化
    var deserializedPerson = Person.Parser.ParseFrom(serializedData);
    Console.WriteLine($"Deserialized Person: Name={deserializedPerson.Name}, Age={deserializedPerson.Age}, Email={deserializedPerson.Email}");

}
代码中我们给person赋值,并通过ToByteArray二进制转化,得到二进制数组后就可以通过网络发送了。
当接收方收到这个二进制数据就可以通过ParseFrom进行解析。

执行结果

Hello, World!
serialsize 2 :Serialized Data: 10-12
Deserialized Person: Name=, Age=18, Email=

我们看到二进制大小是2是因为使用了一种变长编码 (varint) 的优化方案可以看下官方的文档。通常短数据比较多,使用变长编码的方式能够节省一些。

总结

到这里就结束了。以上就是Protobuf的简单使用。

这些仅为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • C#中使用Override和New关键字进行版本控制

    C#中使用Override和New关键字进行版本控制

    在 C# 中,override 和 new 关键字用于控制类之间的成员方法的隐藏和重写,理解它们之间的差异和使用场景对于设计灵活且易于维护的代码至关重要,在这篇博客中,我们将详细探讨这两个关键字的用法,并通过示例来说明它们的实际应用,需要的朋友可以参考下
    2024-10-10
  • C#使用XSLT实现xsl、xml与html相互转换

    C#使用XSLT实现xsl、xml与html相互转换

    这篇文章介绍了C#使用XSLT实现xsl、xml与html相互转换的方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-06-06
  • c#的params参数使用示例

    c#的params参数使用示例

    这篇文章主要介绍了c#的params参数使用示例,需要的朋友可以参考下
    2014-04-04
  • C#基础语法:Base关键字学习笔记

    C#基础语法:Base关键字学习笔记

    这篇文章主要介绍了C#基础语法:Base关键字学习笔记,本文讲解了它的一些基础知识以及测试代码,需要的朋友可以参考下
    2015-06-06
  • C#使用Spire.Doc实现企业级Word文档打印的完整方案

    C#使用Spire.Doc实现企业级Word文档打印的完整方案

    本文介绍了如何使用Spire.Doc实现无需Office的精准Word打印方案,解决传统方法分页复杂和依赖本地Office的问题,提供环境依赖低、跨平台支持及简单API的优势,并涵盖实现步骤、高级配置和注意事项,需要的朋友可以参考下
    2025-08-08
  • C#基础之异步调用实例教程

    C#基础之异步调用实例教程

    这篇文章主要介绍了C#中的异步调用,对比同步调用分析了异步调用的原理及特点,并以实例形式给出了实现方法,需要的朋友可以参考下
    2014-09-09
  • C# 二进制数组与结构体的互转方法

    C# 二进制数组与结构体的互转方法

    本文将和大家介绍 MemoryMarshal 辅助类,通过这个辅助类用来实现结构体数组和二进制数组的相互转换,对C# 二进制数组与结构体的互转方法感兴趣的朋友一起看看吧
    2023-09-09
  • C#中方括号[]的语法及作用介绍

    C#中方括号[]的语法及作用介绍

    C#中方括号[]可用于数组,索引、属性,更重要的是用于外部DLL类库的引用。
    2013-04-04
  • Unity UGUI的EventTrigger事件监听器组件介绍使用示例

    Unity UGUI的EventTrigger事件监听器组件介绍使用示例

    这篇文章主要为大家介绍了Unity UGUI的EventTrigger事件监听器组件介绍及使用,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • winform基于异步委托实现多线程摇奖器

    winform基于异步委托实现多线程摇奖器

    这篇文章主要介绍了winform基于异步委托实现多线程摇奖器的方法,包含了线程的运用及随机数的生成,需要的朋友可以参考下
    2014-10-10

最新评论