Kodierung

Erklärt, wie Protocol Buffers Daten in Dateien oder über das Netzwerk kodiert.

Dieses Dokument beschreibt das Wire Format von Protocol Buffers, das die Details festlegt, wie Ihre Nachricht über das Netzwerk gesendet wird und wie viel Speicherplatz sie auf der Festplatte belegt. Wahrscheinlich müssen Sie dies nicht verstehen, um Protocol Buffers in Ihrer Anwendung zu verwenden, aber es sind nützliche Informationen für Optimierungen.

Wenn Sie die Konzepte bereits kennen, aber eine Referenz suchen, überspringen Sie den Abschnitt Kompakte Referenzkarte.

Protoscope ist eine sehr einfache Sprache zur Beschreibung von Schnipseln des Low-Level-Wire-Formats, die wir für eine visuelle Referenz der Kodierung verschiedener Nachrichten verwenden werden. Die Syntax von Protoscope besteht aus einer Folge von Tokens, die jeweils zu einer bestimmten Byte-Sequenz kodiert werden.

Zum Beispiel kennzeichnen Backticks ein rohes Hex-Literal wie `70726f746f6275660a`. Dies wird in die exakten Bytes kodiert, die im Literal als Hex bezeichnet werden. Anführungszeichen kennzeichnen UTF-8-Zeichenketten wie "Hallo, Protobuf!". Dieses Literal ist gleichbedeutend mit `48656c6c6f2c2050726f746f62756621` (was, wenn Sie genau hinschauen, aus ASCII-Bytes besteht). Wir werden weitere Elemente der Protoscope-Sprache einführen, während wir Aspekte des Wire-Formats diskutieren.

Das Protoscope-Tool kann auch kodierte Protocol Buffers als Text ausgeben. Beispiele finden Sie unter https://github.com/protocolbuffers/protoscope/tree/main/testdata.

Alle Beispiele in diesem Thema gehen davon aus, dass Sie Edition 2023 oder später verwenden.

Eine einfache Nachricht

Nehmen wir an, Sie haben die folgende sehr einfache Nachrichten-Definition

message Test1 {
  int32 a = 1;
}

In einer Anwendung erstellen Sie eine Test1-Nachricht und setzen a auf 150. Anschließend serialisieren Sie die Nachricht in einen Ausgabestrom. Wenn Sie in der Lage wären, die kodierte Nachricht zu untersuchen, würden Sie drei Bytes sehen

08 96 01

Bisher klein und numerisch – aber was bedeutet das? Wenn Sie das Protoscope-Tool verwenden, um diese Bytes auszugeben, erhalten Sie etwas wie 1: 150. Woher weiß es, dass dies der Inhalt der Nachricht ist?

Base 128 Varints

Variable-Breiten-Ganzzahlen oder Varints sind der Kern des Wire-Formats. Sie ermöglichen die Kodierung von vorzeichenlosen 64-Bit-Ganzzahlen mit ein bis zehn Bytes, wobei kleine Werte weniger Bytes verwenden.

Jedes Byte im Varint hat ein Fortsetzungsbit, das angibt, ob das folgende Byte Teil des Varints ist. Dies ist das höchstwertige Bit (MSB) des Bytes (manchmal auch als Vorzeichenbit bezeichnet). Die unteren 7 Bits sind Nutzdaten; die resultierende Ganzzahl wird aufgebaut, indem die 7-Bit-Nutzdaten ihrer konstituierenden Bytes verkettet werden.

Zum Beispiel hier die Zahl 1, kodiert als `01` – es ist ein einzelnes Byte, also ist das MSB nicht gesetzt

0000 0001
^ msb

Und hier ist 150, kodiert als `9601` – das ist etwas komplizierter

10010110 00000001
^ msb    ^ msb

