CGI - Ein Tutorial

CGI-Tutorial von Daniel Schwamm (28.03.1998)

Inhalt

1. CGI versus HTML und JavaScript

Im Gegensatz zu HTML, VRML und JavaScript kommt man beim Common Gateway Interface (CGI) i.d.R. nicht nur mit einem ASCII-Editor aus; man benötigt eine Programmiersprache, die entweder CGI-Scripts interpretieren oder eigenständige Applikationen (EXEs) bzw. Dynamic Link Libraries (DLLs) kompilieren kann. CGI-Scripte sind im Zusammenhang mit Perl unter UNIX sehr beliebt. Auch für NT findet man kostenlose Perl-Interpreter, jedoch wollen wir diese hier nicht weiter betrachten. Unter NT scheinen mir EXEs bzw. DLLs sinnvoller zu sein, zumal sie auch ein deutlich schnelleres Antwortverhalten an den Tag legen. Der Einfachheit halber betrachten wir hier nur EXEs, obwohl DLLs sicherlich die bessere Alternative darstellen (DLLs werden nämlich nur einmal in den Speicher geladen und verbleiben dann dort resistent, d.h., sie müssen im Gegensatz zu EXEs nicht bei jeder CGI-Anfrage neu geladen und gestartet werden).

2. CGI anhand von Beispielen

Als begeisterter Delphi-Programmierer werden die im Folgenden vorgestellten CGI-Applikationen in dieser Sprache programmiert sein. CGI-Applikationen können aber im Prinzip auch in jeder anderen Programmiersprache entwickelt werden, die eigenständig laufende (Windows-)Programme generieren können, wie z.B. C, C++, Assembler, usw.

2.1. Beispiel I: "Hello, world"-Programm

CGI-Anwendungen bestehen i.d.R. aus zwei Teilen: aus dem HTML-File, über welches die CGI-Applikation auf dem Webserver aufgerufen wird, und aus der CGI-Applikation selbst, die, um ein gültiges CGI sein zu können, bestimmte Formalismen erfüllen muss. Wir werden uns nun beide Teile einzeln näher betrachten.

2.1.1. Erster Teil: HTML-Teil auf Client-Seite

Widmen wir uns zuerst der statischen HTML-Datei, die der Anwender in seinen Webbrowser (Client) einlädt. Man nehme dazu einen x-beliebigen ASCII-Editor, z.B. Notepad in Windows, und tippe Folgendes ein:

00001
00002
00003
00004
00005
00006
00007
00008
00009
<html>
 <body>
  <center>
   <a href='/cgi-bin/cgikurs/cgikurs1.cgi'>
    Klick mich, um CGI aufzurufen
   </a>
  </center>
 </body>
</html>

(=> cgikurs1.html)

Man sieht hier eine typische HTML-Referenz (einen Link), die im Gegensatz zu sonst meist üblich kein statisches HTML-File adressiert, sondern ein ausführbares Programm namens "cgikurs1.cgi", welches im "/cgi-bin/cgikurs/"-Verzeichnis liegt. Mit etwas Fantasie und Programmierer-Know-how kann man sich vielleicht bereits vorstellen, was diese ominösen CGI-Programme eigentlich so anstellen: Sie liefern unserem freundlichen Webseitenbesucher eine HTML-Seite zurück - eine HTML-Seite aber wohlgemerkt, die zum Zeitpunkt des Aufrufs i.d.R. noch gar nicht existiert, sondern vom CGI-Programm erst selbst erzeugt und dann auf die Standardausgabe des Betriebssystems geschrieben wird.

2.1.2. Zweiter Teil: CGI-Teil auf Server-Seite

Und wie sieht nun der Source-Code von "cgikurs.cgi" in Delphi 2.0 aus? Also von dem Teil der CGI-Applikation, der auf Webserverseite ausgeführt wird? Let's take a look!

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
program cgikurs;
{$apptype console}
uses
  windows,sysutils,classes;
begin
  writeln('Content-type: text/html');
  writeln;
  writeln;
  writeln('<html><body>');
  writeln('Hello, world!');
  writeln('</body></html>');
