Protocol Buffers .NET 运行时从核心 API 到工程实战
1. 全局地图:三个关键命名空间
-
Google.Protobuf(核心)
- 序列化/反序列化:
CodedInputStream
、CodedOutputStream
- JSON 互转:
JsonFormatter
、JsonParser
- 字节容器:
ByteString
- 解析与扩展:
MessageParser<T>
、MessageExtensions
- 约束与异常:
ProtoPreconditions
、InvalidProtocolBufferException
、InvalidJsonException
- 序列化/反序列化:
-
Google.Protobuf.Collections(集合)
RepeatedField<T>
:repeated 字段容器(不允许null
,支持深拷贝)MapField<TKey, TValue>
:map 字段容器与对应Codec
-
Google.Protobuf.Reflection(反射)
- 描述符体系:
FileDescriptor
、MessageDescriptor
、FieldDescriptor
、EnumDescriptor
、ServiceDescriptor
- oneof 辅助:
OneofDescriptor
、OneofAccessor
- 原始名注解:
OriginalNameAttribute
- 类型仓库:
TypeRegistry
- 描述符体系:
此外,WellKnownTypes(Google.Protobuf.WellKnownTypes
)提供常用内置类型:Timestamp
、Duration
、Any
、FieldMask
、Struct
、各类 *Value
包装器等。
2. 消息的基本功:二进制与 JSON
2.1 二进制序列化/反序列化
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 互转(调试、网关、日志常用)
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.Settings
与JsonParser.Settings
可自定义大小写、默认值输出、枚举样式等。- 与跨语言协作:Protobuf JSON 规范与普通 JSON 略有差异(例如
Timestamp
、Duration
的格式);使用官方工具避免手写转换。
3. 集合类型:Repeated 与 Map 的那些细节
3.1 RepeatedField
- 表示
.proto
中的repeated
字段。 - 不允许
null
元素,支持Add
、AddRange
、索引访问、深拷贝。
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
支持。
var booth = new MerchBooth();
booth.Items["Signed T-Shirt"] = new MerchItem { Price = 99 };
实战建议:业务层可转换为
Dictionary
做 LINQ 处理,但在消息边界(序列化层)请保留MapField
,避免不必要的拷贝。
4. 反射 API:动态场景的瑞士军刀
当消息类型在编译期未知(插件、脚本、网关、动态转换)时,反射很有用:
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 类型间转换
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:承载任意消息
var any = Any.Pack(person); // 包装
var ok = any.TryUnpack(out Person p); // 解包
与
TypeRegistry
配合,能在 JSON 场景保持类型信息。
5.4 包装器(Wrapper)类型
Int32Value
、StringValue
等:表达“可空的标量字段”(与 C# 的int?
、string?
语义对齐)- repeated 中不允许 null;map 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. 常用代码片段速查
从文件读写:
using var fs = File.OpenRead("data.pb");
var data = Person.Parser.ParseFrom(fs);using var ws = File.Create("data.pb");
data.WriteTo(ws);
JSON 设置:
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:
data.Tags.Add("x");
data.Attributes["lang"] = "en";
反射读取字段:
var fd = Person.Descriptor.FindFieldByNumber(1); // 按编号找字段
var val = data.GetType().GetProperty(fd.CsharpOptions.PropertyName)?.GetValue(data);
Any + TypeRegistry + JSON:
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/反射三条链路;
- 用 Collections 的
RepeatedField
与MapField
正确承载集合; - 用 WellKnownTypes 解决“时间/可空/动态结构/Any”的常见痛点;
- 用 Reflection 与 TypeRegistry 支撑网关、插件化、跨语言的高级场景。
把 Protobuf 当作“跨语言、高性能、强约束的传输与存储层”,把业务语义留给领域模型,让你的 .NET 服务既快又稳、演进无痛。