Sdílet prostřednictvím


Vytváření zpráv Protobuf pro aplikace .NET

James Newton-King a Mark Rendle

gRPC používá Protobuf jako svůj jazyk IDL (Interface Definition Language). Protobuf IDL je jazykově neutrální formát pro určení zpráv odeslaných a přijatých službami gRPC. Zprávy protobuf jsou definovány v .proto souborech. Tento dokument vysvětluje, jak se koncepty Protobuf mapují na .NET.

Zprávy ve formátu protobuf

Zprávy jsou hlavním objektem přenosu dat v Protobuf. Jsou koncepčně podobné třídám .NET.

syntax = "proto3";

option csharp_namespace = "Contoso.Messages";

message Person {
    int32 id = 1;
    string first_name = 2;
    string last_name = 3;
}  

Předchozí definice zprávy určuje tři pole jako páry název-hodnota. Podobně jako vlastnosti u typů .NET má každé pole název a typ. Typ pole může být skalárním typem protobuf, např. int32nebo jinou zprávou.

Průvodce stylem Protobuf doporučuje použít underscore_separated_names názvy polí. Nové zprávy Protobuf vytvořené pro aplikace .NET by měly postupovat podle pokynů pro styl Protobuf. Nástroje .NET automaticky generují typy .NET, které používají standardy pojmenování .NET. Například first_name pole Protobuf generuje FirstName vlastnost .NET.

Kromě názvu má každé pole v definici zprávy jedinečné číslo. Čísla polí se používají k identifikaci polí při serializaci zprávy do Protobuf. Serializace malého čísla je rychlejší než serializace celého názvu pole. Vzhledem k tomu, že čísla polí identifikují pole, je důležité se při jejich změně postarat. Další informace o změně zpráv Protobuf naleznete v tématu Služby správy verzí gRPC.

Když je aplikace sestavena nástroj Protobuf generuje typy .NET ze .proto souborů. Zpráva Person vygeneruje třídu .NET:

public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Další informace o zprávách Protobuf naleznete v průvodci jazykem Protobuf.

Skalární typy hodnot

Protobuf podporuje rozsah nativních skalárních hodnot. V následující tabulce jsou uvedeny všechny s odpovídajícím typem jazyka C#:

Protobuf – typ Typ jazyka 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

Skalární hodnoty mají vždy výchozí hodnotu a nelze je nastavit na null. Toto omezení zahrnuje string a ByteString které jsou třídy jazyka C#. string výchozí hodnota prázdné řetězcové hodnoty a ByteString výchozí hodnota je prázdná bajtová hodnota. Pokus o nastavení null vyvolá chybu.

K podpoře hodnot null lze použít typy obálky s možnou hodnotou Null.

Datové typy pro datum a čas

Nativní skalární typy neposkytují hodnoty data a času, které jsou ekvivalentní hodnotě . NET , DateTimeOffsetDateTimea TimeSpan. Tyto typy lze určit pomocí některých rozšíření Protobuf Well-Known Types . Tato rozšíření poskytují podporu generování kódu a modulu runtime pro komplexní typy polí napříč podporovanými platformami.

Následující tabulka uvádí typy data a času:

Typ .NET Známý typ 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;
}  

Vygenerované vlastnosti ve třídě C# nejsou typy data a času rozhraní .NET. Vlastnosti používají třídy TimestampDuration v Google.Protobuf.WellKnownTypes oboru názvů. Tyto třídy poskytují metody pro převod na a z DateTimeOffset, DateTimea 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();

Poznámka

Typ Timestamp funguje s časy UTC. DateTimeOffset hodnoty vždy mají posun nuly a DateTime.Kind vlastnost je vždy DateTimeKind.Utc.

Typy s povolenou hodnotou Null

Generování kódu Protobuf pro jazyk C# používá nativní typy, například int pro int32. Proto jsou hodnoty vždy zahrnuté a nemohou být null.

Pro hodnoty, které vyžadují explicitní null, například použití int? v kódu jazyka C#, Protobuf dobře známé typy obsahují obálky, které jsou kompilovány do typů C# s možnou hodnotou null. Pokud je chcete použít, naimportujte wrappers.proto je do souboru .proto , například do následujícího kódu:

