HTTP/1.1 Multiple Request, Persistent Connection und Pipelining

Connection Keep-Alive und wie das eigentlich wirklich funktioniert

In HTTP/1.0 war alles noch ganz einfach. Jeder Request wurde mit einer Response beantwortet, doch was bedeutet das eigentlich wirklich, wenn der Aufbau einer einzigen HTML-Seite im Browser gleich mehrere Requests erfordert? Weil neben text/html noch ein paar Grafiken image/gif und eine CSS-Datei vom Content-Type: text/css geladen werden müssen?

Richtig, da wäre es doch praktisch, dass der Browser, der die Seite ja kennt, erst einmal alle Requests auf einmal an den Server schickt, also beispielsweise eine HTML-Datei, eine CSS-Datei und acht Grafikdateien anfordert und wenn er alles hat, die Seite dann darstellt. Der Zeitgewinn liegt auf der Hand: Statt 10 Request-Response-Zyklen nur noch einer. Einer der Gründe aus HTTP/1.0 die Version HTTP/1.1 hervorgehen zu lassen.

In Diskussionen merke ich jedoch immer wieder, dass es vielen Kollegen unklar ist, wie sowas eigentlich abläuft. Der erste Trugschluss besteht darin, anzunehmen, dass 10 Request-Response-Zyklen (um in dem Beispiel zu bleiben) über eine Verbindung so ablaufen, dass 10mal Daten hin (10 Requests) und 10mal Daten zurück (10 Responses) gesendet werden.

Tatsächlich jedoch werden auch in HTTP/1.1 nur einmal Daten hin und nur einmal Daten zurück geschickt, auch dann, wenn es 10 Requests sind. Die oben getroffene Annahme ist also falsch, bzw. ein Trugschluss. Des Weiteren ist vielen Kollegen der Dateibegriff nicht klar: Dateien sind Sequenzen (Niklaus Wirth), also Folgen von Bytes beliebiger Oktettenwertigkeiten. Gehen wir doch nun mal weiter ins Detail:

Client sendet Connection: Keep-Alive

Nun, dieser Header weist den Server an, die Verbindung offenzuhalten. Also schickt der Client einen Request, kann jedoch unmöglich gleich danach die Response bekommen. Weil: Er hat ja den Server angewiesen, die Verbindung offenzuhalten. Das heißt: Der Server wartet auf weitere Requests und der Client wartet auf eine Antwort vom Server. Deadlock.

Logisch, es macht natürlich wenig Sinn, bei einem Request ein Connection: Keep-Alive zu senden und danach auf die Response zu warten. Bei 10 Anfragen kann das lange dauern und das kanns ja auch nicht sein, dass es jedesmal eine Phase gibt, in der Client wie Server gleichermaßen warten bis zum Timeout.

Pipelining ist die Lösung

Nimm es einfach mal so hin: Der Client schickt einen Datenstrom mit 10 Anfragen an den Server. Dabei sind 9 Anfragen mit dem Request-Header Connection: Keep-Alive, die 10. Anfrage jedoch mit dem Header Connection: Close ausgestattet.

In dieser Phase gehen also Daten einmal vom Client in Richtung Server. Und nun wird klar, was damit und danach passiert: Der Webserver sendet alles zusammen in einer Response (in einer Datei). Bis zum Senden der Response sucht sich der Server die Antworten zusammen, lädt Dateien, startet CGI-Prozesse oder greift auf ein anderes Gateway. Gleichzeitig und währenddessen wartet der Server auf weitere Anfragen vom Client.

Erst wenn der Client eine Header Connection: Close sendet, schickt der Webserver die Response und schließt danach die Verbindung. Das würde er auch nach einer konfigurierten Auszeit machen, wenn keine weiteren Anfragen mehr kommen.

Fazit: Pipelining ist nur ein anderer Begriff dafür, dass Requests und Responses gebündelt werden. Und Bündeln heißt, dass in Fakt nur eine Datei gesendet und nur eine Datei empfangen wird. Wer gerne mit Begriffen hantiert: Upstream, Downstream.

Beispiel mit mehreren HEAD Requests

Es gehen 3 Requests an 3 verschiedene URLs, dafür gehen einmal Daten hinein in das Socket und danach wird die Response-Datei aus dem Socket gelesen. Für das Beispiel sind es nur HEAD-Requests, ansonsten stünden in der Response die einzelnen Bodies unter den jeweiligen Kopfzeilen.

use strict;
use warnings;
use HTTPRequest;
binmode STDOUT;

my $r =  HTTPRequest->new(
    host => 'rolfrost',
    timeout => 1,
    http => 1.1
) or die $@;

$r->request(
    Connection => 'Keep-Alive',
    uri => '/',
    method => 'HEAD'
);

$r->request(
    Connection => 'Keep-Alive',
    uri => '/red.gif',
    method => 'HEAD'
);


$r->request(
    method => 'HEAD',
    uri => '/mail.html',
    Connection => 'Close' 
);

$r->print_rawdata;

Response-Datei
====================
HTTP/1.1 302 Found
Date: Tue, 01 Dec ...
Server: Apache/2.2.14
Location: http://rolfrost/firnis.html
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive 
Content-Type: text/html; charset=iso-8859-1

