Mit Java auf dem HTTP/2-Zug

Seite 2: Frames, Messages und Streams

Inhaltsverzeichnis

HTTP/2 bringt zunächst eine harte Einschränkung bezüglich der Verbindungsressourcen. Pro Gegenstelle, also pro Server, darf der Client genau eine TCP-Verbindung öffnen. Das klingt zunächst nach einer Verschlechterung gegenüber HTTP/1.1, bei dem das Öffnen mehrerer TCP-Verbindungen die Grundlage der parallelisierten Datenübertragung war. Das neue Protokoll bringt jedoch die entscheidende Verbesserung des Multiplexing. Über die eine zulässige TCP-Verbindung lassen sich simultan beinahe beliebig viele HTTP-Konversationen, sogenannte Streams, zwischen Client und Server abwickeln. RFC 7540 empfiehlt, die Anzahl simultaner Streams pro TCP-Verbindung nicht unter einen Wert von 100 zu konfigurieren, um den Parallelisierungsgrad nicht zu sehr einzuschränken.

In der Konsequenz sind alle zuvor genannten Workarounds hinfällig, die mit HTTP/1.1 nötig waren. Das Verwenden mehrerer TCP-Verbindungen ist nicht mehr möglich und das Zusammenfassen von Bild-, Skript- und CSS-Dateien nicht mehr nötig. Ebenso sind unterschiedliche Hostnamen auf der Serverseite zum Ermöglichen der Parallelisierung überflüssig.

Dabei bleibt die HTTP-Semantik unverändert. Die bekannten HTTP-Schlüsselwörter wie GET, POST, PUT, DELETE oder OPTIONS bleiben erhalten. Auf der Anwendungsschicht ändert sich nichts. Neu ist die Abwicklung des Transports der Header und Nutzdaten. HTTP-Requests und -Responses sind in Streams, Messages und Frames kodiert. Das HTTP/2-Protokoll besteht aus folgenden Bausteinen:

  • Stream: Ein bidirektionaler Austausch von Daten zwischen Server und Client, der aus einer oder mehreren Messages besteht. Die Übertragung eines Streams erfolgt innerhalb einer TCP-Verbindung. Mehrere Streams lassen sich simultan über dieselbe TCP-Verbindung übertragen.
  • Message: Eine komplette Abfolge von Frames, die zu einer logischen Message gehören. Eine Message gehört zu einem Stream.
  • Frame: Die kleinste Kommunikationseinheit innerhalb von HTTP/2. Sie enthält binär kodierte Header- oder Nutzdaten.

Multiplexing durch die Verwendung von Frames in HTTP/2 (Abb. 3)

Abbildung 3 zeigt eine beispielhafte Client-Server-Kommunikation. Alles läuft über genau eine TCP-Verbindung ab. Die dabei versendeten sechs Frames erläutert folgende Auflistung:

  1. Der Client beginnt, indem er einen Request für die Datei "index.html" per HEADERS-Frame an den Server schickt. Dafür eröffnet er einen neuen Stream – einen in sich abgeschlossenen Nachrichtenaustausch mit dem Server –, der die ID 41 erhält. Der Client setzt weiterhin zwei Flags in dem Frame. +END_HEADERS bedeutet, dass der Client im aktuellen Stream keine weiteren HEADERS-Frames mehr versenden wird. +END_STREAM bedeutet, dass der Client nicht beabsichtigt, weitere Frames im aktuellen Stream zu versenden.
  2. Der Server kann die Datei "index.html" ausliefern und schickt innerhalb desselben Streams 41 ebenfalls einen HEADERS-Frame mit dem Statuscode 200 an den Client zurück. Der Code ist bereits aus HTTP/1.1 bekannt und besagt, dass die Datei verfügbar ist und der Client sie demnächst erhalten wird. Neu ist, dass der Server die eigentliche Nutzlast in Form der Datei "index.html" mit einem separaten DATA-Frame verschickt.
  3. Der Server schickt einen PUSH_PROMISE-Frame, eine Neuerung, mit der er von sich aus eine Datenübertragung an den Client initiiert. Im Fall des Beispiels von Abbildung 3 antizipiert der Server, dass der Client direkt nach seinem Request für die Datei "index.html" die Datei "style.css" benötigen wird. Deshalb bietet er an, diese Datei über den neuen Stream 42 an den Client zu versenden.
  4. Wie bei Punkt 2 schickt der Server nun im Stream 42 einen HEADERS-Frame mit dem Statuscode 200 bezogen auf die Datei "style.css" an den Client. Letzterer kündigt die baldige Übertragung der Datei an. Ohne weiteren Widerspruch des Clients würde als nächstes im Stream 42 die Übertragung eines DATA-Frame mit der "style.css" erfolgen.
  5. Im Beispiel benötigt der Client die Datei "style.css" nicht, weil er noch eine Kopie davon in seinem Cache zur Verfügung hat. Er sendet deshalb im Stream 42 einen RST_STREAM-Frame. Damit schließt er den Stream 42, und der Server wird darüber keinerlei Daten mehr versenden.
  6. Bleibt noch die eingangs durch den Client im Stream 41 angefragte Datei "index.html", die der Server nun in einem DATA-Frame an den Client versendet. Damit beendet der Server seinerseits die Kommunikation im Stream 41.

