先富影视 發表於 2025-12-24 10:47:36

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

<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>为什么要使用二进制数据</li><li>初步思考如何方便的使用二进制或者封装</li><li>使用Protobuf</li><ul class="second_class_ul"><li>安装</li><li>第一个协议</li><li>使用</li></ul><li>总结</li><ul class="second_class_ul"></ul></ul></div><p class="maodian"></p><h2>为什么要使用二进制数据</h2>
<p>通常我们写一个简单的网络通讯软件可能使用的最多的是字符串类型,比较简单,例如发送格式为(head)19|Msg:Heart|100,x,y,z&hellip;,在接收端会解析收到的socket数据。</p>
<p>这样通常是完全可行的,但是随着数据量变大,网络吞吐量就变大,可能发送的字符串就不合适了,可能会数据量变大。</p>
<p><strong>举例:</strong></p>
<p>假如你要发送你的年收入和你的坐标,例如你的年收入是一亿两千万(123,456,789)(幸福死了)你的坐标是1.234567,如果通过字符串传输,你的收入就是9位,你的坐标可能你发小数因为精度问题还不准确通常使用二进制发送会大大节省。一个int32是4位,float类型也是4位,这样8位就够了。</p>
<p>看下面的例子:</p>
<div class="jb51code"><pre class="brush:csharp;">      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}");
                }
</pre></div>
<p>输出结果</p>
<div class="jb51code"><pre class="brush:csharp;">Hello, World!
moneybyte: 4,123456789 :xbyte: 4,1.234567
moneyget: 123456789 :xget: 1.234567
</pre></div>
<p>我们看到对于数字32位占4个字节,这样如果是大量的数据就会很节省,甚至你可以使用int16,或者bool占用更小的字节。</p>
<p>对于大量密集的网络程序使用二进制数据进行发送很必要的。</p>
<p class="maodian"></p><h2>初步思考如何方便的使用二进制或者封装</h2>
<p>是不是有这样的疑问,如果要同步一个数据包含很多类型数据,如何拼接和解析呢,好像二进制没有字符串那么直观和好使用。</p>
<p>比如我要同步的数据是如下数据(通常我们把这种格式称作协议),需要发送结构和解析正确的匹配才能解析。</p>
<p>协议头|发送的大小|我的名字|18|123456789|1.234567|我的介绍|结束</p>
<p>对于二进制如果我们有这样的结构</p>
<div class="jb51code"><pre class="brush:csharp;">public struct mydata
{
        public string name;
        public int age;
        public int money;
        public float x;
        public string readme;
       
}

</pre></div>
<p>我们可以根据结构体内的属性进行二进制发送就可以了,接收方也有这样的数据结构也进行解析就可以了,这里要注意每个属性的顺序不能是错误的。</p>
<p>网上有一些把结构体或者类打包成二进制的方法,这里就不过多说明了。</p>
<p class="maodian"></p><h2>使用Protobuf</h2>
<p>protobuf就是专门为实现这个而生的,从名字就可以看出来。</p>
<p>Protobuf 的官方 C# 库是 Google.Protobuf,可以通过 NuGet 包管理器来方便的使用。</p>
<p>我们这里就来简单说一下如何使用:</p>
<p class="maodian"></p><h3>安装</h3>
<p>首先vs里创建一个c#控制台程序。</p>
<p>然后可以通过 NuGet安装</p>
<p style="text-align:center"><img alt="" src="https://img.jbzj.com/file_images/article/202512/2025122410461393.png" /></p>
<p class="maodian"></p><h3>第一个协议</h3>
<p>我们创建一个Person.proto文件</p>
<div class="jb51code"><pre class="brush:csharp;">syntax = "proto3";

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

</pre></div>
<p>我们需要把这个proto转成c#可以解析的c#程序</p>
<p>我们可以来到Protobuf库下载执行程序,这个程序可以把proto解析成c#文件。</p>
<p>我们下载好之后:输入指令</p>
<div class="jb51code"><pre class="brush:csharp;">F:\Downloads\protoc-29.3-win64\bin&gt;protoc --csharp_out=. Person.proto

