REST, GraphQL und gRPC Vergleich Teil 7: Das Datenformat

Von: Thomas Bayer
Datum: 26. April 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.

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.

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.

protobuf erleichtert die Versionierung von Schnittstellen, indem unbekannte Felder bei der Deserialisierung einfach ignoriert werden. Es können sogar Feldnamen umbenannt werden, ohne die Schnittstelle zu brechen.

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
  • Der Entwickler muss sich nicht um das Netzwerk kümmern(Abstraktion)
  • Die Versionierung von Schnittstellen wird erleichtert
  • Ungeeignet für Ad-Hoc Integration

Den Vorteilen stehen die folgenden Nachteile gegenüber:

      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.

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.

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.

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

Vorteile

  • Jedes Format kann verwendet werden
  • 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

Nachteile

  • Kein vorgegebenes Format

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