Wie ermitteln Sie, dass dies 150 ist? Zuerst entfernen Sie das MSB von jedem Byte, da es nur dazu dient, uns mitzuteilen, ob wir das Ende der Zahl erreicht haben (wie Sie sehen können, ist es im ersten Byte gesetzt, da es mehr als ein Byte im Varint gibt). Diese 7-Bit-Nutzdaten sind in Little-Endian-Reihenfolge. Konvertieren Sie in Big-Endian-Reihenfolge, verketten Sie und interpretieren Sie als vorzeichenlose 64-Bit-Ganzzahl

10010110 00000001        // Original inputs.
 0010110  0000001        // Drop continuation bits.
 0000001  0010110        // Convert to big-endian.
   00000010010110        // Concatenate.
 128 + 16 + 4 + 2 = 150  // Interpret as an unsigned 64-bit integer.

Da Varints für Protocol Buffers so wichtig sind, bezeichnen wir sie in der Protoscope-Syntax als einfache Ganzzahlen. 150 ist dasselbe wie `9601`.

Nachrichtenstruktur

Eine Protocol Buffer-Nachricht ist eine Abfolge von Schlüssel-Wert-Paaren. Die Binärversion einer Nachricht verwendet einfach die Feldnummer als Schlüssel – der Name und der deklarierte Typ für jedes Feld können auf der Dekodierungsseite nur durch Bezugnahme auf die Definition des Nachrichtentyps (d. h. die .proto-Datei) ermittelt werden. Protoscope hat keinen Zugriff auf diese Informationen, daher kann es nur die Feldnummern angeben.

Wenn eine Nachricht kodiert wird, wird jedes Schlüssel-Wert-Paar in einen Datensatz umgewandelt, der aus der Feldnummer, einem Wire-Typ und einer Nutzlast besteht. Der Wire-Typ teilt dem Parser mit, wie groß die folgende Nutzlast ist. Dies ermöglicht es alten Parsern, neue Felder zu überspringen, die sie nicht verstehen. Diese Art von Schema wird manchmal als Tag-Length-Value oder TLV bezeichnet.

Es gibt sechs Wire-Typen: VARINT, I64, LEN, SGROUP, EGROUP und I32

IDNameVerwendet für
0VARINTint32, int64, uint32, uint64, sint32, sint64, bool, enum
1I64fixed64, sfixed64, double
2LENstring, bytes, eingebettete Nachrichten, gepackte wiederholte Felder
3SGROUPGruppenstart (veraltet)
4EGROUPGruppenende (veraltet)
5I32fixed32, sfixed32, float

Der „Tag“ eines Datensatzes wird als Varint kodiert, das aus der Feldnummer und dem Wire-Typ mittels der Formel (feld_nummer << 3) | wire_typ gebildet wird. Mit anderen Worten, nach dem Dekodieren des Varints, der ein Feld repräsentiert, geben die unteren 3 Bits den Wire-Typ an, und der Rest der Ganzzahl gibt die Feldnummer an.

Schauen wir uns nun wieder unser einfaches Beispiel an. Sie wissen jetzt, dass die erste Zahl im Stream immer ein Varint-Schlüssel ist, und hier ist es `08` oder (nachdem das MSB entfernt wurde)

000 1000

Sie nehmen die letzten drei Bits, um den Wire-Typ (0) zu erhalten, und verschieben dann nach rechts um drei, um die Feldnummer (1) zu erhalten. Protoscope repräsentiert einen Tag als Ganzzahl gefolgt von einem Doppelpunkt und dem Wire-Typ, so können wir die obigen Bytes als 1:VARINT schreiben.

Da der Wire-Typ 0 oder VARINT ist, wissen wir, dass wir einen Varint dekodieren müssen, um die Nutzlast zu erhalten. Wie wir oben gesehen haben, dekodieren die Bytes `9601` zu 150, was uns unseren Datensatz liefert. Wir können ihn in Protoscope als 1:VARINT 150 schreiben.

