Protocol Buffers .NET 运行时从核心 API 到工程实战

1. 全局地图:三个关键命名空间

  • Google.Protobuf(核心)

    • 序列化/反序列化:CodedInputStreamCodedOutputStream
    • JSON 互转:JsonFormatterJsonParser
    • 字节容器:ByteString
    • 解析与扩展:MessageParser<T>MessageExtensions
    • 约束与异常:ProtoPreconditionsInvalidProtocolBufferExceptionInvalidJsonException
  • Google.Protobuf.Collections(集合)

    • RepeatedField<T>:repeated 字段容器(不允许 null,支持深拷贝)
    • MapField<TKey, TValue>:map 字段容器与对应 Codec
  • Google.Protobuf.Reflection(反射)

    • 描述符体系:FileDescriptorMessageDescriptorFieldDescriptorEnumDescriptorServiceDescriptor
    • oneof 辅助:OneofDescriptorOneofAccessor
    • 原始名注解:OriginalNameAttribute
    • 类型仓库:TypeRegistry

此外,WellKnownTypesGoogle.Protobuf.WellKnownTypes)提供常用内置类型:TimestampDurationAnyFieldMaskStruct、各类 *Value 包装器等。

2. 消息的基本功:二进制与 JSON

2.1 二进制序列化/反序列化

csharp 复制代码
using Google.Protobuf;
using System.IO;

var person = new Person { Id = 1, Name = "Ada" };

// 写二进制
using var ms = new MemoryStream();
person.WriteTo(ms);
var bytes = ms.ToArray();

// 读二进制
var parsed = Person.Parser.ParseFrom(bytes);
// 或者:new CodedInputStream(bytes) / CodedOutputStream
  • 推荐 :优先使用 Message.WriteTo / Parser.ParseFrom,底层已优化。
  • 异常 :格式错误会抛出 InvalidProtocolBufferException,请捕获并统一处理。

2.2 JSON 互转(调试、网关、日志常用)

csharp 复制代码
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Google.Protobuf.Reflection;
using Google.Protobuf.Json;
using Google.Protobuf.JsonParser; // 在旧版本命名空间为 Google.Protobuf

// 序列化为 JSON
var json = JsonFormatter.Default.Format(person);

// 从 JSON 解析
var parsed2 = JsonParser.Default.Parse<Person>(json);
  • JsonFormatter.SettingsJsonParser.Settings 可自定义大小写、默认值输出、枚举样式等。
  • 与跨语言协作 :Protobuf JSON 规范与普通 JSON 略有差异(例如 TimestampDuration 的格式);使用官方工具避免手写转换。

3. 集合类型:Repeated 与 Map 的那些细节

3.1 RepeatedField

  • 表示 .proto 中的 repeated 字段。
  • 不允许 null 元素 ,支持 AddAddRange、索引访问、深拷贝。
csharp 复制代码
person.Phones.Add(new PhoneNumber { Number = "123" });
person.Tags.AddRange(new[] { "ai", "ml" });

3.2 MapField<TKey, TValue>

  • 表示 .proto 中的 map<K, V> 字段。
  • 语义与 Dictionary<K,V> 接近,但具备 Protobuf 序列化行为与 Codec 支持。
csharp 复制代码
var booth = new MerchBooth();
booth.Items["Signed T-Shirt"] = new MerchItem { Price = 99 };

实战建议:业务层可转换为 Dictionary 做 LINQ 处理,但在消息边界(序列化层)请保留 MapField,避免不必要的拷贝。

4. 反射 API:动态场景的瑞士军刀

当消息类型在编译期未知(插件、脚本、网关、动态转换)时,反射很有用:

csharp 复制代码
var md = Person.Descriptor;                         // MessageDescriptor
foreach (var fd in md.Fields.InDeclarationOrder())  // 字段遍历
{
    Console.WriteLine($"{fd.Name} : {fd.FieldType}");
}

// 获取/设置字段(反射方式)
var msg = new DynamicMessage(md);
var nameField = md.FindFieldByName("name");
msg[nameField] = "Turing";
  • 描述符族:FileDescriptor / MessageDescriptor / FieldDescriptor / EnumDescriptor
  • Oneof :用 OneofDescriptor + OneofAccessor 反射地清空或获取当前 case
  • TypeRegistry:Any/JSON 解析时注册已知类型,支持跨消息类型反序列化

5. Well-Known Types:时间、动态结构与 Any

5.1 时间类型

  • Timestamp(UTC 纪元秒+纳秒)、Duration(时长)
  • 搭配 TimeExtensions 在 BCL 类型与 Protobuf 类型间转换
csharp 复制代码
using Google.Protobuf.WellKnownTypes;

var now = Timestamp.FromDateTime(DateTime.UtcNow);
var dt = now.ToDateTime(); // back to DateTime (UTC)

