Rechnernetze / Kommunikationssysteme
Protokollpuffer (Protobuf)
Eigenschaften
- plattformübergreifendes binäres Serialisierungsformat
- effizienten Datenübertragung und -speicherung
- Vorwärts- und Rückwärtskompatibilität bei Beachtung bestimmter Randbedingungen
- Vorwärts: Ältere Clients ignorieren unbekannte Feld‑Nummern (sie überspringen das Payload anhand des Wire‑Types).
- Rückwärts: Neuere Clients können optionale Felder weglassen; fehlende Felder erhalten ihren Default‑Wert
- keine feste Feldreihenfolge: Decoder sucht nach Tags
- deterministische Serialisierung mit deterministic-Option im Encoder (für Hash und Signaturanwendungen)
- Nutzung einer Interface Definition Language (IDL) zur Definition von Datentypen und -formaten (Endung .proto)
- Protobuf-Compiler generiert aus IDL Quellcode für verschiedene Programmiersprachen
- Java-Tutorial
Wire-Types
- (0) VARINT (int32, int64, uint32/64, sint32/64, bool, enum) 1-9 Bytes
- (5) I32 (fixed32, float) 4 Bytes
- (1) I64 (fixed64, double) 8 Bytes
- (2) LEN (string, bytes, embedded messages, packed repeated fields) VARINT + Bytes
Codierung
- Feld-Tag (Feldnummer + Wiretyp) als VARINT
- Payload (Wiretype)
- Message-Grenze
- Byte-Reihenfolge: litte endian
- Beispiel String “Bob” für Feld Nr. 2
Tag für Feld 2, Wire‑Type 2 → (2 << 3) | 2 = 0x12 Length = 3 → 0x03 Payload = ASCII "Bob" → 0x42 0x6F 0x62 Resultat: 12 03 42 6F 62
Beispiel 1
Definition
syntax = "proto3";
message Person {
int32 id = 1; // varint, signed
string name = 2; // length‑delimited
repeated int32 tags = 3 [packed=true];// packed varint
bytes data = 4; // raw bytes
}
Instanz
Person p = Person.newBuilder()
.setId(150) // >127 → 2‑Byte‑Varint
.setName("Ada")
.addAllTags(Arrays.asList(1, 2, 300))
.setData(ByteString.copyFrom(new byte[]{0x01,0x02,0x03}))
.build();
Binäre Darstellung (Hex)
Feld | Tag (hex) | Payload (hex) | Erklärung |
---|---|---|---|
id | 08 | 96 01 | Tag = (1<<3|0)=0x08, Varint 150 = 0x96 0x01 |
name | 12 | 03 41 64 61 | Tag = (2<<3|2)=0x12, Länge=3, ASCII “Ada” |
tags (packed) | 1A | 04 01 02 AC 02 | Tag = (3<<3|2)=0x1A, Länge=4, Varints 1,2,300 |
data | 22 | 03 01 02 03 | Tag = (4<<3|2)=0x22, Länge=3, Roh‑Bytes |
Komplette Byte‑Sequenz: 08 96 01 12 03 41 64 61 1A 04 01 02 AC 02 22 03 01 02 03
Decoder
- Lese Tag‑Varint →
0x08
→ Feld 1, Wire‑Type 0 →int32
. - Lese Payload gemäß Wire‑Type 0 → Varint
0x96 0x01
→ 150. - Nächster Tag →
0x12
→ Feld 2, Wire‑Type 2 → length‑delimited. - Lese Länge →
0x03
→ 3 Bytes → “Ada”. - … und so weiter, bis das Ende des Byte‑Arrays erreicht ist.
Beispiel 2
Definition
syntax = "proto3";
package demo;
// ---------- 1. Die innere Nachricht ----------
message Address {
string street = 1; // z. B. "Hauptstraße 12"
string city = 2; // z. B. "Berlin"
int32 zip = 3; // z. B. 12345
}
// ---------- 2. Die äußere Nachricht ----------
message Person {
int32 id = 1; // varint
string name = 2; // length‑delimited
Address home_address = 3; // **embedded message**
repeated PhoneNumber phones = 4; // repeated embedded message (siehe unten)
// Noch ein eingebettetes Message‑Beispiel für Wiederholungen
message PhoneNumber {
string number = 1; // z. B. "+49‑30‑123456"
bool mobile = 2; // true → Mobil, false → Festnetz
}
}
Darstellung
Person p = Person.newBuilder()
.setId(42)
.setName("Ada")
.setHomeAddress(Address.newBuilder()
.setStreet("Hauptstraße 12")
.setCity("Berlin")
.setZip(12345)
.build())
.addPhones(Person.PhoneNumber.newBuilder()
.setNumber("+49-30-123456")
.setMobile(true)
.build())
.addPhones(Person.PhoneNumber.newBuilder()
.setNumber("+49-30-654321")
.setMobile(false)
.build())
.build();
Tags
Feld (outer) | Nummer | Wire‑Typ | Tag (hex) | Erklärung | |
---|---|---|---|---|---|
id | 1 | 0 (varint) | 08 |
(1 « 3) | 0 = 0x08 |
name | 2 | 2 (length‑delimited) | 12 |
(2 « 3) | 2 = 0x12 |
home_address | 3 | 2 (length‑delimited) | 1A |
(3 « 3) | 2 = 0x1A |
phones (repeated) | 4 | 2 (length‑delimited) | 22 |
(4 « 3) | 2 = 0x22 |
2.2 Kodierung der inneren Message Address
Feld (inner) | Nummer | Wire‑Typ | Tag (hex) | Payload (hex) |
---|---|---|---|---|
street | 1 | 2 | 0A |
Länge = 13 → 0D + ASCII "Hauptstraße 12" → 48 61 75 70 74 73 74 72 61 73 73 65 20 31 32 |
city | 2 | 2 | 12 |
Länge = 6 → 06 + "Berlin" → 42 65 72 6C 69 6E |
zip | 3 | 0 | 18 |
Varint = 12345 → B9 60 (0xB9 0x60) |
Payload‑Bytes der Address (Tag + Length + Wert‑Sequenz):
0A 0D 48 61 75 70 74 73 74 72 61 73 73 65 20 31 32
12 06 42 65 72 6C 69 6E
18 B9 60
Das sind 23 Bytes.
2.3 Länge‑Feld für home_address
Der äußere Tag 1A
wird gefolgt von einer Length‑Varint, die die Größe des gesamten Address
‑Payload angibt:
1A 17 // 0x17 = 23 Dezimal
Damit lautet das komplette Segment für home_address
:
1A 17
0A 0D 48 61 75 70 74 73 74 72 61 73 73 65 20 31 32
12 06 42 65 72 6C 69 6E
18 B9 60
2.4 Kodierung der repeated Message PhoneNumber
Jedes Element wird einmal als length‑delimited Feld mit Tag 22
kodiert.
2.4.1 Erstes PhoneNumber‑Objekt
Feld | Nummer | Wire‑Typ | Tag | Payload |
---|---|---|---|---|
number | 1 | 2 | 0A |
Länge = 13 → 0D + "+49-30-123456" → 2B 34 39 2D 33 30 2D 31 32 33 34 35 36 |
mobile | 2 | 0 | 10 |
Bool = true → Varint = 1 → 01 |
Payload (inner):
0A 0D 2B 34 39 2D 33 30 2D 31 32 33 34 35 36
10 01
Das sind 19 Bytes.
Äußeres Segment (Tag + Length + Payload):
22 13 // 0x13 = 19 Dezimal
0A 0D 2B 34 39 2D 33 30 2D 31 32 33 34 35 36
10 01
2.4.2 Zweites PhoneNumber‑Objekt (mobile = false)
Payload:
0A 0D 2B 34 39 2D 33 30 2D 36 35 34 33 32 31 // "+49-30-654321"
10 00 // false → 0
Wieder 19 Bytes, also:
22 13
0A 0D 2B 34 39 2D 33 30 2D 36 35 34 33 32 31
10 00
2.5 Vollständige Byte‑Sequenz der äußeren Message Person
Teil | Hex‑Bytes |
---|---|
id | 08 2A (42 → 0x2A) |
name | 12 03 41 64 61 (“Ada”) |
home_address | 1A 17 0A 0D 48 61 75 70 74 73 74 72 61 73 73 65 20 31 32 12 06 42 65 72 6C 69 6E 18 B9 60 |
phones (1) | 22 13 0A 0D 2B 34 39 2D 33 30 2D 31 32 33 34 35 36 10 01 |
phones (2) | 22 13 0A 0D 2B 34 39 2D 33 30 2D 36 35 34 33 32 31 10 00 |
Komplett (in einer Zeile)
08 2A
12 03 41 64 61
1A 17 0A 0D 48 61 75 70 74 73 74 72 61 73 73 65 20 31 32 12 06 42 65 72 6C 69 6E 18 B9 60
22 13 0A 0D 2B 34 39 2D 33 30 2D 31 32 33 34 35 36 10 01
22 13 0A 0D 2B 34 39 2D 33 30 2D 36 35 34 33 32 31 10 00
Decoder
Ein protobuf‑Decoder arbeitet sequentiell:
- Lese Tag‑Varint → bestimme Feld‑Nummer und Wire‑Typ.
- Falls Wire‑Typ = 2 (length‑delimited) → lese Length‑Varint, dann die nächsten Length Bytes als Payload.
- Interpretieren des Payloads:
- Wenn das Feld ein embedded message ist, wird der Payload‑Byte‑Stream erneut durch den Decoder geschickt, diesmal mit dem Schema des inneren Message‑Typs.
- Wenn das Feld ein repeated embedded message ist, wird Schritt 2 für jedes Vorkommen wiederholt (der Decoder erkennt das gleiche Tag‑Byte erneut).
Damit kann ein Decoder beliebig tiefe Verschachtelungen verarbeiten, ohne vorher zu wissen, wie tief die Struktur ist – er folgt einfach den Tags.
Protokoll Buffer mit CRC
Wichtige Punkte:
- Serialisiere das Message deterministisch (
deterministic=true
). - Nimm nur das reine Message‑Byte‑Array (ohne äußere Längen‑Prefixe).
- Wähle einen CRC‑Algorithmus (CRC‑32, CRC‑32C, CRC‑64 …) – stelle sicher, dass Sender und Empfänger denselben benutzen.
- Berechne die CRC über das Byte‑Array (einfach
crc.update(bytes)
odercrc32(bytes)
). - Transportiere die CRC (z. B. als separates Feld, Trailer, Header oder in einer Wrapper‑Message).
- Beim Empfänger:
- Deserialisiere das Byte‑Array (ohne die CRC‑Bytes).
- Berechne die CRC erneut und vergleiche mit dem empfangenen Wert.
- Bei Gleichheit ist das Message‑Payload höchstwahrscheinlich unverändert.
Minimal‑Beispiel (Pseudo‑Code, sprachunabhängig)
function encodeWithCrc(message):
bytes = protobuf_serialize(message, deterministic=True)
crc = crc32c(bytes) // oder crc32, crc64 …
return bytes + encode_uint32(crc) // CRC als 4‑Byte‑Trailer
function decodeWithCrc(blob):
payload = blob[0 : len(blob)-4]
received_crc = decode_uint32(blob[-4:])
if crc32c(payload) != received_crc:
raise ChecksumError
return protobuf_parse(payload)
Fragen
- Welche Vorteile bieten Protobufs gegenüber XML und JSON
Letzte Änderung: 17. September 2025 13:48