Protoscope kann den Typ für einen Tag ableiten, wenn nach dem : ein Leerzeichen steht. Dies geschieht, indem es das nächste Token vorausschaut und errät, was Sie gemeint haben (die Regeln sind detailliert in Protoscope’s language.txt dokumentiert). Zum Beispiel, in 1: 150, steht unmittelbar nach dem untypisierten Tag ein Varint, daher leitet Protoscope seinen Typ als VARINT ab. Wenn Sie 2: {} schreiben würden, würde es das { sehen und LEN erraten; wenn Sie 3: 5i32 schreiben würden, würde es I32 erraten und so weiter.

Weitere Integer-Typen

Bools und Enums

Bools und Enums werden beide so kodiert, als wären sie int32s. Bools werden insbesondere immer als `00` oder `01` kodiert. In Protoscope sind false und true Aliase für diese Byte-Strings.

Vorzeichenbehaftete Ganzzahlen

Wie Sie im vorherigen Abschnitt gesehen haben, werden alle Protocol Buffer-Typen, die mit Wire-Typ 0 verbunden sind, als Varints kodiert. Varints sind jedoch vorzeichenlos, daher kodieren die verschiedenen vorzeichenbehafteten Typen sint32 und sint64 im Gegensatz zu int32 oder int64 negative Ganzzahlen unterschiedlich.

Die intN-Typen kodieren negative Zahlen als Zweierkomplement, was bedeutet, dass sie als vorzeichenlose 64-Bit-Ganzzahlen ihr höchstes Bit gesetzt haben. Daraus ergibt sich, dass alle zehn Bytes verwendet werden müssen. Zum Beispiel wird -2 von Protoscope in

11111110 11111111 11111111 11111111 11111111
11111111 11111111 11111111 11111111 00000001

Dies ist das Zweierkomplement von 2, definiert in vorzeichenloser Arithmetik als ~0 - 2 + 1, wobei ~0 die 64-Bit-Ganzzahl mit lauter Einsen ist. Es ist eine nützliche Übung zu verstehen, warum dies so viele Einsen ergibt.

sintN verwendet anstelle des Zweierkomplements die "ZigZag"-Kodierung, um negative Ganzzahlen zu kodieren. Positive Ganzzahlen p werden als 2 * p (die geraden Zahlen) kodiert, während negative Ganzzahlen n als 2 * |n| - 1 (die ungeraden Zahlen) kodiert werden. Die Kodierung "zig-zagt" somit zwischen positiven und negativen Zahlen. Zum Beispiel

Vorzeichenbehaftet OriginalKodiert als
00
-11
12
-23
0x7fffffff0xfffffffe
-0x800000000xffffffff

Mit anderen Worten, jeder Wert n wird kodiert mit

(n << 1) ^ (n >> 31)

für sint32s, oder

(n << 1) ^ (n >> 63)

für die 64-Bit-Version.

Wenn die sint32- oder sint64-Werte analysiert werden, werden sie zurück in die ursprüngliche, vorzeichenbehaftete Version dekodiert.

In Protoscope wird durch Anhängen eines z an eine Ganzzahl diese als ZigZag kodiert. Zum Beispiel ist -500z dasselbe wie der Varint 999.

Nicht-Varint-Zahlen

Numerische Nicht-Varint-Typen sind einfach. double und fixed64 haben den Wire-Typ I64, der dem Parser sagt, dass ein fester Acht-Byte-Datenblock erwartet wird. double-Werte werden im IEEE 754-Doppelpräzisionsformat kodiert. Wir können einen double-Datensatz angeben, indem wir 5: 25.4 schreiben, oder einen fixed64-Datensatz mit 6: 200i64.

Ebenso haben float und fixed32 den Wire-Typ I32, der ihnen sagt, dass stattdessen vier Bytes erwartet werden. float-Werte werden im IEEE 754-Einfachpräzisionsformat kodiert. Die Syntax für diese besteht darin, einen i32-Suffix hinzuzufügen. 25.4i32 gibt vier Bytes aus, ebenso wie 200i32. Tag-Typen werden als I32 abgeleitet.

Längenbegrenzte Datensätze

Längenpräfixe sind ein weiteres wichtiges Konzept im Wire-Format. Der Wire-Typ LEN hat eine dynamische Länge, die durch einen Varint direkt nach dem Tag angegeben wird, gefolgt vom üblichen Nutzlast.

Betrachten Sie dieses Nachrichtenschema

message Test2 {
  string b = 2;
}

Ein Datensatz für das Feld b ist eine Zeichenkette, und Zeichenketten sind LEN-kodiert. Wenn wir b auf "testing" setzen, wird es als LEN-Datensatz mit der Feldnummer 2, der die ASCII-Zeichenkette "testing" enthält, kodiert. Das Ergebnis ist `120774657374696e67`. Wenn wir die Bytes aufteilen,

12 07 [74 65 73 74 69 6e 67]

sehen wir, dass der Tag `12` 00010 010 oder 2:LEN ist. Das folgende Byte ist der int32-Varint 7, und die nächsten sieben Bytes sind die UTF-8-Kodierung von "testing". Der int32-Varint bedeutet, dass die maximale Länge einer Zeichenkette 2 GB beträgt.

In Protoscope wird dies als 2:LEN 7 "testing" geschrieben. Es kann jedoch unpraktisch sein, die Länge der Zeichenkette zu wiederholen (die in Protoscope-Text bereits Anführungszeichen enthält). Das Einbetten von Protoscope-Inhalten in geschweifte Klammern generiert ein Längenpräfix dafür: {"testing"} ist eine Kurzform für 7 "testing". {} wird von Feldern immer als LEN-Datensatz abgeleitet, daher können wir diesen Datensatz einfach als 2: {"testing"} schreiben.

bytes-Felder werden auf die gleiche Weise kodiert.

Unter-Nachrichten

Unter-Nachrichten-Felder verwenden ebenfalls den Wire-Typ LEN. Hier ist eine Nachrichten-Definition mit einer eingebetteten Nachricht unseres ursprünglichen Beispiel-Nachrichten-Typs Test1

message Test3 {
  Test1 c = 3;
}

Wenn das Feld a von Test1 (d. h. das Feld c.a von Test3) auf 150 gesetzt ist, erhalten wir ``1a03089601``. Wenn wir es aufteilen,

 1a 03 [08 96 01]

Die letzten drei Bytes (in []) sind exakt dieselben wie in unserem aller ersten Beispiel. Diese Bytes werden von einem Tag vom Typ LEN und einer Länge von 3 vorangestellt, genau wie Zeichenketten kodiert werden.

In Protoscope sind Unter-Nachrichten sehr prägnant. ``1a03089601`` kann als 3: {1: 150} geschrieben werden.

Fehlende Elemente

Fehlende Felder sind einfach zu kodieren: Wir lassen den Datensatz einfach weg, wenn er nicht vorhanden ist. Dies bedeutet, dass "riesige" Protos mit nur wenigen gesetzten Feldern sehr spärlich sind.

Wiederholte Elemente

Ab Edition 2023 werden repeated-Felder eines primitiven Typs (jeder Skalartyp, der nicht string oder bytes ist) standardmäßig "gepackt".

Gepackte repeated-Felder werden nicht als ein Datensatz pro Eintrag kodiert, sondern als ein einzelner LEN-Datensatz, der jeden verketteten Element enthält. Zum Dekodieren werden Elemente aus dem LEN-Datensatz nacheinander dekodiert, bis die Nutzlast erschöpft ist. Der Anfang des nächsten Elements wird durch die Länge des vorherigen bestimmt, die ihrerseits vom Typ des Feldes abhängt. Somit, wenn wir haben

message Test4 {
  string d = 4;
  repeated int32 e = 6;
}

und wir eine Test4-Nachricht mit d auf "hello" und e auf 1, 2 und 3 setzen, könnte dies als `3206038e029ea705` kodiert werden, oder als Protoscope geschrieben,

4: {"hello"}
6: {3 270 86942}

Wenn jedoch das wiederholte Feld auf erweitert (Überschreiben des Standard-Pack-Zustands) gesetzt ist oder nicht packbar ist (Zeichenketten und Nachrichten), dann wird für jeden einzelnen Wert ein Datensatz kodiert. Außerdem müssen Datensätze für e nicht aufeinander folgen und können mit anderen Feldern verschachtelt werden; nur die Reihenfolge der Datensätze für dasselbe Feld zueinander wird beibehalten. Somit könnte dies wie folgt aussehen

6: 1
6: 2
4: {"hello"}
6: 3

Nur wiederholte Felder von primitiven numerischen Typen können als "gepackt" deklariert werden. Dies sind Typen, die normalerweise die Wire-Typen VARINT, I32 oder I64 verwenden würden.

Beachten Sie, dass Parser auch dann mehrere Schlüssel-Wert-Paare akzeptieren müssen, wenn es normalerweise keinen Grund gibt, mehr als ein Schlüssel-Wert-Paar für ein gepacktes wiederholtes Feld zu kodieren. In diesem Fall sollten die Nutzlasten verkettet werden. Jedes Paar muss eine ganze Anzahl von Elementen enthalten. Die folgende ist eine gültige Kodierung derselben obigen Nachricht, die Parser akzeptieren müssen

6: {3 270}
6: {86942}

Protocol Buffer-Parser müssen in der Lage sein, wiederholte Felder zu parsen, die als packed kompiliert wurden, als ob sie nicht gepackt wären, und umgekehrt. Dies ermöglicht es, [packed=true] auf vorhandene Felder in vorwärts- und rückwärtskompatibler Weise anzuwenden.

Oneofs

Oneof-Felder werden genauso kodiert, als ob die Felder nicht in einem oneof wären. Die Regeln, die für oneofs gelten, sind unabhängig davon, wie sie auf dem Draht dargestellt werden.

Der Letzte gewinnt

Normalerweise hätte eine kodierte Nachricht nie mehr als eine Instanz eines Nicht-repeated-Feldes. Parser werden jedoch erwartet, den Fall zu behandeln, in dem sie dies tun. Bei numerischen Typen und Zeichenketten, wenn dasselbe Feld mehrmals vorkommt, akzeptiert der Parser den letzten gesehenen Wert. Bei eingebetteten Nachrichtenfeldern werden mehrere Instanzen desselben Feldes zusammengeführt, als ob mit der Methode Message::MergeFrom – das heißt, alle singulären Skalarfelder im letzteren Fall ersetzen die im ersteren, singuläre eingebettete Nachrichten werden zusammengeführt und repeated-Felder werden verkettet. Die Auswirkung dieser Regeln ist, dass das Parsen der Verkettung zweier kodierter Nachrichten genau dasselbe Ergebnis liefert, als ob Sie die beiden Nachrichten separat geparst und die resultierenden Objekte zusammengeführt hätten. Das heißt, dies

MyMessage message;
message.ParseFromString(str1 + str2);

ist gleichbedeutend mit diesem

MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);

