Sprachführer (proto 2)

Behandelt die Verwendung der Proto2-Revision der Protocol Buffers-Sprache in Ihrem Projekt.

Dieser Leitfaden beschreibt die Verwendung der Protocol Buffer-Sprache zur Strukturierung Ihrer Protocol Buffer-Daten, einschließlich der .proto-Dateisyntax und wie Datenzugriffsklassen aus Ihren .proto-Dateien generiert werden. Er behandelt die **proto2**-Revision der Protocol Buffers-Sprache.

Informationen zur Syntax für **Editionen** finden Sie im Sprachleitfaden für Protobuf-Editionen.

Informationen zur **proto3**-Syntax finden Sie im Proto3 Sprachleitfaden.

Dies ist ein Referenzhandbuch – ein Schritt-für-Schritt-Beispiel, das viele der in diesem Dokument beschriebenen Funktionen verwendet, finden Sie im Tutorial für Ihre gewählte Sprache.

Definieren eines Nachrichtentyps

Betrachten wir zunächst ein sehr einfaches Beispiel. Angenommen, Sie möchten ein Nachrichtenformat für Suchanfragen definieren, bei dem jede Suchanfrage eine Abfragestrzeichenkette, die gewünschte Ergebnisseite und die Anzahl der Ergebnisse pro Seite enthält. Hier ist die .proto-Datei, die Sie zur Definition des Nachrichtentyps verwenden.

syntax = "proto2";

message SearchRequest {
  optional string query = 1;
  optional int32 page_number = 2;
  optional int32 results_per_page = 3;
}
  • Die erste Zeile der Datei gibt an, dass Sie die Proto2-Revision der protobuf-Sprachspezifikation verwenden.

    • Die syntax muss die erste nicht-leere, nicht-Kommentarzeile der Datei sein.
    • Wenn keine syntax angegeben ist, geht der Protocol Buffer-Compiler davon aus, dass Sie proto2 verwenden.
  • Die SearchRequest-Nachrichtendefinition gibt drei Felder (Namens-/Wertpaare) an, eines für jedes Datenelement, das Sie in diesem Nachrichtentyp aufnehmen möchten. Jedes Feld hat einen Namen und einen Typ.

Festlegen von Feldtypen

Im vorherigen Beispiel sind alle Felder skalare Typen: zwei ganze Zahlen (page_number und results_per_page) und eine Zeichenkette (query). Sie können auch Aufzählungen und zusammengesetzte Typen wie andere Nachrichtentypen für Ihr Feld angeben.

Zuweisen von Feldnummern

Sie müssen jedem Feld in Ihrer Nachrichtendefinition eine Nummer zwischen 1 und 536.870.911 zuweisen, mit den folgenden Einschränkungen:

  • Die angegebene Nummer muss eindeutig für alle Felder dieser Nachricht sein.
  • Feldnummern 19.000 bis 19.999 sind für die Protocol Buffers-Implementierung reserviert. Der Protocol Buffer-Compiler wird sich beschweren, wenn Sie eine dieser reservierten Feldnummern in Ihrer Nachricht verwenden.
  • Sie können keine zuvor reservierten Feldnummern oder Feldnummern verwenden, die Erweiterungen zugewiesen wurden.

Diese Nummer kann nicht geändert werden, sobald Ihr Nachrichtentyp in Gebrauch ist, da sie das Feld im Nachrichten-Wire-Format identifiziert. Eine „Änderung“ einer Feldnummer entspricht dem Löschen dieses Feldes und der Erstellung eines neuen Feldes mit demselben Typ, aber einer neuen Nummer. Weitere Informationen hierzu finden Sie unter Felder löschen.

Feldnummern dürfen niemals wiederverwendet werden. Nehmen Sie niemals eine Feldnummer aus der reservierten Liste zur Wiederverwendung mit einer neuen Felddefinition auf. Siehe Folgen der Wiederverwendung von Feldnummern.

Sie sollten die Feldnummern 1 bis 15 für die am häufigsten gesetzten Felder verwenden. Kleinere Feldnummern benötigen weniger Speicherplatz im Wire-Format. Feldnummern im Bereich 1 bis 15 benötigen beispielsweise ein Byte zur Kodierung. Feldnummern im Bereich 16 bis 2047 benötigen zwei Bytes. Mehr dazu erfahren Sie in Protocol Buffer-Kodierung.

Folgen der Wiederverwendung von Feldnummern

Die Wiederverwendung einer Feldnummer macht die Dekodierung von Wire-Format-Nachrichten mehrdeutig.

Das Protobuf-Wire-Format ist schlank und bietet keine Möglichkeit, Felder zu erkennen, die mit einer Definition kodiert und mit einer anderen dekodiert wurden.

Das Kodieren eines Feldes mit einer Definition und anschließende Dekodieren desselben Feldes mit einer anderen Definition kann zu folgenden Problemen führen:

  • Verlorene Entwicklerzeit durch Debugging
  • Ein Parse-/Merge-Fehler (im besten Fall)
  • Ausgelaufene PII/SPII (personenbezogene/sensible personenbezogene Daten)
  • Datenbeschädigung

Häufige Ursachen für die Wiederverwendung von Feldnummern

  • Neubewertung von Feldern (manchmal zur Erreichung einer ästhetisch ansprechenderen Reihenfolge für Felder). Neubewertung löscht und fügt effektiv alle beteiligten Felder neu hinzu, was zu inkompatiblen Wire-Format-Änderungen führt.

  • Löschen eines Feldes und Nicht-Reservieren der Nummer, um zukünftige Wiederverwendung zu verhindern.

Die Feldnummer ist auf 29 Bits statt 32 Bits begrenzt, da drei Bits zur Angabe des Wire-Formats des Feldes verwendet werden. Mehr dazu finden Sie im Thema Kodierung.

Festlegen der Feldkardinalität

Nachrichtenfelder können einer der folgenden Typen sein:

  • Singular (einzeln):

    In proto2 gibt es zwei Arten von singulären Feldern:

    • optional: (empfohlen) Ein optional-Feld befindet sich in einem von zwei möglichen Zuständen.

      • Das Feld ist gesetzt und enthält einen Wert, der explizit gesetzt oder aus dem Wire-Format gelesen wurde. Es wird an das Wire-Format serialisiert.
      • Das Feld ist nicht gesetzt und gibt den Standardwert zurück. Es wird nicht an das Wire-Format serialisiert.

      Sie können prüfen, ob der Wert explizit gesetzt wurde.

    • required: **Nicht verwenden.** Required-Felder sind so problematisch, dass sie aus proto3 und Editionen entfernt wurden. Semantik für erforderliche Felder sollte auf Anwendungsebene implementiert werden. Wenn es *verwendet* wird, muss eine wohlgeformte Nachricht genau ein solches Feld haben.

  • repeated (wiederholt): Dieser Feldtyp kann in einer wohlgeformten Nachricht null- oder mehrmals vorkommen. Die Reihenfolge der wiederholten Werte wird beibehalten.

  • map (Zuordnung): Dies ist ein Feldtyp mit Schlüssel/Wert-Paaren. Weitere Informationen zu diesem Feldtyp finden Sie unter Maps.

Verwenden Sie Packed Encoding für neue wiederholte Felder

Aus historischen Gründen werden repeated-Felder von skalaren numerischen Typen (z. B. int32, int64, enum) nicht so effizient kodiert, wie sie könnten. Neuer Code sollte die spezielle Option [packed = true] verwenden, um eine effizientere Kodierung zu erhalten. Zum Beispiel:

repeated int32 samples = 4 [packed = true];
repeated ProtoEnum results = 5 [packed = true];

Weitere Informationen zur packed-Kodierung finden Sie unter Protocol Buffer-Kodierung.

Required ist stark veraltet

Ein zweites Problem mit erforderlichen Feldern tritt auf, wenn jemand einem Enum einen Wert hinzufügt. In diesem Fall wird der nicht erkannte Enum-Wert so behandelt, als ob er fehlen würde, was ebenfalls dazu führt, dass die erforderliche Wertprüfung fehlschlägt.

Wohlgeformte Nachrichten

Der Begriff „wohlgeformt“, wenn er auf Protobuf-Nachrichten angewendet wird, bezieht sich auf die serialisierten/deserialisierten Bytes. Der protoc-Parser validiert, ob eine gegebene proto-Definitionsdatei lesbar ist.

Singuläre Felder können mehrfach in den Wire-Format-Bytes vorkommen. Der Parser akzeptiert die Eingabe, aber nur die letzte Instanz dieses Feldes ist über die generierten Bindings zugänglich. Weitere Informationen hierzu finden Sie unter Last One Wins.

Hinzufügen weiterer Nachrichtentypen

Mehrere Nachrichtentypen können in einer einzigen .proto-Datei definiert werden. Dies ist nützlich, wenn Sie mehrere zusammengehörige Nachrichten definieren – zum Beispiel, wenn Sie das Antwortnachrichtenformat definieren möchten, das Ihrer SearchResponse-Nachricht entspricht, können Sie es derselben .proto hinzufügen.

message SearchRequest {
  optional string query = 1;
  optional int32 page_number = 2;
  optional int32 results_per_page = 3;
}

message SearchResponse {
 ...
}

Kombinieren von Nachrichten führt zu bloat Während mehrere Nachrichtentypen (wie Nachrichten, Enums und Dienste) in einer einzigen .proto-Datei definiert werden können, kann dies auch zu Abhängigkeits-Bloat führen, wenn eine große Anzahl von Nachrichten mit unterschiedlichen Abhängigkeiten in einer einzigen Datei definiert wird. Es wird empfohlen, so wenige Nachrichtentypen wie möglich pro .proto-Datei einzuschließen.

Hinzufügen von Kommentaren

So fügen Sie Ihren .proto-Dateien Kommentare hinzu:

  • Bevorzugen Sie Zeilenendkommentare im C/C++/Java-Stil „//“ auf der Zeile vor dem .proto-Codeelement

  • Inline-/Mehrzeilenkommentare im C-Stil /* ... */ werden ebenfalls akzeptiert.

    • Bei mehrzeiligen Kommentaren wird eine Randzeile mit „*“ bevorzugt.
/**
 * SearchRequest represents a search query, with pagination options to
 * indicate which results to include in the response.
 */
message SearchRequest {
  optional string query = 1;

  // Which page number do we want?
  optional int32 page_number = 2;

  // Number of results to return per page.
  optional int32 results_per_page = 3;
}

Löschen von Feldern

Das Löschen von Feldern kann zu schwerwiegenden Problemen führen, wenn es nicht korrekt durchgeführt wird.

Löschen Sie keine required Felder. Dies ist fast unmöglich sicher zu tun. Wenn Sie ein required Feld löschen müssen, sollten Sie das Feld zuerst als optional und deprecated kennzeichnen und sicherstellen, dass alle Systeme, die die Nachricht auf irgendeine Weise beobachten, mit dem neuen Schema bereitgestellt wurden. Dann können Sie das Löschen des Feldes in Betracht ziehen (beachten Sie jedoch, dass dies immer noch ein fehleranfälliger Prozess ist).

Wenn Sie ein Feld, das nicht required ist, nicht mehr benötigen, löschen Sie zuerst alle Verweise auf das Feld aus dem Client-Code und löschen Sie dann die Felddefinition aus der Nachricht. Sie **müssen** jedoch die gelöschte Feldnummer reservieren. Wenn Sie die Feldnummer nicht reservieren, ist es möglich, dass ein Entwickler diese Nummer in Zukunft wiederverwendet und dadurch einen Fehler verursacht.

Sie sollten auch den Feldnamen reservieren, um das Parsen von Nachrichten mit JSON- und TextFormat-Kodierungen weiterhin zu ermöglichen.

Reservierte Feldnummern

Wenn Sie einen Nachrichtentyp aktualisieren, indem Sie ein Feld vollständig löschen oder auskommentieren, können zukünftige Entwickler die Feldnummer wiederverwenden, wenn sie eigene Aktualisierungen an dem Typ vornehmen. Dies kann zu schwerwiegenden Problemen führen, wie in Folgen der Wiederverwendung von Feldnummern beschrieben. Um sicherzustellen, dass dies nicht geschieht, fügen Sie Ihre gelöschte Feldnummer zur reserved-Liste hinzu.

Der protoc-Compiler generiert Fehlermeldungen, wenn zukünftige Entwickler versuchen, diese reservierten Feldnummern zu verwenden.

message Foo {
  reserved 2, 15, 9 to 11;
}