syntax = "proto3";

import "google/protobuf/wrappers.proto";

message Person {
    // ...
    google.protobuf.Int32Value age = 5;
}

wrappers.proto typy nejsou vystaveny ve generovaných vlastnostech. Protobuf je automaticky mapuje na odpovídající typy s možnou hodnotou null .NET ve zprávách jazyka C#. Například google.protobuf.Int32Value pole generuje int? vlastnost. Vlastnosti typu odkazu jako string a ByteString beze změny se dají přiřadit s výjimkou null případů bez chyby.

Následující tabulka uvádí úplný seznam typů obálky s odpovídajícím typem jazyka C#:

Typ jazyka C# Dobře známá obálka typu
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

Přijaté

Binární datové části jsou podporovány v Protobuf s typem bytes skalární hodnoty. Vygenerovaná vlastnost v jazyce C# se používá ByteString jako typ vlastnosti.

Slouží ByteString.CopyFrom(byte[] data) k vytvoření nové instance z pole bajtů:

var data = await File.ReadAllBytesAsync(path);

var payload = new PayloadResponse();
payload.Data = ByteString.CopyFrom(data);

ByteString k datům se přistupuje přímo pomocí ByteString.Span nebo ByteString.Memory. Nebo volání ByteString.ToByteArray() převodu instance zpět na bajtové pole:

var payload = await client.GetPayload(new PayloadRequest());

await File.WriteAllBytesAsync(path, payload.Data.ToByteArray());

Desetinná čísla

Protobuf nativně nepodporuje typ .NET decimal , pouze double a float. V projektu Protobuf probíhá diskuze o možnosti přidání standardního desítkového typu do známých typů s podporou platformy pro jazyky a architektury, které ho podporují. Zatím se nic neimplementovalo.

Je možné vytvořit definici zprávy, která představuje decimal typ, který funguje pro bezpečné serializace mezi klienty .NET a servery. Vývojáři na jiných platformách by ale museli porozumět použitému formátu a implementovat pro něj vlastní zpracování.

Vytvoření vlastního desítkového typu pro 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;
}

Pole nanos představuje hodnoty od 0.999_999_999 do -0.999_999_999. Například decimal hodnota 1.5m by byla reprezentována jako { units = 1, nanos = 500_000_000 }. Toto je důvod, proč nanos pole v tomto příkladu sfixed32 používá typ, který kóduje efektivněji než int32 pro větší hodnoty. units Pokud je pole záporné, nanos mělo by být také záporné.

Poznámka

Další algoritmy jsou k dispozici pro kódování decimal hodnot jako řetězce bajtů. Algoritmus používaný:DecimalValue

  • Je snadno pochopitelný.
  • Není ovlivněný big-endianem nebo malým endianem na různých platformách.
  • Podporuje desetinná čísla od kladných 9,223,372,036,854,775,807.999999999 po záporná 9,223,372,036,854,775,808.999999999 s maximální přesností na devět desetinných míst, což není úplný rozsah decimal.

Převod mezi tímto typem a typem seznamu BCL decimal může být implementován v jazyce C#takto:

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 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);
        }
    }
}

Předchozí kód:

  • Přidá částečnou třídu pro DecimalValue. Částečná třída se zkombinuje s DecimalValue vygenerovaným ze .proto souboru. Vygenerovaná třída deklaruje Units a Nanos vlastnosti.
  • Obsahuje implicitní operátory pro převod mezi DecimalValue a typem seznamu BCL decimal .

Kolekce

Seznamy

Seznamy v Protobuf jsou určeny pomocí klíčového repeated slova předpony v poli. Následující příklad ukazuje, jak vytvořit seznam:

message Person {
    // ...
    repeated string roles = 8;
}

Ve vygenerovaném kódu repeated jsou pole reprezentována obecným typem Google.Protobuf.Collections.RepeatedField<T> .

public class Person
{
    // ...
    public RepeatedField<string> Roles { get; }
}