Diese Eigenschaft ist gelegentlich nützlich, da sie es Ihnen ermöglicht, zwei Nachrichten (durch Verkettung) zusammenzuführen, auch wenn Sie ihre Typen nicht kennen.

Maps (Zuordnungen)

Map-Felder sind nur eine Kurzform für eine spezielle Art von wiederholtem Feld. Wenn wir haben

message Test6 {
  map<string, int32> g = 7;
}

ist dies tatsächlich dasselbe wie

message Test6 {
  message g_Entry {
    string key = 1;
    int32 value = 2;
  }
  repeated g_Entry g = 7;
}

Somit werden Maps fast genauso kodiert wie ein repeated-Nachrichtenfeld: als eine Folge von LEN-getaggten Datensätzen mit jeweils zwei Feldern. Die Ausnahme ist, dass die Reihenfolge bei Maps während der Serialisierung nicht garantiert erhalten bleibt.

Gruppen

Gruppen sind ein veraltetes Feature, das nicht verwendet werden sollte, aber sie bleiben im Wire-Format und verdienen eine kurze Erwähnung.

Eine Gruppe ist ein wenig wie eine Unter-Nachricht, aber sie wird durch spezielle Tags und nicht durch ein LEN-Präfix abgegrenzt. Jede Gruppe in einer Nachricht hat eine Feldnummer, die in diesen speziellen Tags verwendet wird.