Reservierte Feldnummernbereiche sind inklusiv (9 bis 11 ist dasselbe wie 9, 10, 11).

Reservierte Feldnamen

Die spätere Wiederverwendung eines alten Feldnamens ist im Allgemeinen sicher, außer bei der Verwendung von TextProto- oder JSON-Kodierungen, bei denen der Feldname serialisiert wird. Um dieses Risiko zu vermeiden, können Sie den gelöschten Feldnamen zur reserved-Liste hinzufügen.

Reservierte Namen beeinflussen nur das Verhalten des protoc-Compilers und nicht das Laufzeitverhalten, mit einer Ausnahme: TextProto-Implementierungen können unbekannte Felder (ohne eine Fehlermeldung wie bei anderen unbekannten Feldern) mit reservierten Namen beim Parsen verwerfen (nur die C++- und Go-Implementierungen tun dies derzeit). Das Laufzeit-JSON-Parsing wird von reservierten Namen nicht beeinflusst.

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

Beachten Sie, dass Sie Feldnamen und Feldnummern nicht in derselben reserved-Anweisung mischen können.

Was wird aus Ihrer .proto-Datei generiert?

Wenn Sie den Protocol Buffer-Compiler für eine .proto-Datei ausführen, generiert der Compiler den Code in Ihrer gewählten Sprache, den Sie benötigen, um mit den in der Datei beschriebenen Nachrichtentypen zu arbeiten, einschließlich des Abrufens und Setzens von Feldwerten, des Serialisierens Ihrer Nachrichten in einen Ausgabestrom und des Parsens Ihrer Nachrichten aus einem Eingabestrom.

  • Für C++ generiert der Compiler aus jeder .proto-Datei eine .h- und eine .cc-Datei mit einer Klasse für jeden in Ihrer Datei beschriebenen Nachrichtentyp.
  • Für Java generiert der Compiler eine .java-Datei mit einer Klasse für jeden Nachrichtentyp sowie eine spezielle Builder-Klasse zum Erstellen von Nachrichtenklasseninstanzen.
  • Für Kotlin generiert der Compiler zusätzlich zum generierten Java-Code eine .kt-Datei für jeden Nachrichtentyp mit einer verbesserten Kotlin-API. Dies beinhaltet eine DSL, die die Erstellung von Nachrichteninstanzen vereinfacht, einen nullable Feld-Accessor und eine Kopierfunktion.
  • Python ist etwas anders – der Python-Compiler generiert ein Modul mit einer statischen Beschreibung jedes Nachrichtentyps in Ihrer .proto-Datei, die dann mit einer Metaklasse verwendet wird, um die notwendige Python-Datenzugriffsklasse zur Laufzeit zu erstellen.
  • Für Go generiert der Compiler eine .pb.go-Datei mit einem Typ für jeden Nachrichtentyp in Ihrer Datei.
  • Für Ruby generiert der Compiler eine .rb-Datei mit einem Ruby-Modul, das Ihre Nachrichtentypen enthält.
  • Für Objective-C generiert der Compiler aus jeder .proto-Datei eine pbobjc.h- und eine pbobjc.m-Datei mit einer Klasse für jeden in Ihrer Datei beschriebenen Nachrichtentyp.
  • Für C# generiert der Compiler aus jeder .proto-Datei eine .cs-Datei mit einer Klasse für jeden in Ihrer Datei beschriebenen Nachrichtentyp.
  • Für PHP generiert der Compiler eine .php-Nachrichtendatei für jeden in Ihrer Datei beschriebenen Nachrichtentyp und eine .php-Metadatendatei für jede kompilierte .proto-Datei. Die Metadatendatei wird verwendet, um die gültigen Nachrichtentypen in den Deskriptorpool zu laden.
  • Für Dart generiert der Compiler eine .pb.dart-Datei mit einer Klasse für jeden Nachrichtentyp in Ihrer Datei.

Weitere Informationen zur Verwendung der APIs für jede Sprache finden Sie im Tutorial für Ihre gewählte Sprache. Für noch detailliertere API-Informationen siehe die entsprechende API-Referenz.

Skalare Werttypen

Ein skalarer Nachrichtenfeld kann einen der folgenden Typen haben – die Tabelle zeigt den im .proto-Datei angegebenen Typ und den entsprechenden Typ in der automatisch generierten Klasse

Proto-TypHinweise
double
float
int32Verwendet variable Längen-Kodierung. Ineffizient für die Kodierung negativer Zahlen – wenn Ihr Feld wahrscheinlich negative Werte hat, verwenden Sie stattdessen sint32.
int64Verwendet variable Längen-Kodierung. Ineffizient für die Kodierung negativer Zahlen – wenn Ihr Feld wahrscheinlich negative Werte hat, verwenden Sie stattdessen sint64.
uint32Verwendet variable Längen-Kodierung.
uint64Verwendet variable Längen-Kodierung.
sint32Verwendet variable Längen-Kodierung. Signed-Int-Wert. Diese kodieren negative Zahlen effizienter als normale int32s.
sint64Verwendet variable Längen-Kodierung. Signed-Int-Wert. Diese kodieren negative Zahlen effizienter als normale int64s.
fixed32Immer vier Bytes. Effizienter als uint32, wenn Werte oft größer als 228 sind.
fixed64Immer acht Bytes. Effizienter als uint64, wenn Werte oft größer als 256 sind.
sfixed32Immer vier Bytes.
sfixed64Immer acht Bytes.
bool
stringEine Zeichenkette muss immer UTF-8-kodierten oder 7-Bit-ASCII-Text enthalten und darf nicht länger als 232 sein.
bytesKann jede beliebige Byte-Sequenz enthalten, die nicht länger als 232 ist.
Proto-TypC++-TypJava/Kotlin-Typ[1]Python-Typ[3]Go-TypRuby-TypC#-TypPHP-TypDart-TypRust-Typ
doubledoubledoublefloat*float64Floatdoublefloatdoublef64
floatfloatfloatfloat*float32Floatfloatfloatdoublef32
int32int32_tintintint32Fixnum oder Bignum (je nach Bedarf)intinteger*int32i32
int64int64_tlongint/long[4]*int64Bignumlonginteger/string[6]Int64i64
uint32uint32_tint[2]int/long[4]*uint32Fixnum oder Bignum (je nach Bedarf)uintintegerintu32
uint64uint64_tlong[2]int/long[4]*uint64Bignumulonginteger/string[6]Int64u64
sint32int32_tintintint32Fixnum oder Bignum (je nach Bedarf)intinteger*int32i32
sint64int64_tlongint/long[4]*int64Bignumlonginteger/string[6]Int64i64
fixed32uint32_tint[2]int/long[4]*uint32Fixnum oder Bignum (je nach Bedarf)uintintegerintu32
fixed64uint64_tlong[2]int/long[4]*uint64Bignumulonginteger/string[6]Int64u64
sfixed32int32_tintint*int32Fixnum oder Bignum (je nach Bedarf)intintegerinti32
sfixed64int64_tlongint/long[4]*int64Bignumlonginteger/string[6]Int64i64
boolboolbooleanbool*boolTrueClass/FalseClassboolbooleanboolbool
stringstringStringUnicode (Python 2), str (Python 3)*stringString (UTF-8)stringstringStringProtoString
bytesstringByteStringbytes[]byteString (ASCII-8BIT)ByteStringstringListProtoBytes

[1] Kotlin verwendet die entsprechenden Typen von Java, auch für vorzeichenlose Typen, um die Kompatibilität in gemischten Java/Kotlin-Codebasen zu gewährleisten.

[2] In Java werden vorzeichenlose 32-Bit- und 64-Bit-Integer mit ihren vorzeichenbehafteten Gegenstücken dargestellt, wobei das höchste Bit einfach im Vorzeichenbit gespeichert wird.

[3] In allen Fällen führt das Setzen von Werten auf ein Feld eine Typenprüfung durch, um sicherzustellen, dass es gültig ist.

[4] 64-Bit- oder vorzeichenlose 32-Bit-Integer werden beim Dekodieren immer als long dargestellt, können aber int sein, wenn beim Setzen des Feldes ein int angegeben wird. In allen Fällen muss der Wert beim Setzen in den dargestellten Typ passen. Siehe [2].

[5] Proto2 überprüft die UTF-8-Gültigkeit von Zeichenfolgenfeldern normalerweise nie. Das Verhalten variiert jedoch je nach Sprache, und ungültige UTF-8-Daten sollten nicht in Zeichenfolgenfeldern gespeichert werden.

[6] Integer wird auf 64-Bit-Maschinen verwendet und String auf 32-Bit-Maschinen.

Wie diese Typen beim Serialisieren Ihrer Nachricht kodiert werden, erfahren Sie unter Protocol Buffer-Kodierung.

Standard-Feldwerte

Wenn eine Nachricht geparst wird und die kodierten Nachrichtendaten kein bestimmtes Feld enthalten, gibt der Zugriff auf dieses Feld im geparsten Objekt den Standardwert für dieses Feld zurück. Die Standardwerte sind typspezifisch:

  • Für Zeichenketten ist der Standardwert die leere Zeichenkette.
  • Für Bytes ist der Standardwert ein leeres Byte-Array.
  • Für Boolesche Werte ist der Standardwert false.
  • Für numerische Typen ist der Standardwert Null.
  • Bei Nachrichtenfeldern ist das Feld nicht gesetzt. Sein genauer Wert ist sprachabhängig. Details finden Sie im Leitfaden zum generierten Code für Ihre Sprache.
  • Für Enums ist der Standardwert der **erste definierte Enum-Wert**, der 0 sein sollte (empfohlen für Kompatibilität mit offenen Enums). Siehe Enum-Standardwert.

Der Standardwert für wiederholte Felder ist leer (im Allgemeinen eine leere Liste in der entsprechenden Sprache).

Der Standardwert für Map-Felder ist leer (im Allgemeinen eine leere Map in der entsprechenden Sprache).

Standard-Skalarwerte überschreiben

In proto2 können Sie explizite Standardwerte für singuläre Nicht-Nachrichtenfelder angeben. Nehmen wir zum Beispiel an, Sie möchten einen Standardwert von 10 für das Feld SearchRequest.results_per_page bereitstellen.

optional int32 results_per_page = 3 [default = 10];

Wenn der Absender results_per_page nicht angibt, beobachtet der Empfänger den folgenden Zustand:

  • Das Feld results_per_page ist nicht vorhanden. Das heißt, die Methode has_results_per_page() (Hazzer-Methode) würde false zurückgeben.
  • Der Wert von results_per_page (zurückgegeben vom „Getter“) ist 10.

Wenn der Absender einen Wert für results_per_page sendet, wird der Standardwert von 10 ignoriert und der Wert des Absenders wird vom „Getter“ zurückgegeben.

Weitere Details zur Funktionsweise von Standardwerten im generierten Code finden Sie im generierten Code-Leitfaden für Ihre gewählte Sprache.

Da der Standardwert für Enums der erste definierte Enum-Wert ist, achten Sie darauf, wenn Sie einen Wert am Anfang einer Enum-Wertliste hinzufügen. Siehe Abschnitt Nachrichtentyp aktualisieren für Richtlinien, wie Definitionen sicher geändert werden.

Aufzählungen (Enums)

Wenn Sie einen Nachrichtentyp definieren, möchten Sie vielleicht, dass eines seiner Felder nur einen von einer vordefinierten Liste von Werten haben kann. Angenommen, Sie möchten ein Feld corpus für jede SearchRequest hinzufügen, wobei der Korpus UNIVERSAL, WEB, IMAGES, LOCAL, NEWS, PRODUCTS oder VIDEO sein kann. Dies können Sie sehr einfach tun, indem Sie Ihrer Nachrichtendefinition ein enum mit einer Konstanten für jeden möglichen Wert hinzufügen.

Im folgenden Beispiel haben wir ein enum namens Corpus mit allen möglichen Werten und ein Feld vom Typ Corpus hinzugefügt.

enum Corpus {
  CORPUS_UNSPECIFIED = 0;
  CORPUS_UNIVERSAL = 1;
  CORPUS_WEB = 2;
  CORPUS_IMAGES = 3;
  CORPUS_LOCAL = 4;
  CORPUS_NEWS = 5;
  CORPUS_PRODUCTS = 6;
  CORPUS_VIDEO = 7;
}

message SearchRequest {
  optional string query = 1;
  optional int32 page_number = 2;
  optional int32 results_per_page = 3;
  optional Corpus corpus = 4;
}

Standardwert für Enum

