Die Lösung für eine der beiden folgenden Aufgaben müssen Sie schon bis zum 11. Juni 2001 abgeben. Welche Lösung Sie früher abgeben bleibt Ihnen überlassen.
Nehmen Sie zuerst Ihren persönlichen Vorlesungsbegleiter zur Hand
und schlagen diesen auf den Seiten 67/68 auf. Dort finden Sie eine
Erklärung der Bibliotheksbefehle popen()
und pclose()
.
Sie würden nun, nachdem Sie die Beschreibung gelesen haben, gerne
wissen, wie diese implementiert sind. Na das kriegen Sie doch raus!
Sie programmieren popen()
und pclose()
einfach selbst! ;-)
Übertreiben müssen wir es aber dann doch nicht, oder?! Also gut,
nach einem popen()
muss zunächst das zugehörige pclose()
kommen, bevor popen()
erneut aufgerufen wird. Das macht die
Sache jetzt aber wirklich leichter!
Tipps: Mit
execl("/bin/sh", "sh", "-c", cmd, NULL);
können
Sie das Kommando cmd
von der Shell ausführen lassen (unter
Berücksichtigung von PATH). Mit fdopen()
kann aus einem
File Descriptor ein File Pointer gewonnen werden. Den File
Descriptor darf man dann aber nicht mehr schließen, sondern muss
am Ende den File Pointer schließen!
Lösung:
/* * AI4 SS01 Blatt 6 (c) jm sai uni ulm 2001 * Nicht reentrante Implementierung von popen und pclose, * d.h. popen-pclose-Bloecke muessen paarweise disjunkt sein. */ #include <stdio.h> #include <unistd.h> #include <wait.h> int popen_pid = -1; // PID des Prozesses, der das Kommando ausfuehrt FILE *popen_fp; // FP auf das Schreib- bzw. Leseende der Pipe FILE *popen(const char *cmd, const char *mode) { int is_read; // TRUE <=> Vater will von FP lesen int pid; // PID des Sohnes, der das Kommando ausfuehrt FILE *fp; int pfd[2]; /* * Eingabe ist fehlerhaft, wenn das Kommando fehlt * oder der Modus ungleich "r" und ungleich "w" ist. */ if (cmd == NULL || (strcmp(mode, "r") && strcmp(mode, "w"))) return NULL; is_read = (strcmp(mode, "r") == 0); // lesender Modus ? if (pipe(pfd) < 0) { perror("pipe()"); return NULL; } switch (pid = fork()) { case -1 : // Fehler beim forken perror("fork()"); return NULL; case 0 : // Sohn-Prozess => Kommando ausfuehren if (is_read) { // Vater will aus der Pipe lesen close(1); // FD 1 auf das Schreibende ... dup(pfd[1]); // ... der Pipe umlenken close(pfd[1]); // restlichen Enden der ... close(pfd[0]); // ... Pipe dicht machen } else { // Vater will in die Pipe schreiben close(0); // FD 0 auf das Leseende ... dup(pfd[0]); // ... der Pipe umlenken close(pfd[0]); // restlichen Enden der ... close(pfd[1]); // ... Pipe dicht machen } /* Kommando mit der sh-Shell ausfuehren */ execl("/bin/sh", "sh", "-c", cmd, NULL); perror("execl()"); // Fehler bei exec() exit(1); default : // Vater-Prozess => Programmausf. fortsetzen if (is_read) { // Vater will aus der Pipe lesen /* Schreibende dicht machen ... */ close(pfd[1]); /* ... und FP fuer Leseende besorgen */ fp = fdopen(pfd[0], "r"); } else { // Vater will in die Pipe schreiben /* Leseende dicht machen ... */ close(pfd[0]); /* ... und FP fuer Schreibende holen */ fp = fdopen(pfd[1], "w"); } /* * popen/pclose sind nicht reentrant, weil * ein zweiter popen Aufruf die folgenden * globalen Variablen ueberschreiben wuerde! */ popen_fp = fp; // FP und Sohn-PID global ... popen_pid = pid; // ... machen fuer pclose return fp; } } int pclose(FILE *fp) { int status; // Status des Sohn-Prozesses von waitpid if (popen_pid < 0 || fp != popen_fp) // kein zugeh. popen-Aufruf? return -1; if (fclose(popen_fp) < 0) { // Pipe-Ende schliessen fprintf(stderr, "could not close pipe\n"); return -1; } if (waitpid(popen_pid, &status, 0) < 0){ // auf den Sohn-Prozess warten perror("waitpid()"); return -1; } popen_pid = -1; // es gibt jetzt kein zugeh. popen mehr return status; } /* * kleines Testprogramm: * Liest von der Standardeingabe Zeilen bis EOF und * schreibt diese in die Pipe, welche mit popen erzeugt * wurde. Von sort wird die Eingabe dann sortiert auf * die Standardausgabe geschrieben. */ int main() { char buf[128]; FILE *fp; fp = popen("sort", "w"); // Pipe zu "sort" zu Schreiben while (fgets(buf, sizeof(buf), stdin)) // Zeilen von STDIN => fp fprintf(fp, "%s", buf); pclose(fp); // Pipe zumachen return 0; }
Sie haben doch sicherlich in letzter Zeit 'mal in die ,,Glotze`` geschaut? Dann haben Sie bestimmt auch auf die Sendung ,,Wer wir Millionär` gezappt? Na das wäre doch die Idee, dieses Spiel zu implementieren. Damit Sie nicht ganz im Regen stehen, können sie das ausführbare Programm der Beispiellösung auch schon vorab testen. Noch nicht zufrieden? Klar, eine Datenbank mit Fragen bekommen Sie natürlich auch. Jetzt aber ans Werk!
Spielregeln: Der Kandidat bekommt vom Showmaster nacheinander Fragen gestellt bis er eine Frage falsch beantwortet oder Millionär ist. (Mit Jokern befassen wir uns erst gar nicht; das ist doch Kinderkram!) Zu jeder Frage werden dem Kandidaten vier Antworten zur Wahl gestellt, von denen er eine auswählen kann. Wählt er keine aus und beendet das Spiel, so hat er das bisher erspielte Geld gewonnen. Wählt er die falsche Antwort aus, so hat er verloren, das Spiel ist beendet und er erhält 32000 DM bzw. 1000 DM, falls er mindestens so viel zuvor erspielt hatte, und andernfalls geht er leer aus, wenn er zuvor weniger als 1000 DM erspielt hatte. Tippt er die richtige Antwort, so kommt er in die nächste Gewinnstufe - außer er hat schon eine Million erspielt; dann ist das Spiel zu Ende und er hat die Million gewonnen - und bekommt die nächste Frage gestellt. Der Kandidat beginnt zunächst mit 0 DM. Danach kann er in den folgenden Schritten bis zu einer Million ,,klettern``: 100 DM, 200 DM, 300 DM, 500 DM, 1000 DM, 2000 DM, 4000 DM, 8000 DM, 16000 DM, 32000 DM, 64000 DM, 125000 DM, 250000 DM, 500000 DM und 1000000 DM.
Aufbau der Datenbank: Die Datenbank ist ein Datei, die für jede Frage genau sechs Zeilen enthält. Davon enthält jeweils die erste die Frage, die nächsten vier enthalten die Antworten und in der letzten steht die Nummer der richtigen Antwort (zwischen 1 und 4).
Aufgabenstellung: Schreiben Sie eine Client-Server-Anwendung,
wobei der Client aus dem Server durch ein fork()
entsteht.
Beide kommunizieren miteinander über zwei Pipes miteinander (in
jede Richtung eine Pipe).
Der Server (also der Vater-Prozess) liest zunächst die Datenbank mit
den Fragen komplett ein. Danach wählt er zufällig eine dieser
Fragen aus und vertauscht die Antworten dieser Frage auch zufällig
(und vergisst dabei nicht, die Nummer der Lösung anzupassen).
Dann schickt er die Frage mit den Antworten (ohne die Lösung) an
den Clienten zusammen mit dem Geldwert (d.h. auf wieviel Geld er durch
die richtige Beantwortung dieser Frage kommen kann) dieser Frage.
Dann bekommt er vom Clienten den Tipp und antwortet ihm nach der
Auswertung des Tipps. In der Antwort teilt er dem Clienten mit,
ob das Spiel zu Ende ist oder weiter geht. Ist es zu Ende,
dann erfährt der Client durch diese Antwort auch den gewonnenen
Geldbetrag und im Falle einer falschen Antwort die richtige Lösung.
Andernfalls erfährt er den aktuell erspielten Geldbetrag.
Geht das Spiel weiter, so stellt der Server nun die nächste Frage.
Der Server achtet natürlich darauf, dass bei einem Spiel keine
Frage doppelt gestellt wird!
Der Client liest zunächst vom Server die Frage mit Antworten und
den Geldwert der Frage. Nachdem er diese ausgegeben hat, fordert
er den Spieler auf, eine Auswahl zu treffen. Diese Auswahl ist
entweder eine Antwort oder Spielende. Diese Wahl teil er dem
Server mit und erhählt von diesem die Antwort (siehe oben).
Daraufhin beendet er sich oder liest vom Server die nächste
Frage, je nach dem was in der Antwort des Servers stand. Natürlich
macht er für den Spieler entsprechende Ausgaben, damit dieser
mitkriegt wie ihm gerade geschieht.
Tipps: Die Fragen-Datenbank muss sich im selben Katalog befinden wie das Demo-Programm. Werfen Sie doch 'mal einen Blick auf die Beispiel-Programme. Weitere Fragen gibt es unter http://www.rtl.de/. Sie können die Fragen-Datenbank nach Lust und Laune erweitern! :-))
Lösung:
/* * AI4 SS01 Blatt 6 (c) jm sai uni ulm 2001 * Implementierung des Spiels "Wer wird Millionaer?" als * Client-Server-Anwendung. Client und Server kommunizieren * miteinander ueber je eine Pipe in beide Richtungen. * Der Client ist der Ratende. Der Server stellt die Fragen und * kennt die Loesung zu den Fragen. */ #include <stdio.h> #include <signal.h> #include <unistd.h> #include <stdlib.h> #include <wait.h> #include <string.h> #define MAX_FRAGE 200 /* max. Laenge einer Frage */ #define MAX_ANTWORT 50 /* max. Laenge einer Antwort */ #define ANZ_ANTWORT 4 /* Anzahl der Antworten pro Frage */ /* * letztes Zeichen vom String s entfernen; dient zum * Entfernen eines Newlines am Ende eines Strings */ #define CHOMP(s) (s)[strlen(s)-1] = 0; /* * Struktur zur Uebermittlung einer Aufgabe, d.h. Frage mit * den Antworten (aber ohne Loesung!); entspricht dem ersten * Teil einer Datensatz-Struktur (siehe bei server)! */ typedef struct { char frage[MAX_FRAGE]; char antwort[ANZ_ANTWORT][MAX_ANTWORT]; } Aufgabe; /* * Struktur zur Uebermittlung einer Antwort auf eine Loesung. * antwort: OK (=> weiterraten) oder ENDE (=> Schluss) * geld: aktueller Stand (bei OK) bzw. Gewinn (bei ENDE) * loesung: richtige Loesung (im Falle von ENDE durch falsche Antw.) */ typedef struct { int antwort; int geld; int loesung; } Antwort; #define OK 1 /* s.o.; richtige Antwort => weiter machen */ #define ENDE 2 /* s.o.; Abbruch wg. falscher Antwort oder Spielende */ int pid; /* PID des Client (= Sohn-Prozess beim Forken) */ /* * Ausgabe des Strings s mit einem abschliessenden Newline. * Der String wir (falls moeglich) so umgebrochen, dass * in jeder Zeile max. maxcol Zeichen stehen. Der Umbruch * erfolgt bei Leerzeichen. Woerter (d.h. Folgen von Nicht- * Leerzeichen), die laenger als maxcol sind, werden in * jeweils einer eigenen Zeile mit ihrer vollen Laenge ausgegeben. */ void printcol(char *s, int maxcol) { char *sdup = strdup(s); // Duplikat erzeugen (wg. strtok) char *wort; // Zeiger auf ein Wort im String int len = 0; // Anzahl der Zeichen in akt. Zeile wort = strtok(sdup, " "); while (wort) { if (len == 0) { // Zeile ist noch leer printf("%s", wort); // => Wort auf jeden Fall len += strlen(wort); // ... ausgeben } else if (len + 1 + strlen(wort) < maxcol) { // Wort passt hin printf(" %s", wort); // (mit Leerzeichen) len += strlen(wort) + 1; // => ausgeben } else { // Wort passt nicht in akt. Zeile puts(""); // => neue Zeile beginnen und len = 0; // ... das akt. Wort in diese continue; // ... Zeile schreiben } wort = strtok(NULL, " "); } puts(""); // Ausgabe mit Newline abschliessen free(sdup); // Speicher von Duplikat freigeben } /* * CLIENT * Bekommt eine Frage vom Server, schickt diesem dann die * vermutete Loesung und erhaelt dann vom Server eine * Anwort. Entsprechend der Antwort terminiert er oder * macht mit der naechsten Frage weiter. */ void client(int readfd, int writefd) { int tipp; // eingegebener Tipp 0, 1 .. ANZ_ANTWORT int wert; // "wert DM - Frage" momentan Aufgabe auf; // auf = vom Server bekommene Aufgabe Antwort ant; // ant = Antwort vom Server char buf[80]; int i; ant.geld = 0; // ant.geld = akt. Stand (= bish. Gewinn) while (1) { read(readfd, &auf, sizeof(auf)); // Aufgabe lesen read(readfd, &wert, sizeof(wert)); // wert der Frage lesen system("clear"); // Bildschirm loeschen printf("\nIhr aktueller Stand: %d DM\n\n", ant.geld); /* * Den Wert der Frage, die Frage selbst und * die Antworten formatiert ausgeben. */ for (i = 0; i < 70; i++) putchar('-'); printf("\n %d DM - Frage\n", wert); for (i = 0; i < 70; i++) putchar('-'); puts(""); printcol(auf.frage, 70); puts(""); // zwei Antworten pro Zeile ausgeben for (i = 0; i < ANZ_ANTWORT; i += 2) { printf("%d) %-*.*s ", i+1, 30, 30, auf.antwort[i]); if (i+1 < ANZ_ANTWORT) printf("%d) %-*.*s\n", i+2, 30, 30, auf.antwort[i+1]); } for (i = 0; i < 70; i++) putchar('-'); printf("\n\n0) Spiel beenden mit dem bisherigen Gewinn\n\n\n"); tipp = -1; while (tipp < 0 || tipp > ANZ_ANTWORT) { // Tipp erfragen printf("Ihr Tipp: "); fgets(buf, sizeof(buf), stdin); if (sscanf(buf, " %d ", &tipp) != 1) continue; } write(writefd, &tipp, sizeof(tipp)); // Tipp an Server read(readfd, &ant, sizeof(ant)); // Antwort vom Server if (tipp == 0) { // Spielende durch Benutzer system("clear"); if (ant.geld > 0) printf("\nGratulation!\n\nSie haben %d DM gewonnen!\n\n", ant.geld); else printf("\nSchade!\n\nSie haben leider nichts gewonnen!\n\n"); break; } else if (ant.antwort == OK) // richtige Antwort => weiter printf("\n\nRichtig!"); else if (ant.geld == 1000000) { // Millionaer => Spiel beenden system("clear"); printf("\nHerzlichen Glueckwunsch!\n\nSie sind Millionaer!\n\n"); break; } else { // falsche Antwort => Spiel beenden system("clear"); printf("\nFalsch!\n\nDie richtige Loesung waere gewesen:\n\n"); printf(" %d) %s\n\n", ant.loesung, auf.antwort[ant.loesung-1]); if (ant.geld > 0) printf("Ihnen bleiben noch %d DM.\n\n", ant.geld); else printf("Sie haben alles verloren!\n\n"); break; } sleep(1); // kleine Pause zwischen den Fragen } } #define DB "fragen.db" /* Name der Fragen-Datenbank (= Datei) */ #define MAXDB 200 /* max. Anzahl von zu lesenden Datensaetzen */ /* * Datensatz, der einen Eintrag der Fragen-Datenbank beherbergt */ typedef struct { char frage[MAX_FRAGE]; char antwort[ANZ_ANTWORT][MAX_ANTWORT]; int loesung; // antwort[loesung-1] ist Lsg. } Datensatz; /* * Vertauscht die Antworten im uebergebenen Datensatz zufaellig. * (Entsprechend muss natuerlich auch die Loesung angepasst * werden.) */ void vertausche(Datensatz *d) { Datensatz tmp; // Kopie des Datensatzes *d int quelle[ANZ_ANTWORT]; // Marken: kopierte Antworten von tmp int ziel[ANZ_ANTWORT]; // Marken: belegte Antworten von *d int n; int i, j; for (i = 0; i < ANZ_ANTWORT; i++) // alle Antworten als nicht quelle[i] = ziel[i] = 0; // kopiert bzw. belegt mark. memcpy(&tmp, d, sizeof(tmp)); // Duplikat *d wird tmp for (n = ANZ_ANTWORT; n > 0; n--) { // alle Antworten kopieren do { i = rand() % ANZ_ANTWORT; // noch nicht kopierte } while (quelle[i]); // Antwort suchen ... quelle[i] = 1; // und markieren! do { j = rand() % ANZ_ANTWORT; // noch nicht belegte } while (ziel[j]); // Antwort suchen ... ziel[j] = 1; // und markieren! // Antwort kopieren memcpy(d->antwort[j], tmp.antwort[i], MAX_ANTWORT); if (tmp.loesung == i+1) // ist dies Antwort die Loesung? d->loesung = j+1; // dann Loesung in *d aendern } } #define GEWINNE 16 /* Anzahl der moeglichen Gewinne */ /* * SERVER * Liest zunaechst die komplette Datenbank (aber max. MAXDB * Datensaetze) ein. Schickt dann dem Client eine Frage, liest * dessen Tipp und gibt ihm dann die Antwort (=> weiter oder Ende). * War der Tipp korrekt und ist der Client noch nicht Millionaer, * so stellt er die naechste Frage. */ void server(int readfd, int writefd) { const int gewinn[GEWINNE] = {0, 100, 200, 300, 500, // alle mgl. .. 1000, 2000, 4000, 8000, 16000, 32000, // ... Gewinne 64000, 125000, 250000, 500000, 1000000}; const int restgewinn[GEWINNE] = {0, 0, 0, 0, 0, // noch verbleibender .. 1000, 1000, 1000, 1000, 1000, 32000, // ... Gewinn bei ... 32000, 32000, 32000, 32000, 32000}; // ... einem Fehler Datensatz daten[MAXDB]; // alle eingelesenen Datensaetze int verwendet[MAXDB]; // wurde die Frage verwendet? int n; // Anzahl der Datensaetze int m; // akt. Index in gewinn int tipp; // Tipp des Clienten (0=Ende) Antwort ant; // Antwort auf den Tipp char buf[80]; int i; FILE *fp; /* * alle Datensaetze (Fragen mit Antworten und Loesung) * aus der Datei DB in das Array daten[] einlesen */ if (!(fp = fopen(DB, "r"))) fprintf(stderr, "Server: could not open %s\n", DB), kill(pid, 9), // Client abschiessen exit(1); for (n = 0; n < MAXDB; n++) { // Frage einlesen (bzw. EOF feststellen) if(!fgets(daten[n].frage, sizeof(daten[n].frage), fp)) break; CHOMP(daten[n].frage); // Newline entfernen // ... alle Antworten einlesen ... for (i = 0; i < ANZ_ANTWORT; i++) { fgets(daten[n].antwort[i], sizeof(daten[n].antwort[i]), fp); CHOMP(daten[n].antwort[i]); // Newline entfernen } // ... und zugehoerige Loesung holen fgets(buf, sizeof(buf), fp); sscanf(buf, " %d ", &daten[n].loesung); verwendet[n] = 0; // gelesene Frage wurde nicht gestellt } fclose(fp); /* * Wenn nicht genuegend Fragen fuer ein Spiel * bis zu einer Million zur Verfuegung stehen, * soll ein Abbruch erfolgen. */ if (n < GEWINNE - 1) fprintf(stderr, "Server: database has not enough questions\n"), kill(pid, 9), exit(1); srand(time(NULL)); // Zufallszahlengenerator initialisieren m = 0; // bei gewinn[0] starten while (1) { do { i = rand() % n; // noch nicht gestellte Frage ... } while (verwendet[i]); // ... auswaehlen ... verwendet[i] = 1; // ... und als gestellt markieren vertausche(&daten[i]); // Antworten zufaellig vertauschen /* * ersten Teil von daten[i] (ohne die Loesung!) * zum Client schicken; dieser Teil entspricht * genau einer Aufgabe-Struktur! */ write(writefd, &daten[i], sizeof(Aufgabe)); // "Wert" der Frage dem Clienten mitteilen, // d.h. "... DM - Frage" => ... ist der Wert write(writefd, &gewinn[m+1], sizeof(gewinn[m+1])); read(readfd, &tipp, sizeof(tipp)); // Tipp vom Client if (tipp == 0) { // Spielende gewaehlt ant.geld = gewinn[m]; break; } else if (tipp == daten[i].loesung) { // richtige Loesung ant.geld = gewinn[++m]; if (gewinn[m] == 1000000) // Millionaer? break; // ... dann Ende ant.antwort = OK; // ... sonst weiter write(writefd, &ant, sizeof(ant)); } else { // falsche Loesung ant.geld = restgewinn[m]; // nur noch ein Rest break; // ...vom Geld bleibt } } ant.antwort = ENDE; // dem Clienten mit der ant.loesung = daten[i].loesung; // ... Antwort Spielende write(writefd, &ant, sizeof(ant)); // ... signalisieren } int main() { int cs_pfd[2]; // Pipe: Client ----------> Server int sc_pfd[2]; // Pipe: Client <---------- Server pipe(cs_pfd); // Pipe: Client schreibt, Server liest pipe(sc_pfd); // Pipe: Server schreibt, Client liest switch (pid = fork()) { case -1: // Fehler beim Forkeln perror("fork()"); exit(1); case 0: // Sohn-Prozess => Client close(sc_pfd[1]); // nicht benoetigte close(cs_pfd[0]); // Enden dicht machen client(sc_pfd[0], cs_pfd[1]); exit(0); default: // Vater-Prozess => Server close(sc_pfd[0]); // nicht notwendige close(cs_pfd[1]); // Enden zu machen server(cs_pfd[0], sc_pfd[1]); // brav noch auf den Sohn warten while (waitpid(pid, NULL, 0) != pid); exit(0); } return 0; }