当前位置: 首页 > news >正文

Protobuf笔记

部署

官网下载、解压

左为对C#的支持,右为把.proto脚本生成.cs的编译器。

生成dll

VS打开.sln

把4个dll拷贝到项目Plugins

.proto语法

syntax="proto3";//版本号 package XXX;//生成的命名空间 import "YYY.proto";//导入其他proto文件的类型 message Person{//自定义消息类 int64 id=1;//字段的唯一编号,不是字段的值!!!! string name=2; Gender gender=8; int32 age=3;//不适合表示负数。负数用sint32 float height=4; //还有sint32 sint64 uint32 uint64 fixed32 fixed64 sfixed32 sfixed64 bool isAdult=5; //bytes字节字符串类型 //optional可选赋值 repeated string tags=6;//列表 map<string,Person>relationships=7;//字典 reserved 1,2,8,3,4;//即使1字段被在删掉了,也不允许再声明 reserved id,name; 命名空间.HeartBeat heartBeat=10;//其他proto文件的message } enum Gender{ male=0; female=1; }

生成.cs脚本

在protoc.exe文件夹打开,输入

./protoc.exe -I=.proto文件夹 --csharp_out="输出文件夹" .proto文件名

.\protoc.exe -I=. --csharp_out=. UserPropsData.proto pause

使用

序列化存文件

using Google.Protobuf; string path; Person p1=new Person(); //赋值 using(FileStream stream=File.Create(path+"/坑你.kk")){ p1.WriteTo(stream); }

反序列化读文件

using Google.Protobuf; string path; Person p1=null using(FileStream stream=File.OpenRead(path+"/坑你.kk")){ p1=Person.Parser.ParseFrom(stream); }

二进制序列化反序列化。内存流法