RepeatedField<T> implementuje IList<T>. Proto můžete použít dotazy LINQ nebo je převést na pole nebo seznam. RepeatedField<T> vlastnosti nemají veřejnou setter. Položky by se měly přidat do existující kolekce.

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);

Slovníky

Typ .NET IDictionary<TKey,TValue> je reprezentován v Protobuf pomocí map<key_type, value_type>.

message Person {
    // ...
    map<string, string> attributes = 9;
}

Ve vygenerovaném kódu map .NET jsou pole reprezentována obecným typem Google.Protobuf.Collections.MapField<TKey, TValue> . MapField<TKey, TValue> implementuje IDictionary<TKey,TValue>. Vlastnosti, jako repeated jsou vlastnosti, map nemají veřejnou setter. Položky by se měly přidat do existující kolekce.

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);

Nestrukturované a podmíněné zprávy

Protobuf je formát zasílání zpráv na základě kontraktu. Zprávy aplikace, včetně polí a typů, musí být při vytváření aplikace zadány v .proto souborech. Návrh prvního kontraktu protobuf je skvělý při vynucování obsahu zpráv, ale může omezit scénáře, kdy není vyžadován striktní kontrakt:

  • Zprávy s neznámými datovými částmi Například zpráva s polem, které může obsahovat jakoukoli zprávu.
  • Podmíněné zprávy. Například zpráva vrácená ze služby gRPC může být výsledkem úspěchu nebo chybovým výsledkem.
  • Dynamické hodnoty. Například zpráva s polem, které obsahuje nestrukturovanou kolekci hodnot, podobně jako JSON.

Protobuf nabízí jazykové funkce a typy pro podporu těchto scénářů.

Všechny

Tento Any typ umožňuje používat zprávy jako vložené typy bez jejich .proto definice. Chcete-li použít Any typ, importujte 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 pole jsou funkce jazyka. Kompilátor zpracovává oneof klíčové slovo při generování třídy zprávy. Použití oneof k zadání zprávy odpovědi, která by mohla vrátit Person odpověď nebo Error může vypadat takto:

message Person {
    // ...
}

message Error {
    // ...
}

message ResponseMessage {
  oneof result {
    Error error = 1;
    Person person = 2;
  }
}

Pole v oneof sadě musí mít jedinečná čísla polí v celkové deklaraci zprávy.

Při použití oneofobsahuje vygenerovaný kód jazyka C# výčet, který určuje, která pole byla nastavena. Výčet můžete otestovat a zjistit, které pole je nastavené. Pole, která nejsou nastavena jako návratová null nebo výchozí hodnota, místo vyvolání výjimky.

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.");
}

Hodnota

Typ Value představuje dynamicky zadaná hodnota. Může to být buď null, číslo, řetězec, logická hodnota, slovník hodnot (Struct) nebo seznam hodnot (ValueList). Value je známý typ Protobuf, který používá dříve diskutovanou oneof funkci. Chcete-li použít Value typ, importujte struct.proto.

import "google/protobuf/struct.proto";

message Status {
    // ...
    google.protobuf.Value data = 3;
}
// Create dynamic values.
var status = new Status();
status.Data = Value.ForStruct(new Struct
{
    Fields =
    {
        ["enabled"] = Value.ForBool(true),
        ["metadata"] = Value.ForList(
            Value.ForString("value1"),
            Value.ForString("value2"))
    }
});

// Read dynamic values.
switch (status.Data.KindCase)
{
    case Value.KindOneofCase.StructValue:
        foreach (var field in status.Data.StructValue.Fields)
        {
            // Read struct fields...
        }
        break;
    // ...
}

Použití Value přímo může být podrobné. Alternativní způsob použití Value je s integrovanou podporou Protobuf pro mapování zpráv na JSZAPNUTO. Protobuf JsonFormatter a JsonWriter typy lze použít s libovolnou zprávou Protobuf. Value je zvláště vhodný pro převod na a z JSON.

Toto je JSekvivalent ON předchozího kódu:

// 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.Data);
var document = JsonDocument.Parse(json);

Další prostředky