欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 时评 > 2025-05-14 Unity 网络基础13——Protobuf

2025-05-14 Unity 网络基础13——Protobuf

2025/5/15 16:39:08 来源:https://blog.csdn.net/zheliku/article/details/147937838  浏览:    关键词:2025-05-14 Unity 网络基础13——Protobuf

文章目录

  • 1 Protobuf
    • 1.1 导入 Protobuf
    • 1.2 导入编译器
  • 2 配置规则
    • 2.1 注释
    • 2.2 版本号
    • 2.3 命名空间
    • 2.4 消息类
      • 2.4.1 float / double
      • 2.4.2 int/sint/uint
      • 2.4.3 fixed/sfixed
      • 2.4.4 bool/string/bytes
    • 2.5 特殊标识
      • 2.5.1 required
      • 2.5.2 optional
      • 2.5.3 repeated
      • 2.5.4 map
    • 2.6 枚举
    • 2.7 默认值
    • 2.8 允许嵌套
    • 2.9 保留字段
    • 2.10 导入定义
    • 附:完整 .proto 代码
  • 3 生成 C# 代码
  • 4 使用 Protobuf
    • 4.1 序列化
    • 4.2 反序列化
  • 5 Protobuf-Net

1 Protobuf

​ Protobuf 全称为 protocol-buffers(协议缓冲区),是谷歌提供给开发者的开源的协议生成工具。

​ Protobuf 可以基于协议配置文件生成 C++、Java、C#、Objective-C、PHP、Python、Ruby、Go 等语言的代码文件,通用性强、稳定性高,可以节约出开发自定义协议工具的时间,是商业游戏开发中常选择的协议生成工具。

​ protocol-buffers 官网:https://developers.google.com/protocol-buffers。

1.1 导入 Protobuf

  1. 进入官网,左侧选择“Downloads”页面,进入“release page”。

    image-20250514003826165
  2. 网页最下方选择“protobuf-30.2.zip”下载。

    image-20250514003920681
  3. 下载并解压后,进入“\protobuf-30.2\csharp\src”目录下,找到“Google.Protobuf.sln”解决方案并打开。使用 Visual Studio 或者 Rider 都可。

    image-20250514004049190
  4. 以 Rider 为例,右键“Google.Protobuf”项目,点击“构建所选项目”。

    image-20250514004331118
  5. 进入“\protobuf-30.2\csharp\src\Google.Protobuf\bin\Debug”目录,可看到多个版本的构建文件。这里选择 net45,点击进入。

    image-20250514004509181
  6. 在 Unity 中创建“Plugins/Protobuf ”录,将 net45 目录下所有 dll 文件拷贝至其中。等待 Unity 编译完成后,导入 Protobuf 成功。

    image-20250514004716908

1.2 导入编译器

  1. 下载对应操作系统的“protoc.zip”文件。

    image-20250514004858259
  2. 解压后,找到“\protoc-30.2-win64\bin”目录下的“protoc.exe”文件。

    image-20250514005104305
  3. 在 Unity 项目路径下创建文件夹 Protobuf,将“protoc.exe”文件复制到该文件夹中。

    image-20250514005232464

2 配置规则

​ Protobuf 中配置文件的后缀统一使用 .proto,可以通过多个后缀为 .proto 的配置文件进行配置。

​ 在 Unity 的 Assets 文件夹下创建文件夹 Protobuf,在该目录下创建 test.txt 文件,将其后缀名更改为 .proto 并打开。

​ 在 Rider 中,可安装插件“Protobuf”对 .proto 文件提供高亮和智能提示功能。

​ 注意,需要先禁用自带的“Protocol Buffers”插件。

image-20250514022512467

2.1 注释

​ 注释:支持///* */

// 规则1:注释方式
// 注释方式一
/* 注释方式二 */

2.2 版本号

​ 版本声明(必须位于首行)。

syntax = "proto3"; // 默认为 proto2

2.3 命名空间

package GamePlayerTest; // 这决定了命名空间

2.4 消息类

message TestMsg{...
}

​ 成员声明方式:数据类型 字段名 = 唯一编号;

2.4.1 float / double

// = 1 不代表默认值 而是代表唯一编号 方便我们进行序列化和反序列化的处理
float testF = 1; // C# - floatdouble testD = 2; // C# - double

2.4.2 int/sint/uint

  • int:使用可变长度编码。对负数编码效率低下。
  • sint:使用可变长度编码。Signed int 值。这些比常规 int 更有效地编码负数。
  • uint:使用可变长度编码。

​ 变长编码会根据数字的大小使用对应的字节数来存储,如 1、2、4 个字节。是 Protobuf 实现的优化部分,可以尽量少的使用字节数来存储内容。