5.2 结构化动态值

  • Struct / Value / ListValue:用于"半结构化 JSON"存储
  • 场景:配置中心、动态 payload、扩展字段

5.3 Any:承载任意消息

csharp 复制代码
var any = Any.Pack(person);            // 包装
var ok = any.TryUnpack(out Person p);  // 解包

TypeRegistry 配合,能在 JSON 场景保持类型信息。

5.4 包装器(Wrapper)类型

  • Int32ValueStringValue 等:表达"可空的标量字段"(与 C# 的 int?string? 语义对齐)
  • repeated 中不允许 nullmap value 允许 null

6. 错误与健壮性:异常、前置条件、诊断

  • 反序列化异常:

    • InvalidProtocolBufferException:二进制格式错误
    • InvalidJsonException:JSON 格式错误
  • 参数校验:ProtoPreconditions(库内部使用,工程可借鉴同样思路)

  • 诊断输出:实现 ICustomDiagnosticMessage 可自定义 ToString() 风格日志

7. 性能与工程实践

7.1 避免多余的分配与拷贝

  • 优先使用 WriteTo(Stream)/ParseFrom(ReadOnlySpan<byte>) 等流/Span API
  • 大对象/热点链路可复用缓冲区,或使用 CodedOutputStream 的高阶控制

7.2 JSON 只做边界转换

  • 在服务内传递请使用二进制;对外(浏览器/HTTP 调试)再转 JSON
  • JSON 设置统一化(大小写、默认值、枚举文本)以减轻前后端契约摩擦

7.3 与 Domain 模型解耦

  • Protobuf 类是"数据容器",避免将领域逻辑写进生成类
  • 用组装器/Mapper(如 Mapster/手写)在 Protobuf DTO 与领域模型间转换

7.4 版本演进与兼容

  • 遵守字段编号不变、更改只增不删的基本规则
  • 对"不确定是否需要"的字段优先用 Wrapper 表达"未设置"与"空值"的区分

8. 常用代码片段速查

从文件读写:

csharp 复制代码
using var fs = File.OpenRead("data.pb");
var data = Person.Parser.ParseFrom(fs);

using var ws = File.Create("data.pb");
data.WriteTo(ws);

JSON 设置:

csharp 复制代码
var formatter = new JsonFormatter(new JsonFormatter.Settings(formatDefaultValues: false));
var json = formatter.Format(data);

var parser = new JsonParser(new JsonParser.Settings(ignoringUnknownFields: true));
var fromJson = parser.Parse<Person>(json);

Repeated 与 Map:

csharp 复制代码
data.Tags.Add("x");
data.Attributes["lang"] = "en";

反射读取字段:

csharp 复制代码
var fd = Person.Descriptor.FindFieldByNumber(1); // 按编号找字段
var val = data.GetType().GetProperty(fd.CsharpOptions.PropertyName)?.GetValue(data);

Any + TypeRegistry + JSON:

csharp 复制代码
var reg = TypeRegistry.FromMessages(Person.Descriptor);
var jf = new JsonFormatter(new JsonFormatter.Settings(typeRegistry: reg));

var any = Any.Pack(data);
var jsonAny = jf.Format(any);

var jp = new JsonParser(new JsonParser.Settings(typeRegistry: reg));
var anyParsed = jp.Parse<Any>(jsonAny);
anyParsed.Unpack(out Person p2);

结语

  • Google.Protobuf核心 API打通序列化/JSON/反射三条链路;
  • CollectionsRepeatedFieldMapField 正确承载集合;
  • WellKnownTypes 解决"时间/可空/动态结构/Any"的常见痛点;
  • ReflectionTypeRegistry 支撑网关、插件化、跨语言的高级场景。

把 Protobuf 当作"跨语言、高性能、强约束的传输与存储层",把业务语义留给领域模型,让你的 .NET 服务既快又稳、演进无痛。

相关推荐
_BigMao8 小时前
Linux服务器从零开始-部署.net控制台程序(AlmaLinux)
linux·服务器·.net
咕白m62515 小时前
通过 C# 复制 Excel 工作表
c#·.net
一个帅气昵称啊1 天前
在.NET中实现RabbitMQ客户端的优雅生命周期管理及二次封装
分布式·后端·架构·c#·rabbitmq·.net
王维志2 天前
在Unity中使用SQLite(Sqlite-net-pcl)
unity·sqlite·c#·.net
Crazy Struggle4 天前
一个拒绝过度设计的 .NET 快速开发框架:开箱即用,专注"干活"
.net·后台管理系统
唐青枫5 天前
比 AutoMapper 更快?C#.NET Mapster 深度解析
c#·.net
追逐时光者5 天前
一款基于 .NET 开源、免费、命令行式的哔哩哔哩视频内容下载工具
后端·.net
追逐时光者5 天前
一套开源、美观、高性能的跨平台 .NET MAUI 控件库,助力轻松构建美观且功能丰富的应用程序!
后端·.net