Dieses Tutorial bietet eine grundlegende Einführung des Dart-Programmierers in die Arbeit mit Protokollpuffern unter Verwendung der proto3-Version der Sprache Protocol Buffers. Wenn Sie eine einfache Beispielanwendung erstellen, wird Ihnen gezeigt, wie Sie
- Nachrichtenformate in einer
.proto
-Datei definieren. - Verwenden Sie den Protokollpuffer-Compiler.
- Verwenden Sie die Dart-Protokollpuffer-API zum Schreiben und Lesen von Nachrichten.
Dies ist keine umfassende Anleitung zur Verwendung von Protokollpuffern in Dart . Detailliertere Referenzinformationen finden Sie im Handbuch zur Sprache des Protokollpuffers, in der Dart-Sprachtour, in der Dart-API-Referenz, im Handbuch zum von Dart generierten Code und in der Codierungsreferenz.
Warum Protokollpuffer verwenden?
Das Beispiel, das wir verwenden werden, ist eine sehr einfache „Adressbuch“ -Anwendung, die die Kontaktdaten von Personen in und aus einer Datei lesen und schreiben kann. Jede Person im Adressbuch hat einen Namen, eine ID, eine E-Mail-Adresse und eine Kontakttelefonnummer.
Wie serialisieren und rufen Sie strukturierte Daten wie diese ab? Es gibt einige Möglichkeiten, dieses Problem zu lösen:
- Sie können eine Ad-hoc-Methode erfinden, um die Datenelemente in eine einzelne Zeichenfolge zu codieren – z. B. 4 Ints als „12:3:-23:67“. Dies ist ein einfacher und flexibler Ansatz, obwohl das Schreiben von einmaligem Codierungs- und Parsing-Code erforderlich ist und das Parsen geringe Laufzeitkosten verursacht. Dies funktioniert am besten für die Codierung sehr einfacher Daten.
- Serialisieren Sie die Daten in XML. Dieser Ansatz kann sehr attraktiv sein, da XML (irgendwie) lesbar ist und es Bindungsbibliotheken für viele Sprachen gibt. Dies kann eine gute Wahl sein, wenn Sie Daten mit anderen Anwendungen / Projekten teilen möchten. XML ist jedoch notorisch platzintensiv, und das Codieren / Decodieren kann zu enormen Leistungseinbußen für Anwendungen führen. Außerdem ist das Navigieren in einem XML-DOM-Baum erheblich komplizierter als das Navigieren in einfachen Feldern in einer Klasse.
Protokollpuffer sind die flexible, effiziente und automatisierte Lösung, um genau dieses Problem zu lösen. Mit Protokollpuffern schreiben Sie eine .proto
Beschreibung der Datenstruktur, die Sie speichern möchten. Daraus erstellt der Protokollpuffer-Compiler eine Klasse, die die automatische Codierung und Analyse der Protokollpufferdaten mit einem effizienten Binärformat implementiert. Die generierte Klasse stellt Getter und Setter für die Felder bereit, aus denen ein Protokollpuffer besteht, und kümmert sich um die Details zum Lesen und Schreiben des Protokollpuffers als Einheit. Wichtig ist, dass das Protokollpufferformat die Idee unterstützt, das Format im Laufe der Zeit so zu erweitern, dass der Code weiterhin Daten lesen kann, die mit dem alten Format codiert sind.
Wo finden Sie den Beispielcode
Unser Beispiel ist eine Reihe von Befehlszeilenanwendungen zum Verwalten einer Adressbuchdatendatei, die mit Protokollpuffern codiert sind.Der Befehl dart add_person.dart
fügt der Datendatei einen neuen Eintrag hinzu. Der Befehl dart list_people.dart
analysiert die Datendatei und druckt die Daten auf die Konsole.
Das vollständige Beispiel finden Sie im Verzeichnis Beispiele des GitHub-Repositorys.
Definieren Ihres Protokollformats
Um Ihre Adressbuchanwendung zu erstellen, müssen Sie mit einer.proto
-Datei beginnen. Die Definitionen in einer .proto
-Datei sindeinfach: Sie fügen eine Nachricht für jede Datenstruktur hinzu, die Sie serialisieren möchten, und geben dann einen Namen und einen Typ für jedes Feld in der Nachricht an. In diesem Beispiel lautet die .proto
-Datei, die die Nachrichten definiert,addressbook.proto
.
Die .proto
-Datei beginnt mit einer Paketdeklaration, die hilft, Namenskonflikte zwischen verschiedenen Projekten zu vermeiden.
syntax = "proto3";package tutorial;import "google/protobuf/timestamp.proto";
Als nächstes haben Sie Ihre Nachrichtendefinitionen. Eine Nachricht ist nur ein Aggregat, das eine Reihe von typisierten Feldern enthält. Viele einfache Standarddatentypen sind als Feldtypen verfügbar, einschließlich bool
int32
float
double
und string
. Sie können Ihren Nachrichten auch weitere Strukturen hinzufügen, indem Sie andere Nachrichtentypen als Feldtypen verwenden.
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; google.protobuf.Timestamp last_updated = 5;}// Our address book file is just one of these.message AddressBook { repeated Person people = 1;}
Im obigen Beispiel enthält die Person
NachrichtPhoneNumber
Nachrichten, während die AddressBook
Nachricht Person
nachrichten. Sie können sogar Nachrichtentypen definieren, die in anderen Nachrichten verschachtelt sind – wie Sie sehen können, ist der TypPhoneNumber
in Person
definiert. Sie können auch enum
–Typen definieren, wenn eines Ihrer Felder eine vordefinierte Liste von Werten haben soll – hier möchten Sie angeben, dass eine Telefonnummer eine von MOBILE
HOME
oderWORK
sein kann.
Die Markierungen “ = 1″, “ = 2″ auf jedem Element identifizieren das eindeutige „Tag“, das das Feld in der Binärcodierung verwendet. Die Tag-Nummern 1-15 benötigen ein Byte weniger zum Codieren als höhere Zahlen, so dass Sie zur Optimierung entscheiden können, diese Tags für die häufig verwendeten oder wiederholten Elemente zu verwenden, wobei die Tags 16 und höher für weniger häufig verwendete optionale Elemente belassen werden. Für jedes Element in einem wiederholten Feld muss die Tag-Nummer neu codiert werden, sodass wiederholte Felder besonders gute Kandidaten für diese Optimierung sind.
Wenn kein Feldwert gesetzt ist, wird ein Standardwert verwendet: zerofor numerische Typen, die leere Zeichenfolge für Strings, false für Bools. Für embeddedmessages ist der Standardwert immer die „Standardinstanz“ oder der „Prototyp“ der Nachricht, für die keines der Felder festgelegt ist. Wenn Sie den Accessor aufrufen, um den Wert eines Felds abzurufen, das nicht explizit festgelegt wurde, wird immer der Standardwert dieses Felds zurückgegeben.
Wenn ein Feld repeated
ist, kann das Feld beliebig oft wiederholt werden (einschließlich Null). Die Reihenfolge der wiederholten Werte wird im Protokollpuffer beibehalten. Stellen Sie sich wiederholte Felder als Arrays mit dynamischer Größe vor.
Eine vollständige Anleitung zum Schreiben von .proto
–Dateien – einschließlich aller möglichen Feldtypen – finden Sie im Protocol Buffer Language Guide. Suchen Sie jedoch nicht nach Einrichtungen, die der Klassenvererbung ähneln – Protokollpuffer tun dies nicht.
Kompilieren Sie Ihre Protokollpuffer
Nachdem Sie nun eine .proto
haben, müssen Sie als nächstes die Klassen generieren, die Sie lesen und schreiben müssen AddressBook
(und damit Person
und PhoneNumber
) Nachrichten. Dazu müssen Sie den Protokollpuffer-Compiler protoc
auf Ihrem .proto
ausführen:
- Wenn Sie den Compiler nicht installiert haben, laden Sie das Paket herunter und folgen Sie den Anweisungen in der README-Datei.
- Installieren Sie das Dart Protocol Buffer Plugin wie in der README beschrieben. Die ausführbare
bin/protoc-gen-dart
muss sich in IhremPATH
für den Protokollpufferprotoc
befinden, um sie zu finden. - Führen Sie nun den Compiler aus und geben Sie das Quellverzeichnis (in dem sich der Quellcode Ihrer Anwendung befindet – das aktuelle Verzeichnis wird verwendet, wenn Sie keinen Wert angeben), das Zielverzeichnis (in das der generierte Code gehen soll; oft das gleiche wie
$SRC_DIR
) und den Pfad zu Ihrem.proto
. In diesem Fall würden Sie Folgendes aufrufen:protoc -I=$SRC_DIR --dart_out=$DST_DIR $SRC_DIR/addressbook.proto
Da Sie Dart–Code möchten, verwenden Sie die Option
--dart_out
– ähnliche Optionen werden für andere unterstützte Sprachen bereitgestellt.
Dies erzeugt addressbook.pb.dart
in Ihrem angegebenen Zielverzeichnis.
Die Protocol Buffer API
erzeugt addressbook.pb.dart
gibt Ihnen die folgenden nützlichen Typen:
- Eine
AddressBook
Klasse mit einemList<Person> get people
Getter. - Eine
Person
Klasse mit Accessor-Methoden fürname
id
email
undphones
. - Eine
Person_PhoneNumber
Klasse mit Accessor-Methoden fürnumber
undtype
. - Eine
Person_PhoneType
Klasse mit statischen Feldern für jeden Wert in derPerson.PhoneType
enum.
Sie können mehr über die Details lesen, was genau im Dart Generated Code Guide generiert wird.
Schreiben einer Nachricht
Versuchen wir nun, Ihre Protokollpufferklassen zu verwenden. Das erste, was Ihre Adressbuchanwendung tun soll, ist, persönliche Daten in Ihre Adressbuchdatei zu schreiben. Dazu müssen Sie Instanzen Ihrer Protokollpufferklassen erstellen und füllen und sie dann in einen Ausgabestream schreiben.
Hier ist ein Programm, das eine AddressBook
aus einer Datei liest, eine neue Person
basierend auf Benutzereingaben hinzufügt und die neue AddressBook
wieder in die Datei schreibt. Die Teile, die den vom Protokoll-Compiler generierten Code direkt aufrufen oder referenzieren, sind hervorgehoben.
import 'dart:io';import 'dart_tutorial/addressbook.pb.dart';// This function fills in a Person message based on user input.Person promtForAddress() { Person person = Person(); print('Enter person ID: '); String input = stdin.readLineSync(); person.id = int.parse(input); print('Enter name'); person.name = stdin.readLineSync(); print('Enter email address (blank for none) : '); String email = stdin.readLineSync(); if (email.isNotEmpty) { person.email = email; } while (true) { print('Enter a phone number (or leave blank to finish): '); String number = stdin.readLineSync(); if (number.isEmpty) break; Person_PhoneNumber phoneNumber = Person_PhoneNumber(); phoneNumber.number = number; print('Is this a mobile, home, or work phone? '); String type = stdin.readLineSync(); switch (type) { case 'mobile': phoneNumber.type = Person_PhoneType.MOBILE; break; case 'home': phoneNumber.type = Person_PhoneType.HOME; break; case 'work': phoneNumber.type = Person_PhoneType.WORK; break; default: print('Unknown phone type. Using default.'); } person.phones.add(phoneNumber); } return person;}// Reads the entire address book from a file, adds one person based// on user input, then writes it back out to the same file.main(List arguments) { if (arguments.length != 1) { print('Usage: add_person ADDRESS_BOOK_FILE'); exit(-1); } File file = File(arguments.first); AddressBook addressBook; if (!file.existsSync()) { print('File not found. Creating new file.'); addressBook = AddressBook(); } else { addressBook = AddressBook.fromBuffer(file.readAsBytesSync()); } addressBook.people.add(promtForAddress()); file.writeAsBytes(addressBook.writeToBuffer());}
Eine Nachricht lesen
Natürlich würde ein Adressbuch nicht viel nützen, wenn Sie keine Informationen daraus erhalten könnten! Dieses Beispiel liest die im obigen Beispiel erstellte Datei und druckt alle darin enthaltenen Informationen aus.
import 'dart:io';import 'dart_tutorial/addressbook.pb.dart';import 'dart_tutorial/addressbook.pbenum.dart';// Iterates though all people in the AddressBook and prints info about them.void printAddressBook(AddressBook addressBook) { for (Person person in addressBook.people) { print('Person ID: ${ person.id}'); print(' Name: ${ person.name}'); if (person.hasEmail()) { print(' E-mail address:${ person.email}'); } for (Person_PhoneNumber phoneNumber in person.phones) { switch (phoneNumber.type) { case Person_PhoneType.MOBILE: print(' Mobile phone #: '); break; case Person_PhoneType.HOME: print(' Home phone #: '); break; case Person_PhoneType.WORK: print(' Work phone #: '); break; default: print(' Unknown phone #: '); break; } print(phoneNumber.number); } }}// Reads the entire address book from a file and prints all// the information inside.main(List arguments) { if (arguments.length != 1) { print('Usage: list_person ADDRESS_BOOK_FILE'); exit(-1); } // Read the existing address book. File file = new File(arguments.first); AddressBook addressBook = new AddressBook.fromBuffer(file.readAsBytesSync()); printAddressBook(addressBook);}
Erweitern eines Protokollpuffers
Früher oder später, nachdem Sie den Code freigegeben haben, der Ihren Protokollpuffer verwendet, werden Sie zweifellos die Definition des Protokollpuffers „verbessern“ wollen. Wenn Sie möchten, dass Ihre neuen Puffer abwärtskompatibel und Ihre alten Puffer abwärtskompatibel sind – und Sie möchten dies mit ziemlicher Sicherheit -, müssen Sie einige Regeln befolgen. In der neuen Version des Protokollpuffers:
- Sie dürfen die Tag-Nummern vorhandener Felder nicht ändern.
- Sie können Felder löschen.
- Sie können neue Felder hinzufügen, aber Sie müssen frische Tag-Nummern verwenden (d. h. Tag-Nummern, die in diesem Protokollpuffer nie verwendet wurden, auch nicht von gelöschten Feldern).
(Es gibt einige Ausnahmen zudiese Regeln, aber sie werden selten verwendet.)
Wenn Sie diese Regeln befolgen, liest der alte Code gerne neue Nachrichten und ignoriert einfach alle neuen Felder. Im alten Code haben einzelne Felder, die gelöscht wurden, einfach ihren Standardwert, und gelöschte wiederholte Felder sind leer. Neuer Code liest auch alte Nachrichten transparent.
Beachten Sie jedoch, dass neue Felder in alten Nachrichten nicht vorhanden sind, sodass Sie mit dem Standardwert etwas Vernünftiges tun müssen. Atype-specificdefault valueis used: Für Strings ist der Standardwert der leere String. Für boolesche Werte ist der Standardwert false. Für numerische Typen ist der Standardwert Null.