Der beispielhafte Ablauf aus dem letzten Abschnitt zeigt die simultane Übertragung von zwei Streams innerhalb einer TCP-Verbindung. Im dritten Frame versendet der Server einen PUSH_PROMISE-Frame innerhalb des Streams mit der ID 41. Das passiert, weil der Server den nächsten Request des Clients vorhersagt und daraufhin eine Response ankündigt. Im genannten Beispiel sieht der Server eine Anfrage für eine Datei "style.css" kommen und kündigt deren Übertragung an den Client mit einem HEADERS-Frame an, den er im neuen Stream mit der ID 42 versendet.

Der Client kann nun zwei Dinge tun. Um die angebotene Datei zu akzeptieren, muss der Client aktiv nichts weiter tun. Nach dem HEADERS-Frame wird der Server im Stream 42 einen DATA-Frame mit besagter Datei versenden. So erhält der Client die Datei "style.css" ohne sie jemals aktiv angefordert zu haben.

Alternativ kann der Client zu der Entscheidung kommen, dass er die Datei "style.css" nicht benötigt. In diesem Fall schließt der Client den Stream 42, indem er einen RST_STREAM-Frame an den Server schickt. Das Protokoll verbietet es dem Server, über Streams zu kommunizieren, für die der Client einen solchen Frame gesendet hat.

Server-Push verringert die Latenz somit durch das Einsparen von Client-Requests. Dafür muss der Server jedoch in der Lage sein, die Anfragen des Clients zuverlässig zu antizipieren. Er muss den auszuliefernden Content und das Verhalten seiner Clients gut kennen, um nachfolgende Requests nach weiteren Inhalten vorherzusehen.

Es ist Aufgabe der Webserver-Administratoren oder der Webentwickler, die Push-Funktionalität durch Konfiguration und Implementierung zu gewährleisten.

HTTP/1.1 komprimiert lediglich die Nutzlast von Requests und Responses – meist mit dem gzip-Algorithmus. HTTP/2 verwendet gzip weiterhin für ebendiesen Zweck, komprimiert aber neuerdings zusätzlich die Header-Daten. Hierfür kommt ein neuer Algorithmus mit dem Namen HPACK zum Einsatz, der zweistufig arbeitet.

  1. Für 61 bekannte Header existiert ein fest definiertes, statisches Kompressionswörterbuch. Header-Namen, teilweise sogar in Kombination mit häufig vorkommenden Werten, werden als eine einfache Zahl kodiert – beispielsweise der Header :method:GET[i] auf den Wert 2, die Kombination [i]:method:POST auf 3. Der Header-Name :referer: entspricht dem Wert 51. Auf die Weise lassen sich HTTP-Header auf bis zu ein Byte reduzieren.
  2. Für Header, die nicht Teil des statischen Kompressionswörterbuchs sind, handeln Client und Server dynamisch zum Zeitpunkt der Verbindungsaufnahme ein weiteres, dynamisches Wörterbuch aus, das nur für die aktuelle Verbindung gültig ist.

HPACK verringert den Umfang der Header-Daten dramatisch. Die Standard-Header zeichnen sich von Natur aus durch eine hohe Redundanz aus. Die Header der überwiegenden Mehrheit aller HTTP-Verbindungen beginnt mit :method:GET[i]. Weiterhin sind die Header [i]:content-type:application/html sowie :accept:application/html Teil nahezu jeder Kommunikation zwischen Webbrowser und -server. HPACK verkürzt die zu übertragende Datenmenge auf jeweils ein Byte.

Die in den Headern verpackten HTTP Cookies machen üblicherweise den Großteil der Header-Daten in der HTTP-Kommunikation aus. HPACK sorgt erstmals für eine Komprimierung der Cookies.