Der Standardwert für das Feld SearchRequest.corpus ist CORPUS_UNSPECIFIED, da dies der erste in der Enum definierte Wert ist.

Es wird dringend empfohlen, den ersten Wert jedes Enums als ENUM_TYPE_NAME_UNSPECIFIED = 0; oder ENUM_TYPE_NAME_UNKNOWN = 0; zu definieren. Dies liegt an der Art und Weise, wie proto2 unbekannte Werte für Enum-Felder behandelt.

Es wird außerdem empfohlen, dass dieser erste Standardwert keine semantische Bedeutung hat, außer „dieser Wert wurde nicht angegeben“.

Der Standardwert für ein Enum-Feld wie das Feld SearchRequest.corpus kann wie folgt explizit überschrieben werden:

  optional Corpus corpus = 4 [default = CORPUS_UNIVERSAL];

Enum-Wert-Aliase

Sie können Aliase definieren, indem Sie denselben Wert verschiedenen Enum-Konstanten zuweisen. Dazu müssen Sie die Option allow_alias auf true setzen. Andernfalls generiert der Protocol Buffer-Compiler eine Warnmeldung, wenn Aliase gefunden werden. Obwohl alle Alias-Werte für die Serialisierung gültig sind, wird nur der erste Wert beim Deserialisieren verwendet.

enum EnumAllowingAlias {
  option allow_alias = true;
  EAA_UNSPECIFIED = 0;
  EAA_STARTED = 1;
  EAA_RUNNING = 1;
  EAA_FINISHED = 2;
}

enum EnumNotAllowingAlias {
  ENAA_UNSPECIFIED = 0;
  ENAA_STARTED = 1;
  // ENAA_RUNNING = 1;  // Uncommenting this line will cause a warning message.
  ENAA_FINISHED = 2;
}

Enumeratoren-Konstanten müssen im Bereich einer 32-Bit-Ganzzahl liegen. Da enum-Werte Varint-Kodierung im Wire-Format verwenden, sind negative Werte ineffizient und daher nicht empfohlen. Sie können enums innerhalb einer Nachrichtendefinition definieren, wie im vorherigen Beispiel, oder außerhalb – diese enums können in jeder Nachrichtendefinition in Ihrer .proto-Datei wiederverwendet werden. Sie können auch einen in einer Nachricht deklarierten enum-Typ als Typ eines Feldes in einer anderen Nachricht verwenden, indem Sie die Syntax _MessageType_._EnumType_ verwenden.

Wenn Sie den Protocol Buffer-Compiler für eine .proto-Datei ausführen, die ein enum verwendet, generiert der Code für Java, Kotlin oder C++ ein entsprechendes enum oder eine spezielle EnumDescriptor-Klasse für Python, die zur Erstellung einer Reihe von symbolischen Konstanten mit ganzzahligen Werten in der zur Laufzeit generierten Klasse verwendet wird.

Das Entfernen von Enum-Werten ist eine Breaking Change für persistente Protos. Anstatt einen Wert zu entfernen, kennzeichnen Sie den Wert mit dem Schlüsselwort reserved, um die Codegenerierung des Enum-Werts zu verhindern, oder behalten Sie den Wert bei, geben aber an, dass er später entfernt wird, indem Sie die Feldoption deprecated verwenden.

enum PhoneType {
  PHONE_TYPE_UNSPECIFIED = 0;
  PHONE_TYPE_MOBILE = 1;
  PHONE_TYPE_HOME = 2;
  PHONE_TYPE_WORK = 3 [deprecated = true];
  reserved 4,5;
}

Weitere Informationen zur Arbeit mit Nachrichten-enums in Ihren Anwendungen finden Sie im generierten Code-Leitfaden für Ihre gewählte Sprache.

Reservierte Werte

Wenn Sie einen Enum-Typ aktualisieren, indem Sie einen Enum-Eintrag vollständig entfernen oder auskommentieren, können zukünftige Benutzer den numerischen Wert bei eigenen Aktualisierungen des Typs wiederverwenden. Dies kann zu schwerwiegenden Problemen führen, wenn sie später alte Instanzen derselben .proto laden, einschließlich Datenbeschädigung, Datenschutzfehlern usw. Eine Möglichkeit, dies zu verhindern, besteht darin, anzugeben, dass die numerischen Werte (und/oder Namen, die ebenfalls Probleme bei der JSON-Serialisierung verursachen können) Ihrer gelöschten Einträge reserved sind. Der Protocol Buffer-Compiler wird sich beschweren, wenn zukünftige Benutzer diese Bezeichner verwenden. Sie können angeben, dass Ihr reservierter numerischer Wertebereich mit dem Schlüsselwort max bis zum maximal möglichen Wert reicht.

enum Foo {
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

Beachten Sie, dass Sie Feldnamen und numerische Werte nicht in derselben reserved-Anweisung mischen können.

Verwenden anderer Nachrichtentypen

Sie können andere Nachrichtentypen als Feldtypen verwenden. Angenommen, Sie möchten beispielsweise Result-Nachrichten in jeder SearchResponse-Nachricht enthalten – dazu können Sie einen Result-Nachrichtentyp in derselben .proto-Datei definieren und dann ein Feld vom Typ Result in SearchResponse angeben.

message SearchResponse {
  repeated Result results = 1;
}

message Result {
  optional string url = 1;
  optional string title = 2;
  repeated string snippets = 3;
}

Importieren von Definitionen

Im vorherigen Beispiel ist der Result-Nachrichtentyp in derselben Datei wie SearchResponse definiert – was ist, wenn der Nachrichtentyp, den Sie als Feldtyp verwenden möchten, bereits in einer anderen .proto-Datei definiert ist?

Sie können Definitionen aus anderen .proto-Dateien verwenden, indem Sie sie importieren. Um Definitionen aus anderen .proto-Dateien zu importieren, fügen Sie eine Importanweisung am Anfang Ihrer Datei hinzu.

import "myproject/other_protos.proto";

Standardmäßig können Sie nur Definitionen aus direkt importierten .proto-Dateien verwenden. Manchmal müssen Sie jedoch eine .proto-Datei an einen neuen Ort verschieben. Anstatt die .proto-Datei direkt zu verschieben und alle Aufrufe in einer einzigen Änderung zu aktualisieren, können Sie eine Platzhalter-.proto-Datei am alten Speicherort platzieren, um alle Importe mithilfe des Konzepts import public an den neuen Speicherort weiterzuleiten.

Beachten Sie, dass die Funktionalität für öffentliche Importe in Java, Kotlin, TypeScript, JavaScript, GCL sowie C++-Zielen, die statische Reflexion von Protobuf verwenden, nicht verfügbar ist.

import public-Abhängigkeiten können von jedem Code, der das Proto mit der import public-Anweisung importiert, transitiv genutzt werden. Zum Beispiel:

// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto

Der Protokollcompiler sucht nach importierten Dateien in einem Satz von Verzeichnissen, die auf der Protokollcompiler-Befehlszeile mit dem Flag -I/--proto_path angegeben sind. Wenn kein Flag angegeben wurde, sucht er im Verzeichnis, in dem der Compiler aufgerufen wurde. Im Allgemeinen sollten Sie das Flag --proto_path auf den Stamm Ihres Projekts setzen und vollständig qualifizierte Namen für alle Importe verwenden.

Proto3-Nachrichtentypen verwenden

Es ist möglich, proto3 und Edition 2023 Nachrichtentypen zu importieren und sie in Ihren proto2-Nachrichten zu verwenden und umgekehrt. Proto2-Enums können jedoch nicht direkt in proto3-Syntax verwendet werden (es ist in Ordnung, wenn eine importierte proto2-Nachricht sie verwendet).

Verschachtelte Typen

Sie können Nachrichtentypen innerhalb anderer Nachrichtentypen definieren und verwenden, wie im folgenden Beispiel – hier wird die Nachricht Result innerhalb der Nachricht SearchResponse definiert

message SearchResponse {
  message Result {
    optional string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
  repeated Result results = 1;
}

Wenn Sie diesen Nachrichtentyp außerhalb seines übergeordneten Nachrichtentyps wiederverwenden möchten, verweisen Sie darauf als _Elternteil_._Typ_

message SomeOtherMessage {
  optional SearchResponse.Result result = 1;
}

Sie können Nachrichten beliebig tief verschachteln. Beachten Sie im folgenden Beispiel, dass die beiden verschachtelten Typen namens Inner völlig unabhängig voneinander sind, da sie innerhalb verschiedener Nachrichten definiert sind

message Outer {       // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      optional int64 ival = 1;
      optional bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      optional int32  ival = 1;
      optional bool   booly = 2;
    }
  }
}

Gruppen

Beachten Sie, dass die Gruppenfunktion veraltet ist und nicht beim Erstellen neuer Nachrichtentypen verwendet werden sollte. Verwenden Sie stattdessen verschachtelte Nachrichtentypen.

Gruppen sind eine weitere Möglichkeit, Informationen in Ihren Nachrichtendefinitionen zu verschachteln. Um beispielsweise eine SearchResponse mit einer Anzahl von Results anzugeben, könnte dies wie folgt aussehen:

