Urlaub in der Steinzeit (Linux-Magazin, Mai 2001)

Ein kleiner Proxy-Server erlaubt es auch auf superschnellen Internetverbindungen, die Welt durch die Brille armer Modembenutzer zu sehen.

Neulich weilte ich für eine Woche in Deutschland und schaute durch Zufall meine Website durch eine traditionelle Modemverbindung an. Mir fiel die Klappe herunter! Es dauerte ungefähr 30 Sekunden, bis der Browser die Amerika-Rundbriefe auf perlmeister.com anzeigte! Grund dafür war das HTML-Design der Seiten, das aus einer riesigen zweispaltigen Tabelle bestand, die der Browser erst dann anfing darzustellen, als die ganze 50K starke Seite durch die enge Leitung gepumpt war. Unter DSL war mir das nie aufgefallen, da dort 50K ratz-fatz durchrauschen.

Wieder daheim in den USA angekommen, nahm ich mir deshalb vor, alle meine Seiten vor der Veröffentlichung künftig auch auf meinem superschnellen DSL-Anschluss mittels eines kleinen Tricks unter Schneckenmodemgeschwindigkeit zu testen. Hierzu wird einfach der heute vorgestellte Proxy-Server gestartet und ein handelsüblicher Browser darauf eingenordet -- und schon drosselt der Proxy die verfügbare Bandbreite auf beliebig einstellbare Werte herunter.

Abbildung 1: Der Proxy zwischen Browser und Server

Proxyserver in 85 Zeilen

Wie in [2] schon einmal vorgestellt, ist es ein Leichtes, unter Perl einen Proxyserver zu schreiben, der zwischen dem Browser und dem kontaktierten Web-Server steht und allerlei lustige Streiche treibt. Das Modul HTTP::Daemon von Gisle Aas erledigt die Feinheiten, wir müssen nur die Logik zur Verlangsamung des Durchsatzes hinzufügen.

Listing slowie.pl zeigt die Implementierung. Zeile 5 definiert den Port, auf dem der Proxy-Server lauscht und Zeile 6 den maximalen Durchsatz in Bytes pro Sekunde. Die Zeilen 8 und 9 ziehen die benötigten Zusatzmodule herein, die wir im Abschnitt ``Installation'' vom CPAN holen werden.

Zeile 12 setzt einen Signal-Handler auf, der das SIGPIPE-Signal ignoriert, falls es auftreten sollte, wenn ein Browser unvermittelt die Verbindung abbricht. Der zweite Signal-Handler in Zeile 14 erlöst beendete Prozesskinder aus ihrem Zombiestatus -- weiter unten werden wir Parallelprozesse abfeuern.

Zeile 17 erzeugt den neuen HTTP-Dämon, den eigentlichen Proxy, der auf dem eingestellten Port auf Anfragen lauscht, die angeforderten Seiten anschließend vom Web holt und schließlich wieder an den anfragenden Rechner zurückliefert. Der Reuse-Parameter lässt den Server auch dann starten, wenn der Socket einer kurz zuvor rüde unterbrochenen Instanz von slowie.pl noch etwas unschlüssig auf dem Port herumhängt.

Zeile 21 beendet das Programm sofort, falls der Dämon nicht starten kann. Andernfalls schreibt Zeile 24 eine Meldung auf die Standardausgabe, die angibt, unter welchem Port der Proxy zu erreichen ist.

Zeile 26 erzeugt ein Objekt vom Typ LWP::UserAgent, das später beim Einholen von Webpages behilflich sein wird. Die anschließend aufgerufene agent()-Methode bestimmt, wie der Proxy den UserAgent-Header bei Anfragen an den Webserver setzt -- slowie/1.0 wird sicher zur Erheiterung des einen oder anderen Webmasters beitragen.

Die accept()-Methode in Zeile 29 blockt so lange, bis ein Request vom Browser ankommt und besetzt dann $conn mit einer Referenz auf das Verbindungsobjekt. Geht dabei etwas schief, bricht das Programm ab.

