Aus der CGI-Trickkiste (Teil 4) (Linux-Magazin, Juni 98)

Den Ausgaben normaler CGI-Skripts schickt der Web-Server immer einen Header-Leporello mit HTTP-Protokoll-Informationen voraus. Ein absolut minimales CGI-Skript wie

    #!/usr/bin/perl -w
    print "Content-Type: text/html\r\n\r\n";
    print "Hello, CGI!\n";

erzeugt zwar nur eine minimale Ausgabe, schickt aber an den Browser etwa mit

    HTTP/1.0 200 OK
    Connection: close
    Date: Sat, 04 Apr 1998 05:51:35 GMT
    Server: Apache/1.2.4 mod_perl/1.05
    Content-Type: text/html
    Client-Date: Sat, 04 Apr 1998 05:51:37 GMT
    Hello, CGI!

eine Statusmeldung (HTTP/1.0 200 OK) und -- zusätzlich zum explizit angegebenen Content-type -- eine Reihe von informativen Headern, die die Art der Verbindung, den Servertyp und das Datum auf Client- und Serverseite festlegen. Möchte man auf diese Angaben Einfluß nehmen, muß das Skript im NPH-(Non-Parsed-Header)-Modus laufen, in welchem es explizit auf diese Leistung des Servers verzichtet und alles selbst in die Hand nimmt -- ohne Netz und doppelten Boden. Fängt der Name der Skript-Datei mit nph-... an, verzichtet der Server (z. B. der Apache) darauf, seine eigenen Header vor die eigentliche Ausgabe eines Skripts zu knallen oder die Ausgabe zu puffern. Vielmehr gelangen alle Daten, sobald das Skript sie ausgibt, ungefiltert zum Browser.

Hieraus ergeben sich interessante Anwendungen: Statt die Seite im Browser auf einen Schlag aufzubauen, kann man die Daten schrittweise hineinladen. Das dynamische Fortschreiben der Daten vermittelt dem Brower-Benutzer bei langlaufenden CGI-Skripts den Eindruck, daß es vorwärts geht und nicht etwa der Server hängt oder das Netz dicht ist.

It's the fi-nal Countdown - didodiedo!

Als völlig sinnloses Beispiel sei in diesem Zusammenhang das Countdown-Skript nph-append.pl vorgestellt, das nichts anderes tut, als im Sekundentakt zuerst 2, dann 1, und dann Boom! anzuzeigen. Da weder der Server die Ausgabe puffert (nph-Skript) noch das Skript selbst ($| enthält einen wahren Wert), füllt sich die im Browser angezeigte Seite schrittweise.

nph-append.pl

    #!/usr/bin/perl -w
    ######################################################################
    # Michael Schilli, 1998 (mschilli@perlmeister.com)
    ######################################################################
    
    use CGI qw/:standard/;                  # header() exportieren
    
    $| = 1;                                 # Ausgabe entpuffern
    
    print header(-nph => 1);                # NPH-Header ausgeben
    
    print (h1("2"));                        # 2!
    sleep(1);
    
    print (h1("1"));                        # 1!
    sleep(1);
    
    print (h1("Boom!"));                    # Bazong!

nph-append.pl nutzt natürlich Lincoln Steins allgegenwärtiges CGI-Paket, das auch die Konstruktion von nph-Skripts unterstützt: Erhält die header-Funktion zusammen mit der -nph-Option einen wahren Wert überreicht, nimmt sie die Ausgabe der Status-Meldung zusammen mit allen wichtigen Headern in die Hand.

Dokumente schrittweise überladen

Beim sogenannten Server-Push erwartet der Browser vom Server nicht nur ein Dokument, sondern mehrere aufeinanderfolgende, von denen er jeweils nur das neueste darstellt und bei ankommendem Nachschub das bisher dargestellte einfach überlädt. Die Verbindung des Clients zum Server bleibt dabei solange erhalten, bis auch das letzte Dokument eingetrudelt ist -- unter Umständen eine Herausforderung für einen stark beschäftigten Web-Server, also Vorsicht! Den Startschuß hierzu liefert im Response-Header der Eintrag multipart/x-mixed-replace für den Content-type. Dieser spezifiziert gleichzeitig noch einen String, der eindeutig den Übergang zwischen zwei Dokumenten festlegt. Er muß mit zwei Gedankenstrichen (--) beginnen, und so eindeutig sein, daß die Zeichenfolge nirgends im nachfolgenden Dokument vorkommt. Der letzte Trennstring des Multi-Dokuments erhält zusätzlich noch "--" angehängt. Listing nph-sp.pl zeigt die Implementierung, ins cgi-bin-Verzeichnis und über einen Browser aufgerufen, liefert das Skript zunächst die Anzeige 2, die nach einer Sekunde von 1 überladen wird, worauf wiederum nach einer Sekunde schließlich Boom! folgt.