Eine Gruppe mit der Feldnummer 8 beginnt mit einem 8:SGROUP-Tag. SGROUP-Datensätze haben leere Nutzlasten, daher bezeichnet dies nur den Beginn der Gruppe. Sobald alle Felder in der Gruppe aufgeführt sind, kennzeichnet ein entsprechender 8:EGROUP-Tag deren Ende. EGROUP-Datensätze haben ebenfalls keine Nutzlast, so dass 8:EGROUP der gesamte Datensatz ist. Gruppenfeldnummern müssen übereinstimmen. Wenn wir 7:EGROUP antreffen, wo wir 8:EGROUP erwarten, ist die Nachricht fehlerhaft.

Protoscope bietet eine praktische Syntax zum Schreiben von Gruppen. Anstatt zu schreiben

8:SGROUP
  1: 2
  3: {"foo"}
8:EGROUP

Protoscope erlaubt

8: !{
  1: 2
  3: {"foo"}
}

Dies generiert die entsprechenden Start- und Endgruppenmarker. Die !{}-Syntax kann nur unmittelbar nach einem untypisierten Tag-Ausdruck wie 8: auftreten.

Feldreihenfolge

Feldnummern können in einer .proto-Datei in beliebiger Reihenfolge deklariert werden. Die gewählte Reihenfolge hat keinen Einfluss darauf, wie die Nachrichten serialisiert werden.

