REST, GraphQL und gRPC Vergleich Teil 3: Das Datenformat

Von: Thomas Bayer
Datum: 26. April 2021
Aktualisiert: 5. Juni 2021

REST, GraphQL und gRPC unterscheiden sich grundlegend in den Formaten, die verwendet werden, um Daten über das Netzwerk zu übertragen.

Dieser Teil der Artikelserie beschreibt die Datenformate der drei Alternativen. Weitere Teile dieses Artikels behandeln die folgenden Themen:

1. gRPC

gRPC nutz Googles Protocol Buffers-Mechanismus für die Serialisierung von strukturierten Daten. Aus einer protobuf Schnittstellenbeschreibung werden Klassen oder Structs generiert, deren Objekte die Daten aufnehmen, die zwischen Client und Server ausgetauscht werden. Der Code unten zeigt die Beschreibung einer Message mit dem Namen Ware.

message Ware {
  int32 id = 1;
  string name = 2;
}
Listing 1: Beschreibung einer Nachricht mit der protobuf-Beschreibungssprache

Der Protocol Buffers-Kompiler kann die Beschreibung in eine Klasse oder ein Struct übersetzen. protobuf ist unabhängig von der Plattform und der Programmiersprache. Eine Nachricht könnte beispielsweise mit Go erzeugt und mit Java eingelesen werden.

Im Beispiel unten wird das aus der Beschreibung generiete Struct Ware verwendet, um daraus ein Objekt zu erzeugen. Danach wird das Objekt beim Aufruf der remote Funktion CreateWare übergeben:

c.CreateWare(ctx, &pb.Ware{Id: 4, Name: "Lutscher"})

Im Hintergrund wird der Struct in eine Bytefolge serialisiert, wie unten im Hexeditor dargestellt.

Protocol Buffers Nachricht im Hexeditor

Abbildung : protobuf-Nachricht im Hexeditor

Für die Decodierung wird die Formatbeschreibung benötigt, da in der Nachricht selbst keine Feldnamen enthalten sind. Die Zuordnung erfolgt über die Feldnummern, die in der Definition hinter den Gleichheitszeichen stehen. Feldnummern benötigen weniger Speicherplatz und lassen sich effizienter übertragen als Feldnamen.

Die Abbildung unten zeigt wie das Feld Name codiert wird. Das erste Byte wird in eine Feldnummer und einen Teil mit Informationen über die Codierung des Feldes aufgeteilt. Im Beispiel handelt es sich um das Feld Nr. 2 wie oben im Listing mit der Formatbeschreibung erkennbar. Die Codierung entspricht dem Wire Type 2, der für Zeichenketten und Bytefolgen verwendet wird. Wird der Typ 2 verwendet, so enthält das folgende Byte die Länge des Feldwertes. Hier im Beispiel eine 8 für acht Bytes.

Codierung eines protocol buffer Feldes

Abbildung : Codierung eines protobuf-Feld

Aus dem Beispiel könnte man schließen, dass die maximale Länge 256 Bytes beträgt oder dass es nur 2 hoch 5 = 32 Felder geben kann. protobuf verwendet wie die UTF-8 Codierung einen Trick, um größere Zahlen abbilden zu können: Wenn das 8 Bit gesetzt ist, ist die Zahl größer als 8 Bit. Da die meisten Zeichenketten kleiner sind als 128 Zeichen kann genügt meist ein Byte für die Lengenangaben. Für größere Zeichenketten werden dann 2 oder mehr Bytes für die Längenangabe verwendet. Welche Auswirkungen diese Maßnahmen zur Reduzierung der Nachrichtengröße haben wird im Teil zum Thema Performanz dieses Artikels beschrieben.

Nach der Serialisierung werden die Bytes über das Netzerk verschickt und auf der Seite des Empfängers deserialisiert. Der Entwickler muss sich nicht um die Serialisierung kümmern.

1.1. Alternativen zu protobuf

Das protobuf-Binärformat ist bei gRPC der Standard. Neben protobuf können weitere Datenformate wie z.B. Avro, Thrift oder JSON verwendet werden. gRPC ist offen und erweiterbar für andere Formate. Wer ein anderes Format als protobuf nutzen möchte, der muss Marshaller und Unmarshaller, die den Datenstrom serialisieren und deserialisieren, selbst programmieren. Wer in der Praxis ein anderes Format als protobuf einsetzen möchte, wird es nicht so einfach haben wie mit REST.

1.2. Nachrichtenstil

Im Gegensatz zu JSON sind protobuf Nachrichten keine Dokumente d.h. eine Nachricht kann nicht ohne weiteres eingesehen werden. Eine JSON-Nachricht kann problemlos in einem Editor betrachtet werden und ist für Menschen leicht verständlich. Eine binäre Nachricht im protobuf Format kann nicht spontan eingesehen oder verändert werden ohne die Informationen aus der Formatbeschreibung zu verwenden, da in der Nachricht selbst keine Feldnamen enthalten sind. Fehlersuche und Tracing gestalten sich daher bei gRPC aufwendiger als bei REST oder GraphQL.