// 变长编码
// 1 2 4 8
int32 testInt32 = 3; // C# - int 它不太适用于来表示负数 请使用 sint32
int64 testInt64 = 4; // C# - long  它不太适用于来表示负数 请使用 sint64// 更实用与表示负数类型的整数
sint32 testSInt32 = 5; // C# - int 适用于来表示负数的整数
sint64 testSInt64 = 6; // C# - long 适用于来表示负数的整数// 无符号 变长编码
// 1 2 4
uint32 testUInt = 7; // C# - uint 变长的编码
uint64 testULong = 8; // C# - ulong 变长的编码

2.4.3 fixed/sfixed

  • fixed:始终为 4/8 个字节。如果值通常较大,则比 uint 更高效。
  • sfixed:始终为 4/8 个字节。
// 固定字节数的类型
fixed32 testFixed32 = 9; // C# -uint 它通常用来表示大于2的28次方的数 ,比uint32更有效 始终是4个字节
fixed64 testFixed64 = 10; // C# -ulong 它通常用来表示大于2的56次方的数 ,比uint64更有效 始终是8个字节sfixed32 testSFixed32 = 11; // C# - int 始终4个字节
sfixed64 testSFixed64 = 12; // C# - long 始终8个字节

2.4.4 bool/string/bytes

  • bool:布尔值。
  • string:字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且长度不能超过 232
  • bytes:可以包含任何不超过 232 的任意字节序列。
// 其它类型
bool testBool = 13; // C# - bool 
string testStr = 14; // C# - string
bytes testBytes = 15; // C# - BytesString 字节字符串

2.5 特殊标识

2.5.1 required

  • 必须赋值(proto2 特有,proto3 已移除)。
// required 必须赋值的字段
required float testF = 1; //C# - float

2.5.2 optional

  • 可以不赋值的字段(proto3 默认)。

    字段处于以下两种可能的状态之一:

    • 该字段已设置,并包含从连线显式设置或解析的值。它将被序列化。
    • 该字段未设置,并将返回默认值。它不会被序列化。
// optional 可以不赋值的字段
optional double testD = 2; //C# - double

2.5.3 repeated

  • 可重复字段(数组)。
// 数组List
repeated int32 listInt = 16; // C# - 类似List<int>的使用

2.5.4 map

  • 成对的键/值字段类型。
// 字典 Dictionary
map<int32, string> testMap = 17; // C# - 类似Dictionary<int, string> 的使用

2.6 枚举

// 枚举的声明
enum TestEnum {NORMAL = 0; // 第一个常量必须映射到 0BOSS = 5;
}

2.7 默认值

类型默认值
string空字符串
bytes空字节
boolfalse
数值类型0
message取决于语言,C# 为空
enum0

2.8 允许嵌套

​ 嵌套一个类在另一个类当中,相当于是内部类。

message TestMsg{message TestMsg3 {int32 testInt32 = 1;}enum TestEnum2 {NORMAL = 0; // 第一个常量必须映射到 0BOSS = 1;}TestMsg3 testMsg3 = 20;TestEnum2 testEnum2 = 21;
}

2.9 保留字段

​ 如果修改了协议规则,删除了部分内容,为了避免更新时重新使用已经删除了的编号,可以利用 reserved 关键字来保留字段,这些内容就不能再被使用了。

// int32 testInt3233333 = 22;// 告诉编译器 22 被占用 不准用户使用
// 之所以有这个功能 是为了在版本不匹配时 
// 反序列化不会出现结构不统一 解析错误的问题
reserved 22;reserved testInt3233333;message Foo {reserved 2, 15, 9 to 11;reserved "foo", "bar";
}

​ reserved 仅影响 protoc 编译器行为,运行时 JSON 解析不受保留名称的影响。

2.10 导入定义

​ 在 Protobuf 文件夹下创建 test2.proto 文件,并写入如下内容:

syntax = "proto3";// 规则 3:命名空间
package GamePlayerTest;// 这决定了命名空间message HeartMsg {int64 time = 1;
}

​ 在 test.proto 中即可导入 test2.proto:

syntax = "proto3";// 规则 2:版本号// 规则 1:注释方式
// 注释方式一
/* 注释方式二 */// 规则 11:导入定义
import "test2.proto";// 规则 3:命名空间
package GamePlayerTest;// 这决定了命名空间// 规则 4:消息类
message TestMsg {...
}

附:完整 .proto 代码

test.proto

