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 |
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.
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].
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!
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 }
Michael Schilliarbeitet 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. |