Wenn eine Nachricht serialisiert wird, gibt es keine garantierte Reihenfolge, wie ihre bekannten oder unbekannten Felder geschrieben werden. Die Serialisierungsreihenfolge ist ein Implementierungsdetail, und die Details einer bestimmten Implementierung können sich in Zukunft ändern. Daher müssen Protocol Buffer-Parser Felder in beliebiger Reihenfolge parsen können.

Implikationen

  • Verlassen Sie sich nicht auf die Stabilität der Byte-Ausgabe einer serialisierten Nachricht. Dies gilt insbesondere für Nachrichten mit transitiven Byte-Feldern, die andere serialisierte Protocol Buffer-Nachrichten darstellen.
  • Standardmäßig produzieren wiederholte Aufrufe von Serialisierungsmethoden auf derselben Protocol Buffer-Nachrichteninstanz möglicherweise nicht dieselbe Byte-Ausgabe. Das heißt, die Standardserialisierung ist nicht deterministisch.
    • Die deterministische Serialisierung garantiert nur dieselbe Byte-Ausgabe für eine bestimmte Binärdatei. Die Byte-Ausgabe kann sich zwischen verschiedenen Versionen der Binärdatei ändern.
  • Die folgenden Prüfungen können für eine Protocol Buffer-Nachrichteninstanz foo fehlschlagen
    • foo.SerializeAsString() == foo.SerializeAsString()
    • Hash(foo.SerializeAsString()) == Hash(foo.SerializeAsString())
    • CRC(foo.SerializeAsString()) == CRC(foo.SerializeAsString())
    • FingerPrint(foo.SerializeAsString()) == FingerPrint(foo.SerializeAsString())
  • Hier sind einige Beispielszenarien, in denen logisch äquivalente Protocol Buffer-Nachrichten foo und bar zu unterschiedlichen Byte-Ausgaben serialisiert werden können
    • bar wird von einem alten Server serialisiert, der einige Felder als unbekannt behandelt.
    • bar wird von einem Server serialisiert, der in einer anderen Programmiersprache implementiert ist und Felder in einer anderen Reihenfolge serialisiert.
    • bar hat ein Feld, das nicht-deterministisch serialisiert wird.
    • bar hat ein Feld, das eine serialisierte Byte-Ausgabe einer Protocol Buffer-Nachricht speichert, die anders serialisiert wird.
    • bar wird von einem neuen Server serialisiert, der Felder aufgrund einer Implementierungsänderung in einer anderen Reihenfolge serialisiert.
    • foo und bar sind Verkettungen derselben einzelnen Nachrichten in unterschiedlicher Reihenfolge.