message SearchResponse {
  repeated group Result = 1 {
    optional string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
}

Eine Gruppe kombiniert einfach einen verschachtelten Nachrichtentyp und ein Feld in einer einzigen Deklaration. In Ihrem Code können Sie diese Nachricht so behandeln, als hätte sie ein Feld vom Typ Result namens result (der letztere Name wird kleingeschrieben, damit er nicht mit dem ersteren kollidiert). Daher ist dieses Beispiel exakt gleichwertig mit der früheren SearchResponse, außer dass die Nachricht ein anderes Wire-Format hat.

Nachrichtentyp aktualisieren

Wenn ein bestehender Nachrichtentyp nicht mehr all Ihren Anforderungen entspricht – Sie möchten beispielsweise, dass das Nachrichtenformat ein zusätzliches Feld hat –, aber Sie möchten immer noch Code verwenden, der mit dem alten Format erstellt wurde, machen Sie sich keine Sorgen! Es ist sehr einfach, Nachrichtentypen zu aktualisieren, ohne Ihren bestehenden Code zu beeinträchtigen, wenn Sie das binäre Wire-Format verwenden.

Prüfen Sie Proto-Best Practices und die folgenden Regeln

Binärdraht-unsichere Änderungen

Wire-unsichere Änderungen sind Schemaänderungen, die fehlschlagen, wenn Sie Daten parsen, die mit dem alten Schema serialisiert wurden, mit einem Parser, der das neue Schema verwendet (oder umgekehrt). Nehmen Sie nur Wire-unsichere Änderungen vor, wenn Sie wissen, dass alle Serialisierer und Deserialisierer der Daten das neue Schema verwenden.

  • Das Ändern von Feldnummern für vorhandene Felder ist nicht sicher.
    • Das Ändern der Feldnummer entspricht dem Löschen des Feldes und dem Hinzufügen eines neuen Feldes mit demselben Typ. Wenn Sie ein Feld neu nummerieren möchten, lesen Sie die Anweisungen zum Löschen eines Feldes.
  • Das Verschieben von Feldern in eine vorhandene oneof ist nicht sicher.

Binärdraht-sichere Änderungen

Wire-sichere Änderungen sind solche, bei denen es völlig sicher ist, das Schema auf diese Weise zu entwickeln, ohne Risiko von Datenverlust oder neuen Parsing-Fehlern.

Beachten Sie, dass alle Wire-sicheren Änderungen eine breaking change für den Anwendungscode in einer bestimmten Sprache darstellen können. Beispielsweise wäre das Hinzufügen eines Wertes zu einem bereits vorhandenen Enum ein Kompilierungsfehler für jeden Code mit einem erschöpfenden Switch auf diesem Enum. Aus diesem Grund vermeidet Google möglicherweise einige dieser Änderungen an öffentlichen Nachrichten: Die AIPs enthalten Anleitungen, welche dieser Änderungen dort sicher vorgenommen werden können.

  • Das Hinzufügen neuer Felder ist sicher.
    • Wenn Sie neue Felder hinzufügen, können alle von Code serialisierten Nachrichten, die Ihr „altes“ Nachrichtenformat verwenden, immer noch von Ihrem neuen generierten Code geparst werden. Sie sollten die Standardwerte für diese Elemente im Auge behalten, damit neuer Code ordnungsgemäß mit von altem Code generierten Nachrichten interagieren kann. Ebenso können von Ihrem neuen Code erstellte Nachrichten von Ihrem alten Code geparst werden: Alte Binärdateien ignorieren neue Felder beim Parsen einfach. Weitere Details finden Sie im Abschnitt Unbekannte Felder.
  • Das Entfernen von Feldern ist sicher.
    • Derselbe Feldnummer darf in Ihrem aktualisierten Nachrichtentyp nicht wiederverwendet werden. Möglicherweise möchten Sie das Feld stattdessen umbenennen, vielleicht mit dem Präfix „OBSOLETE_“, oder die Feldnummer reservieren, damit zukünftige Benutzer Ihres .proto die Nummer nicht versehentlich wiederverwenden können.
  • Das Hinzufügen zusätzlicher Werte zu einem Enum ist sicher.
  • Das Ändern eines einzelnen expliziten Präsenzfeldes oder einer Erweiterung in ein Mitglied einer **neuen** oneof ist sicher.
  • Das Ändern einer oneof, die nur ein Feld enthält, in ein explizites Präsenzfeld ist sicher.
  • Das Ändern eines Feldes in eine Erweiterung mit derselben Nummer und demselben Typ ist sicher.

Binärdraht-kompatible Änderungen (bedingt sicher)

Im Gegensatz zu Wire-sicheren Änderungen bedeutet Wire-kompatibel, dass dieselben Daten sowohl vor als auch nach einer gegebenen Änderung geparst werden können. Das Parsen der Daten kann jedoch bei dieser Art von Änderung verlustbehaftet sein. Beispielsweise ist die Änderung eines int32 in ein int64 eine kompatible Änderung, aber wenn ein Wert größer als INT32_MAX geschrieben wird, verwirft ein Client, der ihn als int32 liest, die höherwertigen Bits der Zahl.

Sie können kompatible Änderungen an Ihrem Schema nur vornehmen, wenn Sie die Einführung in Ihrem System sorgfältig verwalten. Sie können beispielsweise ein int32 in ein int64 ändern, aber sicherstellen, dass Sie weiterhin nur gültige int32-Werte schreiben, bis das neue Schema auf alle Endpunkte angewendet wurde, und dann anschließend größere Werte schreiben.

Wenn Ihr Schema außerhalb Ihrer Organisation veröffentlicht wird, sollten Sie im Allgemeinen keine Wire-kompatiblen Änderungen vornehmen, da Sie die Einführung des neuen Schemas nicht verwalten können, um zu wissen, wann der unterschiedliche Wertebereich sicher verwendet werden kann.

  • int32, uint32, int64, uint64 und bool sind alle kompatibel.
    • Wenn eine Zahl von der Wire gelesen wird, die nicht in den entsprechenden Typ passt, erhalten Sie denselben Effekt, als ob Sie die Zahl in C++ in diesen Typ umgewandelt hätten (z. B. wenn eine 64-Bit-Zahl als int32 gelesen wird, wird sie auf 32 Bit abgeschnitten).
  • sint32 und sint64 sind miteinander kompatibel, aber **nicht** mit den anderen Ganzzahltypen.
    • Wenn der geschriebene Wert zwischen INT_MIN und INT_MAX (einschließlich) lag, wird er mit beiden Typen als derselbe Wert geparst. Wenn ein sint64-Wert außerhalb dieses Bereichs geschrieben und als sint32 geparst wurde, wird der Varint auf 32 Bit abgeschnitten und dann die Zickzack-Dekodierung durchgeführt (was zu einem anderen beobachteten Wert führt).
  • string und bytes sind kompatibel, solange die Bytes gültiges UTF-8 sind.
  • Eingebettete Nachrichten sind mit bytes kompatibel, wenn die Bytes eine kodierte Instanz der Nachricht enthalten.
  • fixed32 ist mit sfixed32 und fixed64 mit sfixed64 kompatibel.
  • Für string, bytes und Nachrichtenfelder ist singulär mit repeated kompatibel.
    • Bei serialisierten Daten eines wiederholten Feldes als Eingabe nehmen Clients, die dieses Feld als singulär erwarten, den letzten Eingabewert, wenn es sich um ein primitives Feld handelt, oder zusammenführen alle Eingabeelemente, wenn es sich um ein Nachrichtenfeld handelt. Beachten Sie, dass dies für numerische Typen, einschließlich Boolescher Werte und Enums, **nicht** generell sicher ist. Wiederholte Felder von numerischen Typen werden standardmäßig im packed-Format serialisiert, das nicht korrekt geparst wird, wenn ein singuläres Feld erwartet wird.
  • enum ist kompatibel mit int32, uint32, int64 und uint64
    • Beachten Sie, dass Client-Code sie möglicherweise unterschiedlich behandelt, wenn die Nachricht deserialisiert wird: Beispielsweise werden nicht erkannte Proto3-enum-Werte in der Nachricht beibehalten, aber wie dies bei der Deserialisierung der Nachricht dargestellt wird, ist sprachabhängig.
  • Das Ändern eines Feldes von einer map<K, V> zu dem entsprechenden repeated-Nachrichtenfeld ist binär kompatibel (siehe Maps, unten, für das Nachrichtenlayout und andere Einschränkungen).
    • Die Sicherheit der Änderung hängt jedoch von der Anwendung ab: Beim Deserialisieren und erneuten Serialisieren einer Nachricht erzeugen Clients, die die repeated-Felddefinition verwenden, ein semantisch identisches Ergebnis; Clients, die die map-Felddefinition verwenden, können jedoch Einträge neu anordnen und Einträge mit doppelten Schlüsseln verwerfen.

Unbekannte Felder

Unbekannte Felder sind wohlgeformte Protokollpuffer-serialisierte Daten, die Felder darstellen, die der Parser nicht erkennt. Wenn beispielsweise ein altes Binärformat Daten parst, die von einem neuen Binärformat mit neuen Feldern gesendet wurden, werden diese neuen Felder im alten Binärformat zu unbekannten Feldern.

Ursprünglich verwarf proto3-Nachrichten während des Parsens immer unbekannte Felder, aber in Version 3.5 haben wir die Beibehaltung unbekannter Felder wieder eingeführt, um dem proto2-Verhalten zu entsprechen. In Versionen 3.5 und höher werden unbekannte Felder während des Parsens beibehalten und in die serialisierte Ausgabe aufgenommen.

Beibehalten unbekannter Felder

Einige Aktionen können dazu führen, dass unbekannte Felder verloren gehen. Wenn Sie beispielsweise eine der folgenden Aktionen ausführen, gehen unbekannte Felder verloren

  • Serialisieren eines Proto in JSON.
  • Iterieren über alle Felder in einer Nachricht, um eine neue Nachricht zu füllen.

Um den Verlust unbekannter Felder zu vermeiden, gehen Sie wie folgt vor

  • Verwenden Sie binär; vermeiden Sie die Verwendung von Textformaten für den Datenaustausch.
  • Verwenden Sie nachrichtenorientierte APIs wie CopyFrom() und MergeFrom(), um Daten zu kopieren, anstatt Feld für Feld zu kopieren

TextFormat ist ein Sonderfall. Das Serialisieren in TextFormat gibt unbekannte Felder mit ihren Feldnummern aus. Das Parsen von TextFormat-Daten zurück in ein Binär-Proto schlägt jedoch fehl, wenn Einträge mit Feldnummern vorhanden sind.

Erweiterungen (Extensions)

Eine Erweiterung ist ein Feld, das außerhalb seiner Container-Nachricht definiert ist; normalerweise in einer .proto-Datei, die sich von der .proto-Datei der Container-Nachricht unterscheidet.

Warum Erweiterungen verwenden?

Es gibt zwei Hauptgründe für die Verwendung von Erweiterungen:

  • Die .proto-Datei der Container-Nachricht hat weniger Importe/Abhängigkeiten. Dies kann Build-Zeiten verbessern, zirkuläre Abhängigkeiten aufbrechen und ansonsten lose Kopplung fördern. Erweiterungen sind dafür sehr gut geeignet.
  • Ermöglicht Systemen, Daten an eine Container-Nachricht mit minimaler Abhängigkeit und Koordination anzuhängen. Erweiterungen sind hierfür aufgrund des begrenzten Feldnummernraums und der Folgen der Wiederverwendung von Feldnummern keine großartige Lösung. Wenn Ihr Anwendungsfall sehr geringe Koordination für eine große Anzahl von Erweiterungen erfordert, sollten Sie stattdessen den Any Nachrichtentyp verwenden.

Beispielerweiterung

Betrachten wir ein Beispiel für eine Erweiterung:

// file kittens/video_ext.proto

import "kittens/video.proto";
import "media/user_content.proto";

package kittens;

// This extension allows kitten videos in a media.UserContent message.
extend media.UserContent {
  // Video is a message imported from kittens/video.proto
  repeated Video kitten_videos = 126;
}

Beachten Sie, dass die Datei, die die Erweiterung definiert (kittens/video_ext.proto), die Datei der Container-Nachricht (media/user_content.proto) importiert.

Die Container-Nachricht muss einen Teil ihrer Feldnummern für Erweiterungen reservieren.

// file media/user_content.proto

package media;

// A container message to hold stuff that a user has created.
message UserContent {
  // Set verification to `DECLARATION` to enforce extension declarations for all
  // extensions in this range.
  extensions 100 to 199 [verification = DECLARATION];
}

Die Datei der Container-Nachricht (media/user_content.proto) definiert die Nachricht UserContent, die die Feldnummern [100 bis 199] für Erweiterungen reserviert. Es wird empfohlen, verification = DECLARATION für den Bereich festzulegen, um Deklarationen für alle seine Erweiterungen zu erzwingen.

Wenn die neue Erweiterung (kittens/video_ext.proto) hinzugefügt wird, sollte eine entsprechende Deklaration zu UserContent hinzugefügt und verification entfernt werden.

// A container message to hold stuff that a user has created.
message UserContent {
  extensions 100 to 199 [
    declaration = {
      number: 126,
      full_name: ".kittens.kitten_videos",
      type: ".kittens.Video",
      repeated: true
    }
  ];
}

UserContent deklariert, dass Feldnummer 126 von einem wiederholten repeated-Erweiterungsfeld mit dem vollqualifizierten Namen .kittens.kitten_videos und dem vollqualifizierten Typ .kittens.Video verwendet wird. Weitere Informationen zu Erweiterungsdeklarationen finden Sie unter Erweiterungsdeklarationen.

Beachten Sie, dass die Datei der Container-Nachricht (media/user_content.proto) die Kitten-Video-Erweiterungsdefinition (kittens/video_ext.proto) **nicht** importiert.

Es gibt keinen Unterschied im Wire-Format-Encoding von Erweiterungsfeldern im Vergleich zu einem Standardfeld mit derselben Feldnummer, demselben Typ und derselben Kardinalität. Daher ist es sicher, ein Standardfeld aus seinem Container zu einer Erweiterung zu verschieben oder ein Erweiterungsfeld als Standardfeld in seine Container-Nachricht zu verschieben, solange Feldnummer, Typ und Kardinalität konstant bleiben.

Da Erweiterungen jedoch außerhalb der Container-Nachricht definiert sind, werden keine spezialisierten Accessoren generiert, um bestimmte Erweiterungsfelder abzurufen und zu setzen. Für unser Beispiel generiert der protobuf-Compiler **keine** Accessoren wie AddKittenVideos() oder GetKittenVideos(). Stattdessen werden Erweiterungen über parametrisierte Funktionen wie: HasExtension(), ClearExtension(), GetExtension(), MutableExtension() und AddExtension() zugegriffen.

In C++ sähe das ungefähr so aus:

UserContent user_content;
user_content.AddExtension(kittens::kitten_videos, new kittens::Video());
assert(1 == user_content.GetExtensionCount(kittens::kitten_videos));
user_content.GetExtension(kittens::kitten_videos, 0);

Erweiterungsbereiche definieren

Wenn Sie der Besitzer einer Container-Nachricht sind, müssen Sie einen Erweiterungsbereich für die Erweiterungen Ihrer Nachricht definieren.

Feldnummern, die Erweiterungsfeldern zugewiesen sind, können nicht für Standardfelder wiederverwendet werden.

Es ist sicher, einen Erweiterungsbereich nach seiner Definition zu erweitern. Eine gute Standardeinstellung ist, 1000 relativ kleine Nummern zuzuweisen und diesen Raum mit Erweiterungsdeklarationen dicht zu füllen.

message ModernExtendableMessage {
  // All extensions in this range should use extension declarations.
  extensions 1000 to 2000 [verification = DECLARATION];
}

Beim Hinzufügen eines Bereichs für Erweiterungsdeklarationen vor den tatsächlichen Erweiterungen sollten Sie verification = DECLARATION hinzufügen, um die Verwendung von Deklarationen für diesen neuen Bereich zu erzwingen. Dieser Platzhalter kann entfernt werden, sobald eine tatsächliche Deklaration hinzugefügt wurde.

Es ist sicher, einen bestehenden Erweiterungsbereich in separate Bereiche aufzuteilen, die denselben Gesamtbereich abdecken. Dies kann für die Migration eines Legacy-Nachrichtentyps zu Erweiterungsdeklarationen erforderlich sein. Zum Beispiel könnte vor der Migration der Bereich wie folgt definiert werden:

message LegacyMessage {
  extensions 1000 to max;
}

Und nach der Migration (Aufteilung des Bereichs) kann es sein:

message LegacyMessage {
  // Legacy range that was using an unverified allocation scheme.
  extensions 1000 to 524999999 [verification = UNVERIFIED];
  // Current range that uses extension declarations.
  extensions 525000000 to max  [verification = DECLARATION];
}

Es ist nicht sicher, die Startfeldnummer zu erhöhen oder die Endfeldnummer zu verringern, um einen Erweiterungsbereich zu verschieben oder zu verkleinern. Diese Änderungen können eine vorhandene Erweiterung ungültig machen.

Bevorzugen Sie die Verwendung von Feldnummern 1 bis 15 für Standardfelder, die in den meisten Instanzen Ihres Protos populär sind. Es wird nicht empfohlen, diese Nummern für Erweiterungen zu verwenden.

Wenn Ihre Nummerierungskonvention vorsieht, dass Erweiterungen sehr große Feldnummern haben könnten, können Sie angeben, dass Ihr Erweiterungsbereich bis zur maximal möglichen Feldnummer reicht, indem Sie das Schlüsselwort max verwenden.

message Foo {
  extensions 1000 to max;
}

max ist 229 - 1 oder 536.870.911.

Erweiterungsnummern auswählen

Erweiterungen sind einfach Felder, die außerhalb ihrer Container-Nachrichten angegeben werden können. Alle gleichen Regeln für die Zuweisung von Feldnummern gelten für Erweiterungsfeldnummern. Die gleichen Folgen der Wiederverwendung von Feldnummern gelten auch für die Wiederverwendung von Erweiterungsfeldnummern.

Die Auswahl eindeutiger Erweiterungsfeldnummern ist einfach, wenn die Container-Nachricht Erweiterungsdeklarationen verwendet. Bei der Definition einer neuen Erweiterung wählen Sie die niedrigste Feldnummer oberhalb aller anderen Deklarationen aus dem höchsten in der Container-Nachricht definierten Erweiterungsbereich. Wenn beispielsweise eine Container-Nachricht wie folgt definiert ist:

message Container {
  // Legacy range that was using an unverified allocation scheme
  extensions 1000 to 524999999;
  // Current range that uses extension declarations. (highest extension range)
  extensions 525000000 to max  [
    declaration = {
      number: 525000001,
      full_name: ".bar.baz_ext",
      type: ".bar.Baz"
    }
    // 525,000,002 is the lowest field number above all other declarations
  ];
}

Die nächste Erweiterung von Container sollte eine neue Deklaration mit der Nummer 525000002 hinzufügen.

Nicht verifizierte Zuweisung von Erweiterungsnummern (nicht empfohlen)

Der Besitzer einer Container-Nachricht kann sich entscheiden, auf Erweiterungsdeklarationen zugunsten seiner eigenen nicht verifizierten Strategie zur Zuweisung von Erweiterungsnummern zu verzichten.

Ein nicht verifiziertes Zuweisungsschema verwendet einen Mechanismus außerhalb des Protokollpuffer-Ökosystems, um Erweiterungsfeldnummern innerhalb des ausgewählten Erweiterungsbereichs zuzuweisen. Ein Beispiel könnte die Verwendung einer Commit-Nummer eines Monorepos sein. Dieses System ist aus Sicht des Protokollpuffer-Compilers "nicht verifiziert", da keine Möglichkeit besteht zu überprüfen, ob eine Erweiterung eine ordnungsgemäß erworbene Erweiterungsfeldnummer verwendet.

Der Vorteil eines nicht verifizierten Systems gegenüber einem verifizierten System wie Erweiterungsdeklarationen ist die Möglichkeit, eine Erweiterung zu definieren, ohne mit dem Besitzer der Container-Nachricht zu koordinieren.

Der Nachteil eines nicht verifizierten Systems ist, dass der Protokollpuffer-Compiler die Teilnehmer nicht vor der Wiederverwendung von Erweiterungsfeldnummern schützen kann.

Nicht verifizierte Zuweisungsstrategien für Erweiterungsfeldnummern werden nicht empfohlen, da die Folgen der Wiederverwendung von Feldnummern auf alle Erweiterer einer Nachricht fallen (nicht nur auf den Entwickler, der die Empfehlungen nicht befolgt hat). Wenn Ihr Anwendungsfall sehr geringe Koordination erfordert, sollten Sie stattdessen die Any Nachricht verwenden.

Nicht verifizierte Zuweisungsstrategien für Erweiterungsfeldnummern sind auf den Bereich 1 bis 524.999.999 beschränkt. Feldnummern ab 525.000.000 können nur mit Erweiterungsdeklarationen verwendet werden.

Erweiterungstypen angeben

Erweiterungen können von jedem Feldtyp außer oneofs und maps sein.

Verschachtelte Erweiterungen (nicht empfohlen)

Sie können Erweiterungen im Geltungsbereich einer anderen Nachricht deklarieren.

import "common/user_profile.proto";

package puppies;

message Photo {
  extend common.UserProfile {
    optional int32 likes_count = 111;
  }
  ...
}

In diesem Fall lautet der C++-Code für den Zugriff auf diese Erweiterung:

UserProfile user_profile;
user_profile.SetExtension(puppies::Photo::likes_count, 42);

Mit anderen Worten, der einzige Effekt ist, dass likes_count im Geltungsbereich von puppies.Photo definiert ist.

Dies ist eine häufige Quelle der Verwirrung: Das Deklarieren eines extend-Blocks verschachtelt in einem Nachrichtentyp bedeutet nicht, dass eine Beziehung zwischen dem äußeren Typ und dem erweiterten Typ besteht. Insbesondere bedeutet das obige Beispiel nicht, dass Photo irgendeine Art von Unterklasse von UserProfile ist. Es bedeutet lediglich, dass das Symbol likes_count im Geltungsbereich von Photo deklariert ist; es ist lediglich ein statisches Mitglied.

Ein gängiges Muster ist die Definition von Erweiterungen im Geltungsbereich des Feldtyps der Erweiterung – zum Beispiel eine Erweiterung für media.UserContent vom Typ puppies.Photo, wobei die Erweiterung als Teil von Photo definiert ist.

import "media/user_content.proto";

package puppies;

message Photo {
  extend media.UserContent {
    optional Photo puppy_photo = 127;
  }
  ...
}

Es besteht jedoch keine Verpflichtung, dass eine Erweiterung mit einem Nachrichtentyp innerhalb dieses Typs definiert wird. Sie können auch das Standarddefinitionsmuster verwenden:

import "media/user_content.proto";

package puppies;

message Photo {
  ...
}

// This can even be in a different file.
extend media.UserContent {
  optional Photo puppy_photo = 127;
}

Diese **Standard-Syntax (auf Dateiebene) wird bevorzugt**, um Verwirrung zu vermeiden. Die verschachtelte Syntax wird von Benutzern, die mit Erweiterungen noch nicht vertraut sind, oft mit Vererbung verwechselt.

Any

Der Any-Nachrichtentyp ermöglicht es Ihnen, Nachrichten als eingebettete Typen zu verwenden, ohne deren .proto-Definition zu haben. Eine Any enthält eine beliebige serialisierte Nachricht als bytes, zusammen mit einer URL, die als global eindeutiger Identifikator für die Nachricht dient und auf deren Typ verweist. Um den Typ Any zu verwenden, müssen Sie google/protobuf/any.proto importieren.

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2;
}