Person p1=new Person; byte[]bytes; using (MemoryStream stream=new MemoryStream()){ p1.WriteTo(stream); bytes=stream.ToArray(); } using(MemoryStream stream=new MemoryStream(bytes){ p1=Person.Parser.ParseFrom(stream); }

二进制序列化反序列化。不用流。

Person p1=new Person(); byte[]bytes=p1.ToByteArray(); Person.Parser.ParseFrom(bytes);

Protobuf数据类和自己写的C#数据类的对接(或者说从自己写的数据类到pb数据类的迁移)

如果之前用C#写了数据类,现在用protobuf序列化,C#数据类和protobuf数据类的互相转换对于复杂嵌套的数据类会非常麻烦。于是自然想到项目直接用protobuf数据类。此时面临问题:

  1. 项目里引用C#数据类已经非常多了,代码修改量很大;

如果能有一个渐进的方法,每次把一部分引用转移到protobuf数据类,也行。

项目到处在引用CS数据类,保存时把cs数据类转换成pb数据类,读取时相反。如果渐进,需要玩家数据管理器同时维持一个cs数据和pb数据?

  • 玩家数据管理器同时维护一个cs数据类和一个pb数据类
  • 把cs数据类转换成pb数据类的函数改成接受一个pb数据类(而不是在里面new),把cs数据类的一些成员搬运进pb数据类
  • 改成直接用pb数据类的那些数据成员,在cs和pb转换函数里搬运它们的代码删除,把项目个部分对它们的应用改成对pb数据类里它们的引用

现在项目从玩家数据管理器引用的不止整数、字符串、bool这些简单数据类型,还有整个的嵌套数据类比如背包。

注意

  1. 生成的.cs文件的类会直接使用.proto的package定义的命名空间!如果想把protobuf生成类和自己的代码分开,就定义不同的命名空间;
  2. 对于import很多其他.proto的文件,在命令行添加参数可以连带把依赖项也生成了,但是如果懒得写命令行不如把message都写在一个.proto;
  3. repeated数据不能new,可以Add

规则详细拆解


syntax = "proto3";
message User {
int32 id = 1; // 变长int
string name = 2; // 长度前缀字符串
fixed32 age = 3; // 固定4字节int
}

以二进制数据 b'\x08\x01\x12\x04Alice\x18\x1e' 为例,对应 User 协议(id=1/name=2/age=3),逐字节拆解 Protobuf 字段边界识别逻辑,核心是 Tag-Length-Type(TLT)+ 数据绑定:

先明确 2 个基础规则

• Tag 编码:字段标签(如 1/2/3)和类型(如 int32=0、string=2、fixed32=5)拼接成 (tag << 3) | type,再用 Varint 编码存储;

• Varint 特征:字节最高位为 1 表示后续还有字节,为 0 表示当前是最后一字节。

逐字段拆解(按二进制顺序)

1. 字段 1:int32 id = 1(值为 1)

对应二进制片段:\x08\x01

• 第 1 字节 \x08(二进制 00001000):解析 Tag+Type

◦ 低 3 位 000 = 类型 0(VARINT,对应 int32);

◦ 高 5 位 00001 右移 3 位 = 字段标签 1;

• 第 2 字节 \x01(二进制 00000001):Varint 编码的数据,最高位为 0,直接解析为 1,该字段读取结束(共 2 字节)。

2. 字段 2:string name = 2(值为 "Alice")

对应二进制片段:\x12\x04Alice(Alice 是 ASCII 码 \x41\x6c\x69\x63\x65)

• 第 1 字节 \x12(二进制 00010010):解析 Tag+Type

◦ 低 3 位 010 = 类型 2(LENGTH_DELIMITED,长度前缀类型,含字符串/嵌套消息);

◦ 高 5 位 00010 右移 3 位 = 字段标签 2;

• 第 2 字节 \x04(二进制 00000100):长度前缀,标识后续数据占 4 字节;

• 第 3-6 字节 \x41\x6c\x69\x63\x65?不,长度前缀是 4,所以读后续 4 字节?哦不对,"Alice" 是 5 个字符,对应长度前缀应为 \x05,修正二进制片段为 \x12\x05Alice(原数据笔误,不影响逻辑):

◦ 按长度前缀 05 读取后续 5 字节,正好是 "Alice",该字段读取结束(共 1+1+5=7 字节)。

3. 字段 3:fixed32 age = 3(值为 30)

对应二进制片段:\x18\x1e?不,fixed32 是固定 4 字节,类型为 5(FIXED32),修正后二进制片段:\x1d\x1e\x00\x00\x00

• 第 1 字节 \x1d(二进制 00011101):解析 Tag+Type

◦ 低 3 位 101 = 类型 5(FIXED32,固定 4 字节);

◦ 高 5 位 00011 右移 3 位 = 字段标签 3;

• 后续 4 字节 \x1e\x00\x00\x00(小端序):直接读取 4 字节,转成十进制为 30,该字段读取结束(共 1+4=5 字节)。

核心结论

Protobuf 字段“读完”的判断完全由 协议定义+编码规则 自动完成:

• 固定类型(fixed32/fixed64):读满协议规定的字节数即结束;

• 变长类型(Varint):读到最高位为 0 的字节即结束;

• 长度前缀类型(string/嵌套):先读长度,再读对应长度的字节即结束。

需要我用工具帮你可视化任意 Protobuf 二进制数据的拆解过程,或对比不同类型字段(如嵌套消息、repeated 数组)的边界识别差异吗?

问题排查

存入的repeated数据读取出来Count是0

往repeated里加数据,字节数会变多,读取出来Count是0.经过研究是因为读取数据时把读出的protobuf数据类以参数传入,以为数据类是引用传递,在里面修改也会修改外面的对象。但其实好像是值传递,需要加out参数。

再简单总结一下,引用类型输入参数,仅限里面没有赋值new,只对原对象操作时输入能当输出,当里面执行他人的代码时,不知道里面作了什么操作,如果有赋值new,就不会改变外面的对象。

http://www.jsqmd.com/news/793413/

相关文章:

  • ChameleonUltra高级应用:硬嵌套攻击与实时卡数据捕获技术
  • ARMv9 TRBMPAM_EL1寄存器配置与性能监控实战
  • AArch64外部调试架构与Debug State机制详解
  • 开源材料计算自动化平台OpenClaw:从高通量筛选到机器学习集成
  • 终极鼠标性能测试指南:5分钟快速诊断你的鼠标问题
  • DLSS Swapper终极指南:免费提升游戏性能的3个简单步骤
  • CANN/ops-math DropOutV3算子
  • BV 开发者指南:Jetpack Compose 在TV应用中的最佳实践
  • CANN/ops-nn动态量化RMS归一化融合算子
  • CANN/ops-nn AdvanceStep算子
  • CANN/GE模型内存查询接口
  • 耗时3个月整理!K12少儿编程全套学习课件,老师/家长直接用
  • ARMv9 TRBSR寄存器解析与调试实践
  • ARM TLB管理:原理、指令与优化实践
  • 本地化AI代码助手Copaw:设计原理与工程实践指南
  • ContextPilot:优化KV缓存复用,加速RAG与长上下文推理
  • Arm CoreSight SoC-400调试架构与寄存器编程详解
  • 基于Docker容器化部署Atlassian Confluence的完整实践指南
  • 基于Gradio与多模型代理的AI模拟面试系统实战部署指南
  • 安全代码执行沙盒实践:基于Docker与Seccomp的隔离方案
  • 基于MCP协议构建代码库AI助手:原理、部署与最佳实践
  • AI研发团队“隐性崩溃”前的9个信号:SITS2026追踪18个月的142起项目衰变案例全复盘
  • ARM9EJ-S处理器JTAG调试架构与实战技巧
  • Git Magic多人协作:10个高效管理团队项目的终极技巧 [特殊字符]
  • 告别网盘限速!八大平台直链下载助手LinkSwift完整使用指南
  • 多智能体协同框架:从原理到实践,探索AI驱动的自动化开发新范式
  • reverse-shell工作原理深度解析:智能检测与多语言payload实现
  • GE获取模型输出大小
  • 从实测到实战:HIP6601半桥驱动电路在无线信标线圈中的性能剖析
  • ARM CPACR寄存器详解:功能控制与安全配置