Begrenzungen der kodierten Proto-Größe

Protos müssen nach der Serialisierung kleiner als 2 GiB sein. Viele Proto-Implementierungen weigern sich, Nachrichten zu serialisieren oder zu parsen, die diese Grenze überschreiten.

Kompakte Referenzkarte

Das Folgende bietet die wichtigsten Teile des Wire-Formats in einem leicht nachschlagbaren Format.

message    := (tag value)*

tag        := (field << 3) bit-or wire_type;
                encoded as uint32 varint
value      := varint      for wire_type == VARINT,
              i32         for wire_type == I32,
              i64         for wire_type == I64,
              len-prefix  for wire_type == LEN,
              <empty>     for wire_type == SGROUP or EGROUP

varint     := int32 | int64 | uint32 | uint64 | bool | enum | sint32 | sint64;
                encoded as varints (sintN are ZigZag-encoded first)
i32        := sfixed32 | fixed32 | float;
                encoded as 4-byte little-endian (float is IEEE 754
                single-precision); memcpy of the equivalent C types (u?int32_t,
                float)
i64        := sfixed64 | fixed64 | double;
                encoded as 8-byte little-endian (double is IEEE 754
                double-precision); memcpy of the equivalent C types (u?int64_t,
                double)

len-prefix := size (message | string | bytes | packed);
                size encoded as int32 varint
string     := valid UTF-8 string (e.g. ASCII);
                max 2GB of bytes
bytes      := any sequence of 8-bit bytes;
                max 2GB of bytes
packed     := varint* | i32* | i64*,
                consecutive values of the type specified in `.proto`

Siehe auch die Protoscope-Sprachreferenz.

Schlüssel

nachricht := (tag wert)*
Eine Nachricht wird als eine Sequenz von null oder mehr Paaren von Tags und Werten kodiert.
tag := (feld << 3) bit-weise ODER wire_typ
Ein Tag ist eine Kombination aus einem wire_typ, der in den niedrigsten drei Bits gespeichert ist, und der Feldnummer, die in der .proto-Datei definiert ist.
wert := varint für wire_typ == VARINT, ...
Ein Wert wird je nach dem im Tag angegebenen wire_typ unterschiedlich gespeichert.
varint := int32 | int64 | uint32 | uint64 | bool | enum | sint32 | sint64
Sie können Varint verwenden, um alle aufgeführten Datentypen zu speichern.
i32 := sfixed32 | fixed32 | float
Sie können fixed32 verwenden, um alle aufgeführten Datentypen zu speichern.
i64 := sfixed64 | fixed64 | double
Sie können fixed64 verwenden, um alle aufgeführten Datentypen zu speichern.
len-prefix := größe (nachricht | zeichenkette | bytes | gepackt)
Ein längenpräfixierter Wert wird als Länge (als Varint kodiert) und dann als einer der aufgeführten Datentypen gespeichert.
zeichenkette := gültige UTF-8-Zeichenkette (z. B. ASCII)
Wie beschrieben, muss eine Zeichenkette die UTF-8-Zeichenkodierung verwenden. Eine Zeichenkette kann nicht größer als 2 GB sein.
bytes := beliebige Sequenz von 8-Bit-Bytes
Wie beschrieben, können Bytes benutzerdefinierte Datentypen mit einer Größe von bis zu 2 GB speichern.
gepackt := varint* | i32* | i64*
Verwenden Sie den Datentyp packed, wenn Sie aufeinanderfolgende Werte des im Protokolldefinition beschriebenen Typs speichern. Der Tag wird für Werte nach dem ersten weggelassen, was die Kosten für Tags auf einen pro Feld statt pro Element amortisiert.