end.

Na, ist das nicht einfach? "{$apptype console}" sorgt dafür, dass unser Delphi-Programm eine Konsolen-Applikation ist - wie oben erwähnt müssen wir nämlich unsere dynamisch erzeugte HTML-Datei auf die Standardausgabe schreiben (und nicht in ein Formular oder sonst wohin), was Windows-Programme von Hause aus nicht machen (die schreiben i.d.R. nur ins eigene Fenster). Dann kommen jene drei magischen Zeilen, ...

00001
00002
00003
writeln('Content-type: text/html');
writeln;
writeln;

... ohne die gar nix geht. Mein lieber Schwan! Haben die mich bei meinen ersten CGI-Versuchen Nerven gekostet; nur eine Leerzeile weniger, die durch "writeln" realisiert wird, und der Browser meldet nur noch Bullshit zurück. Nun ja, der Rest ist aber eigentlich klar. Über ...

00001
00002
00003
writeln('<html><body>');
writeln('Hello, world!');
writeln('</body></html>');

... produzieren wir das klassische HTML-File ...

00001
00002
00003
<html><body>;
  Hello, world!;
</body></html>

... welches - abgespeichert z.B. als "tst.htm" - mit jedem Webbrowser auch offline betrachtet werden kann.

Klar, das ist alles noch wenig beeindruckend. Aber das Potenzial dahinter ist bereits zu ersehen: Statt dem klassischen "Hello, world"-File kann hier jedes x-beliebige HTML-File generiert werden. HTML-Files z.B., die ihrerseits wiederum CGI-Applikationen referenzieren können. Oder aber auch statische HTML-Dateien, wie wir gleich sehen werden.

2.2. Beispiel II: HTML-Datei wiedergeben per CGI

Wie das vorherige Beispiel demonstrierte, können per CGI dynamische Webseiten generiert und an den Webbrowser-Benutzer zurückgeschickt werden. Es ist aber natürlich auch das Einfachste der Welt, auf herkömmliche Weise HTML-Seiten zu entwickeln, also statisch, und diese dann in eine Delphi-CGI-Applikation einzubinden. Ihr wollt Beweise? Sollt ihr bekommen.

2.2.1. Erster Teil: Mit CGI verlinkte HTML-Datei

Hier haben wir zunächst wieder das CGI-aufrufende HTML-File, über welches wir uns diesmal allerdings zwei verschiedene statische HTML-Dateien anzeigen lassen können.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
<html>
 <body>
  <center>
   <a href='/cgi-bin/cgikurs/cgikurs2.cgi?tst1.htm'>
    Klick mich, um HTML-Datei 'tst1.htm' anzuzeigen
   </a>
   <br>
   <a href='/cgi-bin/cgikurs/cgikurs2.cgi?tst2.htm'>
    Klick mich, um HTML-Datei 'tst2.htm' anzuzeigen
   </a>
  </center>
 </body>
</html>

(=> cgikurs2.html)

2.2.2. Zweiter Teil: Die beiden statischen HTML-Dateien

Da hätten wir einmal die statische HTML-Datei namens "tst1.htm":

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
<html>
 <body bgcolor='#000000' text='#6060ff' link='#ffff00' vlink='#c0c000'>
  <center>
   <font size='6'>
    Ich bin die Datei <strong>'TST1.HTM'</strong>
   </font>
   <br>
   <a href='/cgi-bin/cgikurs/cgikurs2.cgi?tst2.htm'>
    Klick mich, um HTML-Datei 'tst2.htm' anzuzeigen
   </a>
  </center>
 </body>
</html>

Und zum anderen die statische HTML-Datei namens "tst2.htm":

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
<html>
 <body bgcolor='#6060ff' text='#000000' link='#ffff00' vlink='#c0c000'>
  <center>
   <font size='6'>
    Ich bin die Datei <strong>'TST2.HTM'</strong>
   </font>
   <br>
   <a href='/cgi-bin/cgikurs/cgikurs2.cgi?tst1.htm'>
    Klick mich, um HTML-Datei 'tst1.htm' anzuzeigen
   </a>
  </center>
 </body>
