gRPC使用Protobuf作为其接口定义语言(IDL)。Protobuf IDL是一种语言无关的格式,用于指定gRPC服务发送和接收的消息。Protobuf消息在.proto
文件中定义。本文档介绍了Protobuf概念如何映射到.NET。
Protobuf讯息
消息是Protobuf中的主要数据传输对象。它们在概念上类似于.NET类。
syntax = "proto3";
option csharp_namespace = "Contoso.Messages";
message Person {
int32 id = 1;
string first_name = 2;
string last_name = 3;
}
前面的消息定义将三个字段指定为“名称/值”对。像.NET类型的属性一样,每个字段都有一个名称和一个类型。字段类型可以是Protobuf标量值类型,例如int32
,或其他消息。
除名称外,消息定义中的每个字段都有唯一的编号。当消息序列化为Protobuf时,字段号用于标识字段。序列化一个小数字比序列化整个字段名称要快。由于字段号标识一个字段,因此在更改字段时务必小心。有关更改Protobuf消息的更多信息,请参见Versioning gRPC services。
构建应用程序后,Protobuf工具会从.proto
文件生成.NET类型。该Person
消息生成一个.NET类:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
}
有关Protobuf消息的更多信息,请参见Protobuf语言指南。
标量值类型
Protobuf支持多种本机标量值类型。下表列出了所有它们的等效C#类型:
原虫型 | C#类型 |
---|---|
double | double |
float | float |
int32 | int |
int64 | long |
uint32 | uint |
uint64 | ulong |
sint32 | int |
sint64 | long |
fixed32 | uint |
fixed64 | ulong |
sfixed32 | int |
sfixed64 | long |
bool | bool |
string | string |
bytes | ByteString |
标量值始终具有默认值,不能设置为null
。此约束包括string
和ByteString
,它们是C#类。string
默认为空字符串值,ByteString
默认为空字节值。尝试将它们设置为null
会引发错误。
可空包装器类型可用于支持空值。
日期和时间
本机标量类型不提供日期和时间值,等效于.NET的DateTimeOffset,DateTime和TimeSpan。可以使用Protobuf的“知名类型”扩展名来指定这些类型。这些扩展为跨支持的平台的复杂字段类型提供了代码生成和运行时支持。
下表显示了日期和时间类型:
.NET类型 | Protobuf知名类型 |
---|---|
DateTimeOffset | google.protobuf.Timestamp |
DateTime | google.protobuf.Timestamp |
TimeSpan | google.protobuf.Duration |
syntax = "proto3"
import "google/protobuf/duration.proto";
import "google/protobuf/timestamp.proto";
message Meeting {
string subject = 1;
google.protobuf.Timestamp start = 2;
google.protobuf.Duration duration = 3;
}
C#类中生成的属性不是.NET日期和时间类型。这些属性使用名称空间中的Timestamp
和Duration
类Google.Protobuf.WellKnownTypes
。这些类别转换和从提供的方法DateTimeOffset
,DateTime
和TimeSpan
。
// Create Timestamp and Duration from .NET DateTimeOffset and TimeSpan.
var meeting = new Meeting
{
Time = Timestamp.FromDateTimeOffset(meetingTime), // also FromDateTime()
Duration = Duration.FromTimeSpan(meetingLength)
};
// Convert Timestamp and Duration to .NET DateTimeOffset and TimeSpan.
var time = meeting.Time.ToDateTimeOffset();
var duration = meeting.Duration?.ToTimeSpan();
注意
该Timestamp
类型适用于UTC时间。DateTimeOffset
值始终具有零偏移量,并且DateTime.Kind
属性始终为DateTimeKind.Utc
。
可空类型
C#的Protobuf代码生成使用本机类型,例如int
for int32
。因此,值始终包含在内,不能包含在内null
。
对于需要显式的值null
(例如int?
在C#代码中使用),Protobuf的知名类型包括包装器,这些包装器被编译为可为空的C#类型。要使用它们,wrappers.proto
请.proto
像以下代码一样导入文件中:
syntax = "proto3"
import "google/protobuf/wrappers.proto"
message Person {
// ...
google.protobuf.Int32Value age = 5;
}
wrappers.proto
类型不会在生成的属性中公开。Protobuf会自动将它们映射到C#消息中的相应.NET可空类型。例如,一个google.protobuf.Int32Value
字段生成一个int?
属性。像string
和一样的引用类型属性ByteString
不变,但null
可以毫无错误地分配给它们。
下表列出了包装器类型及其等效的C#类型的完整列表:
C#类型 | 知名包装纸 |
---|---|
bool? | google.protobuf.BoolValue |
double? | google.protobuf.DoubleValue |
float? | google.protobuf.FloatValue |
int? | google.protobuf.Int32Value |
long? | google.protobuf.Int64Value |
uint? | google.protobuf.UInt32Value |
ulong? | google.protobuf.UInt64Value |
string | google.protobuf.StringValue |
ByteString | google.protobuf.BytesValue |
字节数
Protobuf支持bytes
标量值类型的二进制有效负载。C#中生成的属性ByteString
用作属性类型。
用于ByteString.CopyFrom(byte[] data)
从字节数组创建新实例:
var data = await File.ReadAllBytesAsync(path);
var payload = new PayloadResponse();
payload.Data = ByteString.CopyFrom(data);
ByteString
使用ByteString.Span
或直接访问数据ByteString.Memory
。或调用ByteString.ToByteArray()
以将实例转换回字节数组:
var payload = await client.GetPayload(new PayloadRequest());
await File.WriteAllBytesAsync(path, payload.Data.ToByteArray());
小数点
Protobuf本身不支持.NETdecimal
类型,仅支持double
和float
。Protobuf项目中正在进行有关将标准十进制类型添加到知名类型的可能性的讨论,平台对支持它的语言和框架提供支持。尚未执行任何操作。
可以创建一个消息定义来表示decimal
可用于.NET客户端和服务器之间安全序列化的类型。但是其他平台上的开发人员将必须了解所使用的格式并对其实施自己的处理。
为Protobuf创建自定义十进制类型
package CustomTypes;
// Example: 12345.6789 -> { units = 12345, nanos = 678900000 }
message DecimalValue {
// Whole units part of the amount
int64 units = 1;
// Nano units of the amount (10^-9)
// Must be same sign as units
sfixed32 nanos = 2;
}
该nanos
字段表示从0.999_999_999
到的值-0.999_999_999
。例如,该decimal
值1.5m
将表示为{ units = 1, nanos = 500_000_000 }
。这就是nanos
本示例中的字段使用sfixed32
类型的原因,该类型的编码比较int32
大的值更有效。如果该units
字段为负,则该nanos
字段也应为负。
注意
还有多种其他算法可将decimal
值编码为字节字符串,但是此消息比任何一种消息都更易于理解。值不受不同平台上的big-endian或little-endian的影响。
这种类型和BCLdecimal
类型之间的转换可以在C#中实现,如下所示:
namespace CustomTypes
{
public partial class DecimalValue
{
private const decimal NanoFactor = 1_000_000_000;
public DecimalValue(long units, int nanos)
{
Units = units;
Nanos = nanos;
}
public long Units { get; }
public int Nanos { get; }
public static implicit operator decimal(CustomTypes.DecimalValue grpcDecimal)
{
return grpcDecimal.Units + grpcDecimal.Nanos / NanoFactor;
}
public static implicit operator CustomTypes.DecimalValue(decimal value)
{
var units = decimal.ToInt64(value);
var nanos = decimal.ToInt32((value - units) * NanoFactor);
return new CustomTypes.DecimalValue(units, nanos);
}
}
}
馆藏
清单
Protobuf中的列表是通过repeated
在字段上使用prefix关键字指定的。以下示例显示了如何创建列表:
message Person {
// ...
repeated string roles = 8;
}
在生成的代码中,repeated
字段由Google.Protobuf.Collections.RepeatedField<T>
通用类型表示。
public class Person
{
// ...
public RepeatedField<string> Roles { get; }
}
RepeatedField<T>
实现IList <T>。因此,您可以使用LINQ查询或将其转换为数组或列表。RepeatedField<T>
属性没有公共设置器。项目应添加到现有集合中。
var person = new Person();
// Add one item.
person.Roles.Add("user");
// Add all items from another collection.
var roles = new [] { "admin", "manager" };
person.Roles.Add(roles);
辞典
.NET IDictionary <TKey,TValue>类型在Protobuf中使用表示map<key_type, value_type>
。
message Person {
// ...
map<string, string> attributes = 9;
}
在生成的.NET代码中,map
字段由Google.Protobuf.Collections.MapField<TKey, TValue>
通用类型表示。MapField<TKey, TValue>
实现IDictionary <TKey,TValue>。像repeated
属性一样,map
属性没有公共设置器。项目应添加到现有集合中。
var person = new Person();
// Add one item.
person.Attributes["created_by"] = "James";
// Add all items from another collection.
var attributes = new Dictionary<string, string>
{
["last_modified"] = DateTime.UtcNow.ToString()
};
person.Attributes.Add(attributes);
非结构化和条件消息
Protobuf是一种合同优先的消息传递格式。.proto
构建应用程序时,必须在文件中指定该应用程序的消息,包括其字段和类型。Protobuf的合同优先设计在强制执行消息内容方面非常出色,但可以限制不需要严格合同的情况:
- 有效负载未知的消息。例如,一条消息的字段可以包含任何消息。
- 有条件的消息。例如,从gRPC服务返回的消息可能是成功结果,也可能是错误结果。
- 动态值。例如,一条消息的字段包含类似于JSON的非结构化值集合。
Protobuf提供语言功能和类型来支持这些方案。
任何
该Any
类型使您可以将消息用作嵌入式类型,而无需对其进行.proto
定义。要使用Any
类型,请导入any.proto
。
import "google/protobuf/any.proto";
message Status {
string message = 1;
google.protobuf.Any detail = 2;
}
// Create a status with a Person message set to detail.
var status = new ErrorStatus();
status.Detail = Any.Pack(new Person { FirstName = "James" });
// Read Person message from detail.
if (status.Detail.Is(Person.Descriptor))
{
var person = status.Detail.Unpack<Person>();
// ...
}
一个
oneof
字段是一种语言功能。编译器oneof
在生成消息类时会处理关键字。使用oneof
指定的响应消息既可以返回一个Person
或Error
可能是这样的:
message Person {
// ...
}
message Error {
// ...
}
message ResponseMessage {
oneof result {
Error error = 1;
Person person = 2;
}
}
在中的字段oneof
集必须在整个消息声明独特的场数。
使用时oneof
,生成的C#代码包括一个枚举,该枚举指定已设置的字段。您可以测试枚举以查找设置了哪个字段。未设置的字段返回null
值或默认值,而不是引发异常。
var response = await client.GetPersonAsync(new RequestMessage());
switch (response.ResultCase)
{
case ResponseMessage.ResultOneofCase.Person:
HandlePerson(response.Person);
break;
case ResponseMessage.ResultOneofCase.Error:
HandleError(response.Error);
break;
default:
throw new ArgumentException("Unexpected result.");
}
值
该Value
类型表示一个动态类型的值。它可以是null
,数字,字符串,布尔值,值字典(Struct
)或值列表(ValueList
)。Value
是使用前面讨论的oneof
功能的Protobuf知名类型。要使用Value
类型,请导入struct.proto
。
import "google/protobuf/struct.proto";
message Status {
// ...
google.protobuf.Value data = 3;
}
// Create dynamic values.
var status = new Status();
status.Data = Value.FromStruct(new Struct
{
Fields =
{
["enabled"] = Value.ForBoolean(true),
["metadata"] = Value.ForList(
Value.FromString("value1"),
Value.FromString("value2"))
}
});
// Read dynamic values.
switch (status.Data.KindCase)
{
case Value.KindOneofCase.StructValue:
foreach (var field in status.Data.StructValue.Fields)
{
// Read struct fields...
}
break;
// ...
}
Value
直接使用可能很冗长。一种替代的使用方式Value
是Protobuf的内置支持,用于将消息映射到JSON。protobuf的是JsonFormatter
和JsonWriter
类型可以与任何protobuf的消息中使用。Value
特别适合与JSON相互转换。
这是与先前代码等效的JSON:
// Create dynamic values from JSON.
var status = new Status();
status.Data = Value.Parser.ParseJson(@"{
""enabled"": true,
""metadata"": [ ""value1"", ""value2"" ]
}");
// Convert dynamic values to JSON.
// JSON can be read with a library like System.Text.Json or Newtonsoft.Json
var json = JsonFormatter.Default.Format(status.Metadata);
var document = JsonDocument.Parse(json);