Da der Browser Requests unter Umständen schnell hintereinander abfeuert und der Proxy mehrere Anfragen quasi gleichzeitig bearbeiten soll, ist es wichtig, dass er, noch während der Request bearbeitet wird und die Daten vom Web geholt werden, schnellstens wieder zur accept()-Methode in Zeile 29 zurückkehrt um gleich die nächste Anfrage des Browsers entgegenzunehmen. Dies löst slowie.pl durch parallele Prozesse, die der fork()-Befehl in Zeile 32 kreiert. Zeile 34 schickt den Vaterprozess sofort wieder zur accept()-Methode am Anfang des Blocks zurück, während der neue Kindprozess in Zeile 37 damit fortfährt, die Requestdaten vom Browser entgegenzunehmen. Zeile 39 nutzt das Objekt vom Typ LWP::UserAgent, um die gewünschten Daten vom Web zu holen. Dabei verwendet es bewusst simple_request() und nicht request(), da wir dem Browser keinerlei Arbeit abnehmen wollen und er demgemäß den Redirects auch gefälligst selber folgen muss.

Zeile 47 ruft daraufhin die Methode send_response() der Browserverbindung auf, um die Antwort zurückzuschicken. send_response() versteht zwei verschiedene Parameterarten: Einen String sendet es sofort zurück zum Browser und eine Referenz auf ein Objekt vom Typ HTTP::Response nutzt es, um dessen content()-Methode immer und immer wieder aufzurufen, und das jeweils zurückgelieferte Ergebnis als String stückweise weiter an den Browser weiterzuleiten.

Genau diesen Mechanismus nutzt slowie.pl, um den Datendurchsatz zum Browser künstlich zu begrenzen. Für den Fall, dass der Webserver auf die gestellte Anfrage hin tatsächlich Daten lieferte, ruft Zeile 43 die Funktion get_slowsub() auf, die eine Referenz auf eine Funktion zurückliefert, die die mit $resp->content() ursprünglich übergebenen Webserverdaten speichert und bei jedem anschließenden Aufruf einen kleinen Bissen davon zurückgibt, bis schließlich alle Daten geliefert wurden. Die in $subref gespeicherte Funktionsreferenz schmuggelt Zeile 44 dem Response-Objekt $resp als Inhalt unter und ersetzt damit die ursprünglich dort gespeicherten Antwortdaten des Webservers.

Für den Gutfall steckt also in Zeile 47 in dem an send_response() übergebenen HTTP::Response-Objekt der Wolf im Schafspelz: send_response() wird feststellen, dass $resp->content() keine Daten, sondern eine Funktionsreferenz liefert und deswegen die dahintersteckende Funktion wieder und wieder aufrufen, bis sie einen leeren String oder einen undefinierten Wert liefert. Alle bis dato von der Funktion gelieferten Daten schickt sie stückweise an den Browser. Danach hat der Kindprozess seine Aufgabe erledigt. Zeigt die while-Schleife in Zeile 37 an, dass der Browser keinen neuen Request in dieser Session mehr hat, wird die Verbindung zu ihm in Zeile 49 gekappt und der Kindprozess mit exit(0) beendet. Der schon erwähnte Signalhandler in Zeile 14 sorgt dafür, dass aus ihm kein Zombie wird.

Nun zur trickreichen Funktion get_slowsub(), die eine Referenz auf eine Funktion liefert, die die von der Webseite schon vollständig empfangenen Daten nur sehr zögerlich, entsprechend der eingestellten Bandbreitenbegrenzung herausgibt.

get_slowsub() nimmt einen String entgegen, der dem Inhalt der angeforderten Website entspricht und definiert eine sogenannte Closure, um eine Funktion mit Gedächtnis zu verwirklichen, deren Referenz sie anschließend zurückgibt.

Geheimnisvolle Closure-Welt

Die Closure ist eine Funktion, die nicht nur ihren Programmcode kennt, sondern auch noch die Zustände der sie umgebenden lexikalischen Variablen behält. Ein Beispiel:

    { my $count = 1;
    
      sub zaehle {
        print($count++, "\n");
      }
    }