Die Standard-Typ-URL für einen gegebenen Nachrichtentyp ist type.googleapis.com/_packagename_._messagename_.

Verschiedene Sprachimplementierungen unterstützen Laufzeitbibliothekshelfer zum packen und entpacken von Any-Werten auf typsichere Weise – z. B. hat der Any-Typ in Java spezielle pack()- und unpack()-Accessoren, während es in C++ PackFrom()- und UnpackTo()-Methoden gibt

// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);

// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const google::protobuf::Any& detail : status.details()) {
  if (detail.Is<NetworkErrorDetails>()) {
    NetworkErrorDetails network_error;
    detail.UnpackTo(&network_error);
    ... processing network_error ...
  }
}

Wenn Sie enthaltende Nachrichten auf eine kleine Anzahl von Typen beschränken und die Erlaubnis einholen möchten, bevor Sie neue Typen zur Liste hinzufügen, sollten Sie stattdessen Erweiterungen mit Erweiterungsdeklarationen anstelle von Any Nachrichtentypen verwenden.

Oneof

Wenn Sie eine Nachricht mit vielen optionalen Feldern haben und zu jedem Zeitpunkt höchstens ein Feld gesetzt werden kann, können Sie dieses Verhalten erzwingen und Speicher sparen, indem Sie die Oneof-Funktion verwenden.

Oneof-Felder sind wie optionale Felder, außer dass sich alle Felder in einem Oneof denselben Speicherplatz teilen und zu jedem Zeitpunkt höchstens ein Feld gesetzt werden kann. Das Setzen eines beliebigen Members des Oneof löscht automatisch alle anderen Members. Sie können überprüfen, welcher Wert in einem Oneof gesetzt ist (falls vorhanden) mit einer speziellen case()- oder WhichOneof()-Methode, abhängig von der von Ihnen gewählten Sprache.

Beachten Sie, dass **wenn mehrere Werte gesetzt sind, der zuletzt gesetzte Wert, wie durch die Reihenfolge im Proto bestimmt, alle vorherigen überschreibt**.

Feldnummern für Oneof-Felder müssen innerhalb der umschließenden Nachricht eindeutig sein.

Verwenden von Oneof

Um ein Oneof in Ihrer .proto-Datei zu definieren, verwenden Sie das Schlüsselwort oneof gefolgt von Ihrem Oneof-Namen, in diesem Fall test_oneof

message SampleMessage {
  oneof test_oneof {
     string name = 4;
     SubMessage sub_message = 9;
  }
}

Sie fügen dann Ihre Oneof-Felder zur Oneof-Definition hinzu. Sie können Felder von jedem Typ außer map-Feldern hinzufügen, aber Sie können die Schlüsselwörter required, optional oder repeated nicht verwenden. Wenn Sie ein wiederholtes Feld zu einem Oneof hinzufügen müssen, können Sie eine Nachricht verwenden, die das wiederholte Feld enthält.

In Ihrem generierten Code haben Oneof-Felder die gleichen Getter und Setter wie normale optional-Felder. Sie erhalten auch eine spezielle Methode, um zu überprüfen, welcher Wert (falls vorhanden) im Oneof gesetzt ist. Weitere Informationen zur Oneof-API für Ihre gewählte Sprache finden Sie in der entsprechenden API-Referenz.