nph-sp.pl

    #!/usr/bin/perl -w
    ######################################################################
    # Michael Schilli, 1998 (mschilli@perlmeister.com)
    ######################################################################
    
    use CGI qw/:standard/;
    
    $| = 1;
    
    $separator = "DerEindeutigeTrenner";
    
    print header(-nph  => 1,                     # Top-Header
                 -type => "multipart/x-mixed-replace;boundary=$separator"
                );
    
    print "--$separator\n";                      # Erster Trenner
    
    print header(-type => 'text/html'), "\n";    # Teil-Dokument
    print h1(2), "\n";
    print "--$separator\n";                      # Trenner
    
    sleep(1);
    
    print header(-type => 'text/html'), "\n";    # Teil-Dokument
    print h1(1), "\n";
    print "--$separator\n";                      # Trenner
    
    sleep(1);
    
    print header(-type => 'text/html'), "\n";    # Teil-Dokument
    print h1("Boom!"), "\n";
    print "--$separator--\n";                    # End-Header

Client-Pull

Das gleiche Problem löst auch ein CGI-Skript, das den Zählerwert eines hereingereichten Query-Parameters anzeigt, diesen herunterzählt und sich selbst nach einer Sekunde wieder mit dem neuen Wert des Query-Parameters aufruft.

Findet der Browser am Beginn eines HTML-Dokuments die Sequenz

    <META HTTP-EQUIV="Refresh" 
          CONTENT="1; URL=http://host/cgi-bin/clientpull.pl?count=2">

veranlaßt ihn das, nach der im CONTENT-Feld eingestellten Zeitspanne (eine Sekunde) den ebenfalls dort spezifierten URL anzufordern. Im Beispiel bezieht sich der URL auf das CGI-Skript, das die HTML-Seite erzeugt hat, und sich so nach der GET-Methode mit dem Wert 2 für den Parameter count aufruft. Aufgabe des CGI-Skripts ist es dann, eine neue Seite zu generieren, die den oben dargestellten HTML-Tag mit einem um eins heruntergesetzten Zählerwert repliziert -- und schon fängt der Countdown an zu laufen.

Zweitens besteht für den Server die Möglichkeit, in den Response-Header einen Refresh-Eintrag einzupacken. Er hat für die Nachladezeit 1s und den URL des Skripts folgende Form:

    Refresh: 1; URL=http://host/cgi-bin/clientpull.pl?count=2

Das Skript clientpull.pl implementiert das geforderte Verhalten. Solange der überlieferte Zählerwert noch größer als Null ist, packt es die Nachlade-Anweisung in den Response-Header, bevor es den aktuellen Zählerstand ausgibt. Die Environment-Variable SCRIPT_NAME enthält (gemäß Server-Standard) den URL-Pfad des ausgeführten Skripts. Erreicht der Zähler die Null, gibt clientpull.pl vor der letzten Ausgabe einen regulären Header aus und beendet so den Nachlade-Reigen.

clientpull.pl

    #!/usr/bin/perl -w
    ######################################################################
    # Michael Schilli, 1998 (mschilli@perlmeister.com)
    ######################################################################
    
    use CGI qw/:standard/;
    
    $count = param('count');         # CGI-Parameter abfragen
    $count ||= 3;                    # Parameter nicht gesetzt? Startwert.
    $count--;
    
    if($count) {
        print header(-Refresh => "1; URL=$ENV{SCRIPT_NAME}?count=$count");
        print h1($count);
    } else {
        print header();
        print h1("Boom!");
    }

Umleitung

