从零到一上手 Protocol Buffers用 C# 打造可演进的通讯录

一、为什么是 Protobuf(而不是 XML/自定义字符串/.NET 二进制序列化)

在需要把结构化对象持久化或跨进程/跨语言传输时,常见方案各有痛点:

  • BinaryFormatter 等 .NET 二进制序列化:对类型签名与版本极其脆弱、体积偏大,且跨语言互通性差
  • 自定义分隔字符串:一次性编码/解析成本高,健壮性与可读性差。
  • XML :可读但冗长、编解码开销大。

Protocol Buffers(Protobuf) 的优势在于:

用一个 .proto 文件声明消息结构,protoc 生成高效的 C# 类型,提供自动 的二进制编码/解码;并且天然支持演进(老代码可读新消息、反之亦然)。

二、工程与示例骨架

本文示例是一个命令行通讯录工具:

  • 可新增联系人并写入文件;
  • 可读取文件并打印联系人列表。

完整示例(Program.csaddressbook.proto 等)可按本文步骤自行创建;若你使用官方示例仓库,也能在 examplescsharp/src/AddressBook 目录中找到等价实现。

三、定义协议:addressbook.proto

proto 复制代码
syntax = "proto3";
package tutorial;

import "google/protobuf/timestamp.proto";

// 覆盖 C# 默认命名空间(否则取 package 名)
option csharp_namespace = "Google.Protobuf.Examples.AddressBook";

message Person {
  string name = 1;
  int32  id   = 2;   // 唯一 ID
  string email = 3;

  enum PhoneType {
    PHONE_TYPE_UNSPECIFIED = 0;
    PHONE_TYPE_MOBILE = 1;
    PHONE_TYPE_HOME   = 2;
    PHONE_TYPE_WORK   = 3;
  }

  message PhoneNumber {
    string   number = 1;
    PhoneType type  = 2;
  }

  repeated PhoneNumber phones = 4;
  google.protobuf.Timestamp last_updated = 5;
}

message AddressBook {
  repeated Person people = 1;
}

要点速记

  1. package 防冲突;csharp_namespace 可定制命名空间。
  2. = 1/2/...标签号 (wire tag),1--15 编码更省字节,优先留给常用repeated 字段。
  3. 未赋值字段使用类型默认值(数字 0、布尔 false、字符串空串、枚举 0 值)。
  4. repeated有序动态数组 ;集合类型在 C# 中为 RepeatedField<T>(只读集合,支持增删元素)。
  5. 嵌套 messageenum 提升结构清晰度;可以引入标准类型(如 Timestamp)。

四、编译生成 C# 代码

安装好 protoc 后执行:

bash 复制代码
protoc -I=$SRC_DIR --csharp_out=$DST_DIR $SRC_DIR/addressbook.proto

输出 Addressbook.cs,其中包含:

  • 静态 Addressbook(元数据);
  • AddressBookPersonPerson.Types.PhoneNumber 类;
  • Person.Types.PhoneType 枚举。

注意RepeatedField<T> 是只读集合属性,不能整体替换,只能调用 Add/Remove/Clear 等方法修改元素。

五、写与读:序列化 / 反序列化

1)写入

csharp 复制代码
using System.IO;
using Google.Protobuf;
using Google.Protobuf.WellKnownTypes;
using Google.Protobuf.Examples.AddressBook;
using static Google.Protobuf.Examples.AddressBook.Person.Types;

var john = new Person {
    Id = 1234,
    Name = "John Doe",
    Email = "jdoe@example.com",
    LastUpdated = Timestamp.FromDateTime(DateTime.UtcNow)
};
john.Phones.Add(new PhoneNumber { Number = "555-4321", Type = PhoneType.HOME });

var book = new AddressBook();
book.People.Add(john);

using var output = File.Create("addressbook.binpb");
book.WriteTo(output); // 二进制高效写入

2)读取

csharp 复制代码
using var input = File.OpenRead("addressbook.binpb");
var parsed = AddressBook.Parser.ParseFrom(input);