</html>

2.2.3. Dritter Teil: Unser CGI-Programm

Das Delphi-CGI-Programm schliesslich, welches unsere beiden statischen HTML-Dateien einladen und zum Webbrowser-Benutzer zurückschicken kann, ist folgendermassen aufgebaut:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
program cgikurs2;
{$apptype console}
uses
  forms,windows,sysutils,classes;
VAR
  homedir,command,s:string;
  tf:textfile;
begin
  command:=paramstr(1);
  homedir:=extractfilepath(application.exename);
  writeln('Content-type: text/html');
  writeln;
  writeln;
  assignfile(tf,homedir+command);
  reset(tf);
  while not eof(tf) do begin
    readln(tf,s);
    writeln(s);
  end;
  closefile(tf);
end.

Im Quellcode von "cgikurs2.html" (Beispiel II, erster Teil) sehen wir, wie durch das Anhängsel "?[parameter]" in der Link-Referenz einem CGI-Programm ein Parameter übergeben werden kann. In unserem Fall verwenden wir als Parameter naheliegenderweise die Dateinamen unserer beiden statischen HTML-Dateien "tst1.htm" und "tst2.htm". Durch die Programmzeile ...

00001
command:=paramstr(1);

... holt sich das Delphi-CGI-Programm eben diesen Parameter in die Variable "command". Anschliessend wird das Verzeichnis ermittelt, in dem die CGI-Applikation ausgeführt wird. Nun muss nur noch die gewünschte Datei im Textmodus geöffnet, zeilenweise eingelesen, nach Standard-Output geschrieben, und so an den Web-Client übermittelt werden.

2.3. Beispiel III: Multi-Parameterübergabe an CGI-Programme

Eben lernten wir, wie man einen einzelnen Parameter an ein CGI-Programm übergeben kann. In diesem Beispiel wollen wir aber gleich mehrere Parameter an unsere "cgi.cgi"-Applikation übergeben. Ausserdem sollen diese Parameter vom Webseitenbenutzer auch noch selbst vorgegeben werden können. Zu diesem Zweck eignen sich die HTML-Tags "<FORM>" und "<INPUT>", die in die CGI-aufrufende Webseite einzubauen sind.

2.3.1. Erster Teil: HTML-Seite mit FORM-Variablen

Unsere sogenanntes Webformular, also eine Webseite mit integriertem FORM-Tag, über das ein Benutzer nahezu beliebige Inhalte an den Webserver übermitteln kann, ist folgendermassen aufgebaut:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
<html>
 <body>
  <center>
   <form action='/cgi-bin/cgikurs/cgikurs3.cgi' method='post'>
    Vorname <input type='text' name='Vorname' value='D.'><br>
    Nachname <input type='text' name='Nachname' value='S.'><br>
    <input type='submit' value='Sende zu CGIKURS3.cgi'><br>
   </form>
  </center>
 </body>
</html>

(=> cgikurs3.html)

Man beachte das Attribut "method='post'" im "<FORM>"-Tag. Dadurch werden alle User-Eingaben auf der Webseite als Standardeingabe-Strom an die CGI-Applikation gesendet, statt sie in die auf 255 Zeichen beschränkte Parameterliste abzulegen (dies liesse sich durch "method='get'" erreichen).

2.3.2. Zweiter Teil: Multi-Parameter-abfragendes CGI-Programm

Das folgende Delphi-CGI-Programm ist in der Lage, die vom Benutzer eingegebenen und an den Webserver gesendeten Formulardaten auszulesen, und dann individuell darauf reagieren zu können:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
program cgikurs3;
{$apptype console}
uses
  forms,windows,sysutils,classes;
VAR
  s,p:string;
  cl,c:integer;
  ch:char;
  pc:array[0..10]of char;
procedure GetInput(var s:string);
begin
  getenvironmentvariable('CONTENT_LENGTH',pc,255);
  cl:=strtoint(strpas(pc));
  for c:=1 to cl do begin
    read(ch);
    s:=s+ch;
  end;