Nach diesem Block gibt es zwar die Funktion zaehle(), aber $count ist wegen seines lexikalischen Scopes verschwunden. Doch, halt, nicht ganz: zaehle() hat während seiner Entstehung von der Variablen $count Kenntnis genommen und deswegen führt das in zaehle() referenzierte $count beim nächsten Aufruf von zaehle() außerhalb des Blocks den Wert 1! Weitere Aufrufe von zaehle() geben 2, 3, usw. aus:

    zaehle();  # => 1
    zaehle();  # => 2
    zaehle();  # => 3

So erzeugt die Definition in Zeile 63 nicht nur eine Referenz auf eine Funktion, sondern schliesst in diese auch gleich die Variablen $content, $start und $followup ein, die sich innerhalb der definierten Funktion ähnlich wie globale Variablen verhalten, aber außerhalb der Funktion nicht sichtbar sind, sobald get_slowsub() verlassen wurde. Die Funktion führt die sie umgebenden lexikalischen Variablen wie in einer Einkaufstasche mit sich und sie enthalten beim ersten Aufruf der Funktion die Werte, die ihnen vor der Erzeugung der Funktion zugewiesen wurden. Wozu das Ganze? get_slowsub() soll eine Referenz auf eine Funktion erzeugen, die persistente Zustandsvariablen mit sich führt. Diese müssen außerdem für jede neue Funktionsinstanz eindeutig sein, da unter Umständen viele dieser Verzögerer gleichzeitig aktiv sind. get_slowsub() erzeugt also mit der Closure eine Art Objekt mit Instanzvariablen.

In der lexikalischen Variablen $content steht die Textantwort der befragten Website als String, in $start die Uhrzeit des letzten Datentransfers (am Anfang die aktuelle Uhrzeit minus eine Sekunde) und $followup zeigt an, ob die Funktion zum ersten Mal oder schon mehrmals aufgerufen wurde.

Zeile 66 prüft, ob die Länge der verbleibenden Nachricht gleich 0 ist und lässt die Closure undef zurückgeben, falls dem so ist, da so send_response() weiter oben den Transfer zum Browser beendet.

Zeile 70 schläft eine Sekunde, falls es sich nicht um den ersten Aufruf der Funktion handelt, um send_response() zu bremsen, das die Closure Schlag auf Schlag aufruft.

Zeile 73 ermittelt aufgrund der zulässigen Bandbreite und der seit dem letzten Datentransfer (oder dem ersten Aufruf) verstrichenen Zeit die Anzahl der Zeichen, die die Funktion freigeben darf. Zeile 76 setzt dem Timer wieder zurück, damit beim nächsten Aufruf die Berechnung der freigegebenen Bytes wieder stimmt. Zeile 80 schlägt zwei Fliegen mit einer Klappe: Sie extrahiert die ermittelte Anzahl von Zeichen aus der Closure-Variablen $content und nutzt die 4-Parameter-Version von substr() um den extrahierten String auch gleich im Original zu löschen. Der Rückgabewert von substr() ist dann ein String mit den ausgeschnittenen Zeichen, die zum Browser geschickt werden dürfen.

Noch einmal: get_slowsub() definiert nur eine Funktion und gibt eine Referenz darauf zurück. Die innen definierte Funktion tut nichts anderes, als bei jedem Aufruf etwas mehr von einem vorgegebenen String herauszurücken, der ihr bei ihrer Definition als Closure-Variable überreicht wurde, und die sie stets mit sich führt. Eine hervorragende Erklärung des komplexen Themas Closures findet sich in übrigens in [4].

Installation

Die verwendeten Module HTTP::Daemon und LWP::UserAgent sind Bestandteil der LWP-Bibliothek von Gisle Aas. Das CPAN-Modul installiert alles zügig mit

    perl -MCPAN -eshell
    cpan> install Bundle::LWP

Dann konfiguriert man den vom Proxy zu verwendenden Port in Zeile 5 von slowie.pl und den gewünschten Durchsatz in Bytes pro Sekunde in Zeile 6. Anschließend wird slowie.pl von der Kommandozeile gestartet, was etwa folgendes anzeigen sollte:

    Server listening at port 8018