1.3. Versionierung

protobuf erleichtert die Versionierung von Schnittstellen, indem unbekannte Felder bei der Deserialisierung einfach ignoriert werden. Selbst Feldnamen können umbenannt werden, ohne die Schnittstelle zu brechen, da die Zuordnung anhand der Feldnummer anstatt des Feldnamens erfolgt. Die Abbildung unten zeigt zwei Versionen einer protobuf-Message. Es wurden Felder umbenannt, gelöscht und neue hinzugefügt. Dennoch kann protobuf die Felder 1, 2 und 4 zuordnen.

gRPC Versioning

Abbildung : Zwei Versionen einer protobuf Message

1.4. Fazit

Das von gRPC verwendete protobuf-Format hat die folgenden Vorteile:

  • Unabhängigkeit von der Plattform und Programmiersprache
  • Kompakte Nachrichtengröße
  • Schnelle und Ressourcen schonende Serialsierung und Deserialisierung
  • Es gibt ein Typ-System sowie ein Language Mapping für (Go, Java, C++,…)
  • Der Entwickler muss sich nicht um das Netzwerk kümmern(Abstraktion)
  • Die Versionierung von Schnittstellen wird erleichtert

Den Vorteilen stehen die folgenden Nachteile gegenüber:

  • Das Format ist komplex und für Menschen nicht lesbar.
  • Nachrichten können nur mit einer Formatbeschreibung gelesen werden.
  • Nicht jede Programmiersprache wird von gRPC unterstützt.
  • Müssen Dateien oder andere Formate übertragen werden, so muss dies mühsam in Bytearrays oder in Chunks erfolgen.
  • Ungeeignet für Ad-Hoc Integration

2. GraphQL

Für Anfragen und Antworten wird bei GraphQL das JSON-Format verwendet. Der HTTP-Request unten zeigt ein Beispiel für eine GraphQL-Anfrage im JSON Format. Das query-Feld enthält die eigentliche Anfrage.

POST /fruit-shop-graphql HTTP/1.1
Content-Type: application/json

{
  "query": "{ products(name:"Coconut") { name price }}"
}
Listing 2: GraphQL Request im JSON Format

Alternativ zu JSON gibt es ein kompakteres Format, bei dem nur die Abfrage selbst übertragen wird. Die Abfrage von oben sieht dann so aus:

POST /fruit-shop-graphql HTTP/1.1
Content-Type: application/graphql

{
  products(name:"Coconut") {
    name
    price
  }
}
Listing 3: GraphQL Request im application/graphql Format

Für die Abfrage wurde ein JSON ähnliches Format verwendet, das nicht mit JSON kompatibel ist. Als Content-Type muss dann application/graphql verwendet werden. Wird dieses Format mit JSON übertragen, so muss es als Zeichenkette als Wert eines Feldes wie oben im Beispiel übertragen werden. Es wurde für die Abfrage bewußt kein JSON verwendet, sondern ein eigenes an JSON angelehntes Format, mit dem Abfragen kompakter und lesbarer ausgedrückt werden können.

2.1. Binärdaten

GraphQL verfügt über keine Unterstützung für Binärformate. Ein Workaround ist die Übertragung von Binärinhalten als Base64 encodierte Strings wie unten dargestellt:

{ "icon": "RcKnNGFhc2RmYXNmZGFmc2ZhZmEzcnF2MzJrNG92aTEyNTltdTUybXF1dm0yODN6NDUxejM1NGV3Cg==" }

Dieses Verfahren ist umständlich und von der Performanz und dem Ressourcenverbrauch nicht ideal. Eine schönere Lösung wären Verweise auf externe Ressourcen über eine URL wie im Beispiel unten:

{ "icon_url": "https://predic8.de/logo6.png" }

Da der Zugriff auf die URL über HTTP ohne GraphQL erfolgt, ist die Frage, ob man das als GraphQL bezeichnen kann oder ob das nicht GraphQL kombiniert mit REST ist. An einer Kombination von beiden Technologien ist aber nichts auszusetzen. Da beide auf HTTP basieren ist eine Kombination auch ohne Weiteres möglich.

2.2. Fazit

Das von GraphQl verwendete Format hat die folgenden Vorteile:

  • Das Format ist vorgegeben
  • Es gibt ein Typ-System

Durch das Format ergeben sich die Nachteile:

  • Andere Formate als JSON werden nicht unterstützt
  • Fehlende Unterstützung für binäre Inhalte

3. REST

Der Buchstabe R in REST steht für Representational. Eine Representation ist eine Kopie einer Ressource und hat immer ein Datenformat. Wie am Namen zu erkennen ist, spielen bei REST Formate eine zentrale Rolle.