end;
begin
  GetInput(s);
  c:=pos('&',s);
  writeln('Content-type: text/html');
  writeln;
  writeln;
  writeln('<html><body><center>');
  writeln(copy(s,1,c-1)+'<br>');
  writeln(copy(s,c+1,length(s)-c)+'<br>');
  writeln('</center></body></html>');
end.

Der Source ist schon etwas anspruchsvoller. Zunächst wird die Länge des Standardeingabe-Stroms über eine vom Webserver zur Verfügung gestellten Umgebungsvariable namens "CONTENT_LENGTH" ermittelt. Dies geschieht über die Zeilen:

00001
00002
getenvironmentvariable('CONTENT_LENGTH',pc,255);
cl:=strtoint(strpas(pc));

Danach wird bewusster Standardeingabe-Strom Zeichen für Zeichen in die String-Variable "s" eingelesen. Die Länge der gesamten Eingabe haben wir ja zuvor ermittelt. Die einzelnen Benutzereingaben (in unserem Fall der Inhalt zweier Input-Felder) sind gemäss der CGI-Spezifikation in der String-Variablen "s" jeweils durch ein "&"-Zeichen voneinander getrennt. Die exakte Position von "&" zwischen unseren beiden Eingabefeldern wird nun mit mithilfe der "pos()"-Funktion von Delphi über die Programmzeile ...

00001
c:=pos('&',s);

... ermittelt. Anschliessend werden beide Inputs über ...

00001
00002
writeln(copy(s,1,c-1)+'<br>');
writeln(copy(s,c+1,length(s)-c)+'<br>');

... getrennt voneinander auf den Standardausgabe-Strom ausgegeben und damit an den Web-Client zurückgesandt.

2.4. Beispiel IV: Mini-Suchmaschine mithilfe von CGI

Gut - kommen wir nun zu einem Beispiel mit praktischerem Nutzen als dem bisher Gezeigtem: eine Suchmaschine! Nun ja, eine Minisuchmaschine zumindest.

2.4.1. Erster Teil: Die Datenbank

Gehen wir von folgender Datenbank namens "db.txt" aus, die pro Eintrag aus zwei Zielen besteht. Die jeweils erste Zeile eines Eintrages nennt den Namen einer Programmiersprache. Und in der zweiten Zeile steht eine subjektive Schulnote, die Auskunft gibt über die Qualität der zugehörigen Programmiersprache, wobei gilt: 0 Punkte mies bis 15 Punkte top.

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
C
10
C++
12
COBOL
4
Delphi
15
FORTRAN
6

2.4.2. Zweiter Teil: Das Suchformular in HTML

Das Suchformular erlaubt es dem Benutzer, einmal einen Buchstaben vorzugeben, der in der zu suchenden Programmiersprache enthalten sein muss. Zum anderen lässt sich die Schulnote vorgeben, die die gesuchte Programmiersprache mindestens innehaben sollte. Der Quellcode dazu sieht so aus:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
<html>
 <body>
  <center>
   <strong>Programmiersprachen</strong><br>
   <form action='/cgi-bin/cgikurs/cgikurs4.cgi' method='post'>
    Sprachname enthaelt <input type='text' name='Name' value='c'><br>
    Sprache hat mehr Punkte als
    <SELECT NAME='Mindestpunkte'>
     <OPTION>0</OPTION>
     <OPTION>5</OPTION>
     <OPTION>10</OPTION>
     <OPTION>15</OPTION>
    </SELECT>
    <br>
    <input type='submit' value='Selektiere aus DB.TXT'><br>
   </form>
  </center>
 </body>
</html>

(=> cgikurs4.html)

2.4.3. Dritter Teil: Die Suchabarbeitung per CGI

Zuletzt betrachten wir den diesmal schon recht umfangreichen Quellcode des Delphi-CGI-Programms, welches die eigentliche Sucharbeit in der Datenbank gemäss der per CGI übergebenen Kriterien ausführt und dann das Ergebnis, also die gefundenen Treffer, an den Benutzer zurückschickt:

00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
00013
00014
00015
00016
00017
00018
00019
00020
00021
00022
00023
00024
00025
00026
00027
00028
00029
00030
00031
00032
00033
00034
00035
00036
00037
00038
00039
00040
00041
00042
00043
00044
00045
00046
00047
00048
00049
00050
00051
00052
00053
00054
00055
00056
00057
00058
program cgikurs4;
{$apptype console}
uses
  forms,windows,sysutils,classes;
const
  _dbfn='db.txt';
VAR
  homedir,s,pname,mpunkte,dbname,dbpunkte:string;
  cl,c:integer;
  ch:char;
  pc:array[0..10]of char;
  tf:textfile;
procedure GetInput(var s:string);
begin
  getenvironmentvariable('CONTENT_LENGTH',pc,255);
  cl:=strtoint(strpas(pc));
  for c:=1 to cl do begin
    read(ch);
    s:=s+ch;
  end;
end;
begin
  homedir:=extractfilepath(application.exename);
  GetInput(s);
  c:=pos('&',s);
  pname:=copy(s,1,c-1);
  mpunkte:=copy(s,c+1,length(s)-c);
  writeln('Content-type: text/html');
  writeln;
  writeln;
  writeln('<html><body><center>');
  writeln(
   'Suche nach: <strong>'+pname+' '+mpunkte+
   '</strong> Gefunden wurden:<br>'
  );
  writeln('<table border=1 cellpadding=5 cellspacing=0>');
  writeln(
   '<tr bgcolor=a0a0a0><th>Sprache</th><th>Punkte</th><tr>'
  );
  pname:=uppercase(copy(pname,pos('=',pname)+1,length(pname)));
  mpunkte:=copy(mpunkte,pos('=',mpunkte)+1,length(mpunkte));
  assignfile(tf,homedir+_dbfn);
  reset(tf);
  while not eof(tf) do begin
    readln(tf,dbname);
    readln(tf,dbpunkte);
    if
     ((pos(pname,uppercase(dbname))<>0)or(pname=''))and
     (strtoint(dbpunkte)>=strtoint(mpunkte))
    then
      writeln(
        '<tr bgcolor=d0d0d0><th>'+dbname+'</th>'+
        '<th>'+dbpunkte+'</th></tr>'
      );
  end;
  closefile(tf);
  writeln('</Table></center></body></html>');
end.

Ja, das ist schon ein etwas dickerer Brocken. Zunächst filtern wir die beiden Eingabe-Variablen des Benutzers aus dem Standardeingabe-Strom. Wir greifen dazu auf die bewährte Methode des vorangegangenen Beispiels zurück und suchen nach dem Trennzeichen "&". In "pname" steht dann der Buchstabe, der in allen Treffern enthalten sein muss. Und in "mpunkte" die Schulnote, die alle Treffer mindestens erreichen müssen.

00001
00002
00003
00004
GetInput(s);
c:=pos('&',s);
pname:=copy(s,1,c-1);
mpunkte:=copy(s,c+1,length(s)-c);

Diese Werte stehen in den Strings "pname" und "mpunkte" allerdings nicht isoliert, sondern noch im Verbund mit dem Namen der HTML-Formular-Variablen, und zwar in der Form "formname=forminhalt". Indem wir jetzt also im Anschluss nach dem Trennzeichen "=" suchen, können wir die genauen Einzelwerte herausfiltern:

00001
00002
pname:=uppercase(copy(pname,pos('=',pname)+1,length(pname)));
mpunkte:=copy(mpunkte,pos('=',mpunkte)+1,length(mpunkte));

Anschliessend öffnen wir die Datenbank "db.txt" und durchlaufen sie vollständig. Je Doppelzeile, eingelesen in die Variablen "dbname" und "dbpunkte", wird dabei überprüft, ob die Suchkriterien "pname" und "mpunkte" erfüllt wurden:

00001
00002
((pos(pname,uppercase(dbname))<>0)or(pname=''))and
(strtoint(dbpunkte)>=strtoint(mpunkte))

Wenn ja, wird der Eintrag ausgegeben, ansonsten ignoriert. Und um das Ganze optisch etwas ansprechender zu machen, wird noch eine HTML-Table um die Ausgabe verpackt. That's all.