Oneof-Funktionen

  • Das Setzen eines Oneof-Feldes löscht automatisch alle anderen Mitglieder des Oneof. Wenn Sie also mehrere Oneof-Felder setzen, behält nur das **letzte** gesetzte Feld seinen Wert.

    SampleMessage message;
    message.set_name("name");
    CHECK(message.has_name());
    // Calling mutable_sub_message() will clear the name field and will set
    // sub_message to a new instance of SubMessage with none of its fields set.
    message.mutable_sub_message();
    CHECK(!message.has_name());
    
  • Wenn der Parser mehrere Mitglieder desselben Oneof auf der Wire findet, wird nur das zuletzt gefundene Mitglied in der geparsten Nachricht verwendet. Beim Parsen von Daten auf der Wire, beginnend am Anfang der Bytes, werten Sie den nächsten Wert aus und wenden die folgenden Parsing-Regeln an

    • Prüfen Sie zuerst, ob ein **anderes** Feld im selben Oneof derzeit gesetzt ist, und löschen Sie es, falls ja.

    • Wenden Sie dann die Inhalte so an, als ob das Feld nicht in einem Oneof wäre

      • Ein Primitiv überschreibt jeden bereits gesetzten Wert
      • Eine Nachricht wird in jeden bereits gesetzten Wert gemergt
  • Erweiterungen werden für Oneofs nicht unterstützt.

  • Ein Oneof kann nicht repeated sein.

  • Reflection-APIs funktionieren für Oneof-Felder.

  • Wenn Sie ein Oneof-Feld auf seinen Standardwert setzen (z. B. ein int32-Oneof-Feld auf 0 setzen), wird der „Fall“ dieses Oneof-Feldes gesetzt und der Wert auf der Wire serialisiert.

  • Wenn Sie C++ verwenden, stellen Sie sicher, dass Ihr Code keine Speicherabstürze verursacht. Der folgende Beispielcode wird abstürzen, da sub_message durch den Aufruf der Methode set_name() bereits gelöscht wurde.

    SampleMessage message;
    SubMessage* sub_message = message.mutable_sub_message();
    message.set_name("name");      // Will delete sub_message
    sub_message->set_...            // Crashes here
    
  • Auch in C++ gilt: Wenn Sie zwei Nachrichten mit Oneofs Swap(), dann hat jede Nachricht am Ende den Oneof-Fall der anderen: Im folgenden Beispiel hat msg1 eine sub_message und msg2 hat einen name.

    SampleMessage msg1;
    msg1.set_name("name");
    SampleMessage msg2;
    msg2.mutable_sub_message();
    msg1.swap(&msg2);
    CHECK(msg1.has_sub_message());
    CHECK(msg2.has_name());
    

Abwärtskompatibilitätsprobleme

Seien Sie vorsichtig beim Hinzufügen oder Entfernen von Oneof-Feldern. Wenn die Überprüfung des Wertes eines Oneof None/NOT_SET zurückgibt, könnte dies bedeuten, dass das Oneof nicht gesetzt wurde oder auf ein Feld in einer anderen Version des Oneof gesetzt wurde. Es gibt keinen Unterschied, da es keine Möglichkeit gibt zu wissen, ob ein unbekanntes Feld auf der Wire ein Mitglied des Oneof ist.

Probleme mit der Wiederverwendung von Tags

  • Optionale Felder in oder aus einem Oneof verschieben: Sie können einige Ihrer Informationen verlieren (einige Felder werden gelöscht), nachdem die Nachricht serialisiert und geparst wurde. Sie können jedoch ein einzelnes Feld sicher in einen **neuen** Oneof verschieben und möglicherweise mehrere Felder verschieben, wenn bekannt ist, dass immer nur eines gesetzt ist. Weitere Details finden Sie unter Nachrichtentyp aktualisieren.
  • Löschen eines Oneof-Feldes und Wiedereinfügen: Dies kann dazu führen, dass Ihr aktuell gesetztes Oneof-Feld nach dem Serialisieren und Parsen der Nachricht gelöscht wird.
  • Oneof aufteilen oder zusammenführen: Dies hat ähnliche Probleme wie das Verschieben von optional-Feldern.

Maps (Zuordnungen)

Wenn Sie eine assoziative Zuordnung als Teil Ihrer Datendefinition erstellen möchten, bietet Protobuf eine praktische Kurzschreibweise

map<key_type, value_type> map_field = N;

…wobei der key_type jeder ganzzahlige oder Zeichenfolgentyp sein kann (also jeder skalare Typ außer Gleitkommatypen und bytes). Beachten Sie, dass weder Enum noch Proto-Nachrichten für key_type gültig sind. Der value_type kann jeder Typ sein, außer einer anderen Map.

Wenn Sie beispielsweise eine Map von Projekten erstellen möchten, bei denen jede Project-Nachricht mit einem Zeichenfolgen-Schlüssel verbunden ist, könnten Sie sie wie folgt definieren

map<string, Project> projects = 3;

Map-Funktionen

  • Erweiterungen werden für Maps nicht unterstützt.
  • Maps können nicht repeated, optional oder required sein.
  • Die Reihenfolge des Wire-Formats und die Iterationsreihenfolge der Map-Werte sind undefiniert, sodass Sie sich nicht auf eine bestimmte Reihenfolge Ihrer Map-Elemente verlassen können.
  • Beim Generieren von Textformat für eine .proto-Datei werden Maps nach Schlüssel sortiert. Numerische Schlüssel werden numerisch sortiert.
  • Beim Parsen von der Wire oder beim Zusammenführen wird bei doppelten Map-Schlüsseln der zuletzt gesehene Schlüssel verwendet. Beim Parsen einer Map aus dem Textformat kann das Parsen fehlschlagen, wenn doppelte Schlüssel vorhanden sind.
  • Wenn Sie einen Schlüssel, aber keinen Wert für ein Map-Feld angeben, ist das Verhalten beim Serialisieren des Feldes sprachabhängig. In C++, Java, Kotlin und Python wird der Standardwert des Typs serialisiert, während in anderen Sprachen nichts serialisiert wird.
  • Kein Symbol FooEntry kann im selben Geltungsbereich wie eine Map foo existieren, da FooEntry bereits von der Implementierung der Map verwendet wird.

Die generierte Map-API ist derzeit für alle unterstützten Sprachen verfügbar. Weitere Informationen zur Map-API für Ihre gewählte Sprache finden Sie in der entsprechenden API-Referenz.

Abwärtskompatibilität

Die Map-Syntax ist auf der Wire äquivalent zu Folgendem, sodass Protobuf-Implementierungen, die Maps nicht unterstützen, Ihre Daten immer noch verarbeiten können

message MapFieldEntry {
  optional key_type key = 1;
  optional value_type value = 2;
}

repeated MapFieldEntry map_field = N;

Jede Protobuf-Implementierung, die Maps unterstützt, muss sowohl Daten produzieren als auch akzeptieren, die von der früheren Definition akzeptiert werden können.

Pakete (Packages)

Sie können eine optionale package-Spezifikation zu einer .proto-Datei hinzufügen, um Namenskonflikte zwischen Protokollnachrichtentypen zu vermeiden.

package foo.bar;
message Open { ... }

Sie können dann die Paket-Spezifikation beim Definieren von Feldern Ihres Nachrichtentyps verwenden

message Foo {
  ...
  optional foo.bar.Open open = 1;
  ...
}

Die Art und Weise, wie eine Paket-Spezifikation den generierten Code beeinflusst, hängt von Ihrer gewählten Sprache ab

  • In **C++** werden die generierten Klassen in einen C++-Namespace eingeschlossen. Beispielsweise wäre Open im Namespace foo::bar.
  • In **Java** und **Kotlin** wird das Paket als Java-Paket verwendet, es sei denn, Sie geben explizit eine option java_package in Ihrer .proto-Datei an.
  • In **Python** wird die Direktive package ignoriert, da Python-Module nach ihrem Speicherort im Dateisystem organisiert sind.
  • In **Go** wird die Direktive package ignoriert, und die generierte Datei .pb.go befindet sich im Paket, das nach der entsprechenden go_proto_library Bazel-Regel benannt ist. Für Open-Source-Projekte **müssen** Sie entweder eine go_package-Option angeben oder das Bazel-Flag -M setzen.
  • In **Ruby** werden die generierten Klassen in verschachtelte Ruby-Namespaces eingeschlossen, die in den erforderlichen Ruby-Namenskonventionstil (erstes Zeichen groß; wenn das erste Zeichen kein Buchstabe ist, wird PB_ vorangestellt) konvertiert werden. Zum Beispiel wäre Open im Namespace Foo::Bar.
  • In **PHP** wird das Paket als Namespace nach der Konvertierung in PascalCase verwendet, es sei denn, Sie geben explizit eine option php_namespace in Ihrer .proto-Datei an. Zum Beispiel wäre Open im Namespace Foo\Bar.
  • In **C#** wird das Paket als Namespace nach der Konvertierung in PascalCase verwendet, es sei denn, Sie geben explizit eine option csharp_namespace in Ihrer .proto-Datei an. Zum Beispiel wäre Open im Namespace Foo.Bar.

Beachten Sie, dass, auch wenn die Direktive package den generierten Code nicht direkt beeinflusst (z. B. in Python), es dennoch dringend empfohlen wird, das Paket für die .proto-Datei anzugeben, da dies andernfalls zu Namenskonflikten in Deskriptoren führen und das Proto für andere Sprachen nicht portierbar machen kann.

Pakete und Namensauflösung

Die Typnamensauflösung in der Protokollpuffer-Sprache funktioniert wie in C++: Zuerst wird der innerste Geltungsbereich durchsucht, dann der nächstinnere usw., wobei jedes Paket als „innerhalb“ seines übergeordneten Pakets betrachtet wird. Ein führender Punkt (z. B. .foo.bar.Baz) bedeutet, stattdessen vom äußersten Geltungsbereich aus zu starten.

Der Protokollpuffer-Compiler löst alle Typnamen auf, indem er die importierten .proto-Dateien parst. Der Code-Generator für jede Sprache weiß, wie auf jeden Typ in dieser Sprache verwiesen werden kann, auch wenn er unterschiedliche Geltungsbereichsregeln hat.

Definieren von Diensten

Wenn Sie Ihre Nachrichtentypen mit einem RPC-System (Remote Procedure Call) verwenden möchten, können Sie eine RPC-Dienstschnittstelle in einer .proto-Datei definieren, und der Protokollpuffer-Compiler generiert Dienstschnittstellen-Code und Stubs in Ihrer gewählten Sprache. Wenn Sie beispielsweise einen RPC-Dienst mit einer Methode definieren möchten, die Ihre SearchRequest entgegennimmt und eine SearchResponse zurückgibt, können Sie ihn wie folgt in Ihrer .proto-Datei definieren

service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

Standardmäßig generiert der Protokollcompiler dann eine abstrakte Schnittstelle namens SearchService und eine entsprechende "Stub"-Implementierung. Der Stub leitet alle Aufrufe an einen RpcChannel weiter, der wiederum eine abstrakte Schnittstelle ist, die Sie selbst im Sinne Ihres eigenen RPC-Systems definieren müssen. Zum Beispiel könnten Sie einen RpcChannel implementieren, der die Nachricht serialisiert und über HTTP an einen Server sendet. Mit anderen Worten, der generierte Stub bietet eine typsichere Schnittstelle für RPC-Aufrufe auf Basis von Protocol Buffer, ohne Sie an eine bestimmte RPC-Implementierung zu binden. In C++ könnten Sie also Code wie diesen erhalten:

using google::protobuf;

protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;

void DoSearch() {
  // You provide classes MyRpcChannel and MyRpcController, which implement
  // the abstract interfaces protobuf::RpcChannel and protobuf::RpcController.
  channel = new MyRpcChannel("somehost.example.com:1234");
  controller = new MyRpcController;

  // The protocol compiler generates the SearchService class based on the
  // definition given earlier.
  service = new SearchService::Stub(channel);

  // Set up the request.
  request.set_query("protocol buffers");

  // Execute the RPC.
  service->Search(controller, &request, &response,
                  protobuf::NewCallback(&Done));
}

void Done() {
  delete service;
  delete channel;
  delete controller;
}

Alle Service-Klassen implementieren auch die Service-Schnittstelle, die eine Möglichkeit bietet, spezifische Methoden aufzurufen, ohne den Methodennamen oder seine Eingabe- und Ausgabetypen zur Kompilierzeit zu kennen. Auf der Serverseite kann dies verwendet werden, um einen RPC-Server zu implementieren, bei dem Sie Dienste registrieren könnten.

using google::protobuf;

class ExampleSearchService : public SearchService {
 public:
  void Search(protobuf::RpcController* controller,
              const SearchRequest* request,
              SearchResponse* response,
              protobuf::Closure* done) {
    if (request->query() == "google") {
      response->add_result()->set_url("http://www.google.com");
    } else if (request->query() == "protocol buffers") {
      response->add_result()->set_url("http://protobuf.googlecode.com");
    }
    done->Run();
  }
};

int main() {
  // You provide class MyRpcServer.  It does not have to implement any
  // particular interface; this is just an example.
  MyRpcServer server;

  protobuf::Service* service = new ExampleSearchService;
  server.ExportOnPort(1234, service);
  server.Run();

  delete service;
  return 0;
}

Wenn Sie Ihr eigenes bestehendes RPC-System nicht einbinden möchten, können Sie gRPC verwenden: ein sprach- und plattformunabhängiges Open-Source-RPC-System, das bei Google entwickelt wurde. gRPC funktioniert besonders gut mit Protokollpuffern und ermöglicht es Ihnen, den relevanten RPC-Code direkt aus Ihren .proto-Dateien mit einem speziellen Protokollpuffer-Compiler-Plugin zu generieren. Da es jedoch potenzielle Kompatibilitätsprobleme zwischen mit proto2 und proto3 generierten Clients und Servern gibt, empfehlen wir die Verwendung von proto3 oder Edition 2023 für die Definition von gRPC-Diensten. Weitere Informationen zur proto3-Syntax finden Sie im Proto3 Sprachleitfaden und zur Edition 2023 im Sprachleitfaden für Edition 2023.