syntax = "proto3";// 规则 2:版本号// 规则 1:注释方式
// 注释方式一
/* 注释方式二 */// 规则 11:导入定义
import "test2.proto";// 规则 3:命名空间
package GamePlayerTest;// 这决定了命名空间// 规则 4:消息类
message TestMsg {// 浮点数// = 1 不代表默认值 而是代表唯一编号 方便我们进行序列化和反序列化的处理// required 必须赋值的字段float testF = 1; // C# - float// optional 可以不赋值的字段optional double testD = 2; // C# - double// 变长编码// 所谓变长 就是会根据 数字的大小 来使用对应的字节数来存储  1 2 4 // Protobuf 帮助我们优化的部分 可以尽量少的使用字节数 来存储内容int32 testInt32 = 3; // C# - int 它不太适用于来表示负数 请使用 sint32// 1 2 4 8int64 testInt64 = 4; // C# - long  它不太适用于来表示负数 请使用 sint64// 更实用与表示负数类型的整数sint32 testSInt32 = 5; // C# - int 适用于来表示负数的整数sint64 testSInt64 = 6; // C# - long 适用于来表示负数的整数// 无符号 变长编码// 1 2 4uint32 testUInt = 7; // C# - uint 变长的编码uint64 testULong = 8; // C# - ulong 变长的编码// 固定字节数的类型fixed32 testFixed32 = 9; // C# -uint 它通常用来表示大于2的28次方的数 ,比uint32更有效 始终是4个字节fixed64 testFixed64 = 10; // C# -ulong 它通常用来表示大于2的56次方的数 ,比uint64更有效 始终是8个字节sfixed32 testSFixed32 = 11; // C# - int 始终4个字节sfixed64 testSFixed64 = 12; // C# - long 始终8个字节// 其它类型bool testBool = 13; // C# - bool string testStr = 14; // C# - stringbytes testBytes = 15; // C# - BytesString 字节字符串// 数组Listrepeated int32 listInt = 16; // C# - 类似List<int>的使用// 字典 Dictionarymap<int32, string> testMap = 17; // C# - 类似Dictionary<int, string> 的使用// 枚举成员变量的声明 需要唯一编码TestEnum testEnum = 18;// 声明自定义类对象 需要唯一编码// 默认值是nullTestMsg2 testMsg2 = 19;// 规则 8:允许嵌套// 嵌套一个类在另一个类当中 相当于是内部类message TestMsg3 {int32 testInt32 = 1;}TestMsg3 testMsg3 = 20;// 规则 9:允许嵌套enum TestEnum2 {NORMAL = 0; // 第一个常量必须映射到 0BOSS = 1;}TestEnum2 testEnum2 = 21;// int32 testInt3233333 = 22;bool testBool2123123 = 23;GamePlayerTest.HeartMsg testHeart = 24;// 告诉编译器 22 被占用 不准用户使用// 之所以有这个功能 是为了在版本不匹配时 // 反序列化不会出现结构不统一 解析错误的问题reserved 22;
//    reserved testInt3233333;
}// 枚举的声明
enum TestEnum {NORMAL = 0; // 第一个常量必须映射到 0BOSS = 5;
}message TestMsg2 {int32 testInt32 = 1;
}

test2.proto

syntax = "proto3";// 规则 3:命名空间
package GamePlayerTest;// 这决定了命名空间message HeartMsg {int64 time = 1;
}

