原文链接:https://developers.google.com/protocol-buffers/docs/csharptutorial#the-addressbook-classes
[TOC]
本教程给C#程序员基础的介绍了通过使用Protocol Buffers语言的proto3版本来操作协议缓冲区。通过创建一个简单的示例引用程序,来展示如何:
1. 在`.proto`文件中定义消息格式。
2. 使用ProtocolBuffer编译器。
3. 使用C#语言的ProtocolBuffer API来编写和读取消息。
本教程不是在C#中使用Protocol Buffers的全面指南。更多详细信息,请参阅 `Protocol Buffer Language Guide` 、`C# API Reference` 、`C# Generated Code Guide` 、`Encoding Reference` 。
## 简介
Google Protocol Buffer(简称ProtoBuf)是Google开发的用于内部使用的序列化结构化数据的一种方法。主要用来进行网络间通信以及数据存储。现在Google已经为开源许可协议下的多种语言提供了Protocol Buffer的代码生成器。
ProtoBuf是一种轻便高效的结构化序列化数据存储格式。适合做数据存储或者RPC数据交换格式。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
## 为什么要使用ProtoBuf?
我们将通过一个简单的“地址簿”示例程序(一个可以读写文件中的联系人信息的应用程序。地址簿中的每一个联系人都有姓名、ID、email地址、联系电话)来说明这个问题。
那么接下来的问题就是你该如何序列化以及检索这样的结构化数据呢?这里罗列三种常见的处理方法:
- 使用.Net二进制序列化
`System.Runtime.Serialization.Formatters.Binary.BinaryFormatter` 类以及关联类。这种方法的缺点,一是缺乏灵活性,二是数据量大,三是跨平台共享数据方面效果也不是很好。
- XML序列化数据
这种方法可能是非常有吸引力的,因为XML是可读的,并且有许多语言都有相应的支持库。而且在与其他应用程序/项目共享数据方面,也是个不错的选择。然而,XML也是众所周知的空间密集,在编码/解码方面会带来巨大的性能损失。另外,XML Dom树的导航要比类中简单字段的导航要复杂的多。
- 自定义
你可以创建一种特别的方式将数据编码为字符串。例如:把4个int型数据编码成“12:3:-23:67”。尽管它需要一次性的编码和解析,解析时会造成一旦运行时的损耗,但是它是非常简单灵活的做法。
Protocol Buffers是解决这类问题的一种灵活的、高效的自动化解决方案。使用ProtoBuf,你可以将你想要存储的结构化数据写在一个`.proto` 的文件里,然后ProtoBuf编译器会创建一个使用高效的二进制格式实现了自动编码和解析ProtoBuf数据的类。这个类还为组成ProtoBuf的数据字段提供getter和setter,并将ProtoBuf数据的读写细节做个为一个单元来处理。更重要的是,ProtoBuf支持日后的扩展,也就是说代码仍然可以读取使用旧格式编码的数据。
## 在哪里可以找到示例代码?
示例程序是一个采用ProtoBuf编码格式的用于管理地址簿数据文件的命令行应用程序。这个`AddressBook` 示例项目,可以给数据文件添加新的条目,也可以解析数据文件并输出到控制台。
你可以在GitHub仓库的 **[examples directory](https://github.com/google/protobuf/tree/master/examples)** 或者 **[csharp/src/AddressBook directory](https://github.com/google/protobuf/tree/master/csharp/src/AddressBook)** 目录中找到完整的示例。
## 定义你的协议格式
要创建地址簿应用程序,你需要从一个后缀为 `.proto` 的文件开始。`.proto` 文件中的定义内容非常简单:为每个你想要序列化的数据结构添加*message* ,然后在*message*中为每个字段指定名称和类型。也就是我们的示例中的`addressbook.proto` 文件。
`.proto` 文件的起始行申明了一个包,这样有助于避免在不同项目间出现命名空间冲突。
```C#
syntax = "proto3";
package tutorial;
```
在C#中,如果没有指定命名空间( `cshap_namespace`),你创建的类会被房子到和包名称一致的命名空间中。在我们的示例中,`cshap_namespace` 选项已经被重新赋值,所以生成的代码使用命名空间`Google.Protobuf.Examples.AddressBook ` 而不是默认的`Tutorial ` 。
```c#
option csharp_namespace = "Google.Protobuf.Examples.AddressBook";
```
接下来,定义你自己的message。message是一个包含一助类型字段的聚合。许多简单标准数据类型可以用作字段类型,包括`bool` 、`int32` 、`float` 、`double` 和`string` 。你也可以通过使用其他message类型作为字段类型,来增加更多的结构。
```c#
message Person {
string name = 1;
int32 id = 2; // Unique ID number for this person.
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
}
// Our address book file is just one of these.
message AddressBook {
repeated Person people = 1;
}
```
上面的示例中,Person message包含PhoneNumber message,AddressBook message包含Person message。你甚至也可以在message中定义message类型。如你所见,PhoneNumber类型就定义在Person中。如果你需要字段拥有预定义好的集合中的一个值,你也可以定义一个枚举类型。示例中指定一个手机号可以是PhoneType为Mobile、Home、Work中的一种。
每个字段都有“=1”、“=2”这样的标记,这些标记是字段在二进制编码内的唯一标识标签。标签数字1-15比更大的数字所需的字节编码少1位,所以作为优化,你可以将这些标签数字用在常用的或重复字段,将16及以后的标签数字留给不常用的可无字段。重复字段里的每个元素都需要重新编码标签数字,所以重复字段特别适合这种优化。
如果字段的值没有被设置,它会使用默认值:数值类型使用0,字符串类型使用空字符串,布尔类型为false。对于内嵌message,默认值一般是message的默认实例或原型。调用没有赋值的字段的访问器的时候,通常会返回这个字段的默认值。
如果字段被设置为 `repeated` ,那么这个字段可以重复任意次(包含0)。重复的值按序保存到ProtoBuf。可以把重复字段看做是一个动态大小的数组。
你可以在 [Protocol Buffer Language Guide](https://developers.google.com/protocol-buffers/docs/proto3) 找到一个完整编写 `.proto` 文件的指南(包含所有可能用到的字段类型)。ProtoBuf没有类似面向对象的类继承的功能。
## 编译Protocol Buffers文件
你现在拥有一个后缀为`.proto` 的文件,接下来你需要做的是生成供你读写 `AddressBook` 消息的类(以及Person、PhoneNumber消息)。你需要通过对你的`.proto` 文件运行Protocol Buffer编译器`protoc` 来实现:
1. 如果你还没有安装编译器,请[下载软件](https://developers.google.com/protocol-buffers/docs/downloads)并按照README中的介绍进行操作。
2. 运行编译器,指定源文件目录(编译器的安装目录。如果你不指定,就使用当前目录),目标目录(生成代码的保存目录;一般是$SRC+DIR目录)以及你的`.proto` 文件路径。在这里例子中,你可以如下操作:
```c#
protoc -I=$SRC_DIR --csharp_out=$DST_DIR $SRC_DIR/addressbook.proto
```
使用`--csharp_out` 选项,是因为你需要C#类。其他语言也都有对应支持的选项进行操作。
这样就会在你指定的目录下生成`Addressbook.cs` 类。为了能编译这个代码,你需要一个引用了`Google.Protobuf` 程序集的项目工程。
## 地址簿类
生成的`Addressbook.cs` 提供了五种类型:
- 一个包含了Protocol Buffer的元数据的静态`Addressbook` 类。
- 一个有只读`People` 属性的`AddressBook` 类。
- 一个有`Name` 、`Id` 、`Email` 、`Phones` 属性的`Person` 类。
- 嵌入在静态 `Person.Types` 类中的`PhoneNumber` 类。
- 嵌入在静态 `Person.Types` 类中的`PhoneType` 枚举。
在 [C#Generated Code Guide](https://developers.google.com/protocol-buffers/docs/reference/csharp-generated) 中你可以找到更详细的生成信息说明,不过大部分的生成信息你都可以看做是普通的C#类。需要注意的一点,任何被声明为 `repeated` 的属性字段都是只读的。你可以添加删除项目,但是不能用一个独立的集合去替换它。被声明为`repeated` 的字段是类型为`RepeatedField` 的集合类型。类似于`List` ,只是多了一些方便使用的额外方法,例如`Add` 方法的一个重载,可以在集合初始化的时候接收一个项的集合对象。
下面的例子说明如何创建一个Person实体对象:
``` c#
Person john = new Person
{
Id = 1234,
Name = "John Doe",
Email = "jdoe@example.com",
Phones = { new Person.Types.PhoneNumber { Number = "555-4321", Type = Person.Types.PhoneType.HOME } }
};
```
注意如果你使用的是C#6.0,你可以通过使用`using static` 来避免`Person.Types` 这种写法:
```C#
// Add this to the other using directives
using static Google.Protobuf.Examples.AddressBook.Person.Types;
...
// The earlier Phones assignment can now be simplified to:
Phones = { new PhoneNumber { Number = "555-4321", Type = PhoneType.HOME } }
```
## 解析和序列化
使用Protocol Buffers的目的是序列化数据以便它在任何地方都可以被解析。每个生成的类里面都包含一个`WriteTo(CodedOutputStream)` 的方法,参数`CodeOutputStream` 是一个Protocol Buffer运行时库中的一个类。通常情况下,你使用`WriteTo` 的扩展方法将数据写入到一个常规的IO流或者转换成二进制数组或者ByteString。这些扩展方法在`Google.Protobuf.MessageExtensions` 类,所以如果你需要序列化的时候,你需要引入命名空间`Google.Protobuf` 的引用。例如:
``` C#
using Google.Protobuf;
...
Person john = ...; // Code as before
using (var output = File.Create("john.dat"))
{
john.WriteTo(output);
}
```
解析也非常简单。每个生成的类都有一个对应该类的类型为 `MessageParser` 的静态属性`Parser` 。`MessageParser` 依次提供解析流、二进制数组和ByteString的方法。所以要解析我们上一步创建的文件,我们可以这样做:
``` C#
Person john;
using (var input = File.OpenRead("john.dat"))
{
john = Person.Parser.ParseFrom(input);
}
```
在Github上可获得使用这种方式维护一个地址簿(添加新实体以及列出存在的实体)的完整示例。
## Protocol Buffer扩展
在使用Protocol Buffer发布了你的第一版代码后,迟早你会毫无疑问的发现,你需要改善你的ProtocolBuffer文件中的定义。如果你想要新的ProtocolBuffer的定义向后兼容,同时你的旧的ProtocolBuffer的定义向前兼容(你当然想这样了),那么以下这些规则你必须遵守:
- 不可以修改已存在字段的标签数字。
- 可以删除字段。
- 可以添加新字段,不过必须使用新的标签数字(也就是说,标签数字必须是从未被使用过的,即使是被删除的字段使用过的也不可以使用)。
(也有一些[例外情况](https://developers.google.com/protocol-buffers/docs/proto3#updating),但很少使用。)
按照这些规则,旧代码可以正常的读取新消息,并且直接忽略任何新添加的字段。对于旧代码,被删除的字段将采用默认值,被删除的重复字段(repeated)将为空。新代码也可以无障碍的读取旧消息。
但是需要注意,旧消息中不会出现新字段,所以你需要使用新字段的默认值来进行一些合理性操作。一些特定类型的默认值如下:对于string类型,默认值是空字符串;对于bool类型,默认值是false;对于数字类型,默认值是0。
## 反射
我们可以通过使用反射API以编程方式进行检查消息描述符(`.proto` 文件中的信息)和消息实例。在编写通用代码(如编写不同文本格式的代码或智能比较工具)时,这将非常有用。每个生成的类中都包含一个静态`Descriptor` 属性。可以通过`IMessage.Descriptor` 属性,来检索任何实体的描述。下面是一个如何使用上述内容的例子(一个用来输出任何消息的顶级字段的方法):
``` C#
public void PrintMessage(IMessage message)
{
var descriptor = message.Descriptor;
foreach (var field in descriptor.Fields.InDeclarationOrder())
{
Console.WriteLine(
"Field (): ",
field.FieldNumber,
field.Name,
field.Accessor.GetValue(message);
}
}
```
领取专属 10元无门槛券
私享最新 技术干货