Grundlagen von Protocol Buffers: C++
Dieses Tutorial bietet eine grundlegende Einführung für C++-Programmierer in die Arbeit mit Protocol Buffers. Indem wir eine einfache Beispielanwendung durchgehen, zeigen wir Ihnen, wie Sie
- Nachrichtenformate in einer
.proto-Datei definieren. - Den Protocol Buffer-Compiler verwenden.
- Die C++-Protocol-Buffer-API zum Schreiben und Lesen von Nachrichten verwenden.
Dies ist kein umfassender Leitfaden zur Verwendung von Protocol Buffers in C++. Für detailliertere Referenzinformationen siehe den Protocol Buffer Language Guide, die C++ API-Referenz, den C++ Generated Code Guide und die Encoding Reference.
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
- Die rohen In-Memory-Datenstrukturen können in binärer Form gesendet/gespeichert werden. Mit der Zeit ist dies ein anfälliger Ansatz, da der empfangende/lesende Code mit exakt demselben Speicherlayout, Endianness usw. kompiliert werden muss. Auch wenn Dateien im Laufe der Zeit Daten im Rohformat ansammeln und Kopien von Software, die für dieses Format verdrahtet ist, verbreitet sind, ist es sehr schwierig, das Format zu erweitern.
- 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.
Anstelle dieser Optionen können Sie Protocol Buffers verwenden. Protocol Buffers sind die flexible, effiziente und automatisierte Lösung für genau dieses Problem. Mit Protocol Buffers schreiben Sie eine .proto-Beschreibung der Datenstruktur, die Sie speichern möchten. Daraus erstellt der Protocol Buffer-Compiler eine Klasse, die eine automatische Kodierung und ein Parsing der Protocol Buffer-Daten mit einem effizienten Binärformat implementiert. Die generierte Klasse bietet Getter und Setter für die Felder, aus denen ein Protocol Buffer besteht, und kümmert sich um die Details des Lesens und Schreibens des Protocol Buffers 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
Der Beispielcode ist im Quellcode-Paket im Verzeichnis „examples“ enthalten.
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. Hier ist die .proto-Datei, die Ihre Nachrichten definiert, addressbook.proto.
edition = "2023";
package tutorial;
message Person {
string name = 1;
int32 id = 2;
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;
}
message AddressBook {
repeated Person people = 1;
}
Wie Sie sehen, ist die Syntax ähnlich wie bei C++ oder Java. Lassen Sie uns jeden Teil der Datei durchgehen und sehen, was er tut.
Die .proto-Datei beginnt mit einer edition-Deklaration. Editionen ersetzen die älteren Deklarationen syntax = "proto2" und syntax = "proto3" und bieten eine flexiblere Möglichkeit, die Sprache im Laufe der Zeit weiterzuentwickeln.
Als Nächstes kommt eine Paketdeklaration, die hilft, Namenskonflikte zwischen verschiedenen Projekten zu vermeiden. In C++ werden Ihre generierten Klassen in einem Namespace platziert, der dem Paketnamen entspricht.
Nach der Paketdeklaration folgen Ihre Nachrichtendefinitionen. Eine Nachricht ist einfach eine Aggregation, die eine Reihe von typisierten Feldern enthält. Viele Standard-Datentypen 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 – im obigen Beispiel enthält die Person-Nachricht PhoneNumber-Nachrichten, während die AddressBook-Nachricht Person-Nachrichten enthält. Sie können sogar Nachrichtentypen verschachtelt in anderen Nachrichten definieren – wie Sie sehen, ist der PhoneNumber-Typ innerhalb von Person definiert. Sie können auch Aufzählungstypen definieren, wenn Sie möchten, dass eines Ihrer Felder einen von einer vordefinierten Liste von Werten hat – hier möchten Sie angeben, dass eine Telefonnummer einer von mehreren Typen sein kann.
Die Markierungen „ = 1“, „ = 2“ für jedes Element identifizieren die eindeutige Feldnummer, die dieses Feld in der Binärkodierung verwendet. Feldnummern 1-15 erfordern ein Byte weniger zur Kodierung als höhere Nummern. Daher können Sie als Optimierung diese Nummern für häufig verwendete oder wiederholte Elemente verwenden und die Feldnummern 16 und höher für seltener verwendete Elemente belassen.
Felder können eines der folgenden sein
singulär: Standardmäßig sind Felder optional, was bedeutet, dass das Feld gesetzt sein kann oder nicht. Wenn ein singuläres Feld nicht gesetzt ist, wird ein typspezifischer Standardwert verwendet: Null für numerische Typen, der leere String für Strings, false für boolesche Werte und der erste definierte Aufzählungswert für Aufzählungen (der 0 sein muss). Beachten Sie, dass Sie ein Feld nicht explizit auf
singularsetzen können. Dies ist eine Beschreibung eines nicht wiederholten Feldes.repeated: Das Feld kann beliebig oft wiederholt werden (einschließlich null). Die Reihenfolge der wiederholten Werte wird beibehalten. Betrachten Sie wiederholte Felder als dynamisch größenveränderliche Arrays.
In älteren Versionen von protobuf gab es das Schlüsselwort required, aber es hat sich als fehleranfällig erwiesen und wird in modernen protobufs nicht mehr unterstützt (obwohl Editionen eine Funktion haben, mit der Sie es für die Abwärtskompatibilität aktivieren können).
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 Funktionen, 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, folgen Sie den Anweisungen in Protocol Buffer Compiler Installation.
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 hingehen soll; oft dasselbe wie
$SRC_DIR) und den Pfad zu Ihrer.proto-Datei an. In diesem Fall:protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.protoDa Sie C++-Klassen wünschen, verwenden Sie die Option
--cpp_out– ähnliche Optionen werden für andere unterstützte Sprachen bereitgestellt.
Dies generiert die folgenden Dateien in Ihrem angegebenen Zielverzeichnis:
addressbook.pb.h, die Header-Datei, die Ihre generierten Klassen deklariert.addressbook.pb.cc, das die Implementierung Ihrer Klassen enthält.
Die Protocol Buffer API
Schauen wir uns einen Teil des generierten Codes an und sehen, welche Klassen und Funktionen der Compiler für Sie erstellt hat. Wenn Sie in addressbook.pb.h nachsehen, sehen Sie, dass für jede Nachricht, die Sie in addressbook.proto angegeben haben, eine Klasse existiert. Wenn Sie sich die Person-Klasse genauer ansehen, stellen Sie fest, dass der Compiler Accessoren für jedes Feld generiert hat. Zum Beispiel haben Sie für die Felder name, id, email und phones diese Methoden:
// name
bool has_name() const; // Only for explicit presence
void clear_name();
const ::std::string& name() const;
void set_name(const ::std::string& value);
::std::string* mutable_name();
// id
bool has_id() const;
void clear_id();
int32_t id() const;
void set_id(int32_t value);
// email
bool has_email() const;
void clear_email();
const ::std::string& email() const;
void set_email(const ::std::string& value);
::std::string* mutable_email();
// phones
int phones_size() const;
void clear_phones();
const ::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >& phones() const;
::google::protobuf::RepeatedPtrField< ::tutorial::Person_PhoneNumber >* mutable_phones();
const ::tutorial::Person_PhoneNumber& phones(int index) const;
::tutorial::Person_PhoneNumber* mutable_phones(int index);
::tutorial::Person_PhoneNumber* add_phones();
Wie Sie sehen, haben die Getter genau den Namen des Feldes in Kleinbuchstaben, und die Setter-Methoden beginnen mit set_. Es gibt auch has_-Methoden für singuläre Felder mit expliziter Anwesenheitsverfolgung, die true zurückgeben, wenn das Feld gesetzt wurde. Schließlich hat jedes Feld eine clear_-Methode, die das Feld wieder in seinen Standardzustand zurücksetzt.
Während das numerische id-Feld nur den grundlegenden Accessor-Satz wie oben beschrieben hat, haben die Felder name und email ein paar zusätzliche Methoden, da es sich um Strings handelt – einen mutable_-Getter, der Ihnen einen direkten Zeiger auf den String gibt, und einen zusätzlichen Setter. Beachten Sie, dass Sie mutable_email() aufrufen können, auch wenn email noch nicht gesetzt ist; es wird automatisch als leerer String initialisiert. Hätten Sie in diesem Beispiel ein wiederholtes Nachrichtenfeld, hätte es ebenfalls eine mutable_-Methode, aber keine set_-Methode.
Wiederholte Felder haben auch einige spezielle Methoden – wenn Sie sich die Methoden für das wiederholte Feld phones ansehen, werden Sie feststellen, dass Sie Folgendes tun können:
- die Größe des wiederholten Feldes (
_size) überprüfen (d. h. wie viele Telefonnummern mit dieserPersonverknüpft sind). - eine bestimmte Telefonnummer anhand ihres Index abrufen.
- eine vorhandene Telefonnummer am angegebenen Index aktualisieren.
- eine weitere Telefonnummer zur Nachricht hinzufügen, die Sie dann bearbeiten können (wiederholte Skalartypen haben eine
add_-Methode, die es Ihnen nur erlaubt, den neuen Wert zu übergeben).
Weitere Informationen darüber, welche Member der Protokollcompiler für eine bestimmte Felddefinition genau generiert, finden Sie in der C++ generierten Code-Referenz.
Aufzählungen und verschachtelte Klassen
Der generierte Code enthält eine PhoneType-Aufzählung, die Ihrer .proto-Aufzählung entspricht. Sie können diesen Typ als Person::PhoneType und seine Werte als Person::PHONE_TYPE_MOBILE, Person::PHONE_TYPE_HOME und Person::PHONE_TYPE_WORK bezeichnen (die Implementierungsdetails sind etwas komplizierter, aber Sie müssen sie nicht verstehen, um die Aufzählung zu verwenden).
Der Compiler hat auch eine verschachtelte Klasse namens Person::PhoneNumber für Sie generiert. Wenn Sie sich den Code ansehen, stellen Sie fest, dass die „echte“ Klasse tatsächlich Person_PhoneNumber heißt, aber ein Typalias innerhalb von Person ermöglicht es Ihnen, sie so zu behandeln, als wäre sie eine verschachtelte Klasse. Der einzige Fall, in dem dies einen Unterschied macht, ist, wenn Sie die Klasse in einer anderen Datei vorab deklarieren möchten – Sie können verschachtelte Typen in C++ nicht vorab deklarieren, aber Sie können Person_PhoneNumber vorab deklarieren.
Standard-Nachrichtenmethoden
Jede Nachrichtenklasse enthält außerdem eine Reihe weiterer Methoden, mit denen Sie die gesamte Nachricht überprüfen oder bearbeiten können, darunter:
bool IsInitialized() const;: prüft, ob alle erforderlichen Felder gesetzt wurden.string DebugString() const;: gibt eine menschenlesbare Darstellung der Nachricht zurück, besonders nützlich zum Debuggen.void CopyFrom(const Person& from);: überschreibt die Nachricht mit den Werten der gegebenen Nachricht.void Clear();: löscht alle Elemente zurück in den leeren Zustand.
Diese und die im folgenden Abschnitt beschriebenen I/O-Methoden implementieren die Message-Schnittstelle, die von allen C++-Protocol-Buffer-Klassen gemeinsam genutzt wird. Weitere Informationen finden Sie in der vollständigen API-Dokumentation für Message.
Parsen und Serialisieren
Schließlich verfügt jede Protocol Buffer-Klasse über Methoden zum Schreiben und Lesen von Nachrichten Ihres gewählten Typs unter Verwendung des Binärformats von Protocol Buffers. Dazu gehören:
bool SerializeToString(string* output) const;: serialisiert die Nachricht und speichert die Bytes in der angegebenen Zeichenkette. Beachten Sie, dass die Bytes binär und nicht Text sind; wir verwenden nur diestring-Klasse als praktischen Container.bool ParseFromString(const string& data);: parst eine Nachricht aus der angegebenen Zeichenkette.bool SerializeToOstream(ostream* output) const;: schreibt die Nachricht in das angegebene C++ostream.bool ParseFromIstream(istream* input);: parst eine Nachricht aus dem angegebenen C++istream.
Dies sind nur einige der Optionen für das Parsen und Serialisieren. Eine vollständige Liste finden Sie in der Message API-Referenz.
Wichtig
Protocol Buffers und Objektorientiertes Design Protocol Buffer-Klassen sind im Grunde reine Datenhalter (wie Strukturen in C), die keine zusätzliche Funktionalität bieten; sie eignen sich nicht gut als primäre Bestandteile eines Objektmodells. Wenn Sie generierten Klassen reichhaltigere Funktionalität hinzufügen möchten, ist der beste Weg, die generierte Protocol Buffer-Klasse in eine anwendungsspezifische Klasse zu wrappen. Das Wrappen von Protocol Buffers ist auch dann eine gute Idee, wenn Sie keinen Einfluss auf das Design der .proto-Datei haben (z. B. wenn Sie eine aus einem anderen Projekt wiederverwenden). In diesem Fall können Sie die Wrapper-Klasse verwenden, um eine Schnittstelle zu erstellen, die besser auf die einzigartige Umgebung Ihrer Anwendung zugeschnitten ist: Ausblenden einiger Daten und Methoden, Bereitstellen von Komfortfunktionen usw. **Sie können den generierten Klassen keine Funktionalität hinzufügen, indem Sie von ihnen erben**, da sie final sind. Dies verhindert, dass interne Mechanismen beschädigt werden, und ist ohnehin keine gute objektorientierte Praxis.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 referenzieren, der vom Protocol Compiler generiert wurde, sind hervorgehoben.
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// This function fills in a Person message based on user input.
void PromptForAddress(tutorial::Person& person) {
cout << "Enter person ID number: ";
int id;
cin >> id;
person.set_id(id);
cin.ignore(256, '\n');
cout << "Enter name: ";
getline(cin, *person.mutable_name());
cout << "Enter email address (blank for none): ";
string email;
getline(cin, email);
if (!email.empty()) {
person.set_email(email);
}
while (true) {
cout << "Enter a phone number (or leave blank to finish): ";
string number;
getline(cin, number);
if (number.empty()) {
break;
}
tutorial::Person::PhoneNumber* phone_number = person.add_phones();
phone_number->set_number(number);
cout << "Is this a mobile, home, or work phone? ";
string type;
getline(cin, type);
if (type == "mobile") {
phone_number->set_type(tutorial::Person::PHONE_TYPE_MOBILE);
} else if (type == "home") {
phone_number->set_type(tutorial::Person::PHONE_TYPE_HOME);
} else if (type == "work") {
phone_number->set_type(tutorial::Person::PHONE_TYPE_WORK);
} else {
cout << "Unknown phone type. Using default." << endl;
}
}
}
// Main function: Reads the entire address book from a file,
// adds one person based on user input, then writes it back out to the same
// file.
int main(int argc, char* argv[]) {
// Verify that the version of the library that we linked against is
// compatible with the version of the headers we compiled against.
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
return -1;
}
tutorial::AddressBook address_book;
{
// Read the existing address book.
fstream input(argv[1], ios::in | ios::binary);
if (!input) {
cout << argv[1] << ": File not found. Creating a new file." << endl;
} else if (!address_book.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
}
// Add an address.
PromptForAddress(*address_book.add_people());
{
// Write the new address book back to disk.
fstream output(argv[1], ios::out | ios::trunc | ios::binary);
if (!address_book.SerializeToOstream(&output)) {
cerr << "Failed to write address book." << endl;
return -1;
}
}
// Optional: Delete all global objects allocated by libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
Beachten Sie das Makro GOOGLE_PROTOBUF_VERIFY_VERSION. Es ist eine gute Praxis – obwohl nicht unbedingt erforderlich –, dieses Makro auszuführen, bevor Sie die C++ Protocol Buffer-Bibliothek verwenden. Es überprüft, ob Sie nicht versehentlich gegen eine Version der Bibliothek gelinkt haben, die mit der Version der Header, mit denen Sie kompiliert haben, inkompatibel ist. Wenn ein Versionskonflikt erkannt wird, bricht das Programm ab. Beachten Sie, dass jede .pb.cc-Datei dieses Makro automatisch beim Start aufruft.
Beachten Sie auch den Aufruf von ShutdownProtobufLibrary() am Ende des Programms. Alles, was dies tut, ist, alle globalen Objekte zu löschen, die von der Protocol Buffer-Bibliothek zugewiesen wurden. Dies ist für die meisten Programme unnötig, da der Prozess ohnehin beendet wird und das Betriebssystem alle seine Speicherressourcen freigibt. Wenn Sie jedoch einen Speicherleckprüfer verwenden, der verlangt, dass jedes letzte Objekt freigegeben wird, oder wenn Sie eine Bibliothek schreiben, die mehrmals von einem einzigen Prozess geladen und entladen werden kann, dann möchten Sie möglicherweise erzwingen, dass Protocol Buffers alles bereinigt.
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.
#include <iostream>
#include <fstream>
#include <string>
#include "addressbook.pb.h"
using namespace std;
// Iterates though all people in the AddressBook and prints info about them.
void ListPeople(const tutorial::AddressBook& address_book) {
for (const tutorial::Person& person : address_book.people()) {
cout << "Person ID: " << person.id() << endl;
cout << " Name: " << person.name() << endl;
if (!person.has_email()) {
cout << " E-mail address: " << person.email() << endl;
}
for (const tutorial::Person::PhoneNumber& phone_number : person.phones()) {
switch (phone_number.type()) {
case tutorial::Person::PHONE_TYPE_MOBILE:
cout << " Mobile phone #: ";
break;
case tutorial::Person::PHONE_TYPE_HOME:
cout << " Home phone #: ";
break;
case tutorial::Person::PHONE_TYPE_WORK:
cout << " Work phone #: ";
break;
case tutorial::Person::PHONE_TYPE_UNSPECIFIED:
default:
cout << " Phone #: ";
break;
}
cout << phone_number.number() << endl;
}
}
}
// Main function: Reads the entire address book from a file and prints all
// the information inside.
int main(int argc, char* argv[]) {
// Verify that the version of the library that we linked against is
// compatible with the version of the headers we compiled against.
GOOGLE_PROTOBUF_VERIFY_VERSION;
if (argc != 2) {
cerr << "Usage: " << argv[0] << " ADDRESS_BOOK_FILE" << endl;
return -1;
}
tutorial::AddressBook address_book;
{
// Read the existing address book.
fstream input(argv[1], ios::in | ios::binary);
if (!address_book.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
}
ListPeople(address_book);
// Optional: Delete all global objects allocated by libprotobuf.
google::protobuf::ShutdownProtobufLibrary();
return 0;
}
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 Feldnummern bestehender Felder *nicht* ändern.
- Sie *dürfen* singuläre oder wiederholte Felder löschen.
- Sie *dürfen* neue singuläre oder wiederholte Felder hinzufügen, aber Sie müssen frische Feldnummern verwenden (d. h. Feldnummern, die in diesem Protocol Buffer noch nie verwendet wurden, auch nicht von gelöschten Feldern).
(Es gibt einige Ausnahmen von diesen Regeln, aber sie werden selten verwendet.)
Wenn Sie diese Regeln befolgen, liest alter Code neue Nachrichten problemlos und ignoriert einfach neue Felder. Für den alten Code haben gelöschte Felder 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. Sie müssen also auf ihre Anwesenheit prüfen, indem Sie prüfen, ob sie den Standardwert (z. B. einen leeren String) haben, bevor Sie sie verwenden.
Optimierungstipps
Die C++ Protocol Buffers-Bibliothek ist extrem stark optimiert. Die richtige Verwendung kann die Leistung jedoch noch weiter verbessern. Hier sind einige Tipps, um die letzte Tropfen Geschwindigkeit aus der Bibliothek herauszuholen
Verwenden Sie Arenen für die Speicherzuweisung. Wenn Sie viele Protocol Buffer-Nachrichten in einer kurzlebigen Operation (wie dem Parsen einer einzelnen Anfrage) erstellen, kann der Speicherzuweiser des Systems zu einem Engpass werden. Arenen sollen dies mildern. Durch die Verwendung einer Arena können Sie viele Zuordnungen mit geringem Aufwand und eine einzige Freigabe für alle auf einmal durchführen. Dies kann die Leistung in nachrichtenintensiven Anwendungen erheblich verbessern.
Um Arenen zu verwenden, weisen Sie Nachrichten auf einem
google::protobuf::Arena-Objekt zugoogle::protobuf::Arena arena; tutorial::Person* person = google::protobuf::Arena::Create<tutorial::Person>(&arena); // ... populate person ...Wenn das Arena-Objekt zerstört wird, werden alle darauf zugewiesenen Nachrichten freigegeben. Weitere Details finden Sie im Arenen-Leitfaden.
Wiederverwenden Sie Nicht-Arena-Nachrichtenobjekte, wenn möglich. Nachrichten versuchen, jeden von ihnen zugewiesenen Speicher für die Wiederverwendung zu behalten, auch wenn sie geleert werden. Wenn Sie also viele Nachrichten desselben Typs und ähnlicher Struktur nacheinander verarbeiten, ist es eine gute Idee, jedes Mal dasselbe Nachrichtenobjekt wiederzuverwenden, um den Speicherzuweiser zu entlasten. Objekte können jedoch mit der Zeit aufgebläht werden, insbesondere wenn Ihre Nachrichten in ihrer „Form“ variieren oder wenn Sie gelegentlich eine Nachricht erstellen, die deutlich größer als üblich ist. Sie sollten die Größen Ihrer Nachrichtenobjekte überwachen, indem Sie die Methode
SpaceUsedaufrufen, und sie löschen, sobald sie zu groß werden.Das Wiederverwenden von Arena-Nachrichten kann zu unbegrenztem Speicherwachstum führen. Das Wiederverwenden von Heap-Nachrichten ist sicherer. Selbst bei Heap-Nachrichten können Sie jedoch immer noch Probleme mit dem Höchststand der Felder haben. Wenn Sie zum Beispiel Nachrichten sehen
a: [1, 2, 3, 4] b: [1]und
a: [1] b: [1, 2, 3, 4]und die Nachrichten wiederverwenden, dann werden beide Felder über genügend Speicher für den größten Wert verfügen, den sie jemals gesehen haben. Wenn also jede Eingabe nur 5 Elemente enthielt, wird die wiederverwendete Nachricht Speicher für 8 haben.
Der Speicherzuweiser Ihres Systems ist möglicherweise nicht gut optimiert für die Zuweisung vieler kleiner Objekte aus mehreren Threads. Versuchen Sie stattdessen, Googles TCMalloc zu verwenden.
Erweiterte Nutzung
Protocol Buffers haben Anwendungen, die über einfache Accessoren und Serialisierung hinausgehen. Schauen Sie sich unbedingt die C++ API-Referenz an, um zu sehen, was Sie sonst noch damit machen können.
Ein wichtiges Merkmal von Protocol Message-Klassen ist die Reflexion. Sie können die Felder einer Nachricht durchlaufen und ihre Werte manipulieren, ohne Ihren Code gegen einen bestimmten Nachrichtentyp zu schreiben. Eine sehr nützliche Anwendung der Reflexion ist die Konvertierung von Protokollnachrichten in und aus anderen Kodierungen, wie z. B. XML oder JSON. Eine fortgeschrittenere Anwendung der Reflexion könnte darin bestehen, Unterschiede zwischen zwei Nachrichten desselben Typs zu finden oder eine Art „Reguläre Ausdrücke für Protokollnachrichten“ zu entwickeln, mit denen Sie Ausdrücke schreiben können, die bestimmte Nachrichtenkontexte abgleichen. Wenn Sie Ihre Vorstellungskraft einsetzen, ist es möglich, Protocol Buffers auf eine viel breitere Palette von Problemen anzuwenden, als Sie vielleicht anfangs erwarten!
Reflexion wird über die Message::Reflection Schnittstelle bereitgestellt.