Das Dateiformat für die Nachrichten ist bei REST frei wählbar. Über das Content-Type Header-Feld wird dem Empfänger der Datentyp einer Nachricht mitgeteilt. Die Nachricht im Beispiel unten enthält im Body ein JSON Dokument und zeigt dies mit einem Content-Type mit dem Wert application/json an.

POST /shop/products/ HTTP/1.1
Content-Type: application/json

{
    "name": "Mango",
	"preis": 1.70
}
Listing 4: Content-Type Header mit dem Datenformat

Welche Formate zum Einsatz kommen, kann zwischen Client und Server verhandelt werden. HTTP bietet dafür ein Verfahren das sich Content-Negociation nennt. Im Beispiel unten drückt der Client mit dem Accept Header aus, dass er JSON preferiert, aber zur Not sich auch mit XML zufrieden gibt.

GET /shop/products/23 HTTP/1.1
Accept: application/json,application/xml;q=0.1
Listing 5: Accept-Type Header mit den unterstützen Mime-Types

Oft wird für REST ein Textformat verwendet. JSON ist populär, es können aber andere Formate wie z.B. XML und CSV ebenfalls verwendet werden.

Die freie Wahl des Formats hat den Vorteil, dass z.B. PDF-Dokumente oder Bilder ohne zusätzliche Konvertierung übertragen werden können. Bei GraphQL und gRPC sind die Formate und Datentypen vorgeschrieben. GraphQL verwendet ein JSON-ähnliches Textformat. Bei gRPC werden die Daten mit Protocol Buffer serialisiert und anschließend als binäre Dateien verschickt. Durch die Verwendung des binären Formats wird die Größe der Nachrichten reduziert.

3.1. Binäre Inhalte

Binäre Inhalte können mit HTTP direkt ohne eine Codierung im Textformat übertragen werden. Das Listing unten zeigt die Übertragung eines Bildes im JPeg-Format. Wie an den Sonderzeichen erkennbar werden alle 8 Bit genutzt, was an den nicht druckbaren Zeichen erkennbar ist.

POST /shop/products/7/photo HTTP/1.1
Content-Type: image/jpeg
Content-Length: 19323

�H�|$(H$�H�L$HH�$��$�L�	...
Listing 6: Accept-Type Header mit den unterstützen Mime-Types

3.2. Hypermedia

Bei REST wird eine Anfrage immer an eine Ressource geschickt, die über eine URI adressierbar ist. Die URI kann einfach als Link in Dokumenten hinterlegt werden. Dadurch, dass jede Ressource ihre eigene Adresse hat, ermöglicht REST die Verwendung von Hypermedia.

Die Abbildung unten zeigt die Repräsentation einer Ware, die auf ihren Hersteller über einen Link verweist. Der Client kann den Link verfolgen und einen GET-Request an die Ressource des Herstellers schicken.

REST und Hypermedia

Abbildung : Hypermedia mit REST

3.3. Fazit

Bei REST ergeben sich die folgenden Vorteile aus dem Format:

  • Jedes Format kann verwendet werden ( JSON, OData, XML, PDF, CSV, ...)
  • In einer Schnittstelle können mehrere Formate verwendet werden
  • Funktionen von HTTP stehen zur Verfügung: Chunking, Caching, Content Negociation
  • Gut geeignet für Binäre Inhalte
  • Hypermedia Unterstützung

Nachteile

  • Kein vorgegebenes Format
  • Kein Typ-System und kein „Standard“-Mapping auf Objekte

4. Zusammenfassung

Die Tabelle unten führt die wichtigsten Unterschiede, die sich aus dem Nachrichtenformat ergeben zwischen REST, GraphQL und gRPC auf.

GRPC
GRPC
GRPC
Format protobuf Request: JSON oder JSON artig
Response: JSON
Beliebig: JSON, YAML, XML, CSV, PDF, ...
Art des Formates Binärformat Textformat beliebig, meist Textformat
Nachrichtenstil Parameterliste Dokument beliebig z.B. Dokument mit JSON
Für Menschen lesbar ❌/✅
Binärdaten bytes von Hand Base64 oder Link Binäre 8 Bit Inhalte
Effizienz (Speicher,Serialisierung) 👍
Hypermedia Unterstützung
Komplexität hoch gering mittel
Abstraktion hoch mittel gering
Typ-System
Language Mapping (Go, C#, Java, C++, …) (✅) über Bibliotheken

Wird REST in Verbindung mit einer Schnittstellenbeschreibung z.B. OpenAPI eingesetzt, so relativieren sich die Punkte Typ-System oder Abstraktion.

Quellen

  1. gRPC + JSON, Carl Mastrangelo (Google), gRPC Blog am 15. August 2018
  2. Sending files via gRPC von Ciro S. Costa am 2 Januar 2018