F:\Downloads\protoc-29.3-win64\bin&gt;
</pre></div>
<p>具体指令可以参考库里的文档</p>
<p>&ndash;csharp_out输出cs文件 .是当前路径</p>
<p>执行成功后会有一个Person.cs我们可以放入我们的项目,这样就很容易解析协议了。</p>
<p>生成的cs如下:</p>
<div class="jb51code"><pre class="brush:csharp;">// &lt;auto-generated&gt;
//   Generated by the protocol buffer compiler.DO NOT EDIT!
//   source: test.proto
// &lt;/auto-generated&gt;
#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;
/// &lt;summary&gt;Holder for reflection information generated from test.proto&lt;/summary&gt;
public static partial class TestReflection {

#region Descriptor
/// &lt;summary&gt;File descriptor for test.proto&lt;/summary&gt;
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

public sealed partial class Person : pb::IMessage&lt;Person&gt;
#if !GOOGLE_PROTOBUF_REFSTRUCT_COMPATIBILITY_MODE
    , pb::IBufferMessage
#endif
{
private static readonly pb::MessageParser&lt;Person&gt; _parser = new pb::MessageParser&lt;Person&gt;(() =&gt; new Person());
private pb::UnknownFieldSet _unknownFields;


public static pb::MessageParser&lt;Person&gt; Parser { get { return _parser; } }



public static pbr::MessageDescriptor Descriptor {
    get { return global::TestReflection.Descriptor.MessageTypes; }
}



pbr::MessageDescriptor pb::IMessage.Descriptor {
    get { return Descriptor; }
}



public Person() {
    OnConstruction();
}

partial void OnConstruction();



public Person(Person other) : this() {
    name_ = other.name_;
    age_ = other.age_;
    email_ = other.email_;
    _unknownFields = pb::UnknownFieldSet.Clone(other._unknownFields);
}



public Person Clone() {
    return new Person(this);
}

/// &lt;summary&gt;Field number for the "name" field.&lt;/summary&gt;
public const int NameFieldNumber = 1;
private string name_ = "";


public string Name {
    get { return name_; }
    set {
      name_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
    }
}

/// &lt;summary&gt;Field number for the "age" field.&lt;/summary&gt;
public const int AgeFieldNumber = 2;
private int age_;


public int Age {
    get { return age_; }
    set {
      age_ = value;
    }
}

/// &lt;summary&gt;Field number for the "email" field.&lt;/summary&gt;
public const int EmailFieldNumber = 3;
private string email_ = "";


public string Email {
    get { return email_; }
    set {
      email_ = pb::ProtoPreconditions.CheckNotNull(value, "value");
    }
}



public override bool Equals(object other) {
    return Equals(other as Person);
}



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);
}



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;
}



public override string ToString() {
    return pb::JsonFormatter.ToDiagnosticString(this);
}



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


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



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;
}



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);
}



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 &amp; 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


void pb::IBufferMessage.InternalMergeFrom(ref pb::ParseContext input) {
    uint tag;
    while ((tag = input.ReadTag()) != 0) {
    if ((tag &amp; 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

</pre></div>
<p class="maodian"></p><h3>使用</h3>
<p>我们开始使用</p>
<div class="jb51code"><pre class="brush:csharp;">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进行解析。
</pre></div>
<p>执行结果</p>
<div class="jb51code"><pre class="brush:csharp;">Hello, World!
serialsize 2 :Serialized Data: 10-12
Deserialized Person: Name=, Age=18, Email=
</pre></div>
<p>我们看到二进制大小是2是因为使用了一种变长编码 (varint) 的优化方案可以看下官方的文档。通常短数据比较多,使用变长编码的方式能够节省一些。</p>
<p class="maodian"></p><h2>总结</h2>
<p>到这里就结束了。以上就是Protobuf的简单使用。</p>
<p>这些仅为个人经验,希望能给大家一个参考,也希望大家多多支持琼殿技术社区。</p>
                           
                            <div class="art_xg">
                              <b>您可能感兴趣的文章:</b><ul><li>C#使用protobuf-net进行序列化的详细操作</li><li>Google.Protobuf工具在C#中的使用方法</li><li>C#语言使用gRPC、protobuf(Google Protocol Buffers)实现文件传输功能</li><li>详解C# Protobuf如何做到0分配内存的序列化</li><li>C# protobuf自动更新cs文件</li><li>C#使用Protocol Buffer(ProtoBuf)进行Unity中的Socket通信</li></ul>
                            </div>

                        </div>
                        <!--endmain-->
頁: [1]
查看完整版本: C#网络协议第三方库Protobuf的使用详解