HTML5 bringt Websockets, damit Clients über stehende Verbindungen mit Webservern schnattern können. Eine Webapplikation zeigt in Echtzeit im Browser, welche Seiten User von einem Webserver aufrufen.
Statt dem simplen Anfrage/Antwort-Spiel im traditionellen HTTP-Protokoll versetzten die im HTML5-Standard enthaltenen Websockets Standardbrowser in die Lage, länger über persistente Verbindungen birektional mit dem Webserver zu kommunizieren. Dazu nimmt JavaScript-Code auf einer heruntergeladenen Seite im Browser eine Websocket-Verbindung zur URL ws://server/pfad auf und definiert einen Callback, den der Code jedesmal sofort anspringt, falls der Server eine Nachricht über den Socket sendet. Die Browserapplikation reagiert demnach sofort auf Serversignale, ohne diesen in regelmäßigen Abständen pollen zu müssen.
Ob ein Browser Websockets unterstützt, lässt sich in JavaScript leicht
feststellen. Nur falls das DOM-Element window.WebSocket
existiert,
sind Websockets implementiert und aktiviert. Die Webseite websocket.org
bietet auf [2] eine kleine Applikation, die die Browserfertigkeiten
mit grüner oder oranger Farbe anzeigt und mit einer kleinen Echo-Applikation
zum Spielen anregt. Die Abbildungen 1 und 2 zeigen Firefox 4 erst mit
deaktiviertem, dann mit aktiviertem Websocket-Modus.
Abbildung 1: websocket.org bestätigt, dass Firefox 4 ohne network.websocket.enabled nicht über Websockets kommunizieren kann. |
Abbildung 2: Aktiviert der User die im Abschnitt "Sicherheitslücken" erklärte Konfiguration, nutzt Firefox 4 die Websocket-API nach dem alten Protokoll. |
Derzeit unterstützen Firefox 4 und Google Chrome das Protokoll zumindest eingeschränkt (siehe Abschnitt "Sicherheitslücken"), für eine vollständige Implementierung nach dem neuesten Standard muss sogar Firefox 6 (Aurora) her.
Abbildung 3 zeigt eine Testapplikation, bei der der Browser in unregelmäßigen
Abständen absteigende Zählerwerte vom Server empfängt. Letzterer zählt von
10 an abwärts, schickt den jeweiligen Zählerstand über einen Websocket zur
dargestellten
Browserseite und schläft einen per rand()
gesteuerten zufälligen
Sekundenbruchteil lang bis zum nächsten Schleifendurchgang.
Bei Null angelangt, sendet der Server die Zeichenkette "BOOM!" und beendet die
Websocket-Kommunikation.
Im Browser erscheinen die Servernachrichten verzögerungsfrei in Echtzeit, an die Applikation geliefert über einen effizienten Callback-Mechanismus, ohne dass der Client jedesmal um Nachschlag bitten müsste.
Abbildung 3: Das Websocket-Testskript zählt einen ruckelnden Countdown. |
Zur Implementierung des Testservers greift Listing 1 auf das bereits im letzten Snapshot vorgestellte Framework Mojolicious::Lite zu, mit dem experimentierfreudige Programmierer in Minuten fertige Webapplikationen zusammenklopfen. Es unterstützt neben normalen Web-Protokollen auch Websockets und hält den Zustand jedes Websocket-Clients mittels Closures im Speicher.
01 #!/usr/local/bin/perl -w 02 use strict; 03 use Mojolicious::Lite; 04 use Mojo::IOLoop; 05 06 my $listen = "http://localhost:8083"; 07 @ARGV = (qw(daemon --listen), $listen); 08 09 my $loop = Mojo::IOLoop->singleton(); 10 11 ########################################### 12 websocket "/myws" => sub { 13 ########################################### 14 my($self) = @_; 15 16 my $timer_cb; 17 my $counter = 10; 18 19 $timer_cb = sub { 20 $self->send_message( "$counter" ); 21 if( $counter-- > 0 ) { 22 $loop->timer(rand(1), $timer_cb); 23 } else { 24 $self->send_message( "BOOM!" ); 25 } 26 }; 27 28 $timer_cb->(); 29 }; 30 31 ########################################### 32 get '/' => sub { 33 ########################################### 34 my ($self) = @_; 35 36 ( my $ws_url = $listen ) =~ s/^http/ws/; 37 $ws_url .= "/myws"; 38 $self->{stash}->{ws_url} = $ws_url; 39 } => 'index'; 40 41 app->start(); 42 43 __DATA__ 44 @@ index.html.ep 45 % layout 'default'; 46 Random counter: 47 <font size=+2 id="counter"></font> 48 49 @@ layouts/default.html.ep 50 <!doctype html><html> 51 <head><title>Random Countdown</title> 52 <script type="text/javascript"> 53 var socket = new WebSocket( 54 "<%== $ws_url %>" ); 55 socket.onmessage = function (msg) { 56 document.getElementById( 57 "counter").innerHTML = msg.data; 58 }; 59 </script> 60 </head> 61 <body> <%== content %> </body> 62 </html>
Die Eventschleife Mojo::IOLoop bietet einer Applikation ein Timer-gesteuertes Hook-Framework, um in einem laufenden Mojolicious-Prozess hin und wieder die Kontrolle zu erlangen, kleine Aufgaben zu erledigen und den nächsten Update zu planen.
Statt eines Konstruktors new()
nutzt Zeile 9 die Methode singleton()
,
die eine weitere Referenz auf eine Eventschleife liefert, falls vorher
schon eine definiert wurde.
Das ist wichtig, denn mehrere verschiedene Eventschleifen würden
jeweils ihre Vorgänger auslöschen.
Im Testskript definiert der Callback
$timer_cb
ab Zeile 19 eine Funktion, die den globalen Countdown-Wert
mittels der Mojomethode send_message()
über den Websocket zum
Browser sendet und ihn anschließend um Eins herunterzählt. Nach getaner
Arbeit ruft Zeile 22 die Methode timer()
des Moduls Mojo::IOLoop auf,
das die Anzahl der zu schlafenden Sekunden und eine Callback-Funktion
entgegennimmt. rand(1)
liefert einen Fließkommawert
zwischen 0 und 1, also wacht die Eventschleife nach weniger als einer
Sekunde wieder auf
und springt wieder die gleiche in $timer_cb
gespeicherte Callbackfuntion
an. Um den Reigen anfangs in Schwung zu bringen, ruft Zeile 28 einmalig
den Callback auf, der sich ab dann selbständig weiter perpetuiert.
Den Ansprungpunkt im Server bei eingehenden Websocket-Requests definiert
das Kommando websocket
in Zeile 12, im Gegensatz zu get
in Zeile
32, die die Reaktion des Testservers auf GET-Anfragen zum Root-Pfad "/"
des Webservers beantwortet. Der Websocket-Handler
rechnet die gegebene "http://"-URL mittels
eines regulären Ausdrucks in eine "ws://"-URL für Websocket-Requests um,
fügt den Pfad /myws
an und
stopft den Gesamt-URL
unter dem Schlüssel ws_url
in den Stash des Template-Automaten.
Zeile 39 verweist mit 'index'
auf das weiter unten im __DATA__-Bereich
definierte Template @@ index.html.ep
.
Der zugehörige HTML-Code enthält etwas Text ("Random Counter") sowie ein font-Element mit der ID "counter". Zwar springen CSS-Puristen jetzt im Dreieck, aber schließlich geht es nur darum, irgendein HTML-Element mit einer bekannten ID zu definieren, damit der JavaScript-Code es später aus der DOM herausfieseln und seinen Inhalt auffrischen kann.
Das damit verknüpfte Layout 'default' ab Zeile 49 macht ein ordentliches
HTML-Dokument aus dem oberen HTML-Schnipsel und fügt
JavaScript-Code zur Websocket-Ansteuerung hinzu. Zeile 53 erzeugt ein
neues Objekt der Klasse WebSocket
, unter Verwendung der vorher
erzeugten "ws://"-URL. Nimmt der Websocket erfolgreich Kontakt zum
Server auf und absolviert den erforderlichen Handshake,
erzeugt eine später vom Server an den Browser über den Websocket
geschickte Nachricht einen Event namens socket.onmessage
, dessen
Callback-Funktion Zeile 55 setzt.
Abbildung 4: Nachdem der Client einen Websocket mit dem Server eröffnet hat, kann der Server asynchron Daten an den Client schicken. |
Im Attribut data
der übersandten
Nachricht steht der Textstring, den send_message()
auf der Serverseite
am anderen Ende der Rohrpost eingetütet hat. Erwartungsgemäß steht dort
also der aktuelle Zählerwert im Countdown, und Zeile 56 braucht nur
das DOM-Element mit der ID "counter" zu suchen und dessen Attribut
innerHTML
auf den Zählerwert zu setzen, um letzteren zur
Anzeige zu bringen. Geht der User mit dem Browser auf die in Zeile
6 gesetzte URL http://localhost:8032, fängt der Countdown an zu zuckeln.
Als praktische Applikation sieht Listing 2 aktiven Usern einer Webseite über die Schulter und zeigt in einem Fenster in Echtzeit die Seiten an, die auch die Surfer zu Gesicht bekommen, zusammen mit dem URL und dem anzeigenden Host (Abbildung 6). Der Kontrolleur braucht nur den Mojolicious-Server in der URL-Zeile des Browsers anzugeben und schon frischt dieser in Echtzeit die Anzeige mit den gerade ausgelieferten Webseiten auf. Wie ist das möglich?
Abbildung 5: Ein Request im Logfile des Webservers. |
Die Funktion tail()
ab Zeile 69 in Listing 2
sieht einmal pro Sekunde nach, ob
der sysread
-Befehl auf ein im O_NONBLOCK-Modus geöffnetes access.log
des Webservers neue Daten zutage fördert. Findet das Skript so GET-Requests
wie in Abbildung 5, die auf eine HTML-Seite zeigen und nicht von der
IP-Addresse des Skripts selbst stammen, verpackt es den angeforderten URL
zusammen mit der in einen Hostnamen verwandelten IP-Addresse des Anfragenden
in ein JSON-Konstrukt und schickt es in Zeile 46 zum Websocket des
Browsers.
Abbildung 6: Im Schaufenster zeigt der Surfer-Kibitz an, welcher Surfer gerade welche Webseite ansteuert. |
Der Callback socket.onmessage entpackt in Zeile 118
das JSON-Format, indem er es
in runde Klammern einschließt und mit JavaScripts eval()
-Funktion
ausführt.
Er sucht die HTML-Elemente mit den IDs "host", "url" und "pageview" im
HTML-Code (Template ab Zeile 102) und pack die aus JSON extrahierten
Daten in die entsprechenden Anzeigefelder.
Das hat zur Folge, dass die im Browser angezeigte HTML-Seite bei jeden
eintreffenden Request für eine HTML-Seite auf dem Webserver den in
ihr eingebetteten iframe
(Zeile 108) mit der in der Logdatei gefundenen URL
aktualisiert, also dem Kontrolleur die Webseite anzeigt, die auch der
Surfer sieht. Gleichzeitig aktualisiert das Skript am oberen Seitenrand
auch noch den angeforderten URL mitsamt dem aufgelösten Hostnamen des
anfragenden Surfers.
Mit der blitzschnellen asynchronen, vom Server initiierten Aktualisierung
lässt sich so in Echtzeit verfolgen, wer was auf dem Webserver ansieht.
Damit das Skript bei prasselnden Anfragen nicht ins Schlingern gerät,
beschränkt Zeile 51 die Untersuchung des Access-Logs auf einmal alle
5 Sekunden, und nimmt jeweils nur den ersten Treffer, der den Anforderungen
in den Zeilen 33-35 genügt: Nur GET-Requests, um ungewollte
Replays auf datenverändernde POSTs zu unterbinden, nur HTML-Seiten
(falls jemand andere Endungen als .html?
nutzt, ist dies anzupassen)
und keine Requests, die von der IP des Kontroller selbst kommen.
Der Browser frischt die gerade angezeigten URLs nur alle fünf Sekunden
auf, egal wie schnell die Client-Requests ankommen.
Die in Zeile 31 aufgerufene Funktion parse_line_to_hash
analysiert
die ihr überreichte Accesslog-Zeile und wandelt sie in einen Hash
mit den Schlüsseln "client" (Client-IP), "file" (angeforderter Dateipfad im
URL) usw. um. revlookup()
ab Zeile 91 transformiert eine IP über
Reverse-DNS in einen Hostnamen, behält aber die ursprüngliche IP bei,
falls dies fehlschlägt.
001 #!/usr/local/bin/perl -w 002 use strict; 003 use Mojolicious::Lite; 004 use ApacheLog::Parser 005 qw(parse_line_to_hash); 006 use Mojo::IOLoop; 007 use POSIX; 008 use Socket; 009 use JSON qw(encode_json); 010 011 my $listen = "http://website.com:8083"; 012 @ARGV = (qw(daemon --listen), $listen); 013 014 my $base_url = "http://website.com"; 015 016 my $file = "access.log"; 017 sysopen my $fh, "$file", 018 O_NONBLOCK|O_RDONLY or die $!; 019 020 my $loop = Mojo::IOLoop->singleton(); 021 022 ########################################### 023 websocket "/myws" => sub { 024 ########################################### 025 my($self) = @_; 026 027 my $timer_cb; 028 $timer_cb = sub { 029 for my $line ( @{ tail( $fh ) } ) { 030 my %fields = 031 parse_line_to_hash( $line ); 032 033 if( $fields{ request } eq "GET" and 034 $fields{ file } =~ /\.html?$/ and 035 # skip our own requests 036 $fields{ client } ne 037 $self->tx->remote_address 038 ) { 039 my $url = $base_url . 040 $fields{ file }; 041 my $data = { 042 url => $url, 043 host => 044 revlookup( $fields{ client } ), 045 }; 046 $self->send_message( 047 encode_json( $data ) ); 048 last; 049 } 050 } 051 $loop->timer( 5, $timer_cb); 052 }; 053 $timer_cb->(); 054 }; 055 056 ########################################### 057 get '/' => sub { 058 ########################################### 059 my ($self) = @_; 060 061 (my $ws_url = $listen) =~ s/http/ws/; 062 $ws_url .= "/myws"; 063 $self->{stash}->{ws_url} = $ws_url; 064 } => 'index'; 065 066 app->start(); 067 068 ########################################### 069 sub tail { 070 ########################################### 071 my($fh) = @_; 072 073 my($buf, $chunk, $result); 074 075 while( $result = sysread $fh, 076 $chunk, 1024 ) { 077 $buf .= $chunk; 078 } 079 080 if( defined $result and defined $buf) { 081 chomp $buf; 082 my @lines = map { s/\s+$//g; $_; } 083 split /\n/, $buf; 084 return \@lines; 085 } 086 087 return []; 088 } 089 090 ########################################### 091 sub revlookup { 092 ########################################### 093 my($ip) = @_; 094 095 my $host = (gethostbyaddr( 096 inet_aton($ip), AF_INET ))[0]; 097 $host = $ip unless defined $host; 098 return $host; 099 } 100 101 __DATA__ 102 @@ index.html.ep 103 % layout 'default'; 104 105 Host: <em id="host"></em> 106 URL: <em id="url"></em> 107 108 <iframe width=100% height=800 src="" 109 id="pageview"></iframe> 110 111 @@ layouts/default.html.ep 112 <!doctype html><html> 113 <head><title>Apache Peek</title> 114 <script type="text/javascript"> 115 var socket = new WebSocket( 116 "<%== $ws_url %>" ); 117 socket.onmessage = function (msg) { 118 var data = eval( "(" + 119 msg.data + ")" ); 120 document.getElementById( 121 "host").innerHTML = data.host; 122 document.getElementById( 123 "url").innerHTML = data.url; 124 document.getElementById( 125 "pageview").setAttribute( "src", 126 data.url ); 127 }; 128 </script> 129 </head> 130 <body> <%== content %> </body> 131 </html>
Die Websockets-Implementierung in Firefox 4 und Google Chrome basiert auf der Draft-Version 76 des Protokolls, das Sicherheitsmängel aufweist. Zwar treten diese nur bei unverschlüsselter Kommunikation und fehlerhaft programmierten Proxies auf, doch im Endeffekt wäre der Enduser im realen Internet Attacken ausgesetzt.
Die aktuelle Version von Mojolicious auf dem CPAN (1.42) unterstützt deswegen nur die korrigierte Version des Protokolls, basierend auf der IETF-08-Spezifikation. Firefox 4 oder Google Chrome können mit letzterer allerdings nichts anfangen, doch Firefox 6 (Aurora) weiß damit umzugehen. Wer allerdings mit älteren Browsern testen möchte, der kann die alte Mojolicious-Version 1.16 vom CPAN laden, die damals nach dem Draft 76 des Websocket-Protokolls programmiert wurde. Für Produktionssysteme ist davon jedoch wegen der Sicherheitsmängel dringend abzuraten.
Abbildung 7: Ein neuer Firefox-Eintrag in about:config aktiviert die Websocket-API. |
Firefox 4 schaltet seine Websockets wegen der veralteten Implementierung
deshalb von Haus aus ab und nur wenn ein
waghalsige Teufelskerl im Dialog about:config
die Boolean-Variablen
network.websocket.enabled
und network.websocket.override-security-block
auf true
setzt, schaltet Firefox 4 das Feature frei (Abbildungen 7 und 8).
Abbildung 8: Wegen Sicherheitsbedenken muss der Anwender Websockets im Firefox 4 explizit aktivieren. |
Die notwendigen CPAN-Module Mojolicious, ApacheLog::Parser und JSON lassen
sich mit einer CPAN-Shell installieren.
Das Modul ApacheLog::Parser bereitet einige Schwierigkeiten bei der
Installation, da es auf Time::Piece beruht, dessen Testsuite fehlschlägt.
Der Grund ist ein seit einem Jahr bestehender Bug im Zusammenspiel mit
Test::Harness, der der Funktion des Moduls keinen Abbruch tut, aber seine
per CPAN-Shell ausgeführten Tests scheitern lässt. Ein force install
führt mit Gewalt zur erfolgreichen Installation.
Websockets stecken sicher noch in den Kinderschuhen und es wird wohl noch einige Zeit dauern, bis alle gängigen Browser auch die aktuelle Protokollversion fahren. Doch das Potential für Desktop-gleiche Browserapplikationen ist enorm, besonders im Chat- oder Videobereich lassen sich praktische Anwendungen denken.
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2011/08/Perl
Websocket Testseite, http://websocket.org/echo.html
"Watch Your Processes Remotely with Mojolicious and a Smartphone", Jamie Popkin, Linux Journal, 2011/07, p58
"Webstecker", Stefan Neufeind, iX 06/2011, Seite 60
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. |