Zusätzlich zu gRPC gibt es auch eine Reihe von laufenden Drittanbieterprojekten zur Entwicklung von RPC-Implementierungen für Protocol Buffers. Eine Liste der uns bekannten Projekte finden Sie auf der Wiki-Seite zu Drittanbieter-Add-ons.

JSON-Mapping

Das Standard-Binär-Wire-Format von Protocol Buffers ist das bevorzugte Serialisierungsformat für die Kommunikation zwischen zwei Systemen, die Protobufs verwenden. Für die Kommunikation mit Systemen, die JSON anstelle des Protobuf-Wire-Formats verwenden, unterstützt Protobuf eine kanonische Kodierung in JSON.

Optionen

Einzelne Deklarationen in einer .proto-Datei können mit einer Reihe von Optionen annotiert werden. Optionen ändern nicht die Gesamtbedeutung einer Deklaration, können aber beeinflussen, wie sie in einem bestimmten Kontext behandelt wird. Die vollständige Liste der verfügbaren Optionen ist in /google/protobuf/descriptor.proto definiert.

Einige Optionen sind Dateiebene-Optionen, d. h. sie sollten im obersten Geltungsbereich geschrieben werden, nicht innerhalb von Nachrichten-, Enum- oder Dienstdefinitionen. Einige Optionen sind Nachrichtenebene-Optionen, d. h. sie sollten innerhalb von Nachrichtendefinitionen geschrieben werden. Einige Optionen sind Feldebene-Optionen, d. h. sie sollten innerhalb von Felddefinitionen geschrieben werden. Optionen können auch für Enum-Typen, Enum-Werte, Oneof-Felder, Diensttypen und Dienstmethoden geschrieben werden; derzeit gibt es jedoch keine nützlichen Optionen für diese.

Hier sind einige der am häufigsten verwendeten Optionen

  • java_package (Date Option): Das Paket, das Sie für Ihre generierten Java/Kotlin-Klassen verwenden möchten. Wenn keine explizite java_package-Option in der .proto-Datei angegeben ist, wird standardmäßig das Proto-Paket (angegeben mit dem Schlüsselwort „package“ in der .proto-Datei) verwendet. Proto-Pakete eignen sich jedoch im Allgemeinen nicht gut als Java-Pakete, da Proto-Pakete nicht mit umgekehrten Domainnamen beginnen müssen. Wenn kein Java- oder Kotlin-Code generiert wird, hat diese Option keine Auswirkung.

    option java_package = "com.example.foo";
    
  • java_outer_classname (File Option): Der Klassenname (und damit der Dateiname) für die Wrapper-Java-Klasse, die Sie generieren möchten. Wenn keine explizite java_outer_classname-Option in der .proto-Datei angegeben ist, wird der Klassenname durch Konvertierung des .proto-Dateinamens in Camelcase gebildet (so wird foo_bar.proto zu FooBar.java). Wenn die Option java_multiple_files deaktiviert ist, werden alle anderen Klassen/Enums/etc., die für die .proto-Datei generiert werden, **innerhalb** dieser äußeren Wrapper-Java-Klasse als verschachtelte Klassen/Enums/etc. generiert. Wenn kein Java-Code generiert wird, hat diese Option keine Auswirkung.

    option java_outer_classname = "Ponycopter";
    
  • java_multiple_files (Dateioption): Wenn falsch, wird nur eine einzige .java-Datei für diese .proto-Datei generiert, und alle Java-Klassen/Enums/etc., die für die Top-Level-Nachrichten, Dienste und Aufzählungen generiert werden, werden innerhalb einer äußeren Klasse verschachtelt (siehe java_outer_classname). Wenn wahr, werden separate .java-Dateien für jede der Java-Klassen/Enums/etc. generiert, die für die Top-Level-Nachrichten, Dienste und Aufzählungen generiert werden, und die Wrapper-Java-Klasse, die für diese .proto-Datei generiert wird, enthält keine verschachtelten Klassen/Enums/etc. Dies ist eine boolesche Option, die standardmäßig false ist. Wenn kein Java-Code generiert wird, hat diese Option keine Auswirkung.

    option java_multiple_files = true;
    
  • optimize_for (Datei-Option): Kann auf SPEED, CODE_SIZE oder LITE_RUNTIME gesetzt werden. Dies beeinflusst die C++- und Java-Code-Generatoren (und möglicherweise Drittanbieter-Generatoren) wie folgt:

    • SPEED (Standard): Der Protocol-Buffer-Compiler generiert Code für die Serialisierung, das Parsen und die Ausführung anderer gängiger Operationen mit Ihren Nachrichtentypen. Dieser Code ist hochoptimiert.
    • CODE_SIZE: Der Protocol-Buffer-Compiler generiert minimale Klassen und verlässt sich auf gemeinsam genutzten, reflexionsbasierten Code zur Implementierung von Serialisierung, Parsing und verschiedenen anderen Operationen. Der generierte Code wird dadurch viel kleiner sein als bei SPEED, aber die Operationen werden langsamer sein. Klassen implementieren weiterhin genau die gleiche öffentliche API wie im SPEED-Modus. Dieser Modus ist am nützlichsten in Apps, die eine sehr große Anzahl von .proto-Dateien enthalten und nicht alle davon blitzschnell benötigen.
    • LITE_RUNTIME: Der Protocol-Buffer-Compiler generiert Klassen, die nur von der „Lite“-Runtime-Bibliothek (libprotobuf-lite anstelle von libprotobuf) abhängen. Die Lite-Runtime ist viel kleiner als die vollständige Bibliothek (ungefähr um eine Größenordnung kleiner), aber sie lässt bestimmte Funktionen wie Deskriptoren und Reflexion weg. Dies ist besonders nützlich für Apps, die auf eingeschränkten Plattformen wie Mobiltelefonen laufen. Der Compiler generiert weiterhin schnelle Implementierungen aller Methoden, wie er es im SPEED-Modus tut. Generierte Klassen implementieren nur die MessageLite-Schnittstelle in jeder Sprache, die nur eine Teilmenge der Methoden der vollständigen Message-Schnittstelle bereitstellt.
    option optimize_for = CODE_SIZE;
    
  • cc_generic_services, java_generic_services, py_generic_services (Datei-Optionen): Generische Dienste sind veraltet. Ob der Protocol-Buffer-Compiler abstrakten Dienstcode basierend auf Dienstdefinitionen in C++, Java bzw. Python generieren soll. Aus Kompatibilitätsgründen sind diese standardmäßig auf true gesetzt. Seit Version 2.3.0 (Januar 2010) wird es jedoch als vorzuziehen angesehen, dass RPC-Implementierungen Code-Generator-Plugins bereitstellen, um Code zu generieren, der für jedes System spezifischer ist, anstatt sich auf die „abstrakten“ Dienste zu verlassen.

    // This file relies on plugins to generate service code.
    option cc_generic_services = false;
    option java_generic_services = false;
    option py_generic_services = false;
    
  • cc_enable_arenas (Datei-Option): Aktiviert die Arena-Speicherverwaltung für C++-generierten Code.

  • objc_class_prefix (Datei-Option): Legt das Objective-C-Klassenpräfix fest, das allen von diesem .proto generierten Objective-C-Klassen und Enums vorangestellt wird. Es gibt keinen Standardwert. Sie sollten Präfixe zwischen 3-5 Großbuchstaben verwenden, wie von Apple empfohlen. Beachten Sie, dass alle 2-Buchstaben-Präfixe von Apple reserviert sind.

  • message_set_wire_format (Nachrichtenoption): Wenn auf true gesetzt, verwendet die Nachricht ein anderes Binärformat, das mit einem alten Format kompatibel ist, das innerhalb von Google verwendet wurde und MessageSet genannt wird. Benutzer außerhalb von Google werden diese Option wahrscheinlich nie benötigen. Die Nachricht muss genau wie folgt deklariert werden:

    message Foo {
      option message_set_wire_format = true;
      extensions 4 to max;
    }
    
  • packed (Feldoption): Wenn für ein wiederholtes Feld eines einfachen numerischen Typs auf true gesetzt, wird eine kompaktere Kodierung verwendet. Der einzige Grund, diese Option nicht zu verwenden, ist die Kompatibilität mit Parsern vor Version 2.3.0. Diese älteren Parser würden gepackte Daten ignorieren, wenn sie nicht erwartet wurden. Daher war es nicht möglich, ein vorhandenes Feld in gepacktes Format zu ändern, ohne die Wire-Kompatibilität zu brechen. In Version 2.3.0 und höher ist diese Änderung sicher, da Parser für packbare Felder immer beide Formate akzeptieren, aber seien Sie vorsichtig, wenn Sie mit alten Programmen arbeiten, die alte Protobuf-Versionen verwenden.

    repeated int32 samples = 4 [packed = true];
    
  • deprecated (Feldoption): Wenn auf true gesetzt, zeigt dies an, dass das Feld veraltet ist und von neuem Code nicht verwendet werden sollte. In den meisten Sprachen hat dies keine tatsächliche Auswirkung. In Java wird dies zu einer @Deprecated-Annotation. Für C++ generiert clang-tidy Warnungen, wenn veraltete Felder verwendet werden. In Zukunft können andere sprachspezifische Code-Generatoren Deprecations-Annotationen für die Accessoren des Feldes generieren, was wiederum eine Warnung auslöst, wenn Code kompiliert wird, der versucht, das Feld zu verwenden. Wenn das Feld von niemandem verwendet wird und Sie verhindern möchten, dass neue Benutzer es verwenden, sollten Sie die Felddeklaration durch eine reservierte Anweisung ersetzen.

    optional int32 old_field = 6 [deprecated = true];
    

Optionen für Enum-Werte

Enum-Wert-Optionen werden unterstützt. Sie können die Option deprecated verwenden, um anzuzeigen, dass ein Wert nicht mehr verwendet werden sollte. Sie können auch benutzerdefinierte Optionen mithilfe von Erweiterungen erstellen.

Das folgende Beispiel zeigt die Syntax zum Hinzufügen dieser Optionen.

import "google/protobuf/descriptor.proto";

extend google.protobuf.EnumValueOptions {
  optional string string_name = 123456789;
}

enum Data {
  DATA_UNSPECIFIED = 0;
  DATA_SEARCH = 1 [deprecated = true];
  DATA_DISPLAY = 2 [
    (string_name) = "display_value"
  ];
}

Der C++-Code zum Lesen der Option string_name könnte ungefähr so aussehen:

const absl::string_view foo = proto2::GetEnumDescriptor<Data>()
    ->FindValueByName("DATA_DISPLAY")->options().GetExtension(string_name);

Siehe Benutzerdefinierte Optionen, um zu erfahren, wie benutzerdefinierte Optionen auf Enum-Werte und Felder angewendet werden.

Benutzerdefinierte Optionen

Protocol Buffers ermöglicht es Ihnen auch, Ihre eigenen Optionen zu definieren und zu verwenden. Beachten Sie, dass dies eine **fortgeschrittene Funktion** ist, die die meisten Leute nicht benötigen. Da Optionen durch die in google/protobuf/descriptor.proto definierten Nachrichten (wie FileOptions oder FieldOptions) definiert werden, ist die Definition Ihrer eigenen Optionen einfach eine Angelegenheit des Erweiterns dieser Nachrichten. Zum Beispiel:

import "google/protobuf/descriptor.proto";

extend google.protobuf.MessageOptions {
  optional string my_option = 51234;
}

message MyMessage {
  option (my_option) = "Hello world!";
}

Hier haben wir eine neue Nachrichten-Ebene-Option definiert, indem wir MessageOptions erweitert haben. Wenn wir dann die Option verwenden, muss der Optionsname in Klammern eingeschlossen werden, um anzuzeigen, dass es sich um eine Erweiterung handelt. Wir können nun den Wert von my_option in C++ wie folgt lesen:

string value = MyMessage::descriptor()->options().GetExtension(my_option);

Hier gibt MyMessage::descriptor()->options() die Protokollnachricht MessageOptions für MyMessage zurück. Das Lesen benutzerdefinierter Optionen daraus ist genau wie das Lesen jeder anderen Erweiterung.

