Algorithmen & Datenstrukturen
Programmieren 1

von Prof. Jürgen Plate

Programmierübungen in "C"

Zur Vorbereitung der Aufgabe gehören: Je nach Komplexität der Aufgabe können einzelne Punkte stark gekürzt werden.


Projekt: Web-Lexikon (Wiki)

Zu schreiben ist ein Programm für ein Web-basiertes elektronisches Lexikon mit folgenden Eigenschaften:
  1. Jeder Benutzer kann einen Eintrag vornehmen oder korrigieren (Passwortschutz wird später implementiert).

  2. Ein Lexikoneintrag ("Artikel") besteht aus:

  3. Die Eingabe bzw. Änderung eines Artikels erfolgt über ein Webformular. Die vom Formular kommenden Daten müssen aufbereitet werden. Beschreibung siehe unten.

  4. Die Speicherung erfolgt in Form von Dateien

  5. Auswahl der Artikel über ein Menü der Begriffe.

  6. Alternativ soll nach beliebigen Worten im Artikeltext gesucht werden können.

  7. Die Ausgabe erfolgt als Webseite (vordefinierte Templates für Seitenkopf und -fuss werden von Datei eingelesen.

Die Realisierung erfolgt in kleinen Teilaufgaben. Teile des Web-Frontends in HTML werden fertig zur Verfügung gestellt. Der Webserver nimmt uns viel Arbeit ab. Dahinter steckt das Common Gateway Interface.

Testumgebung

Damit Sie Ihre Programme auch testen können, wurde eine Testumgebung auf dem Praktikumsrechner eingerichtet. Es läuft ein Webserver, der ein sogenanntes "User-Directory" erlaubt. Jeder Benutzer kann damit Webseiten und auch CGI-Programme anbieten. Dazu wird im Heimatverzeichnis ein Unterverzeichnis www eingerichtet, in das alle Webseiten gelegt werden (Dateiendung .html). Das www-Verzeichnis enthält ein Unterverzeichnis namens cgi-bin, in dem alle Programme untergebracht sind (Dateiendung .cgi).

Zur Einrichtung des Ganzen öffnen Sie ein Shell-Fenster und rufen Sie dann das Kommando machwww auf. Dieses Programm richtet die Verzeichnisse ein und erzeugt auch eine (sehr einfache) Datei index.html sowie ein ebenso einfaches CGI-Programm namens huhu.cgi (einschl. Quelltext). Zum Testen können im Browser dann die beiden Webadressen

http://localhost/~username/index.html
bzw.
http://localhost/~username/cgi-bin/huhu.cgi

verwendet werden.

Eigene CGI-Programme müssen aus dem Kdevelop-Projektverzeichnis ins CGI-Verzeichnis umkopiert und umbenannt werden. Das geschieht durch das Kommando (aus dem Heimatverzeichnis /home/Username heraus). Ausserdem müssen die Zugriffsrechte gesetzt werden:

cp   Projektname/debug/src/Projektname   www/cgi-bin/Projektname.cgi
chmod   755   www/cgi-bin/Projektname.cgi

Für Projektname müssen Sie natürlich den von Ihnen verwendeten Namen des Projekts (in der Regel wiki) eingeben. Damit Sie sich leichter tun, gibt es zwei Hilfskommandos:

copycgi Projektname    kopiert Ihr Binärprogramm ins CGI-Verzeichnis und versieht es dort auch gleich mit der Endung .cgi.

gibfrei    setzt die Zugriffsrechte für alle Dateien in Ihrem www-Verzeichnis so, dass auf diese per Browser zugegriffen werden kann.

1. Aufgabe

Schreiben Sie ein Programm, das von der Standardeingabe beliebig viele und beliebig lange Textzeilen entgegennimmt und diese weiterbehandelt: In die Datei werden die Daten folgendermassen gespeichert: Hinweise zur Lösung: Verwenden Sie das in der Vorlesung besprochene Rumpfprogramm wiki.c und erweitern Sie es um die Speicherung der Daten in einer Datei. Beachten Sie auch die Kommentare dazu im Programm.

2. Aufgabe

In dieser Aufgabe geht es um die Erfassung aller in einem Verzeichnis gespeicherter Dateien. Um ein Verzeichnis auszulesen, wird es zuerst mit dem Aufruf opendir() geöffnet, dann können die Einträge nacheinander mit readdir() gelesen werden. Mit closedir() wird es wieder geschlossen. Analog dem Dateihandle gibt es ein Verzeichnishandle, das vom Datentyp DIR* ist. Informationen über den Eintrag liefert die von readdir() gelieferte Struktur dirent. Die Funktionen sind folgendermassen definiert:
#include <sys/param.h> 
#include <dirent.h>

DIR *opendir(const char *pfadname); 
struct dirent *readdir(DIR *dir);
int closedir(DIR *dir);
Die Funktion opendir() erhält als Parameter den Namen des Verzeichnisses. Der Rückgabewert ist ein Zeiger auf das Verzeichnis-Handle. Ein Fehler wird dadurch angezeigt, dass dieser Zeiger den Wert NULL besitzt.

Die Funktion readdir() liest den nächsten Eintrag im Verzeichnis und erhält als Rückgabewert einen Zeiger auf einen Wert vom Typ dirent:

struct dirent 
  {
  long            d_ino;  /* Inode Nummer */
  off_t           d_off;  /* Offset zum naechsten dirent */
  unsigned short  d_reclen;/* Laenge dieses Eintrags */
  char            d_name[MAXNAMELEN+1]; /* Dateiname */
  };

Für das Anwenderprogramm ist eigentlich nur der Name des Eintrags interessant. Wollen Sie mehr über diesen Eintrag erfahren, beispielsweise, ob es wieder ein Verzeichnis ist, so brauchen Sie dazu andere Systemaufrufe (z. B. stat()). Zuletzt wird das Verzeichnis mit closedir() wieder geschlossen. Ein Beispielprogramm für das Auslesen eines Verzeichnisses sieht so aus:
#include <stdio.h>
#include <stdlib.h>
#include <sys/param.h> 

// fuer Anzeige des Verzeichnis-Inhalts
#include <dirent.h> 

int main(void) 
  {
  DIR *dir;
  struct dirent *dirzeiger;
  char dateiname[MAXNAMELEN+1];      /* MAXNAMELEN definiert in param.h */
  char dirname[MAXNAMELEN+1] = ".";  /* --> Laenge reicht garantiert  */

  printf("Alle Dateien im aktuellen Verzeichnis: %s\n",dirname);

  dir = opendir(dirname);
  if(dir != NULL)
    {
    while((dirzeiger = readdir(dir)) != NULL)
      {
      strcpy(dateiname,(*dirzeiger).d_name);
      printf("%s\n",dateiname);
      }
    }
  closedir(dir);
  
  return 0;
  }
Das Programm können Sie unter dir.c herunterladen.

Aufgabenstellung:

  1. Ändern Sie das Programm der 1. Aufgabe so ab, dass die Dateien nicht im aktuellen Verzeichnis, sondern in einem vorgegebenen Verzeichnis abgelegt werden, das durch
    #define DATADIR "/home/wiki"
    
    definiert ist (Stringfunktionen strcat bzw. strcpy!).

  2. Schreiben Sie nun eine Funktion, die ein Auswahlformular für alle Dateien im oben angegebenen Verzeichnis ausgibt. Dabei sollen Dateien, die mit einem Punkt beginnen, nicht berücksichtigt werden. Das Formular beginnt mit den Zeilen (Erläuterung in der Vorlesung):
    <HTML>
    <HEAD>
    <TITLE>Lexikon-Ausgabeformular</TITLE>
    </HEAD>
    <BODY>
    <H1>Lexikon-Ausgabe</H1>
    
    <P>
    <FORM method=POST action="http://localhost/~testuser/www/cgi-bin/wiki.cgi">
    <SELECT NAME="Datei" SIZE="5">
    
    Danach folgt ihre Dateiausgabe, wobei für jede Datei eine Zeile der Form
    <OPTION>Dateiname</OPTION>
    
    ausgegeben werden soll. Die Ausgaben schliesst mit den Zeilen:
    </SELECT>
    <P>
    <INPUT TYPE="SUBMIT" NAME="SUBMIT" VALUE="Absenden">
    </FORM>
    </BODY>
    </HTML>
    

  3. Schreiben Sie eine Funktion, welche eine dem Eingabeformular (ppform.html) vergleichbare Ausgabe erzeugt, jedoch die Inhalte einer vorgegenen Datei (Name wird als Parameter übergeben) in das Formular einfügt. Dazu wird bei jedem Eingabefeld vor der schliessenden spitzen Klammer die Angaben VALUE="....." eingefügt, aus der Originalzeile <INPUT TYPE=TEXT SIZE=60 NAME="Begriff"> wird dann beispielsweise printf("<INPUT TYPE=TEXT SIZE=60 NAME=\"Begriff\" VALUE=\"%s\">\n",Begriff);. Beim Eingabefeld "Textarea" erfolgt die Ausgabe direkt zwischen den Tags <TEXTAREA> und </TEXTAREA>.

    Dabei ist es nur nötig, die erste Zeile in die drei Felder "Begriff", "Autor" und "Datum" zu zerlegen. Der Rest der Datei kann an der oben genannten Stelle (Textarea) einfach auf die Standardausgabe kopiert werden. Beachten Sie dazu auch die Erläterungen in der Vorlesung.

Hinweis: Wir brauchen die Webseitensprache HTML zwar nur als Vehikel (weshalb die Sprachelemente in der Regel vorgegeben werden), aber wer sich dafür interessiert, findet eine knappe Einführung unter http://www.netzmafia.de/skripten/easyhtml/index.html.

3. Aufgabe

Nun wollen wir das bisher geleistete in den Webserver integrieren. Dazu müssen Sie noch kleine Modufikationen an den Programmen vornehmen, die Quellcodes übersetzen und das ausfübare Programm in Ihr cgi-bin-Verzeichnis kopieren (Hinweis: Programm copycgi verwenden). Für die weiten Beschreibungen und Aufgaben gebe ich den Programmen folgende Namen (bei Ihnen heissen sie unter Umständen anders): Wobei letzteres erst jetzt fertiggestellt wird.

Beim Programm aus Aufgabe 1 müssen Sie nur noch die #define-Zeilen ändern, indem Sie bei den folgenden Zeilen die Kommantare ("// ") entfernen:

// #define REQUEST_METHOD getenv("REQUEST_METHOD")
// #define CONTENT_LENGTH getenv("CONTENT_LENGTH")
// #define QUERY_STRING getenv("QUERY_STRING")
Die folgenden Test-Zeilen werden gelöscht.
/* Fuer's Testen definieren wir die Dinger selbst */
#define REQUEST_METHOD "GET"
#define CONTENT_LENGTH "119"
#define QUERY_STRING "Begriff=G%E4nsegeier&Autor=...
Dann muss das CGI-Programm ja noch eine Antwort geben, z. B. "Ihre Eingabe wurde gespeichert". Das muss natürlich in einem HTML-Umfeld verpackt werden ("Content-Type"-Zeile nicht vergessen!). Das könnte etwa folgendermassen aussehen:
printf("Content-Type: text/html\r\n\r\n");
printf("<HTML><HEAD><TITLE>Wiki-Eingabe</TITLE></HEAD>\n");
printf("<BODY><H1>Wiki-Eingabe</H1>\n");
.
.
.
printf("Ihre Eingabe wurde gespeichert\n");
printf("</BODY></HTML>\n");
Nun können Sie das schon mal testen. Dazu übersetzen Sie das Programm und kopieren dann den erzeugten Binärcode in Ihr cgi-bin-Verzeichnis (Programm copycgi). Anschriessend sind die Execute-Rechte zu setzen (Programm gibfrei). Bei eventuell fehlerhaften Verhalten können Sie die Fehlerausgabe des Webservers in einem Shell-Fenster mithilfe des Kommandos tail -f /var/apache/logs/error_log beobachten (Abbruch des Programms mit Strg-C).

Damit es beim Testen keine "Interferenzen" gibt (53 mal der "Gänsegeier"), nehmen Sie als Begriff Ihren Usernamen oder geben etwas Sinnvolles ein.

Das Programm aus Aufgabe 2 ist noch nicht fertig und wird nun fertiggestellt. Bisher gibt es zwei Funktionen, die bei mir die Namen

heissen. Um das Programm zu vervollständigen, fischen Sie sich aus der originalen Datei wiki.c die Funktionen
char x2c(char hex[]);
void unescape(char *url);
void CGIReadInput(void);
heraus und hängen diese an Ihren Programmcode an. Die Funktion CGIReadInput() erweitern Sie diesmal so, dass der Formularwert "Datei" herausgeholt wird. Da aber nun die Möglichkeit besteht, dass CGIReadInput() auch ohne Parameter aufgerufen wird, ist noch eine weitere Änderung notwendig:
.
.
.
if (CONTENT_LENGTH == NULL)
{
/* Hier Dateiname auf "" setzen o. ä */
return;   /* statt exit(1) */
}
Nun gibt es zwei Möglichkeiten: Nach dem Erzeugen des Binärcodes können Sie nun testen, ob Sie Ihre vorher erstellte Datei auch Ändern können.

Anmerkung: Alle Programme im cgi-bin-Verzeichnis laufen mit der Benutzerberechtigung des Webserver-Programms. Das Verzeichnis /home/wiki hat Schreibberechtigung fü diesen Benutzer. Beim lokalen Test in der KDevelop-Umgebung laufen die Programme natürlich mit Ihrer Benuterberechtigung, weshalb Sie auf /home/wiki nicht problemlos zugreifen können. Es ist also sinnvoll diesen Pfad als Konstante zu definieren (#define ...) und vor dem Erzeugen der Endversion umzustellen.

4. Aufgabe

Bleibt noch die Suche nach Dateiinhalten. Auch das soll in kleinen Schritten erarbeitet werden:
  1. Beginnen Sie ein neues Projekt und schreiben Sie eine Funktion void nextword(FILE *fp, char *wort), welche aus einer zuvor geöffneten Datei das nächste "Wort" liefert. Als "Wort" gilt alles, was mit einem Buchstaben beginnt und aus Buchstaben und Zahlen besteht. Bedenken Sie dabei, dass die deutschen Umlaute und das scharfe S nicht Bestandteil des ASCII-Zeichensatzes sind (und damit auch nicht von den Bibliotheksfunktionen als Buchstaben erkannt werden). Sie müssen im Programm deshalb getrennt behandelt werden. Andere Zeilen (die ggf. vor dem Wort stehen, werden überlesen. Ist das Dateiende erreicht, wird ein Leerstring zurückgegeben. Das Hauptprogramm zum Testen hat folgendes Aussehen:
    int main(int argc, char *argv[])
      {
      char Dateiname[MAXNAMELEN+1] = "Textdatei";
      char Wort[1000];
      FILE *fp;
    
      fp = fopen(Dateiname,"r");
      if (fp == NULL)
        {
        printf("Kann Datei %s nicht oeffnen\n",Dateiname);
        exit(1);
        }
      do
        {
        nextword(fp,Wort);
        printf(">>>%s&tl;<<\n",Wort);
        }
      while (Wort[0] != '\0');
      fclose(fp);
    
      return EXIT_SUCCESS;
      }
    
  2. Ändern Sie nun die Schleife so, dass sie beendet wird, sobald das gefundene Wort mit einem vorgegebenen Suchwort übereinstimmt und "verpacken" Sie den gesamten Code (fopen ... fclose) in einer weiteren Funktion int scanfile(char *Dateiname, char *Suchwort), die 0 zurückgibt, wenn das Wort nicht gefunden wurde und 1, wenn es in der Datei enthalten war. Das (Test-)Hauptprogramm lautet nun:
    int main(int argc, char *argv[])
      {
      char Dateiname[MAXNAMELEN+1] = "Textdatei";
      char Suchwort[1000] = "Desoxyribonukleinsäure";
      int Erg;
    
      Erg = scanfile(Dateiname,Suchwort);
      if (Erg) printf("WORT GEFUNDEN!\n");
      else     printf("NICHTS GEFUNDEN!\n");
    
      return EXIT_SUCCESS;
      }
    
  3. Verwenden Sie nun das bereits bekannte Programm dir.c, um die Funktion nacheinander scanfile auf alle Dateien in einem Verzeichnis anzuwenden.
  4. Zuallerletzt wird das Programm wieder Web- oder Browserfähig gemacht. Sie können nun viele Anleihen beim vorhergehenden Programm machen. Wird das Programmsuche.cgi ohne Parameter aufgerufen, präsentiert es ein einfaches Suchformular. Existiert ein Parameter Namens "Suchwort" wird die neu geschriebene Suchfunktion aktiviert. Sie kön die Funktion CGIReadInput komplett übernehen. Es wird lediglich statt "Datei=..." nun "Suchwort=..." genommen. Das Hauptprogramm sieht dann etwa so aus:
       .
       .
       .
    /* #define ... nicht vergessen  */
       .
       .
       .
    #define FILEPATH "/home/wiki/"
       .
       .
    int main(int argc, char *argv[])
      {
      DIR *dir;
      struct dirent *dirzeiger;
      char Dateiname[MAXNAMELEN+1] = "";
      char Dateipfad[MAXNAMELEN+1] = "";
      char Suchwort[1000] = "";
      int Erg;
    
      printf("Content-Type: text/html\r\n\r\n");
    
      printf("<HTML>\n<HEAD>\n<TITLE>Lexikon-Suche</TITLE>\n</HEAD>\n<BODY>\n");
      printf("<H1>Lexikon-Suche</H1>\n");
    
      CGIReadInput(Suchwort);
      if (strcmp(Suchwort,"") == 0)
        {
        printf("<FORM method=POST action=\"http://localhost/~testuser/www/cgi-bin/wiki.cgi\">\n");
        printf("Suchbegriff: <INPUT TYPE=TEXT NAME=\"Suchwort\" SIZE=\"20\">\n");
        printf("<INPUT TYPE=\"SUBMIT\" NAME=\"SUBMIT\" VALUE=\"Suchen\">");
        printf("</FORM>\n</BODY>\n</HTML>");
        }
      else
        {
        dir = opendir(dirname);
        if(dir != NULL)
          {
          printf("<UL>\n");
          while((dirzeiger = readdir(dir)) != NULL)
            {
            strcpy(Dateiname,(*dirzeiger).d_name);
            if (Dateiname[0] != '.')
              {
              strcpy(Dateipfad,FILEPATH);
              strcat(Dateipfad,Dateiname);
              if(scanfile(Dateipfad,Suchwort)) 
                printf("<LI>%s\n",Dateiname);
              }
            }
          printf("</UL>\n");
          closedir(dir);
          }
        }
      printf("</BODY>\n</HTML>\n");
      return EXIT_SUCCESS;
      }
    
    In dieser Stufe werden nur die Dateinamen ausgegeben. In einer späteren Erweiterung können dann auch noch die Dateiinhalte ausgegeben werden.

    Erweiterungen und Anmerkungen

    In der Vorlesung wurde eine Alternative für die Funktion CGIReadInput besprochen, bei der alle Name-Wert-Paare in einer linearen Liste gespeichert werden und so für den späteren Zugriff zur Verfügung stehen. Diese Variante der Funktion ist wesentlich universeller einsetzbar, da nicht für jeden Anwendungsfall individuelle Anpassungen vorgenommen werden müssen. Diese Variante soll zwar nicht im Praktikum eingesetzt werden, aber der Quellcode steht als Datei liste.c zur Verfügung.

    Im Praktikum und seinem abgeschlossenen System konnten Sicherheitsbetrachtungen zurückgestellt werden, um die Programmierung leichter und einfacher zu gestalten. Bei Verwendung der einzelnen Funktionen in der realen Welt sind aber noch weitere Sicherheitsbetrachtungen anzustellen.

    Zum Inhaltsverzeichnis


    Copyright © FH München, FB 04, Prof. Jürgen Plate
    Letzte Aktualisierung: 03. Jun 2006