Grundlagen von Protocol Buffers: Dart
Dieses Tutorial bietet eine grundlegende Einführung für Dart-Programmierer in die Arbeit mit Protocol Buffers, wobei die proto3-Version der Protocol Buffers-Sprache verwendet wird. Anhand eines einfachen Beispielprogramms wird gezeigt, wie Sie
- Nachrichtenformate in einer
.proto-Datei definieren. - Den Protocol Buffer-Compiler verwenden.
- Die Dart-Protocol-Buffer-API zum Schreiben und Lesen von Nachrichten verwenden.
Dies ist keine umfassende Anleitung zur Verwendung von Protocol Buffers in Dart. Detailliertere Referenzinformationen finden Sie im Leitfaden zur Protocol Buffer-Sprache, im Dart Language Tour, im Dart API-Referenz, im Leitfaden zum generierten Dart-Code und in der Encoding-Referenz.
Das Problemfeld
Das Beispiel, das wir verwenden werden, ist eine sehr einfache "Adressbuch"-Anwendung, die Kontaktdaten von Personen lesen und in eine Datei schreiben kann. Jede Person im Adressbuch hat einen Namen, eine ID, eine E-Mail-Adresse und eine Telefonnummer.
Wie serialisiert und ruft man strukturierte Daten wie diese ab? Es gibt mehrere Möglichkeiten, dieses Problem zu lösen
- Man kann sich einen Ad-hoc-Weg ausdenken, um die Datenpunkte in einen einzigen String zu kodieren – z. B. 4 ganze Zahlen als „12:3:-23:67“ kodieren. Dies ist ein einfacher und flexibler Ansatz, obwohl er das Schreiben von einmaligen Kodierungs- und Parsing-Codes erfordert und das Parsen einen geringen Laufzeitaufwand verursacht. Dies funktioniert am besten für die Kodierung sehr einfacher Daten.
- Serialisieren Sie die Daten in XML. Dieser Ansatz kann sehr attraktiv sein, da XML (sozusagen) menschenlesbar 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 bekanntermaßen speicherintensiv, und die Kodierung/Dekodierung kann Anwendungen eine enorme Leistungseinbuße bescheren. Außerdem ist die Navigation durch einen XML-DOM-Baum erheblich komplizierter als die normale Navigation durch einfache Felder in einer Klasse.
Protocol Buffers sind die flexible, effiziente, automatisierte Lösung, um genau dieses Problem zu lösen. Mit Protocol Buffers schreiben Sie eine .proto-Beschreibung der Datenstruktur, die Sie speichern möchten. Daraus erstellt der Protocol Buffer-Compiler eine Klasse, die die automatische Kodierung und das Parsen von Protocol Buffer-Daten in einem effizienten Binärformat implementiert. Die generierte Klasse bietet Getter und Setter für die Felder, die eine Protocol Buffer ausmachen, und kümmert sich um die Details des Lesens und Schreibens der Protocol Buffer als Einheit. Wichtig ist, dass das Protocol Buffer-Format die Idee unterstützt, das Format im Laufe der Zeit so zu erweitern, dass der Code Daten, die mit dem alten Format kodiert wurden, weiterhin lesen kann.
Wo man den Beispielcode findet
Unser Beispiel besteht aus einer Reihe von Kommandozeilenanwendungen zur Verwaltung einer Adressbuch-Datendatei, die mit Protocol Buffers kodiert ist. Der Befehl dart add_person.dart fügt dem Datenfile einen neuen Eintrag hinzu. Der Befehl dart list_people.dart parst die Datendatei und gibt die Daten auf der Konsole aus.
Das vollständige Beispiel finden Sie im Beispielverzeichnis 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 sind einfach: 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 unserem Beispiel ist 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 Nachrichten-Definitionen. Eine Nachricht ist einfach eine Aggregation, die eine Reihe von typisierten Feldern enthält. Viele einfache Standarddatentypen sind als Feldtypen verfügbar, darunter bool, int32, float, double und string. Sie können Ihren Nachrichten auch weitere Struktur verleihen, 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 {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
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;
}
In dem obigen Beispiel enthält die Person-Nachricht PhoneNumber-Nachrichten, während die AddressBook-Nachricht Person-Nachrichten enthält. Sie können sogar Nachrichtentypen definieren, die in andere Nachrichten verschachtelt sind – wie Sie sehen, ist der PhoneNumber-Typ in Person definiert. Sie können auch enum-Typen definieren, wenn Sie möchten, dass eines Ihrer Felder einen Wert aus einer vordefinierten Liste hat – hier möchten Sie angeben, dass eine Telefonnummer entweder PHONE_TYPE_MOBILE, PHONE_TYPE_HOME oder PHONE_TYPE_WORK sein kann.
Die Markierungen " = 1", " = 2" bei jedem Element identifizieren den eindeutigen "Tag", den dieses Feld in der Binärkodierung verwendet. Tag-Nummern von 1-15 erfordern ein Byte weniger zur Kodierung als höhere Nummern. Daher können Sie diese Tags als Optimierung für häufig verwendete oder wiederholte Elemente verwenden und die Tags 16 und höher für weniger häufig verwendete optionale Elemente übrig lassen. Jedes Element in einem wiederholten Feld erfordert eine erneute Kodierung der Tag-Nummer. Wiederholte Felder sind daher besonders gute Kandidaten für diese Optimierung.
Wenn ein Feldwert nicht gesetzt ist, wird ein Standardwert verwendet: Null für numerische Typen, eine leere Zeichenfolge für Zeichenfolgen, false für boolesche Werte. Für eingebettete Nachrichten ist der Standardwert immer die "Standardinstanz" oder das "Prototyp" der Nachricht, bei der keine Felder gesetzt sind. Das Aufrufen des Accessors zum Abrufen des Werts eines Feldes, das nicht explizit gesetzt wurde, gibt immer den Standardwert dieses Feldes zurück.
Wenn ein Feld repeated ist, kann das Feld beliebig oft (einschließlich null Mal) wiederholt werden. Die Reihenfolge der wiederholten Werte wird in der Protocol Buffer beibehalten. Betrachten Sie wiederholte Felder als dynamisch dimensionierte Arrays.
Einen vollständigen Leitfaden 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 – Protocol Buffers tun das nicht.
Kompilieren Ihrer Protocol Buffers
Nachdem Sie nun eine .proto-Datei haben, müssen Sie als Nächstes die Klassen generieren, die Sie zum Lesen und Schreiben von AddressBook (und damit Person und PhoneNumber) Nachrichten benötigen. Dazu müssen Sie den Protocol Buffer-Compiler protoc auf Ihrer .proto-Datei ausführen.
Wenn Sie den Compiler noch nicht installiert haben, laden Sie das Paket herunter und befolgen Sie die Anweisungen in der README-Datei.
Installieren Sie das Dart Protocol Buffer-Plugin wie in dessen README beschrieben. Die ausführbare Datei
bin/protoc-gen-dartmuss sich in IhremPATHbefinden, damit der Protocol Buffer-protoc-Compiler sie findet.Führen Sie nun den Compiler aus und geben Sie das Quellverzeichnis (wo sich der Quellcode Ihrer Anwendung befindet – das aktuelle Verzeichnis wird verwendet, wenn Sie keinen Wert angeben), das Zielverzeichnis (wo der generierte Code gespeichert werden soll; oft dasselbe wie
$SRC_DIR) und den Pfad zu Ihrer.proto-Datei an. In diesem Fall würden Sie aufrufenprotoc -I=$SRC_DIR --dart_out=$DST_DIR $SRC_DIR/addressbook.protoDa Sie Dart-Code wünschen, verwenden Sie die Option
--dart_out– ähnliche Optionen werden für andere unterstützte Sprachen bereitgestellt.
Dadurch wird addressbook.pb.dart in Ihrem angegebenen Zielverzeichnis generiert.
Die Protocol Buffer API
Durch die Generierung von addressbook.pb.dart erhalten Sie die folgenden nützlichen Typen:
- Eine
AddressBook-Klasse mit einem GetterList<Person> get people. - Eine
Person-Klasse mit Zugriffsmethoden fürname,id,emailundphones. - Eine
Person_PhoneNumber-Klasse mit Zugriffsmethoden fürnumberundtype. - Eine
Person_PhoneType-Klasse mit statischen Feldern für jeden Wert im EnumPerson.PhoneType.
Mehr über die Details dessen, was genau generiert wird, erfahren Sie im Leitfaden zum generierten Dart-Code.
Eine Nachricht schreiben
Lassen Sie uns nun versuchen, Ihre Protocol Buffer-Klassen zu verwenden. Das Erste, was Ihre Adressbuchanwendung tun können sollte, ist das Schreiben von persönlichen Details in Ihre Adressbuchdatei. Dazu müssen Sie Instanzen Ihrer Protocol Buffer-Klassen erstellen und befüllen und sie dann in einen Ausgabestrom schreiben.
Hier ist ein Programm, das ein AddressBook aus einer Datei liest, basierend auf Benutzereingaben eine neue Person hinzufügt und das neue AddressBook wieder in die Datei schreibt. Die Teile, die direkt Code aufrufen oder darauf verweisen, der vom Protocol Compiler generiert wurde, sind hervorgehoben.
import 'dart:io';
import 'dart_tutorial/addressbook.pb.dart';
// This function fills in a Person message based on user input.
Person promptForAddress() {
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.PHONE_TYPE_MOBILE;
break;
case 'home':
phoneNumber.type = Person_PhoneType.PHONE_TYPE_HOME;
break;
case 'work':
phoneNumber.type = Person_PhoneType.PHONE_TYPE_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(promptForAddress());
file.writeAsBytes(addressBook.writeToBuffer());
}
Eine Nachricht lesen
Natürlich wäre ein Adressbuch nicht sehr nützlich, wenn man keine Informationen daraus abrufen könnte! Dieses Beispiel liest die Datei, die vom obigen Beispiel erstellt wurde, und gibt 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.PHONE_TYPE_MOBILE:
print(' Mobile phone #: ');
break;
case Person_PhoneType.PHONE_TYPE_HOME:
print(' Home phone #: ');
break;
case Person_PhoneType.PHONE_TYPE_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 Protocol Buffers
Früher oder später, nachdem Sie den Code veröffentlicht haben, der Ihre Protocol Buffer verwendet, werden Sie zweifellos die Definition des Protocol Buffers „verbessern“ wollen. Wenn Sie möchten, dass Ihre neuen Buffer abwärtskompatibel sind und Ihre alten Buffer vorwärtskompatibel sind – und das wollen Sie fast sicherlich –, dann gibt es einige Regeln, die Sie befolgen müssen. In der neuen Version des Protocol Buffers
- Sie dürfen die Tag-Nummern vorhandener Felder nicht ändern.
- Sie dürfen Felder löschen.
- Sie dürfen neue Felder hinzufügen, müssen aber neue Tag-Nummern verwenden (d. h. Tag-Nummern, die in dieser Protocol Buffer noch nie verwendet wurden, auch nicht von gelöschten Feldern).
(Es gibt einige Ausnahmen zu diesen Regeln, aber sie werden selten verwendet.)
Wenn Sie diese Regeln befolgen, liest alter Code neue Nachrichten problemlos und ignoriert einfach alle neuen Felder. Für alten Code haben einzelne Felder, die gelöscht wurden, einfach ihren Standardwert, und gelöschte wiederholte Felder sind leer. Neuer Code liest ebenfalls alte Nachrichten transparent.
Beachten Sie jedoch, dass neue Felder in alten Nachrichten nicht vorhanden sind. Sie müssen also etwas Sinnvolles mit dem Standardwert tun. Ein typspezifischer Standardwert wird verwendet: für Zeichenfolgen ist der Standardwert die leere Zeichenfolge. Für boolesche Werte ist der Standardwert false. Für numerische Typen ist der Standardwert Null.