Der Redirect-Mechanismus dient dazu, Benutzer beim Klick auf ein Dokument automatisch (und unsichtbar) auf einen anderen URL umzuleiten. Der Server spuckt hierzu lediglich den neuen URL mitsamt einiger Header-Informationen aus, und zack! springt der Browser dorthin - ohne eine entsprechende Meldung anzuzeigen, nur die URL-Anzeige verändert sich. Verschiebt sich der Zugriffspfad einer Webseite oder zieht diese gar auf einen anderen Rechner um, lassen sich Benutzer, die noch den alten URL benutzen, zuverlässig umdirigieren.

Listing nph-redir.pl zeigt, wie das mit CGI.pm funktioniert: Die Option -redirect der Header-Funktion bewirkt das gewünschte Verhalten.

Listing nph-redir.pl

    #!/usr/bin/perl -w
    ######################################################################
    # Michael Schilli, 1998 (mschilli@perlmeister.com)
    ######################################################################
    
    use CGI qw/:standard/;
    
    print redirect(-nph => 1,
                   -uri => 'http://other.host.com');

Allerdings kostet diese Art der Umleitung relativ viel Strom: Der Perl-Interpreter muß hochfahren, das Skript parsen, CGI.pm laden -- da können auf einem lahmen PC schon mal zwei Sekunden verstreichen. Thema einer der kommenden Folgen wird deswegen der Apache-Server und mitsamt dem Modul mod_perl sein. Diese Kombination stellt eine Schnittstelle zur Verfügung, um derlei Tricks bei minimaler Serverbelastung zu realisieren.

Alterungskontrolle

Damit ein Browser z.B. ein stündlich modifiziertes Dokument nicht über Gebühr in seinem Cache hält und rechtzeitig die neueste Version holt, setzt man einfach serverseitig das Verfallsdatum des Dokuments auf die Erscheinungszeit der neuen Version.

Das CGI-Skript expire.pl zeigt zu Testzwecken den Stundenanteil der aktuellen Uhrzeit an und teilt dem Client mit, daß das Dokument zum nächsten Stundenwechsel seine Gültigkeit verliert.

Listing expire.pl

    #!/usr/bin/perl -w
    ######################################################################
    # Michael Schilli, 1998 (mschilli@perlmeister.com)
    ######################################################################
    
    use CGI qw/:standard/;
    
    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) =
                                                localtime(time());
    
    $minutes_to_expiration = 60 - $min;
        
    print header(-expires => "+${minutes_to_expiration}m");
    
    print h1("Die volle Stunde ist: $hour");

Hierzu rechnet expire.pl die verbleibenden Minuten bis zum Anfang einer neuen Stunde aus und schickt mittels der -expires-Option der header-Funktion einen Expires-Header, der besagt, daß der Browser das Dokument nach X Minuten nicht mehr aus dem Cache hervorschummeln darf sondern tatsächlich vom Server holen muß. Ist die aktuelle Uhrzeit auf dem Server etwa 07:18:05, fehlen bis zum Stundenwechsel noch etwa 42 Minuten und mit -expires => "+42m" (die Option versteht die gleiche Syntax wie das letzten Monat vorgestellte Cookie-expire) sehen die zwei ausschlaggebenden Header folgendermaßen aus:

    Date: Sat, 18 Apr 1998 07:18:05 GMT
    Expires: Sat, 18 Apr 1998 08:00:05 GMT

Damit ist zwar keineswegs garantiert, daß der Browser das Dokument in seinem Cache hält (manche Browser cachen keine CGI-Skripts oder haben in den Options-Menüs die Cache-Strategie deaktiviert), doch falls er den Mechanismus unterstützt, führt er bei weiteren Aufrufen der URL http://host/cgi-bin/expire.pl zwischen 07:18:05 und 08:00:05 keinen Netzzugriff mehr aus, sondern zeigt die olle Cache-Kopie. Danach oder beim Druck auf den Reload-Button (bei manchen Browsern CTRL-Reload) holt er sich die neueste Version vom Server.

So, damit ist erstmal Schluß mit dem CGI-Schmarr'n! Das nächste Mal gibt's einen Clipping-Agenten, der Nachrichten aus allerlei Zeitungen zusammensucht! Da werd's spitz'n! Bleibt's g'sund miteinand'!

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.