Ähnlich würden wir in Java schreiben:

String value = MyProtoFile.MyMessage.getDescriptor().getOptions()
  .getExtension(MyProtoFile.myOption);

In Python wäre es:

value = my_proto_file_pb2.MyMessage.DESCRIPTOR.GetOptions()
  .Extensions[my_proto_file_pb2.my_option]

Benutzerdefinierte Optionen können für jede Art von Konstrukt in der Protocol Buffers-Sprache definiert werden. Hier ist ein Beispiel, das jede Art von Option verwendet:

import "google/protobuf/descriptor.proto";

extend google.protobuf.FileOptions {
  optional string my_file_option = 50000;
}
extend google.protobuf.MessageOptions {
  optional int32 my_message_option = 50001;
}
extend google.protobuf.FieldOptions {
  optional float my_field_option = 50002;
}
extend google.protobuf.OneofOptions {
  optional int64 my_oneof_option = 50003;
}
extend google.protobuf.EnumOptions {
  optional bool my_enum_option = 50004;
}
extend google.protobuf.EnumValueOptions {
  optional uint32 my_enum_value_option = 50005;
}
extend google.protobuf.ServiceOptions {
  optional MyEnum my_service_option = 50006;
}
extend google.protobuf.MethodOptions {
  optional MyMessage my_method_option = 50007;
}

option (my_file_option) = "Hello world!";

message MyMessage {
  option (my_message_option) = 1234;

  optional int32 foo = 1 [(my_field_option) = 4.5];
  optional string bar = 2;
  oneof qux {
    option (my_oneof_option) = 42;

    string quux = 3;
  }
}

enum MyEnum {
  option (my_enum_option) = true;

  FOO = 1 [(my_enum_value_option) = 321];
  BAR = 2;
}

message RequestType {}
message ResponseType {}

service MyService {
  option (my_service_option) = FOO;

  rpc MyMethod(RequestType) returns(ResponseType) {
    // Note:  my_method_option has type MyMessage.  We can set each field
    //   within it using a separate "option" line.
    option (my_method_option).foo = 567;
    option (my_method_option).bar = "Some string";
  }
}

Beachten Sie, dass, wenn Sie eine benutzerdefinierte Option in einem anderen Paket als dem, in dem sie definiert wurde, verwenden möchten, Sie den Optionsnamen mit dem Paketnamen präfixieren müssen, genau wie Sie es für Typnamen tun würden. Zum Beispiel:

// foo.proto
import "google/protobuf/descriptor.proto";
package foo;
extend google.protobuf.MessageOptions {
  optional string my_option = 51234;
}
// bar.proto
import "foo.proto";
package bar;
message MyMessage {
  option (foo.my_option) = "Hello world!";
}

Eine letzte Sache: Da benutzerdefinierte Optionen Erweiterungen sind, müssen ihnen Feldnummern zugewiesen werden, wie jede andere Feld- oder Erweiterungsnummer auch. In den obigen Beispielen haben wir Feldnummern im Bereich 50000-99999 verwendet. Dieser Bereich ist für die interne Verwendung innerhalb einzelner Organisationen reserviert, sodass Sie Nummern in diesem Bereich frei für interne Anwendungen verwenden können. Wenn Sie benutzerdefinierte Optionen in öffentlichen Anwendungen verwenden möchten, müssen Sie jedoch sicherstellen, dass Ihre Feldnummern global eindeutig sind. Um global eindeutige Feldnummern zu erhalten, senden Sie eine Anfrage, um einen Eintrag im protobuf global extension registry hinzuzufügen. Normalerweise benötigen Sie nur eine Erweiterungsnummer. Sie können mehrere Optionen mit nur einer Erweiterungsnummer deklarieren, indem Sie sie in eine Unternachricht aufnehmen.

message FooOptions {
  optional int32 opt1 = 1;
  optional string opt2 = 2;
}

extend google.protobuf.FieldOptions {
  optional FooOptions foo_options = 1234;
}

// usage:
message Bar {
  optional int32 a = 1 [(foo_options).opt1 = 123, (foo_options).opt2 = "baz"];
  // alternative aggregate syntax (uses TextFormat):
  optional int32 b = 2 [(foo_options) = { opt1: 123 opt2: "baz" }];
}

Beachten Sie auch, dass jeder Optionstyp (Datei-, Nachrichten-, Feldebene usw.) seinen eigenen Nummernraum hat, sodass Sie beispielsweise Erweiterungen von FieldOptions und MessageOptions mit derselben Nummer deklarieren könnten.

Option-Aufbewahrung

Optionen haben eine Vorstellung von Aufbewahrung (retention), die steuert, ob eine Option im generierten Code beibehalten wird. Optionen haben standardmäßig Laufzeitaufbewahrung (runtime retention), was bedeutet, dass sie im generierten Code beibehalten werden und somit zur Laufzeit im generierten Deskriptorpool sichtbar sind. Sie können jedoch retention = RETENTION_SOURCE setzen, um anzugeben, dass eine Option (oder ein Feld innerhalb einer Option) zur Laufzeit nicht beibehalten werden muss. Dies wird als Source Retention bezeichnet.

Die Aufbewahrung von Optionen ist ein fortgeschrittenes Feature, um das sich die meisten Benutzer keine Sorgen machen müssen, aber es kann nützlich sein, wenn Sie bestimmte Optionen verwenden möchten, ohne die Code-Größenkosten für die Beibehaltung in Ihren Binärdateien zu zahlen. Optionen mit Source Retention sind für protoc und protoc Plugins weiterhin sichtbar, sodass Code-Generatoren sie zur Anpassung ihres Verhaltens verwenden können.

Die Aufbewahrung kann direkt auf einer Option gesetzt werden, so:

extend google.protobuf.FileOptions {
  optional int32 source_retention_option = 1234
      [retention = RETENTION_SOURCE];
}

Sie kann auch auf einem einfachen Feld gesetzt werden, in diesem Fall tritt sie nur in Kraft, wenn dieses Feld innerhalb einer Option vorkommt.

message OptionsMessage {
  optional int32 source_retention_field = 1 [retention = RETENTION_SOURCE];
}

Sie können gerne retention = RETENTION_RUNTIME setzen, aber dies hat keine Auswirkung, da es das Standardverhalten ist. Wenn ein Nachrichtenfeld als RETENTION_SOURCE markiert ist, werden seine gesamten Inhalte verworfen; Felder darin können dies nicht überschreiben, indem sie versuchen, RETENTION_RUNTIME zu setzen.

Option-Ziele

Felder haben eine targets-Option, die die Arten von Entitäten steuert, auf die das Feld angewendet werden kann, wenn es als Option verwendet wird. Wenn ein Feld beispielsweise targets = TARGET_TYPE_MESSAGE hat, kann dieses Feld nicht in einer benutzerdefinierten Option auf einer Aufzählung (oder einer anderen Nicht-Nachrichten-Entität) gesetzt werden. Protoc erzwingt dies und löst einen Fehler aus, wenn die Zielbeschränkungen verletzt werden.

Auf den ersten Blick mag dieses Feature unnötig erscheinen, da jede benutzerdefinierte Option eine Erweiterung der Optionsnachricht für eine bestimmte Entität ist, was die Option bereits auf diese eine Entität beschränkt. Allerdings sind Optionsziele nützlich, wenn Sie eine gemeinsam genutzte Optionsnachricht haben, die auf mehrere Entitätstypen angewendet wird, und Sie die Verwendung einzelner Felder in dieser Nachricht steuern möchten. Zum Beispiel:

message MyOptions {
  optional string file_only_option = 1 [targets = TARGET_TYPE_FILE];
  optional int32 message_and_enum_option = 2 [targets = TARGET_TYPE_MESSAGE,
                                              targets = TARGET_TYPE_ENUM];
}

extend google.protobuf.FileOptions {
  optional MyOptions file_options = 50000;
}

extend google.protobuf.MessageOptions {
  optional MyOptions message_options = 50000;
}

extend google.protobuf.EnumOptions {
  optional MyOptions enum_options = 50000;
}

// OK: this field is allowed on file options
option (file_options).file_only_option = "abc";

message MyMessage {
  // OK: this field is allowed on both message and enum options
  option (message_options).message_and_enum_option = 42;
}

enum MyEnum {
  MY_ENUM_UNSPECIFIED = 0;
  // Error: file_only_option cannot be set on an enum.
  option (enum_options).file_only_option = "xyz";
}

Generieren Ihrer Klassen

Um den Java-, Kotlin-, Python-, C++-, Go-, Ruby-, Objective-C- oder C#-Code zu generieren, den Sie zum Arbeiten mit den Nachrichtentypen in einer .proto-Datei benötigen, müssen Sie den Protocol-Buffer-Compiler protoc auf die .proto-Datei anwenden. Wenn Sie den Compiler noch nicht installiert haben, laden Sie das Paket herunter und befolgen Sie die Anweisungen in der README. Für Go müssen Sie außerdem ein spezielles Code-Generator-Plugin für den Compiler installieren; dieses und die Installationsanweisungen finden Sie im golang/protobuf Repository auf GitHub.

Der Protocol Compiler wird wie folgt aufgerufen:

protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
  • IMPORT_PATH gibt ein Verzeichnis an, in dem nach .proto-Dateien gesucht werden soll, wenn import-Anweisungen aufgelöst werden. Wenn weggelassen, wird das aktuelle Verzeichnis verwendet. Mehrere Importverzeichnisse können angegeben werden, indem das --proto_path-Flag mehrmals übergeben wird; sie werden in der Reihenfolge durchsucht. -I=_IMPORT_PATH_ kann als Kurzform von --proto_path verwendet werden.

Hinweis: Dateipfade relativ zu ihrem proto_path müssen in einer bestimmten Binärdatei global eindeutig sein. Wenn Sie beispielsweise proto/lib1/data.proto und proto/lib2/data.proto haben, können diese beiden Dateien nicht zusammen mit -I=proto/lib1 -I=proto/lib2 verwendet werden, da es mehrdeutig wäre, welche Datei import "data.proto" bedeutet. Stattdessen sollte -Iproto/ verwendet werden und die globalen Namen werden lib1/data.proto und lib2/data.proto sein.

Wenn Sie eine Bibliothek veröffentlichen und andere Benutzer Ihre Nachrichten direkt verwenden dürfen, sollten Sie einen eindeutigen Bibliotheksnamen in den Pfad aufnehmen, unter dem sie voraussichtlich verwendet werden, um Namenskollisionen zu vermeiden. Wenn Sie mehrere Verzeichnisse in einem Projekt haben, ist es bewährte Praxis, einen einzigen -I auf ein übergeordnetes Verzeichnis des Projekts zu setzen.

  • Sie können eine oder mehrere Ausgaberichtlinien angeben:

    Als zusätzlichen Komfort, wenn die DST_DIR mit .zip oder .jar endet, schreibt der Compiler die Ausgabe in eine einzelne ZIP-formatierte Archivdatei mit dem angegebenen Namen. .jar-Ausgaben erhalten auch eine Manifestdatei gemäß den Anforderungen der Java JAR-Spezifikation. Beachten Sie, dass das Archiv überschrieben wird, wenn es bereits existiert.

  • Sie müssen eine oder mehrere .proto-Dateien als Eingabe angeben. Mehrere .proto-Dateien können gleichzeitig angegeben werden. Obwohl die Dateien relativ zum aktuellen Verzeichnis benannt sind, muss jede Datei in einem der IMPORT_PATHs liegen, damit der Compiler ihren kanonischen Namen ermitteln kann.

Dateiposition

Es ist ratsam, .proto-Dateien nicht im selben Verzeichnis wie andere Sprachquellen abzulegen. Erwägen Sie die Erstellung eines Unterpakets proto für .proto-Dateien unter dem Stammverzeichnis Ihres Projekts.

Position sollte sprachunabhängig sein

Beim Arbeiten mit Java-Code ist es praktisch, zusammengehörige .proto-Dateien im selben Verzeichnis wie den Java-Quellcode abzulegen. Wenn jedoch Nicht-Java-Code jemals dieselben Protos verwendet, ist das Pfadpräfix nicht mehr sinnvoll. Legen Sie daher im Allgemeinen die Protos in ein sprachunabhängiges Verzeichnis, z. B. //myteam/mypackage.

Die Ausnahme von dieser Regel ist, wenn klar ist, dass die Protos nur in einem Java-Kontext verwendet werden, z. B. zum Testen.

Unterstützte Plattformen

Für Informationen über