Protocol Buffers (Protobuf) ist eine Methode zur Serialisierung strukturierter Daten. Protobuf umfasst eine Schnittstellenbeschreibungssprache, um die Strukturen der Daten zu definieren und Compiler zur Generierung der Strukturen und (De-)Serialisierung. Es gibt mittlerweile Protobuf Compiler für C#, C, C++, Go, Objective-C, Java, Python, Swift und Ruby.
Google hat Protobuf ursprünglich für die interne Verwendung entwickelt und im Juli 2008 veröffentlicht. Es gibt mittlerweile drei Versionen, die nicht miteinander kompatible sind: Google intern (<2008), proto2 und proto3.
Die Entwurfsziele für Protobuf sind Einfachheit und Leistung. Protobuf wurde so konzipiert, dass es kleiner und schneller ist als XML und JSON. XML und JSON sind nicht für den Datenaustausch zwischen zwei verschiedenen Plattformen optimiert.
Protocol Buffers bietet viele Vorteile:
Protobuf wird in gRPC verwendet.
Warum ist Protobuf so schnell und kompakt?
In XML und JSON enthält jeder Eintrag den Kontext und die Daten.
Beispiel XML:
<Person> <FirstName>Peter</FirstName> <LastName>Muster</LastName> </Person>
Beispiel JSON:
{ “firstName”: “Peter”, “lastName”: “Muster” }
Bei Protobuf werden die Datenstrukturen (‘message’) in einer Konfigurationsdatei definiert (*.proto). Der Kontext ist in den Datenstrukturen definiert:
Message Person{ String first_name = 1; String last_name = 2; }
Aus dieser Datei werden durch den Compiler die dazugehörigen Klassen generiert und die Methoden zur Serialisierung und Deserialisierung.
Eine detaillierte Beschreibung der binären Kodierung würde den Ramen dieses Blogs sprengen, dazu wird zur originalen Quelle verwiesen: https://developers.google.com/protocol-buffers/docs/encoding.
Jedes Feld hat folgendes Format:
{field_number << 3 | field_type} + {length of data} + {data}
Nehmen wir als Beispiel die minimale Struktur Person:
message Person{ string name = 1; }
Es gibt eine Instanz mit folgendem Inhalt:
person.name = “Erik Stroeken”
Die binäre Darstellung für ‘name’ sieht dann so aus:
0a 0d 45 72 69 6b 20 53 74 72 6f 65 65 6e => {10} + {13} + {Erik Stroeken}
Die Felder werden wie folgt interpretiert:
Die Strukturen der Protobuf Messages werden in einer separaten Textdatei (*.proto) definiert und dann in die Sprache kompiliert, in der die Nachrichten verwendet werden.
Die Namen der Messages sollten CamelCase sein (z.B. ‹message SongServerRequest›).
Jede Nachricht hat vier Felder:
In der ‘proto2’ Version von Protobuf war es noch möglich Standardwerte für jedes Feld zu definieren. Das geht nicht mehr in ‘proto3’: der default Wert ist der Standardwert vom Typ (0 oder string.Empty).
message Customer {
int32 id = 1;
string username = 2;
repeated google.protobuf.Any details = 3;
}
Für Rule gibt es nur zwei Möglichkeiten:
Bei wiederholten Feldern (z.B.’repeated string keys = 1′) werden am besten pluralisierte Namen verwenden.
‘repeated’ Felder sind read-only. Man kann Elemente hinzufügen oder löschen, aber nicht die ganze Kollektion ersetzen.
Der Protobuf-Compiler für C# übersetzt ‘repeated’ in den Typ RepeatedField<T>. Dieser Typ ist wie List<T> mit ein paar extra Methoden wie Add() ausgestattet, welche eine Sammlung entgegennimmt zur Verwendung in Collection-Initialisatoren.
message Customer {
int32 id = 1;
string username = 2;
repeated google.protobuf.Any details = 3;
}
Es gibt folgende Typen in Protobuf:
Mögliche Werte:
Die Typen mit den Präfixen ‘s’ oder ‘sfixed’ sind nur Varianten zur Optimierung.
Enumeratortypnamen sind alle CamelCase, Enumeratorwertnamen sind alle UPPER_CASE. Der nach 0 benannte Enumeratorwert sollte mit nnn_UNDEFINED = 0; enden. Ein Enumerator kann innerhalb einer anderen Message definiert werden. Der Scope ist dann nur die Message.
message Customer { enum CustomerType{ UNDEFINED = 0; REGULAR = 1; MEMBER = 2; SPONSOR =3; } CustomerType customer_type = 1 }
Google-Richtlinien bevorzugen globale Aufzählungstypen mit dem Typ Namen als Präfix in jedem Wertefeld.
enum PhoneType { PHONETYPE_UNDEFINED = 0; PHONETYPE_MOBILE = 1; PHONETYPE_HOME = 2; PHONETYPE_WORK = 3; } enum Gender { GENDER_UNDEFINED = 0; GENDER_MALE = 1; GENDER_FEMALE = 2; }
Der Protobuf-Compiler für C # generiert die folgenden C # enum Typen:
public enum PhoneType { [pbr::OriginalName("PHONETYPE_UNDEFINED")] Undefined = 0, [pbr::OriginalName("PHONETYPE_MOBILE")] Mobile = 1, [pbr::OriginalName("PHONETYPE_HOME")] Home = 2, [pbr::OriginalName("PHONETYPE_WORK")] Work = 3, } public enum Gender { [pbr::OriginalName("GENDER_UNDEFINED")] Undefined = 0, [pbr::OriginalName("GENDER_MALE")] Male = 1, [pbr::OriginalName("GENDER_FEMALE")] Female = 2, }
Diese Felder definieren verschachtelte Messages mit dem Scope des Eltern Message.
message Customer { message Address { … } Repeated Address addresses = 1 }
‘one of’ ist wie ‘union’ in Sprachen wie Pascal, in denen nur ein Feld einen Wert haben kann. ‘one of’ wird aus Gründen der Effizienz eingesetzt:
message Customer { one of access_type { string email = 1; string username = 2; } }
‘map’ ist ein einfaches Dictionary mit int oder string Typen.
message Customer { map<string, string> email_addresses = 1; }
‘Any’ verhält sich wie Variant in Basic oder var in C#.
message Customer { int32 id = 1; string username = 2; repeated google.protobuf.Any details = 3; }
message Customer {
int32 id = 1;
string username = 2;
google.protobuf.Any repeated details = 3;
}
Es gelten folgende Namenskonventionen:
Feldnamen werden für jede Sprache im richtigen Stil kompiliert.
message Customer {
int32 id = 1;
string username = 2;
google.protobuf.Any repeated details = 3;
}
Numerische Platzhalter des Feldes:
Implementieren als string oder wie beschrieben in https://github.com/protocolbuffers/protobuf/issues/2224.
Grosse Inhalte bis 1 kB kann man als byte Array definieren. Grössere Inhalte sollten gestreamed werden (‘chuncking’). Streaming wird in gRPC in beide Richtungen unterstützt. In der unteren Message wird ‘imageChunk’ gestreamed.
message PersonImageMessage { int32 personId = 1; ImageType imageType = 2; bytes imageChunk = 3; }
Proto-Dateien werden am Besten in einem Verzeichnis ‘Protos’ gespeichert. Enumeratoren werden gemäss Richtlinien von Google global definiert und sind im Beispiel unten ausgelagert in der Datei ‘enums.proto’.
Die Datei ‘enums.proto’:
syntax = "proto3"; option csharp_namespace = "Addressbook.Services"; enum Gender { GENDER_UNDEFINED = 0; GENDER_MALE = 1; GENDER_FEMALE = 2; } enum PhoneType { PHONETYPE_UNDEFINED = 0; PHONETYPE_MOBILE = 1; PHONETYPE_HOME = 2; PHONETYPE_WORK = 3; }
Die ‘option csharp_namespace’ definiert den Namensraum für die generierten Klassen. Es ersetzt das mehr generische ‘package package_name’.
Hier ist der Inhalt für ‘addressBook.proto’:
syntax = "proto3"; import "enums.proto"; import "google/protobuf/timestamp.proto"; option csharp_namespace = "Addressbook.Services"; message Person { int32 id = 1; string name = 2; int32 age = 3; string email = 4; Gender gender = 5; message PhoneNumber { string number = 1; PhoneType phoneType = 2; } repeated PhoneNumber phone_numbers = 6; google.protobuf.Timestamp last_updated = 7; } message AddressBook { repeated Person persons = 1; } message AddressBookMessage { AddressBook addressBook = 1; }
Mit ‘import’ werden andere Proto-Dateien importiert.
Im unteren Beispiel wird das Feld ‘name’ aufgeteilt in ‘first_name’ und ‘last_name’. Danach wird ein neues Feld ‘email’ hinzugefügt mit dem Tag 2. Wenn sich jetzt ein neuer Client mit einem alten Server verbindet, wird das Feld ‘email’ mit dem Inhalt vom Feld ‘name’ abgefüllt.
Nach der Revision soll das Schlüsselwort ‘reserved’ verwendet werden, um dem Entwickler und Compiler mitzuteilen, dass 2 nicht mehr verwendet werden darf.
Gültige Definitionen:
Gleicher UseCase wie oben, aber jetzt wird der Namen wiederverwendet.
Aus der Protobuf-Sicht gibt es kein Problem, aber wenn der Inhalt zu JSON serialisiert wird, wird ein Konflikt entstehen wenn ein neuer Client sich mit einem alten Server verbindet.
Gültige Definitionen:
Es gibt zwei Wege um die proto-Datei zu kompilieren:
Ab .NET Core 3.0 ist gRPC und Protobuf integriert. Die Kompilierung der proto-Dateien geschieht daher automatisch bei jeder Build-Aktion.
Nach dem Kompilieren befinden sich die cs-Dateien in ‘object\Debug\netcoreapp3.0\’:
Installiere NuGet Paketen ‘Google.Protobuf’ und ‘Google.Protobuf.Tools’.
Die NuGet-Pakete warden hier installiert:
C:\Users\[User]\.nuget\packages\google.protobuf\3.10.1
C:\Users\[User]\.nuget\packages\google.protobuf.tools\3.10.1
Entweder setzte die Path Variable zu:
C:\Users\[User]\.nuget\packages\google.protobuf.tools\3.10.1\tools\windows_x64
Oder kopiere ‘protoc.exe’ und das Unterverzeichnis ‘google’ in die Solution:
C:\Users\[User]\.nuget\packages\google.protobuf.tools\3.10.1\tools\windows_x64\protoc.exe
C:\Users\[User]\.nuget\packages\google.protobuf.tools\3.10.1\tools\windows_x64\google
Führe dann folgender Befehl aus (achtung: –csharp hat zwei ‹-‹ Zeichen):
protoc –csharp_out=ProtocolBuffers ProtocolBuffers\addressbook.proto
Die generierte Klasse ‘Person’ kann jetzt instanziiert und abgefüllt werden.
Person person = new Person { Name = "Jan Muster", Age = 50, Gender = Gender.Male, Email = "[email protected]", PhoneNumbers = { new PhoneNumber {Number = "00 41 76 417 12 87", PhoneType = PhoneType.Mobile}, new PhoneNumber {Number = "00 41 44 741 22 16", PhoneType = PhoneType.Home}, new PhoneNumber {Number = "00 41 41 455 45 45", PhoneType = PhoneType.Work} } };
Zur Serialisierung und Deserialisierung gib es statische und extension Methoden.
// Serializing person to byte array and back. byte[] personByteArray = person.ToByteArray(); Person personFromByteArray = Person.Parser.ParseFrom(personByteArray);
// Writing person to disk and reading it again. using (FileStream output = File.Create("person.dat")) { person.WriteTo(output); } using (FileStream input = File.OpenRead("person.dat")) { Person personToDisk = Person.Parser.ParseFrom(input); }
// Serializing person to JSON and back. string jsonMessage = Google.Protobuf.JsonFormatter.Default.Format((IMessage)person); IMessage message = (IMessage)Activator.CreateInstance(typeof(Person)); Person personFromJSON = (Person)Google.Protobuf.JsonParser.Default.Parse(jsonMessage, message.Descriptor);
Macht keinen Sinn und gibt es auch nicht.
Protobuf ist schnell zu lernen und dank der Integration in Visual Studio 2019 einfach anzuwenden. Protocol Buffer ist die Technologie, die verwendet wird für die Serialisierung und Deserialisierung von DTOs in gRPC. In den nächsten Blogs wird gRPC und ASP.NET Core ausführlich erklärt.
[…] Verträge werden mit Protocol Buffers definiert. Eine Methode hat keinen Parameter (google.protobuf.Empty) oder einen Parameter vom Typ […]
[…] Blog «Tutorial: Protocol Buffers in ASP.NET Core» wird die Anwendung von Protocol Buffers in .NET und Visual Studio erklärt. Protocol Buffers […]