foreach (var p in parsed.People) {
    Console.WriteLine($"{p.Id} | {p.Name} | {p.Email}");
    foreach (var ph in p.Phones) {
        Console.WriteLine($"  - {ph.Type}: {ph.Number}");
    }
}

六、生成代码的常见模式

  • 属性访问 :普通字段直接 get/set;repeated 用集合操作。
  • using static 简化枚举/嵌套名using static Person.Types; 之后可写 PhoneType.HOME
  • Debug 与 JSON :调试可用 ToString()(等同 JsonFormatter 的简版),生产落盘/传输请使用二进制 WriteTo/ParseFrom不要把调试输出当数据格式使用。

七、向前/向后兼容的演进法则

演进 .proto 需遵守:

  1. 不要修改 已有字段的标签号
  2. 可以删除字段;
  3. 可以新增 字段,但必须使用从未使用过的新标签号(包括曾被删除的号也不能复用)。

这样:

  • 旧程序读新消息 :会忽略看不懂的新字段;
  • 新程序读旧消息 :新加字段会呈现其默认值(比如空串/0/false)。

小贴士:把最常用/可能 repeated 的字段优先安排在 1--15,可观节省体积。

八、反射(Reflection)一键遍历任意消息

当你需要写通用逻辑(如通用打印、对比、转其它文本格式)时,反射很有用:

csharp 复制代码
using Google.Protobuf;
using Google.Protobuf.Reflection;

static void DumpTopLevel(IMessage msg) {
    var desc = msg.Descriptor;
    foreach (var f in desc.Fields.InDeclarationOrder()) {
        var val = f.Accessor.GetValue(msg);
        Console.WriteLine($"#{f.FieldNumber} {f.Name} = {val}");
    }
}

九、最佳实践与易踩坑

  • 不要把生成的 Protobuf 类当领域模型的"上帝类" :它们是数据载体 。复杂业务建议用封装类包一层,便于隐藏实现细节与组合额外行为。
  • 文件扩展名约定 :二进制 .binpb、文本 .txtpb、JSON .json,保持项目内一致性。
  • 调试输出 ≠ 数据格式 :日志用调试格式即可;要传输/落盘 请用二进制或显式 TextFormat/JSON
  • 版本演进前先留余量:给可能增长的 repeated 预留 1--15;对未来"也许有用"的字段先不上线,避免随后改 tag。
  • 单元测试 :比较对象请用值相等Equals/字段比对)而非字节流相等;序列化非确定性 ,不同运行时/版本字节序可能不同但语义相同

十、把示例跑起来(最短路径)

  1. 安装 Google.Protobuf NuGet 与 protoc
  2. 写好 addressbook.proto
  3. 执行 protoc --csharp_out 生成 Addressbook.cs
  4. 在项目中引用生成文件与 Google.Protobuf
  5. 按第 节代码写入/读取,验证通讯录功能。
相关推荐
nmxiaocui2 小时前
openssl升级
linux·运维·服务器
树码小子2 小时前
Java网络初识(4):网络数据通信的基本流程 -- 封装
java·网络
稻草人想看远方2 小时前
GC垃圾回收
java·开发语言·jvm
初学者_xuan2 小时前
零基础快速了解掌握Linux防火墙-Iptables
linux·服务器·防火墙·linux新手小白
浪扼飞舟3 小时前
c#基础(一)
开发语言·c#
HetFrame3 小时前
John the Ripper jumbo + HashCat 破解压缩密码 ubuntu amd GPU
linux·ubuntu·amd·密码破解·john·压缩密码·hashcat
en-route3 小时前
如何在 Spring Boot 中指定不同的配置文件?
java·spring boot·后端
百锦再3 小时前
在 CentOS 系统上实现定时执行 Python 邮件发送任务
java·linux·开发语言·人工智能·python·centos·pygame
echoyu.3 小时前
消息队列-kafka完结
java·分布式·kafka