HTTP/1.1 200 OK
Date: Tue, 01 Dec ...
Server: Apache/2.2.14
Last-Modified: Sun, 05 Feb 2012 21:40:05 GMT
Accept-Ranges: bytes
Content-Length: 522
Keep-Alive: timeout=5, max=99
Connection: Keep-Alive 
Content-Type: image/gif

HTTP/1.1 200 OK
Date: Tue, 01 Dec ...
Server: Apache/2.2.14
Content-Language: de
x-class: HTMLfile
Expires: Tue, 01 Nov 2016 10:30:32 GMT
Set-Cookie: FRAMEWORK=52c572e28e23b21e0075de7d087b1da8
Content-Length: 5162
Last-Modified: Mon, 02 Nov 2012 10:30:32 GMT
Connection: close 
Content-Type: text/html; charset=UTF-8

Parsen der multipart Response

Die o.g. Response-Datei hat keine Bodies, weil nur HEAD-Request. Andererseits stünden die Bodies unter den jeweiligen Response-Headers, von letzteren durch eine Leerzeile getrennt. Diese Leerzeile sendet jeder Webserver weltweit einheitlich als Sequenz CRLFCRLF.

Programmiertechnisch gibt es drei Fälle, die beim´sequentiellen Lesen des Streams unterschieden werden müssen, zu finden im jeweiligen Response-Header:

  1. Transfer-Encoding: chunked (verteilte Längenangaben)
  2. Content-Length: [in bytes] (eine Längenangabe)
  3. Weder noch (keine Längenangabe)

Ein sequentielles Lesen der Response ist unbedingt erforderlich, weil die einzelnen Parts auch Binärdaten darstellen können. Ein einfaches Splitten ode Tokenizing der Response-Datei ist demnach nicht zielführend. Wichtig sind also die Längenangaben, die sind bei (1) und (2) gegeben. Bei (3) wird einfach nur solange gelesen, bis das Handle (Socket) keine Daten mehr liefert, praktisch funktional nur bei der letzten in der Datei stehenden Response. Ein Wiederherstellen der einzelnen Parts muss bytegenau stimmen, sonst können die Inhalte (z.B. Grafiken) nicht dargestellt werden.

Das von mir entwickeltes Perl-Modul kann solche Responsedateien parsen. Der Content-Type einer solchen Multipart-Response ist nicht näher klassifiziert. Es gibt eine Menge Geschriebenes und jede Menge RFCs zu HTTP, aber zu dieser Sache hier gibt es gar keine Informationen. So hoffe ich, dass ich mit diesem Artikel eine kleine Lücke schließen konnte.

Praktische Anwendung

Zum automatiserten Überprüfen der Erreichbarkeit einer Website mit vielen Unterseiten weiß ich HTTP/1.1 und Keep-Alive sehr zu schätzen. Das verringert den Zeitaufwand der Prüfungen merklich. Hier ist der Ablauf: Die Liste aller auf dem Werver konfigurierten URLs wird angefordert, das ist der erste Request. Unmittelbar nach dem Erhalt der Liste gehen entsprechende viele HEAD-Requests an den Server, mit Connection: Keep-Alive. Damit es keinen Zeitverzug gibt, sendet daran anschließend ein zusätzlicher Dummy-Request den Header Connection: Close. Bis hierhin geht ein Datenstrom in Richtung Server. Daraufhin kommt die Response als ein Datenstrom (Datei) zurück. Sie enthält enthält die einzelnen Response-Header in derselben Reihenfolge wie sie angefordert wurden. Ausgewertet wird der HTTP-Statuscode. Der Zeitgewinn gegenüber einzelnen Requests liegt ungefähr bei Faktor 10 (bei 200 URLs nur 3 statt 30 Sekunden).

Keep-Alive Proxy-Server

Je nach Konfiguration könnte ein Webserver innerhalb einer persistent Connection solch einen Response-Header senden:

Keep-Alive: timeout=5, max=100

Des bedeutet, er wartet 5 Sekunden auf etwaige weitere Requests, nimmt jedoch nur noch maximal 100 Requests an. In der Response-Datei ist dann gut zu sehen, wie der Webserver den Wert max=100 heruntergezählt hat. Es gibt jedoch auch Server, die sind so konfiguriert, dass es keine Begrenzung gibt, z.B. der Proxy-Server von T-Online. Ein Proxy-Server, so konfiguriert, kann einige hundert Requests entgegennehmen, wobei die einzelnen Requests an völlig verschiedene Domänen gerichtet sein können. Die Response-Datei enthält alle Antworten gebündelt in einer Datei. Wer automatisiert die Erreichbarkeit vieler verteilter Systeme zu prüfen hat, ist also gut beraten, die Requests über einen Proxy-Server zu bündeln.


Datenschutzerklärung: Diese Seite dient rein privaten Zwecken. Auf den für diese Domäne installierten Seiten werden grundsätzlich keine personenbezogenen Daten erhoben. Das Loggen der Zugriffe mit Ihrer Remote Adresse erfolgt beim Provider soweit das technisch erforderlich ist. s​os­@rolf­rost.de.