Anschließend muss der Web-Browser auf den Proxy zeigen, im Netscape wird hierzu im Menü Edit->Preferences->Advanced->Proxies der Punkt Manual Proxy Configuration selektiert und nach dem Drücken des View-Knopfes folgende Einträge gesetzt:

    HTTP Proxy: localhost 
    Port: 8018

Der Secure Proxy-Eintrag bleibt frei. Im Falle des Internet Explorers ist's die Seite View->Internet Options->Connection, wo die Checkbox Access the Internet using a proxy server anzukreuzen und ebenfalls localhost und Port 8018 einzutragen ist. Dann einfach die Konfigurationsfenster schließen, einen URL in den Browser tippen, zurücklehnen und langsam genießen!

Macht eure Webseiten auch für arme Modembenutzer erträglich!

Listing 1: slowie.pl

    01 #!/usr/bin/perl -w
    02 
    03 use strict;
    04 
    05 my $PORT      = 8018;
    06 my $BYTE_RATE = 1000;
    07 
    08 use HTTP::Daemon;
    09 use LWP::UserAgent;
    10 
    11     # Falls der Browser plötzlich abbricht
    12 $SIG{PIPE} = 'IGNORE';
    13     # Reaper für terminierte Kindprozesse
    14 $SIG{CHLD} = sub { wait(); };
    15 
    16     # Neuen Dämon erzeugen
    17 my $srv = HTTP::Daemon->new( LocalPort => $PORT, 
    18                              Reuse     => 1 );
    19 
    20     # Fehler aufgetreten?
    21 die "Can't start server ($@)" unless defined $srv;
    22 
    23     # Erfolgsmeldung
    24 print "Server listening at port $PORT\n";
    25 
    26 my $ua = LWP::UserAgent->new();
    27 $ua->agent("slowie/1.0");
    28 
    29 while(1) {
    30     my $conn = $srv->accept();
    31 
    32         # Parallelprozess abfeuern
    33     defined(my $pid = fork()) or die "Can't fork!";
    34         # Vater kehrt zurück zum accept()
    35     next if $pid;
    36 
    37         # Kind bearbeitet Requests der Verbindung
    38     while (my $request = $conn->get_request) {
    39 
    40         my $resp = $ua->simple_request($request);
    41 
    42         if($resp->is_success()) {
    43             my $subref = 
    44                 get_slowsub($resp->content());
    45             $resp->content($subref);
    46         } 
    47 
    48         $conn->send_response($resp);
    49     }
    50     $conn->close;
    51         # Kind beendet sich
    52     exit(0);
    53 }
    54 
    55 ##################################################
    56 sub get_slowsub {
    57 ##################################################
    58     my ($content) = @_;
    59 
    60     my $start      = time() - 1;
    61     my $followup   = 0;
    62 
    63         # Closure erzeugen
    64     my $subref = sub {
    65 
    66             # Ende der Übertragung?
    67         if(0 == length($content)) {
    68             return undef;
    69         }
    70 
    71         sleep(1) if $followup++;
    72 
    73             # Maximal verfügbare Bytes
    74         my $max = (time() - $start) * $BYTE_RATE;
    75 
    76             # Timer zurücksetzen
    77         $start = time();
    78                       
    79             # Bereich aus $content ausschneiden
    80             # und zurückgeben
    81         my $chunk = substr($content, 0, $max, "");
    82         return($chunk);
    83     };
    84 
    85     return $subref;
    86 }

Literaturhinweise

[1]
Die Amerika-Rundbriefe auf perlmeister.com: http://perlmeister.com/private

[2]
``Loggender Proxy'', Michael Schilli, Linux-Magazin 04/2000, http://www.linux-magazin.de/ausgabe/2000/04/Proxy/proxy.htm

[3]
``Slow down the download!'', Randal Schwartz, 1998, ein ähnliches Skript, allerdings als CGI: http://web.stonehenge.com/merlyn/WebTechniques/col21.html

[4]
Zum Thema Closures: ``Object Oriented Perl'', Damian Conway, Manning, 1999, Seite 56ff.

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.