3 生成 C# 代码

  1. 在 Protobuf 目录下创建文件夹 CodeGen,用于存放生成的 C# 代码。

  2. 打开 Powershell 窗口,进入 protoc.exe 所在文件夹,输入转换指令生成 C# 代码。

    protoc -I="配置路径" --csharp_out="输出路径" 配置文件名

    protoc -I="你的项目路径\Assets\Protobuf" --csharp_out="你的项目路径\Assets\Protobuf\CodeGen" test.proto
    
    image-20250514022827278

    注意

    ​ 路径不要有中文和特殊符号,避免生成失败。

  3. 在 Unity 中引入生成的 .cs 文件,通过 Google.Protobuf 命名空间使用。

    using UnityEngine;namespace Lesson
    {using GamePlayerTest;public class Lesson40 : MonoBehaviour{private void Start(){var msg = new TestMsg();msg.TestBool = true;// 对应的和List以及Dictionary使用方式一样的 数组和字典对象msg.ListInt.Add(1);print(msg.ListInt[0]);msg.TestMap.Add(1, "xxx");print(msg.TestMap[1]);// 枚举msg.TestEnum = TestEnum.Boss;// 内部枚举msg.TestEnum2 = TestMsg.Types.TestEnum2.Boss;// 其它类对象msg.TestMsg2           = new TestMsg2();msg.TestMsg2.TestInt32 = 99;// 其它内部类对象msg.TestMsg3           = new TestMsg.Types.TestMsg3();msg.TestMsg3.TestInt32 = 55;// 在另一个生成的脚本当中的类 如果命名空间不同 需要命名空间点出来使用msg.TestHeart = new GamePlayerTest.HeartMsg();}}
    }
    

4 使用 Protobuf

4.1 序列化

​ 先向 TestMsg 中写入一些内容,然后序列化为二进制文件。

  • 使用方法
    • msg.WriteTo(fs)
    • msg.ToByteArray()
using UnityEngine;namespace Lesson
{using System.IO;using GamePlayerTest;using Google.Protobuf;public class Lesson41 : MonoBehaviour{private void Start(){// 主要使用// 1.生成的类中的 WriteTo方法// 2.文件流FileStream对象var msg = new TestMsg();msg.ListInt.Add(1);msg.TestBool  = false;msg.TestD     = 5.5;msg.TestInt32 = 99;msg.TestMap.Add(1, "xxx");msg.TestMsg2           = new TestMsg2();msg.TestMsg2.TestInt32 = 88;msg.TestMsg3           = new TestMsg.Types.TestMsg3();msg.TestMsg3.TestInt32 = 66;msg.TestHeart      = new GamePlayerTest.HeartMsg();msg.TestHeart.Time = 7777;print(Application.persistentDataPath);using (FileStream fs = File.Create(Application.persistentDataPath + "/TestMsg.bytes")){// 需要 using Google.Protobuf; 才不会报错// Google.Protobuf 中会调用扩展方法进行隐式转换msg.WriteTo(fs);}}}
}

​ 将上述代码挂载到 Unity 场景中并运行后,在 Application.persistentDataPath 目录下可看到序列化文件。

image-20250514024353114

​ 在网络传输中,使用内存流进行传输。

using UnityEngine;namespace Lesson
{using System.IO;using GamePlayerTest;using Google.Protobuf;public class Lesson41 : MonoBehaviour{private void Start(){// 主要使用// 1.生成的类中的 WriteTo方法// 2.内存流MemoryStream对象var msg = new TestMsg();...using (var ms = new MemoryStream()){msg.WriteTo(ms);// var bytes = msg.ToByteArray();var bytes = ms.ToArray();print("字节数组长度:" + bytes.Length);}}}
}

4.2 反序列化

​ 将本地二进制文件反序列化为 TestMsg 对象。

  • 使用方法:TestMsg.Parser.ParseFrom(fs)
using UnityEngine;namespace Lesson
{using System.IO;using GamePlayerTest;using Google.Protobuf;public class Lesson41 : MonoBehaviour{private void Start(){// 主要使用// 1.生成的类中的 Parser.ParseFrom方法// 2.文件流FileStream对象using (FileStream fs = File.OpenRead(Application.persistentDataPath + "/TestMsg.bytes")){TestMsg msg2 = null;msg2 = TestMsg.Parser.ParseFrom(fs);print(msg2.TestMap[1]);print(msg2.ListInt[0]);print(msg2.TestD);print(msg2.TestMsg2.TestInt32);print(msg2.TestMsg3.TestInt32);print(msg2.TestHeart.Time);}}}
}

​ 将上述代码挂载到 Unity 场景中并运行后,结果如下。

image-20250514024611411

​ 在网络传输中,使用内存流进行传输。

using UnityEngine;namespace Lesson
{using System.IO;using GamePlayerTest;using Google.Protobuf;public class Lesson41 : MonoBehaviour{private void Start(){byte[] bytes;...// 主要使用// 1.生成的类中的 Parser.ParseFrom方法// 2.内存流MemoryStream对象using (var ms = new MemoryStream(bytes)){print("内存流当中反序列化的内容");TestMsg msg2 = TestMsg.Parser.ParseFrom(ms);print(msg2.TestMap[1]);print(msg2.ListInt[0]);print(msg2.TestD);print(msg2.TestMsg2.TestInt32);print(msg2.TestMsg3.TestInt32);print(msg2.TestHeart.Time);}}}
}

5 Protobuf-Net

​ 早期的 Protobuf 并不支持 C#,国外大佬 Marc Gravell 在 Protobuf 基础上进行 .Net 环境下的移植,并发布到 GitHub。

  • Github 地址:https://github.com/protobuf-net/protobuf-net。

与官方 Protobuf 的区别

特性Google.ProtobufProtobuf-net
开发语言多语言支持专为 .NET 优化
API 设计基于代码生成支持属性标记+动态序列化
Unity 兼容性需新版本 Unity支持老版本 Unity
性能优势通用性强GC 优化更好,IL 动态生成代码

注意

  1. Protobuf 不支持 .Net3.5 及以下版本。

    如果想在 Unity 老版本中使用 Protobuf,只能使用 Protobuf-Net。

    在较新版本的 Unity 中不存在这个问题。

  2. 如何判断是否支持?

    把 Protobuf 相关 dll 包导入后能够正常使用不报错,则证明支持。

​ Protobuf-Net 是较老的生产方式,用于解决老版本 Unity 使用 Protobuf 的问题,使用方式和 Protobuf 类似,只是获取 DLL 文件、protoc.exe 文件的方式不同。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词