C-Workshop
Urheber: Andre Adrian
Datum: 12.Feb.2008
Einleitung
Das Linux Betriebssystem ist kein klassisches Echtzeitsystem
(hard real time system), ist aber sehr gut für "soft real
time" Anforderungen verwendbar. Ebenso wenig ist Linux von sich aus
redundant, lässt sich aber auf Hardware-Ebene (Beispiel
Doppelnetzteil, Festplatten RAID, ECC RAM), auf
Betriebssystem-Ebene (Beispiel IP-Cluster, Heartbeat Fail-over, LAN
Anschlüsse Link Aggregation) und auf Applikation-Ebene
(Beispiel TCP Client für redundante
TCP-Server) redundant machen.
Der C-Workshop soll die Programmierung in C von typischen Aufgaben
der Prozessdatenverarbeitung
(PDV) unter Linux zeigen. Die Schwerpunkte sind:
- Wohl geordneter Zustand von Variablen
- Zeiger/Größe Doppel-Parameter
- Zugriffs-Funktionen
- Programmierung der seriellen Schnittstelle
- Multi Ein-/Ausgabe
- Programmierung von TCP Kommunikation
- Behandlung von interval timer und time outs
- Zustands Übergang Diagramm (state diagram)
- Behandlung von "slow system-calls"
- TCP Client State Diagram für redundante TCP
Kommunikation
- TJSON LL(1) Grammatik Parser
- Prozesspriorität
Redewendungen in Programmiersprachen
Gute Templates, Pattern, Schablonen, Vorlagen, Redewendungen helfen
bei der Programmierung. In diesem Text versuche ich die mir
bekannten besten Schablonen der C Programmierung zu verwenden. Im
eigentlichen Text weise ich nicht auf die einzelnen Vorlagen hin -
man sieht sie ja im Quelltext. Eigentlich ist der ganze C-Workshop
eine Sammlung von Redewendungen welche bei der Programmierung von
typischen PDV Aufgaben unter Linux helfen.
Ausblicke auf C++
An einigen Stellen werden C++ Programme vorgestellt. Um die
größten Vorteile von C++ nutzen zu können muss ein
C Programmierer nur wenig neues lernen: Die Klasse (class) und die
Fehler Behandlung (try, throw, catch). Übrigens bietet C++
auch hyperkomplexe Lösungen für die man im Bereich PDV
erst noch die passenden Probleme erfinden muss...
Quelltext
Den kompletten Quelltext gibt es als tarball für den download.
Es dürfte für das Lernen aber sinnvoller sein die Dateien
per Copy und Paste selbst zu erstellen. Oder gar die Quelltext
Zeilen abzutippen. Sie wollen das Thema doch verstehen, oder?
Hier ist der Quelltext: c-workshop_10_src.tgz
Auspacken unter Linux mit
tar xzf c-workshop_10_src.tgz
Es entsteht ein Unterverzeichnis c-workshop mit den Dateien.
Inhaltsverzeichnis
Wohl geordneter Zustand von Variablen
Die Theorie des state diagrams (Zustandsübergangsdiagramms)
und damit zusammenhängend die Theorie der state variables
(Zustands-Variablen) ist sehr wichtig für die Programmierung
von fehlerfreier Software. Was ist die Kern-Idee des state diagrams
oder der state machine?
Das Programm befindet sich immer in einem wohl geordneten Zustand.
In diesem Zustand haben alle Zustands-Variablen einen sinnvollen
Inhalt. Als Reaktion auf eine Einwirkung von außen (z.B.
Benutzer-Eingabe, aber auch Ablauf eines time outs) ändert die
state machine ihren Zustand von einem wohl geordneten Zustand zu
einem anderen wohl geordneten Zustand.
Im Moment des Zustandsüberganges können die
Zustands-Variablen in einem "ungeordneten" Zustand sein, am Ende
des Zustandsübergangs müssen die Zustands-Variablen
wieder in einem wohl geordneten Zustand sein.
Programm: An C-String ein Zeichen
anhängen
Ein einfaches Beispiel für Zustands-Variable mit wohl
geordneten Zuständen: Eine C-Funktion soll an einen
vorhandenen String s welcher maximal n Bytes Speicher halten kann
ein Zeichen c anhängen. Die Funktion soll heißen
strncatc(s, n, c);
Der Funktions-Header ist
char *strncatc(char *s, int n, const char
c);
Vor der Entwicklung der Funktion ist es notwendig über den
Normalfall und die Sonderfälle nachzudenken. Für das
Beispiel gilt:
Normalfall: Es gibt einen wohl geordneten Zustand in der Variablen.
Die Variable s ist 0-terminiert (ein korrekter C-String).
Sonderfall 1: Definition der Variablen s. Mit der üblichen
Schreibweise
char s[5];
wird die Variable s erzeugt. Der Inhalt von s ist aber nicht
wohldefiniert. Mit
char s[5] = "";
wird die Variable s erzeugt und das erste Byte in s wird mit dem
String-Endzeichen '\0' belegt. Der Inhalt von s ist somit
wohldefiniert.
Sonderfall 2: Durch das Anhängen reicht der n Bytes
große Speicher nicht mehr aus. Wegen dem C-String Endzeichen
'\0' können in einem n Bytes langen Speicher maximal n-1 Bytes
als String untergebracht werden. Damit der wohl geordnete Zustand
von s erhalten bleibt gibt es mehrere Möglichkeiten.
Üblicherweise wird im Fall von "Speicher voll" kein Zeichen c
mehr angehängt. Diese Sonderfall Behandlung wird truncation
(abschneiden) genannt.
Sonderfall 3: Die Variable c enthält das Zeichen '\0'. Dieses
Zeichen kann wie jedes andere Zeichen behandelt werden. Im
Unterschied zu allen anderen Werten für c wird bei dem Wert
'\0' die String-Länge von s nicht erhöht, d.h. strlen()
liefert den selben Wert.
Programm Test
Programm kompilieren mit
gcc -Wall -Wextra -o strncatc strncatc.c
Im xterm Fenster eingeben:
stty -F /dev/tty -icanon
./strncatc
Nach Test wieder Keyboard auf Zeilen-Pufferung stellen
stty -F /dev/tty icanon
Datei strncatc.c
#include <stdio.h>
#include <string.h>
// an C-String s mit Speichergroesse n das Zeichen c anhaengen
// Returnwert ist s
char *strncatc(char *s, int n, const char c) {
int len = strlen(s);
if (len + 1 >= n) {
return
s;
// wohlgeordneter Zustand von s
}
s[len] =
c;
// nicht wohlgeordneter Zustand von s
s[len+1] =
'\0';
// wohlgeordneter Zustand von s
return s;
}
int main() {
char s[5] =
"";
// wohlgeordneter Zustand von s
printf("sizeof \"\" = %d\n", sizeof "");
printf("strlen(\"\") = %d\n", strlen(""));
printf(" s = %s\n", s);
for(;;) {
int c = getchar();
strncatc(s, sizeof(s), c);
printf(" s = %s\n", s);
}
return 0;
}
Das strncatc() Beispiel zeigt den korrekten Umgang mit C-String
Variablen. Es wird die Adresse des C-Strings und die
Speichergröße des C-Strings übergeben.
Zeiger/Größe Doppelparameter
Diese Zeiger/Größe Übergabe in zwei Variablen ist
weit verbreitet. Ein Beispiel aus multi_io.c:
read(fdKbd, buf, sizeof buf)
Hier ist buf der Zeiger und sizeof buf die
Speichergröße. Ein anderes Beispiel aus
tcp_client.c:
connect(fdServer, (struct sockaddr
*)&serv_addr, sizeof serv_addr)
Hier ist serv_addr der Zeiger und sizeof serv_addr die
Speichergrösse.
Programm: C-Strings ohne
Zeiger/Größe Doppelparameter
Die einzige korrekte Alternative zu Zeiger/Größe bei
C-Strings ist die Verwendung von nur einer Größe
für alle C-Strings. In einer Funktion wie strkcatc() kann dann
gegen diesen konstanten Wert geprüft werden:
Datei strkcatc.c
#include <stdio.h>
#include <string.h>
#define MAXLEN 100 // C-String Groesse fuer alle
C-Strings im Programm
// an C-String s mit Speichergroesse n das Zeichen c anhaengen
// Returnwert ist s
char *strkcatc(char *s, const char c) {
int len = strlen(s);
if (len + 1 >= MAXLEN) {
return
s;
// wohlgeordneter Zustand von s
}
s[len] =
c;
// nicht wohlgeordneter Zustand von s
s[len+1] =
'\0';
// wohlgeordneter Zustand von s
return s;
}
int main() {
char s[MAXLEN] = "";
// wohlgeordneter Zustand von s
printf("sizeof \"\" = %d\n", sizeof "");
printf("strlen(\"\") = %d\n", strlen(""));
printf(" s = %s\n", s);
for(;;) {
int c = getchar();
strkcatc(s, c);
printf(" s = %s\n", s);
}
}
Sichere C-String Funktionen
Einige C-Bibliothek-Funktionen sind unsicher. Funktionen wie gets()
oder sprintf() verwenden nicht Zeiger/Größe.
Die Funktionen strcpy_s() und strcat_s() gibt es beim Microsoft
C-Compiler. Für den Gnu C-Compiler sind diese sicheren
Funktionen schnell programmiert.
| unsicher |
sicher |
| gets(char *s) |
fgets(char *s, int size, FILE *stream) |
| sprintf(char *str, const char *format, ...) |
snprintf(char *str, size_t size, const char *format, ...) |
| strcpy(char *dest, const char *src) |
strcpy_s(char *dest, unsigned size, const char *src) |
| strncpy(char *dest, const char *src, size_t n) |
strncpy_s(char *dest, unsigned size, const char *src, size_t
n) |
| strcat(char *dest, const char *src) |
strcat_s(char *dest, unsigned size, const char *src) |
| strncat(char *dest, const char *src, size_t n) |
strncat_s(char *dest, unsigned size, const char *src, size_t
n) |
scanf(const char *format, ...)
fscanf(FILE *stream, const char *format, ...) |
Eingabe zuerst mit fgets() oder fread() einlesen, dann mit
sscanf() verarbeiten |
Programm Test
Programm kompilieren mit
gcc -Wall -o test_string_s test_string_s.c string_s.c
Im xterm Fenster eingeben:
./test_string_s
Das Programm liefert einen Speicherzugriffsfehler. Programm erneut
kompilieren mit
gcc -Wall -fstack-protector-all -o test_string_s test_string_s.c
string_s.c
Das Programm liefert nun die Meldung "stack smashing detected".
Datei string_s.c
#include <string.h>
char *strcat_s(char *dest, unsigned n, const char *src) {
strncat(dest, src, n-strlen(dest));
dest[n-1] =
'\0'; // C-String
Endzeichen sicherstellen
return dest;
}
char *strcpy_s(char *dest, unsigned n, const char *src) {
strncpy(dest, src, n);
dest[n-1] =
'\0'; // C-String
Endzeichen sicherstellen
return dest;
}
Datei string_s.h
char *strcat_s(char *dest,
unsigned n, const char *src);
char *strcpy_s(char *dest, unsigned n, const char *src);
Datei test_string_s.c
#include <stdio.h>
#include <string.h>
#include "string_s.h"
void test_strcat_s() {
char d[16] = "12345678";
const char s[] = "abcdefghABCDEFGH";
strcat_s(d, sizeof d, s);
printf("strcat_s() = %s\n", d);
printf("sizeof d = %d\n", sizeof d);
printf("strlen(d) = %d\n", strlen(d));
}
void test_snprintf() {
char d[16] ="";
const char s1[] = "12345678";
const char s2[] = "abcdefghABCDEFGH";
snprintf(d, sizeof d, "%s%s", s1, s2);
printf("snprintf() = %s\n", d);
printf("sizeof d = %d\n", sizeof d);
printf("strlen(d) = %d\n", strlen(d));
}
void test_strncat() {
char d[16] = "12345678";
const char s[] = "abcdefghABCDEFGH";
strncat(d, s, sizeof
d);
// Achtung: strncat() ist unsicher !
printf("strncat() = %s\n", d);
printf("sizeof d = %d\n", sizeof d);
printf("strlen(d) = %d\n", strlen(d));
}
int main() {
test_strcat_s();
test_snprintf();
test_strncat();
// Achtung: strncat() ist unsicher !
return 0;
}
Programm: Integer nach Char Umwandlung
Schreiben Sie ein neues Programm inttochr.c. Dieses Programm ist
eine Test-Umgebung für die Funktion
unsigned char inttochr(int i)
Die Funktion inttochr() soll eine int Variable wohl geordnet in
eine unsigned char Variable umwandeln. Die Sonderfälle
sind:
Sonderfall 1: Wenn int Variable < 0 ist, dann soll char Variable
= 0 werden
Sonderfall 2: Wenn int Variable > 255, dann soll char Variable =
255 werden
Diese Sonderfall Behandlung heißt saturation
(Sättigung).
Benutzen Sie folgende Vorlage:
Datei inttochr.c
#include <stdio.h>
#include <stdlib.h>
unsigned char inttochr(int i) {
return i;
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: inttochr number\n");
exit(1);
}
int n = atoi(argv[1]);
unsigned char c = inttochr(n);
printf("%d = inttochr(%d)\n", c, n);
return 0;
}
Programm: Mit Uhrzeiten rechnen
Die übliche Uhrzeit-Darstellung ist Stunde:Minute:Sekunde.
Dabei dauert ein Tag von 00:00:00 bis 23:59:59. Diese Darstellung
ist für Berechnungen im Computer schlecht geeignet. Im
Programm zeit.c wird die Umwandlung zwischen externer Darstellung
in Stunde:Minute:Sekunde und interner Darstellung in Sekunden
gezeigt. Die Rechnung mit Uhrzeiten wird auch gezeigt.
Umwandlung zwischen Stunde:Minute:Sekunde und
Sekunden
Die Umrechnung ist Sekunden = 3600 * Stunde + 60 * Minute +
Sekunde.
Die umgekehrte Umrechnung ist
Stunde = Sekunden / 3600
Dabei ist / die Integer Division. Beispiel: 7/2 ergibt 3 bei der
Integer Division.
Sekunden = Sekunden - 3600 * Stunde
Nun sind von Sekunden die vollen Stunden abgezogen.
Minute = Sekunden / 60
Sekunde = Sekunden - 60 * Minute
Uhrzeit Addition
In der Sekunden Darstellung ist die Uhrzeit Addition eine Integer
Addition. Eine wohl geordnete Variable enthält immer einen
Wert kleiner 24*60*60 = 86400. Dieser Wert sind die Sekunden eines
Tages. Nach der Integer Addition kann ein Wert größer
86400 entstehen. Dann muss 86400 abgezogen werden um die Uhrzeit
Variable zu normalisieren (wieder in den Bereich der
wohldefinierten Variablen bringen). Diese Sonderfall Behandlung
wird Modulo-N Arithmetik genannt (mit N=86400).
Programm Test
Programm kompilieren mit
gcc -Wall -o zeit zeit.c
Programm-Aufruf mit den beiden Uhrzeiten 10:29:01 und 15:30:59
./zeit 10 29 01 15 30 59
Übung 1
Ändern Sie zeit.c nach zeit1.c. Programmieren Sie die
Funktion
ZEIT ZEIT_sub(ZEIT z1, ZEIT z2);
um die Uhrzeit z2 von der Uhrzeit z1 abzuziehen. Das Ergebnis soll
eine normalisierte Uhrzeit im Bereich 0 bis 86399 sein. Fügen
Sie nach dem printf("Zeit Summe ...) ein weiteres printf("Zeit
Differenz ...) ein.
Übung 2
Die Eingangsparameter von ZEIT_fromHMS() werden nicht
überprüft. Legen Sie zuerst fest was die gültigen
Werte für h, m und s sind. Ändern Sie dann zeit1.c nach
zeit2.c. Bei schlechten Eingangsparametern soll die Funktion
ZEIT_fromHMS() als return value -1 liefern. Prüfen Sie den
return value und beenden Sie wenn nötig das Programm nach
einer fprintf() Fehlermeldung mit exit(). Testen Sie das Programm
mit allen möglichen schlechten Eingangsparametern (h zu klein,
h zu groß, m zu klein, m zu groß, s zu klein, s zu
groß).
Datei zeit.c
#include <stdio.h>
#include <stdlib.h>
#define EINTAG (24*60*60)
typedef int ZEIT;
ZEIT ZEIT_fromHMS(int h, int m, int s) {
return 3600 * h + 60 * m + s;
}
ZEIT ZEIT_add(ZEIT z1, ZEIT z2) {
ZEIT sum = z1 + z2;
if (sum >= EINTAG) {
sum -= EINTAG;
}
return sum;
}
void ZEIT_toHMS(int z, int *h, int *m, int *s) {
*h = z / 3600;
z -= 3600 * (*h); // die vollen Stunden
abziehen
*m = z / 60;
*s = z - 60 * (*m); // die vollen Minuten abziehen
}
int main(int argc, char *argv[]) {
if (argc != 7) {
fprintf(stderr, "usage: zeit h1 m1 s1 h2 m2
s2\n");
exit(1);
}
int h1 = atoi(argv[1]);
int m1 = atoi(argv[2]);
int s1 = atoi(argv[3]);
ZEIT z1 = ZEIT_fromHMS(h1, m1, s1);
int h2 = atoi(argv[4]);
int m2 = atoi(argv[5]);
int s2 = atoi(argv[6]);
ZEIT z2 = ZEIT_fromHMS(h2, m2, s2);
ZEIT sum = ZEIT_add(z1, z2);
int h, m, s;
ZEIT_toHMS(sum, &h, &m, &s);
printf("Zeit Summe = %02d:%02d:%02d\n", h, m, s);
return 0;
}
Ausblick: Uhrzeiten rechnen als C++
Programm
Die Schreibweise des C Programms zeit.c erscheint seltsam solange
man nicht die C++ Versionen des Programms kennt. Dann wird klar das
die Schreibweise ZEIT_add() in C die Schreibweise ZEIT::add() in
C++ vorweg nehmen will.
Das Programm zeita.cpp ist noch sehr nahe an der Vorlage zeit.c.
Das Programm zeitb.cpp benutzt die C++ Eigenschaften
- Konstruktor mit Parameter aufrufen
- Operator überladen
Das Programm zeitc.cpp benutzt noch Exceptions (Fehler
Ausnahme).
Programm Test
Programm kompilieren mit
g++ -Wall -o zeita zeita.cpp
g++ -Wall -o zeitb zeitb.cpp
g++ -Wall -o zeitc zeitc.cpp
Programm-Aufruf mit den beiden Uhrzeiten 10:29:01 und 15:30:59
./zeita 10 29 01 15 30 59
./zeitb 10 29 01 15 30 59
./zeitc 10 29 01 15 30 59
Übung
Keine. Einfach nur den C++ Quelltext ansehen und überlegen ob
C++ die Mühe wert ist. Hier ein kleiner C++ mit C
Vergleich:
| C++ |
C |
| class |
struct und Funktionen mit struct-Zeiger *this als ersten
Parameter |
| try, throw, catch |
setjmp(), longjmp() |
| Operator überladen |
nicht möglich, Funktionen anstelle von Operatoren
nutzen |
Datei zeita.cpp
#include <cstdio>
#include <cstdlib>
#define EINTAG (24*60*60)
class ZEIT {
int z;
public:
void fromHMS( int h, int m, int s);
void add(ZEIT z1, ZEIT z2);
void toHMS(int *h, int *m, int *s);
};
void ZEIT::fromHMS( int h, int m, int s) {
z = 3600 * h + 60 * m + s;
};
void ZEIT::add(ZEIT z1, ZEIT z2) {
z = z1.z + z2.z;
if (z >= EINTAG) {
z -= EINTAG;
}
};
void ZEIT::toHMS(int *h, int *m, int *s) {
*h = z / 3600;
z -= 3600 * (*h);
*m = z / 60;
*s = z - 60 * (*m);
};
int main(int argc, char *argv[]) {
if (argc != 7) {
fprintf(stderr, "usage: zeita h1 m1 s1 h2 m2
s2\n");
exit(1);
}
int h1 = atoi(argv[1]);
int m1 = atoi(argv[2]);
int s1 = atoi(argv[3]);
ZEIT z1;
z1.fromHMS(h1, m1, s1);
int h2 = atoi(argv[4]);
int m2 = atoi(argv[5]);
int s2 = atoi(argv[6]);
ZEIT z2;
z2.fromHMS(h2, m2, s2);
ZEIT sum;
sum.add(z1, z2);
int h, m, s;
sum.toHMS(&h, &m, &s);
printf("Zeit Summe = %02d:%02d:%02d\n", h, m, s);
return 0;
}
Datei zeitb.cpp
#include <cstdio>
#include <cstdlib>
#define EINTAG (24*60*60)
class ZEIT {
int z;
public:
ZEIT() { z = 0;
};
// Konstruktor ohne Parameter
ZEIT(int h, int m, int s) { // Konstruktor mit
Parameter
z = 3600 * h + 60 * m + s;
};
ZEIT operator+(ZEIT z1) { //
Operator Ueberladung
z += z1.z;
if (z >= EINTAG) {
z -= EINTAG;
}
return *this;
};
void toHMS(int *h, int *m, int *s) {
*h = z / 3600;
z -= 3600 * (*h);
*m = z / 60;
*s = z - 60 * (*m);
};
};
int main(int argc, char *argv[]) {
if (argc != 7) {
fprintf(stderr, "usage: zeitb h1 m1 s1 h2 m2
s2\n");
exit(1);
}
int h1 = atoi(argv[1]);
int m1 = atoi(argv[2]);
int s1 = atoi(argv[3]);
ZEIT z1(h1, m1, s1); //
Konstruktor benutzen
int h2 = atoi(argv[4]);
int m2 = atoi(argv[5]);
int s2 = atoi(argv[6]);
ZEIT z2(h2, m2, s2);
ZEIT sum = z1 + z2; //
Operator Ueberladung benutzen
int h, m, s;
sum.toHMS(&h, &m, &s);
printf("Zeit Summe = %02d:%02d:%02d\n", h, m, s);
return 0;
}
Datei zeitc.cpp
#include <cstdio>
#include <cstdlib>
#define EINTAG (24*60*60)
class ZEIT {
int z;
public:
ZEIT() { z = 0;
};
// Konstruktor ohne Parameter
ZEIT(int h, int m, int s)
{ // Konstruktor mit
Parameter
if (h < 0 || h > 23 || m < 0 || m >
59 || s < 0 || s > 59) {
throw
-1;
// Exception ausloesen
}
z = 3600 * h + 60 * m + s;
};
ZEIT operator+(ZEIT z1)
{ // Operator
Ueberladung
z += z1.z;
if (z >= EINTAG) {
z -= EINTAG;
}
return *this;
};
void toHMS(int *h, int *m, int *s) {
*h = z / 3600;
z -= 3600 * (*h);
*m = z / 60;
*s = z - 60 * (*m);
};
};
int main(int argc, char *argv[]) {
if (argc != 7) {
fprintf(stderr, "usage: zeitc h1 m1 s1 h2 m2
s2\n");
exit(1);
}
try
{
// Exception Block
int h1 = atoi(argv[1]);
int m1 = atoi(argv[2]);
int s1 = atoi(argv[3]);
ZEIT z1(h1, m1,
s1);
// Konstruktor benutzen
int h2 = atoi(argv[4]);
int m2 = atoi(argv[5]);
int s2 = atoi(argv[6]);
ZEIT z2(h2, m2, s2);
ZEIT sum = z1 +
z2;
// Operator Ueberladung benutzen
int h, m, s;
sum.toHMS(&h, &m, &s);
printf("Zeit Summe = %02d:%02d:%02d\n", h, m,
s);
}
catch (int err)
{
// Exception Handler
fprintf(stderr, "zeitc exception: %d\n",
err);
exit(1);
}
return 0;
}
Programm: Zugriffs-Funktionen für globale
C-String Variable
Manchmal liest man kluge Sätze wie "vermeiden Sie globale
Variablen". Dieser Rat läßt sich in der Praxis nicht
umsetzen. Zustands-Variablen müssen meistens als globale
Variablen definiert werden. Ein guter Rat ist hingegen "globale
Variablen dürfen nur über Zugriffs-Funktionen
geändert werden". Dadurch wird sichergestellt das ihr Inhalt
immer wohldefiniert bleibt.
Damit eine globale Variable vor dem direkten Zugriff geschützt
wird müssen in der Programmiersprache C mindestens zwei C
Quelltext Dateien und eine Header Datei verwendet werden:
- in der Variable Definition C Datei wird die globale Variable
mit der Speicherklasse static definiert und es werden die
Zugriffs-Funktionen definiert.
- in der Header Datei werden die Funktionsprototypen der
Zugriffs-Funktionen genannt.
- in den Variable Nutzung C Dateien ist die globale Variable
unbekannt und kann nur über die Zugriffs-Funktionen
angesprochen werden.
Die globale Variable hat den Name userName. Die Zugriffs-Funktionen
sind
- userName_set() um den Inhalt der Variablen zu ändern
- userName_get() um die Adresse der Variablen zu holen
- userName_cpy() um eine Kopie der Variablen zu erzeugen
Das Programm palindrom testet ob der Inhalt von userName ein
Palindrom ist. Hierzu werden die Funktion strreverse() und
strcasecmp() benutzt. Beide Funktionen arbeiten nur mit dem ASCII
Zeichensatz. Umlaute funktionieren nicht.
Programm Test
Programm kompilieren mit
gcc -Wall -o palindrom palindrom.c username.c string_s.c
Programm-Aufruf
./palindrom Reliefpfeiler
Übung 1
Testen Sie das Programm mit langen Namen wie
Karl-Otto-der-Zweiundvierzigste. Ändern Sie das Programm damit
Namen bis zu einer Länge von 100 Zeichen korrekt verarbeitet
werden.
Tipp: Sie müssen zwei Zahlen-Konstanten ändern, mehr
nicht!
Übung 2
Das Programm palindrom1.c soll die gleiche Aufgabe ausführen,
aber nicht mehr die Zugriffs-Funktion userName_cpy() verwenden.
Datei username.h
#ifndef
_USERNAME_H
#define
_USERNAME_H
// Zugriffs Funktionen fuer
C-String userName globale Variable
void userName_set(const char *s);
const char *userName_get();
void userName_cpy(char *s, int n);
#endif
Datei username.c
#include "string_s.h"
#include "username.h"
static char userName[20] = "";
void userName_set(const char *s) {
strcpy_s(userName, sizeof userName, s);
}
const char *userName_get() {
return userName;
}
void userName_cpy(char *s, int n) {
strcpy_s(s, n, userName);
}
Datei palindrom.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "username.h"
// String rueckwaerts
void strreverse(char *s)
{
int a = 0;
int z = strlen(s)-1;
while (a < z) {
char tmp = s[a]; // Dreier
Tausch
s[a] = s[z];
s[z] = tmp;
++a;
--z;
}
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: palindrom
userName\n");
exit(1);
}
userName_set(argv[1]);
printf("userName ist %s\n", userName_get());
char s[40] = "";
userName_cpy(s, sizeof s);
strreverse(s);
printf("userName rueckwaerts ist %s\n", s);
int rv = strcasecmp(userName_get(), s);
if (0 == rv) {
printf("userName ist ein Palindrom\n");
}
return 0;
}
Ausblick: Zugriffs-Funktionen in C++ für
C-String Variablen
In der C Lösung sind die Variable userName und die
Zugriffsfunktionen wie userName_set() ganz eng verbunden.
In der C++ Lösung lassen sich die Variable und ihre
Zugriffsfunktionen voneinander trennen. Die Klasse CSTRING20
erlaubt beliebig vielen 20 Zeichen langen C-String Variablen den
Schutz durch die Zugriffsfunktionen. Diese Verallgemeinerung von
Zugriffsfunktionen ist der Kern der objekt orientierten
Programmierung (OOP).
In der C++ Standardbibliothek gibt es die Klasse string welche
umfangreicher und flexibler als CSTRING20 ist. Siehe z.B. das Buch
C++ von A bis Z von Jürgen Wolf Kapitel 7.1 Die
String-Bibliothek (string-Klasse).
Programm Test
Programm kompilieren mit
g++ -Wall -o palindroma palindroma.cpp cstring20.cpp string_s.c
Programm-Aufruf
./palindroma Reliefpfeiler
Übung
Keine. Einfach nur den C++ Quelltext ansehen und überlegen ob
C++ die Mühe wert ist.
Datei cstring20.hpp
#ifndef
_CSTRING20_HPP
#define _CSTRING20_HPP
class CSTRING20 {
char cstr[20];
public:
CSTRING20();
void set(const char *s);
const char *get();
void cpy(char *s, unsigned n);
};
#endif
Datei cstring20.cpp
#include <cstring>
#include "string_s.h"
#include "cstring20.hpp"
CSTRING20::CSTRING20() { // Konstruktor
cstr[0] =
'\0';
// erzeugt wohlgeordnete Variable
}
void CSTRING20::set(const char *s) {
strcpy_s(cstr, sizeof cstr, s);
}
const char *CSTRING20::get() {
return cstr;
}
void CSTRING20::cpy(char *s, unsigned n) {
strcpy_s(s, n, cstr);
}
Datei palindroma.cpp
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include "cstring20.hpp"
// String rueckwaerts
void strreverse(char *s)
{
int a = 0;
int z = strlen(s)-1;
while (a < z) {
char tmp = s[a]; // Dreier
Tausch
s[a] = s[z];
s[z] = tmp;
++a;
--z;
}
}
CSTRING20 userName;
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: palindrom
userName\n");
exit(1);
}
userName.set(argv[1]);
printf("userName ist %s\n", userName.get());
char s[40] = "";
userName.cpy(s, sizeof s);
strreverse(s);
printf("userName rueckwaerts ist %s\n", s);
int rv = strcasecmp(userName.get(), s);
if (0 == rv) {
printf("userName ist ein Palindrom\n");
}
return 0;
}
Programm: ASCII Datei in Großbuchstaben
wandeln
In diesem Programm geht es um die gepufferte Ein-/Ausgabe
(stream-io, Datenstrom-Ein/Ausgabe) unter Linux. Der Dateiname
einer ASCII Datei wird als Parameter übergeben. Der
Dateiinhalt in Großbuchstaben wird auf stdout ausgegeben. Die
drei Filepointer stdin, stdout und stderr stehen jedem C-Programm
direkt zur Verfügung.
| Filepointer |
file-descriptor |
Bedeutung |
| stdin |
STDIN_FILENO |
Standard Eingabe, üblicherweise die Tastatur |
| stdout |
STDOUT_FILENO |
Standard Ausgabe, üblicherweise das xterm Fenster |
| stderr |
STDERR_FILENO |
Standard Fehlerausgabe, üblicherweise das xterm
Fenster |
Der Aufruf
./toupper toupper.c
liefert die Ausgabe:
/* TOUPPER.C
*
* GCC -WALL -O TOUPPER TOUPPER.C
*
*/
#INCLUDE <STDIO.H>
#INCLUDE <STDLIB.H>
#INCLUDE <STRING.H>
#INCLUDE <CTYPE.H>
// STRING IN GROSSBUCHSTABEN WANDELN
VOID STRTOUPPER(CHAR *S) {
...
Programm Test
Programm kompilieren mit
gcc -Wall -o toupper toupper.c
Im xterm Fenster eingeben:
./toupper toupper.c
Übung 1
Die Variable buf hat eine Größe von 100 Zeichen.
Ändern Sie für die Datei toupper1.c die Größe
auf 2 Zeichen und lassen Sie das Programm laufen. Gibt es
Unterschiede in der Ausgabe? Testen Sie die Programmlaufzeit der
beiden Versionen mit
time ./toupper toupper.c
time ./toupper1 toupper.c
Was bemerken Sie?
Was passiert bei Puffer-Größe 1? Warum?
Übung 2
Ändern Sie die Datei toupper.c in tolower.c. Der Dateiinhalt
soll nun in Kleinbuchstaben umgewandelt werden.
Tipp: Sie müssen nur eine Quelltextzeile ändern.
Datei toupper.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
// String in Grossbuchstaben wandeln
void strtoupper(char *s) {
int i;
for (i = 0; s[i] != '\0'; ++i) {
s[i] = toupper(s[i]);
}
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: toupper file\n");
exit(1);
}
FILE *fp = fopen(argv[1], "r");
if (NULL == fp) {
perror("toupper fopen");
exit(1);
}
for(;;)
{
// Endlosschleife
char buf[100];
char *rv = fgets(buf, sizeof buf, fp);
if (NULL == rv)
break;
// Schleife beenden
strtoupper(buf);
printf("%s", buf);
}
fclose(fp);
return 0;
}
Vorbereitung serielle Schnittstelle (V.24,
RS232)
Vor der Programmierung wird mit Hilfe von UNIX Kommandos der
Hardware Aufbau getestet. Für eine Kommunikation über
serielle Schnittstelle zwischen zwei Computern (DTE
Geräten)
- muß ein Null-Modem Kabel verwendet werden
- müssen beide serielle Schnittstellen auf die gleiche
Geschwindigkeit, Parität, usw. gestellt werden
Die Einstellung auf 9600 bit/s, 8 Data bits, odd parity, 1 Stop bit
erfolgt mit
stty -F /dev/ttyS0 9600 cs8 -cstopb parenb parodd -ignpar inpck raw
-echo clocal -crtscts
Die Parameter im Detail:
| -F /dev/ttyS0 |
Gerätespezialdatei, hier COM1 Schnittstelle |
| 9600 cs8 -cstopb |
9600 bit/s, 8 Datenbit, 1 Stopbit (-cstopb bedeutet nicht 2
Stopbits) |
| parenb parodd |
Ungerade Parität |
| -ignpar inpck |
Parität bei Eingangsdaten prüfen (-ignpar bedeutet
nicht ignorieren) |
| raw |
Zeichen werden transparent (ohne Ersetzungen)
übertragen |
| -echo |
Empfangene Zeichen werden nicht wieder zurückgesendet |
| clocal |
Hardware-Handshake Signale DCD, DSR werden ignoriert |
| -crtscts |
Hardware-Handshake Signal CTS wird ignoriert |
Test der seriellen Schnittstelle mit UNIX
Kommandos
Am ersten Rechner eingeben
stty -F /dev/ttyS0 9600 cs8 -cstopb parenb parodd -ignpar inpck raw
-echo clocal -crtscts
cat </dev/ttyS0
Am zweiten Rechner eingeben
stty -F /dev/ttyS0 9600 cs8 -cstopb parenb parodd -ignpar inpck raw
-echo clocal -crtscts
echo Hallo >/dev/ttyS0
Programm: Serielle Schnittstelle senden
Die UNIX system-calls für I/O (Input/Output, Ein-/Ausgabe)
sind
- open() zum Öffnen der Schnittstelle
- read() zum Lesen von Bytes von der
Schnittstelle
- write() zum Schreiben von Bytes auf die
Schnittstelle
Linux Besonderheiten:
- Bei der seriellen Schnittstelle immer nur 1 Byte lesen oder
schreiben
- Die Tastatur-Schnittstelle arbeitet gepuffert (Zeichen werden
erst bei <RETURN> übertragen. Für nicht gepufferte
Tastatur-Schnittstelle das Kommando benutzen:
stty -F /dev/tty -icanon
Das Programm führt eine Endlosschleife aus. Hole ein Zeichen
von der Tastatur (dem stdin Gerät) und schreibe es auf die
Schnittstelle.
Programm Test
Programm kompilieren mit
gcc -Wall -o seriell_send seriell_send.c
Am ersten Rechner eingeben
cat </dev/ttyS0
Am zweiten Rechner eingeben
./seriell_send
Datei seriell_send.c
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int fd = open("/dev/ttyS0", O_RDWR);
if (fd < 0) {
// Fehler bei open()
perror("seriell_send open");
exit(1);
}
for(;;)
{
// Endlosschleife
char buf[1];
int bytes = read(STDIN_FILENO, buf, sizeof
buf);
// ein Zeichen von Tastatur holen
write(fd, buf,
bytes); // ein Byte an serielle
Schnittstelle ausgeben
}
// hierher kommt das Programm nie
return 0;
}
Übung
Schreiben Sie ein Programm seriell_recv welches Zeichen von der
seriellen Schnittstelle liest und auf dem Bildschirm ausgibt.
Testen Sie das Programm mit:
Am ersten Rechner eingeben
./seriell_recv
Am zweiten Rechner eingeben
./seriell_send
Programm: Multi-IO mit select()
Für Duplex Betrieb (gleichzeitig senden und empfangen) sind
seriell_send und seriell_recv nicht geeignet. Die system-calls
read() oder getchar() blockieren (warten bis Daten vorhanden sind).
Eine Lösung für das "mehrere Dinge gleichzeitig tun"
Problem ist select() der system-call für Ein-Ausgabe
Multiplexing.
Dem select() wird eine Liste von file-descriptors übergeben.
Das Betriebssystem beendet den select() call wenn Daten für
mindestens einen der file-descriptors vorliegen.
Programm Test
Programm kompilieren mit
gcc -Wall -o multi_io multi_io.c
Am ersten Rechner eingeben
./multi_io
Am zweiten Rechner eingeben
./multi_io
Testen Sie auch das unterschiedliche Verhalten bei -icanon und
icanon. Welche Betriebsart finden Sie für ein Kommunikation
Programm angenehmer?
Datei multi_io.c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/select.h>
int main() {
int fdCom1 = open("/dev/ttyS0", O_RDWR);
if (fdCom1 < 0)
{
// Fehler bei open()
perror("multi_io COM1");
exit(1);
}
for(;;) {
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fdCom1, &readfds);
FD_SET(STDIN_FILENO, &readfds);
int rv = select(32, &readfds, NULL, NULL,
NULL);
if (-1 == rv) {
perror("multi_io select");
exit(1);
}
if (rv > 0) {
if (FD_ISSET(STDIN_FILENO,
&readfds)) { // Daten vom Keyboard verfuegbar
char buf[1];
int bytes =
read(STDIN_FILENO, buf, sizeof buf);
write(fdCom1, buf,
bytes);
// auf RS232 ausgeben
}
if (FD_ISSET(fdCom1, &readfds))
{ // Daten von der RS232
verfuegbar
char buf[1];
int bytes = read(fdCom1,
buf, sizeof buf);
write(STDOUT_FILENO,
buf, bytes); // auf xterm Fenster
ausgeben
}
}
}
return 0;
}
Übung
Ändern Sie das Programm multi_io.c zu multi_io1.c. Der buf
für den read() system-call soll 40 Bytes groß werden.
Testen Sie das Programm mit Eingaben die weniger und auch die mehr
als 40 Byte enthalten. In beiden Fällen soll multi_io1.c
so wie multi_io.c funktionieren.
Tipp: Sie müssen nur zwei konstante Zahlen ändern.
Vorbereitung TCP Kommunikation
Das Programm multi_io konnten wir in beiden Endgeräten
benutzen. Bei TCP Kommunikation ist ein Endgerät der TCP
Client, das andere Endgerät der TCP Server. Ein Webbrowser wie
Firefox ist ein TCP Client welcher das HTTP Protokoll
verwendet.
Der TCP Client muss über den TCP Server zwei Parameter
wissen:
Der TCP Server muss nur einen Parameter wissen:
- Port-Nummer (well known port number)
Der TCP Server arbeitet nach dem Prinzip "sende zurück an
Absender". Die nötige Absender Information ist Teil jedes TCP
Datenpaketes welches folgende Adressinformation enthält:
- Source (Absender) IP, Source Port-Nummer
- Destination (Empfänger) IP, Destination Port-Nummer
Die IP Adresse kann direkt angegeben werden als 10.111.26.61 oder
kann als Name (z.B. eddfl1a) in der Datei /etc/hosts nachgesehen
werden oder kann über DNS (Domain Name System) von einem
DNS-Server Computer geholt werden (z.B. im Internet
www.google.de).
Die Port-Nummer kann direkt angegeben werden (z.B. 80) oder kann
als Name (z.B. http) in der Datei /etc/services nachgesehen werden
oder kann über den Portmapper Dienst von anderen Computern
geholt werden.
Die Port-Nummern von 0 bis 1023 sind "superuser" Port-Nummern.
Für eigene Programme empfiehlt sich Port-Nummern aus dem
Bereich 49152 bis 65535 zu benutzen. Diese sind "private"
Port-Nummern. Dazwischen liegen die "well known Port-Nummern", z.B.
5060 für den SIP Port von Voice-over-IP.
TCP Test mit UNIX Kommandos
Das Programm netcat kann die Rolle eines TCP Clients oder eines TCP
Servers übernehmen. Eventuell müssen Sie netcat noch mit
YAST o.ä. installieren.
Im ersten xterm Fenster eingeben für TCP-Server:
netcat -l -p 55555
Im zweiten xterm Fenster eingeben für TCP-Client
netcat 127.0.0.1 55555
Übung
Benutzen Sie netcat zwischen zwei Computern. Welche Parameter
müssen gegenseitig bekannt sein? Schreiben Sie Ihre well-known
IP-Adresse nach /etc/hosts und ihre well-known Port-Nummer nach
/etc/services und arbeiten Sie mit Namen anstelle von Nummern.
Funktioniert die Datenübertragung duplex? Wie reagiert netcat
auf -icanon?
Programm: TCP client
Eine prima Einführung in die TCP Programmierung unter C ist
"Beej's Guide to Network Programming" auf http://beej.us/guide/bgnet
Der open() in multi_io.c wird geändert für die TCP
Kommunikation. Die verschiedenen TCP Parameter werden mit mehreren
system-calls mitgeteilt. Diese TCP client Start Sequenz ist immer
gleich. Sie besteht aus:
- gethostbyname() IP-Adresse des TCP Server
holen
-
socket()
Socket-descriptor (file-descriptor) holen
-
connect()
Verbindung zum TCP-Server aufbauen
Network Byte Order
Ein kleines aber wichtiges Detail bei der Behandlung von
Port-Nummern und IP-Adressen ist die Network Byte Order. Die
Integer Werte im IP oder TCP Header müssen in Network Byte
Order oder Big Endian angegeben werden. Die Intel CPU arbeitet mit
Little Endian. Es gibt folgende Funktionen zum Umwandlung von Host
Byte Order (Integer Darstellung der CPU) nach Network Byte
Order:
| htons() |
Host Byte Order nach Network Byte Order Port Nummer
(16bit) |
| htonl() |
Host Byte Order nach Network Byte Order IPv4 Adresse
(32bit) |
| ntohs() |
Network Byte Order nach Host Byte Order Port Nummer
(16bit) |
| ntohl() |
Network Byte Order nach Host Byte Order IPv4 Adresse
(32bit) |
Programm Test
Programm kompilieren mit
gcc -Wall -o tcp_client tcp_client.c
Im ersten xterm Fenster eingeben für TCP-Server:
netcat -l -p 55555
Im zweiten xterm Fenster eingeben für TCP-Client
./tcp_client localhost
Datei tcp_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#define PORT 55555
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "tcp_client usage: tcp_client
hostname\n");
exit(1);
}
struct hostent *he;
he = gethostbyname(argv[1]); // Name nach
IP Adresse umsetzen
if (NULL == he) {
perror("tcp_client gethostbyname");
exit(1);
}
int fdServer = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == fdServer) {
perror("tcp_client socket");
exit(1);
}
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof
serv_addr); // mit 0
initialisieren
serv_addr.sin_family =
AF_INET;
// IPv4 Protokoll
serv_addr.sin_addr = *((struct in_addr *)
he->h_addr);
serv_addr.sin_port =
htons(PORT); // PORT in Network
Byte Order bringen
int rv = connect(fdServer, (struct sockaddr
*)&serv_addr, sizeof serv_addr);
if (-1 == rv) {
perror("tcp_client connect");
exit(1);
}
for(;;) {
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fdServer, &readfds);
FD_SET(STDIN_FILENO, &readfds);
int rv = select(32, &readfds, NULL, NULL,
NULL);
if (-1 == rv) {
perror("tcp_client select");
exit(1);
}
if (rv > 0) {
if (FD_ISSET(STDIN_FILENO,
&readfds)) { // Daten vom Keyboard verfuegbar
char buf[1];
int bytes =
read(STDIN_FILENO, buf, sizeof buf);
write(fdServer, buf,
bytes); //
auf das Netzwerk ausgeben
}
if (FD_ISSET(fdServer,
&readfds)) { // Daten vom Netzwerk
verfuegbar
char buf[1];
int bytes =
read(fdServer, buf, sizeof buf);
if (bytes <= 0) {
//
Verbindung von Gegenseite geschlossen oder Fehler
close(fdServer);
exit(1);
}
write(STDOUT_FILENO,
buf, bytes); // auf xterm Fenster
ausgeben
}
}
}
return 0;
}
Übung 1
Entfernen Sie htons(), d.h. aus
serv_addr.sin_port = htons(PORT);
wird
serv_addr.sin_port = PORT;
und testen Sie das Programm. Funktioniert es noch?
Übung 2
Ändern Sie das Programm zu tcp_client1.c. Die Portnummer soll
nicht mehr als Konstante festgelegt sein sondern als zweiter
Parameter beim Programmstart angegeben werden.
Aus "tcp_client localhost" wird "tcp_client1 localhost 55555".
Übung 3
Ändern Sie das Programm tcp_client1.c zu tcp_client2.c. Der
buf für den read() system-call soll 40 Bytes groß
werden. Testen Sie das Programm mit TCP Datenpaketen die weniger
und auch die mehr als 40 Byte enthalten. In beiden Fällen
soll tcp_client2.c so wie tcp_client1.c funktionieren.
Tipp: Sehen Sie bei mult_io1.c nach.
Übung 4
Der system-callconnect() ist ein "slow system-call". Wie lange dauert ein connect() wenn der TCP-Server Computer ausgeschaltet ist (z.B.
eigene IP=192.168.0.1, nicht vorhandener Computer=192.168.0.99)?
Ändern Sie das Programm tcp_client2.c zu tcp_client3.c. Setzen
Sie vor und nach dem connect() system-call einen time() system-call. Geben Sie die Zeitdifferenz in Sekunden der
beiden time() system-calls aus. Vergleichen Sie ihr Ergebnis
mit
time./tcp_client3 192.168.0.99
55555
Dabei ist 192.168.0.99 die IP Adresse
des TCP-Servers auf einem nicht vorhandenen
Computer.
Übung 5
Anstelle von read() kann bei TCP Verbindungen auch recv() verwendet
werden. Anstelle von write() funktioniert auch send(). Der vierte
Parameter von recv() und send() ist dabei 0, wenn nur das read()
und write() Verhalten gewünscht wird. Ersetzen Sie die
system-calls und prüfen Sie ob das Programm sich wie zuvor
verhält.
Programm: TCP server für nur einen TCP
client
Das erste TCP server Programm kann nur mit einem TCP client
gleichzeitig eine Verbindung unterhalten. Aber auch für diesen
einfachen Fall muss die TCP Server Start Sequenz durchlaufen
werden.
- socket() Socket-Descriptor (file-descriptor)
holen
- bind() Socket-Descriptor mit
TCP-Server IP-Adresse und Port-Nummer verbinden
- listen() Socket als "LISTEN" Socket
konfigurieren zur Verbindung-Annahme
- accept() Auf Verbindung-Aufbau Meldungen von
TCP-Clients warten
Programm Test
Programm kompilieren mit
gcc -Wall -o tcp_server tcp_server.c
Im ersten xterm Fenster eingeben für TCP-Server:
./tcp_server
Im zweiten xterm Fenster eingeben für TCP-Client
./tcp_client localhost
Datei tcp_server.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT 55555
// Funktion fuer TCP Client Kommunikations Schleife
int do_client(int fdKbd, int fdClient) {
for(;;) {
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fdClient, &readfds);
FD_SET(fdKbd, &readfds);
int rv = select(32, &readfds, NULL, NULL,
NULL);
if (-1 == rv) {
perror("tcp_server select");
exit(1);
}
if (rv > 0) {
if (FD_ISSET(STDIN_FILENO,
&readfds)) {
char buf[1];
int bytes =
read(STDIN_FILENO, buf, sizeof buf);
write(fdClient, buf,
bytes);
}
if (FD_ISSET(fdClient,
&readfds)) {
char buf[1];
int bytes =
read(fdClient, buf, sizeof buf);
if (bytes <= 0) {
close(fdClient);
return
-1;
}
write(STDOUT_FILENO,
buf, bytes);
}
}
}
return 0;
}
int main() {
int fdListen = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == fdListen) {
perror("tcp_server socket");
exit(1);
}
int val = 1;
int rv = setsockopt(fdListen, SOL_SOCKET, SO_REUSEADDR,
&val, sizeof val);
if (-1 == rv) {
perror("tcp_server setsockopt");
exit(1);
}
struct sockaddr_in
my_addr;
// TCP Server IP Informationen
memset(&my_addr, 0, sizeof my_addr);
my_addr.sin_family = AF_INET;
my_addr.sin_addr.s_addr = INADDR_ANY; // bedeutet TCP-Server
IP Adresse
my_addr.sin_port =
htons(PORT); // PORT in Network
Byte Order bringen
rv = bind(fdListen, (struct sockaddr *) &my_addr, sizeof
my_addr);
if (-1 == rv) {
perror("tcp_server bind");
exit(1);
}
rv = listen(fdListen, 5);
if (-1 == rv) {
perror("tcp_server listen");
exit(1);
}
for(;;) {
struct sockaddr_in addr;
// TCP Client IP
Informationen
socklen_t addrlen = sizeof addr;
int fdClient = accept(fdListen, (struct sockaddr
*)&addr, &addrlen);
if (-1 == fdClient) {
perror("tcp_server accept");
continue;
}
printf("tcp_server connected from %s\n",
inet_ntoa(addr.sin_addr));
do_client(STDIN_FILENO, fdClient);
printf("tcp_server closed from %s\n",
inet_ntoa(addr.sin_addr));
}
return 0;
}
Übung 1
Benutzen Sie tcp_server zusammen mit zwei tcp_client Programmen.
Was passiert genau wenn das zweite tcp_client Programm gestartet
wird, während das erste noch läuft? Nachdem das erste
tcp_client Programm beendet wird?
Übung 2
Bringen Sie die Änderungen von tcp_client1.c in die neue TCP
Server Version tcp_server1.c. Die Port-Nummer soll als
Kommandozeilenparameter übergeben werden.
Übung 3
Bringen Sie die Änderungen von tcp_client2.c in die neue TCP
Server Version tcp_server2.c. Die buf Variable in do_client() soll
40 Bytes fassen. Testen Sie mit Eingaben die länger als 40
Zeichen sind.
Zeiger/Länge mit Länge als
Ein-/Ausgabeparameter
Der Zeiger/Größe Doppel-Parameter kann noch weiter
entwickelt werden. Bei der Socket-Schnittstelle (die
C-Bibliotheks-Funktionen welche die TCP Schnittstelle realisieren)
gibt es bei einigen Funktionen den Längen Parameter als
Eingabeparameter (von aufrufende Funktion an aufgerufene Funktion)
und als Ausgabeparameter (von aufgerufener Funktion zu aufrufender
Funktion). In C wird ein Ausgabeparameter als Zeiger-Variable
realisiert. Aus einem Längenparameter für Eingabe
int n
wird als Längenparameter für Ein-/Ausgabe
int *n
Im Beispiel aus tcp_server.c:
struct
sockaddr_in addr;
// TCP Client IP
Informationen
socklen_t addrlen = sizeof addr;
int fdClient = accept(fdListen, (struct sockaddr
*)&addr, &addrlen);
ist der Zeiger die Variable addr und die Länge als In/Out
Parameter ist addrlen.
Übung 1
int reuseaddr = 0;
unsigned len = sizeof reuseaddr;
rv = getsockopt(fdListen, SOL_SOCKET, SO_REUSEADDR,
&reuseaddr, &len);
printf("SO_REUSEADDR = %d\n", reuseaddr);
Bauen Sie das obige Stück Quelltext nach dem setsockopt()
system-call in tcp_server.c ein. Die geänderte Datei soll
tcp_servera.c heißen. Testen Sie ob tcp_servera sich so
verhält wie tcp_server.
Übung 2
Geben Sie den Inhalt der Variablen len direkt vor und nach dem
Aufruf von getsockopt() aus.
TCP und Serialization
Serialization (Marshalling, Serialisieren) bedeutet strukturierte
Daten (z.B. die Datensätze einer Datenbank) so in eine
unstrukturierte äußere Form (z.B. eine Datei auf der
Festplatte) zu bringen, das später die strukturierten Daten
wieder zurückgewonnen werden können (die Datensätze
der Datenbank). Bei der Übertragung von Datensätzen
über TCP müssen die Datensätze serialisiert
werden.
Das TCP Protokoll ist ein Stream Protokoll. Damit passt TCP gut zu
den UNIX Ein-/Ausgabe Streams (fopen(), fputs(), fgets(), ...).
Wenn verschiedene einzelne Datensätze in einem Datenstrom
transportiert werden muss der Daten-Empfänger den Datenstrom
wieder in einzelne Datensätze zurück wandeln können.
Ein Datenfehler in einem Datensatz darf nur diesen Datensatz
unbrauchbar machen - der Fehler darf nicht dazu führen das
alle folgenden korrekten Datensätze nicht mehr vom Datenstrom
in Datensätze zurück gewandelt werden können.
Im Datenstrom werden zwischen den einzelnen Datensätzen
Delimiter (Separatoren, Abgrenzer, Trennzeichen)
eingefügt.

Die Delimiter müssen im Datenstrom immer sicher erkannt
werden. Es gibt mehrere Lösungen:
- Als Delimiter Zeichen wird ein Zeichen verwendet welches nicht
im Datensatz auftaucht bzw. im Datensatz schon als Delimiter
Zeichen verwendet wird. Werden die Daten in ASCII Kodierung
übertragen sind die ASCII Zeichen FS (file separator), GS
(group separator), RS (record separator) und US (unit separator)
als Delimiter vorgesehen. Üblich ist aber auch LF (line feed)
als Delimiter.
- Vor dem eigentlichen Datensatz wird die Datensatz-Länge
übertragen. Der Empfänger liest zuerst die
Datensatz-Länge und kann dann den Datensatz aus dem Datenstrom
lesen. Diese length/data Kodierung ist sehr empfindlich gegen
Fehler. Ist eine einzige Datensatz-Länge falsch (z.B.
Datensatz-Länge = 999, echte Länge des Datensatzes =
1000) werden alle folgenden Datensätze falsch aus dem
Datenstrom gelesen!
- Die Datensätze werden in einzelnen TCP-Paketen
übertragen (TCP ist ein stream-Protokoll, aber die Funktionen
send() und recv() arbeiten block-weise). Damit das Betriebssystem
nicht die Daten von mehreren send() Aufrufen in einem TCP Paket
unterbringt wird die Socket-Option TCP_NODELAY benutzt um den
"Nagle algorithm" auszuschalten. Zwischen Sender und Empfänger
muss eine maximale Datensatz-Größe festgelegt werden.
Diese sollte kleiner gleich der MTU (Maximal Transfer Unit) sein.
Für TCP auf Ethernet sollte Datensatz-Größe kleiner
gleich 1400 Bytes gewählt werden.
- IP fragmentation ist eine Fähigkeit von IP welche dazu
führt das aus einem send() Aufruf im Empfänger mehrere
recv() Aufrufe werden. Deshalb sollte die length/data Kodierung mit
der TCP_NODELAY Datenpaket Übertragung kombiniert werden. Auch
hier ist eine maximale Datensatz-Größe nötig. Diese
kann größer als 1400 Bytes sein. Sie muss aber kleiner
gleich 65535 Bytes (16Bit Size Feld im TCP Header) sein.
Für die Serialization gibt es für C die tpl Bibliothek:
http://tpl.sourceforge.net/
Für C++ bietet sich Boost an: http://www.boost.org/libs/serialization/doc/index.html
Programm: TCP Client und Server mit C-String
Schnittstelle
Der folgende Quelltext benutzt '\n' (ASCII Zeichen LF) als
Delimiter Zeichen. C-Strings können mit den system-calls
fputs() oder fprintf() gesendet werden und können mit fgets()
empfangen werden.
Der system-call fflush() sorgt dafür das die Daten sofort im
Netzwerk übertragen werden. Normalerweise wartet die
C-Bibliothek mit dem Aussenden bis der Sende-Puffer voll ist.
Dieses Verhalten ist bei interaktiven System nicht gewünscht.
Hier erwartet der Benutzer umgehende System-Reaktion.
Programm Test
Programme kompilieren mit
gcc -Wall -o stream_server stream_server.c
gcc -Wall -o stream_client stream_client.c
Im ersten xterm Fenster eingeben für TCP-Server:
./stream_server
Im zweiten xterm Fenster eingeben für TCP-Client
./stream_client hostname
Übung 1
Ändern Sie für die Dateien stream_client1.c und
stream_server1.c die Konstante SIZE auf 41 und testen Sie mit
Eingaben die länger als 40 Zeichen sind. Gibt es einen
Unterschied zwischen der tcp_server/tcp_client Lösung und der
stream_server1/stream_client1 Lösung?
Geben Sie vor dem Aufruf von stream_client1 oder stream_server1 das
Kommando ein:
stty -F /dev/tty -icanon
Testen Sie erneut mit Eingaben die länger als 40 Zeichen sind.
Welche Auswirkung hat das Ausschalten der Zeilenpufferung des
Terminals?
Übung 2
Testen Sie tcp_server zusammen mit stream_client1. Testen Sie
stream_server1 zusammen mit tcp_client. Funktionieren Eingaben
kürzer als 40 Zeichen und Eingaben länger als 40 Zeichen
wie erwartet?
Datei stream_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define PORT 55555
#define SIZE 1400
// Funktion fuer TCP Partner Kommunikations Schleife
int do_chat(int fdKbd, int fdPartner) {
FILE *fpPartner = fdopen(fdPartner, "a+"); // mit
buffered IO arbeiten
if(NULL == fpPartner) {
perror("do_chat fdopen");
exit(1);
}
for(;;) {
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(fdPartner, &readfds);
FD_SET(fdKbd, &readfds);
int rv = select(32, &readfds, NULL, NULL,
NULL);
if (-1 == rv) {
perror("do_chat select");
exit(1);
}
if (rv > 0) {
if (FD_ISSET(fdKbd, &readfds))
{ // Daten vom Keyboard
verfuegbar
char buf[SIZE];
fgets(buf, sizeof buf,
stdin); // Zeile bis \n einlesen
fputs(buf,
fpPartner);
fflush(fpPartner);
// sofort auf das Netzwerk ausgeben
}
if (FD_ISSET(fdPartner,
&readfds)) { // Daten vom Netzwerk verfuegbar
char buf[SIZE];
char *rv = fgets(buf,
sizeof(buf), fpPartner);
if (NULL == rv) {
//
Verbindung von Gegenseite geschlossen oder Fehler
perror("do_chat fgets");
fclose(fpPartner);
return
-1;
}
printf("%s", buf);
fflush(stdout);
}
}
}
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "stream_client usage:
stream_client hostname\n");
exit(1);
}
struct hostent *he;
he = gethostbyname(argv[1]);
if (NULL == he) {
perror("stream_client gethostbyname");
exit(1);
}
int fdServer = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == fdServer) {
perror("stream_client socket");
exit(1);
}
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof serv_addr);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr = *((struct in_addr *)
he->h_addr);
serv_addr.sin_port = htons(PORT);
int rv = connect(fdServer, (struct sockaddr
*)&serv_addr, sizeof serv_addr);
if (-1 == rv) {
perror("stream_client connect");
exit(1);
}
do_chat(STDIN_FILENO, fdServer);
return 0;
}
Datei stream_server.c
// #include und #define wie
bei stream_client.c
// do_chat() wie bei
stream_client.c
int main() {
int fdListen = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == fdListen) {
perror("stream_server socket");
exit(1);
}
int val = 1;
int rv = setsockopt(fdListen, SOL_SOCKET, SO_REUSEADDR,
&val, sizeof val);
if (-1 == rv) {
perror("stream_server setsockopt");
exit(1);
}
struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof my_addr);
my_addr.sin_family = AF_INET;
my_addr.sin_addr.s_addr = INADDR_ANY;
my_addr.sin_port = htons(PORT);
rv = bind(fdListen, (struct sockaddr *) &my_addr, sizeof
my_addr);
if (-1 == rv) {
perror("stream_server bind");
exit(1);
}
rv = listen(fdListen, 5);
if (-1 == rv) {
perror("stream_server listen");
exit(1);
}
for(;;) {
struct sockaddr_in addr;
socklen_t addrlen = sizeof addr;
int fdClient = accept(fdListen, (struct sockaddr
*)&addr, &addrlen);
if (-1 == fdClient) {
perror("stream_server accept");
continue;
}
printf("stream_server connected from %s\n",
inet_ntoa(addr.sin_addr));
do_chat(STDIN_FILENO, fdClient);
printf("stream_server closed from %s\n",
inet_ntoa(addr.sin_addr));
}
return 0;
}
Programm: TCP Client/Server mit TCP
keep-alive
Wird nach dem Verbindungsaufbau zwischen TCP Client und TCP Server
die LAN Verbindung unterbrochen bemerken dies weder der Client noch
der Server. Mit der Option SO_KEEPALIVE von setsockopt() wird das
Aussenden von Verbindung-Test-Meldungen (keep-alive messages)
aktiviert. In der Default Einstellung werden die ersten keep-alive
Meldungen nach 2 Stunden Inaktivität (idle, kein Transport von
Nutzdaten) ausgesandt. Unter Linux lassen sich die keep-alive
Parameter nach Belieben einstellen. Die nötigen Optionen sind
TCP_KEEPIDLE für die Idle Zeit, TCP_KEEPINTVL für die
Zeit zwischen zwei keep-alive Meldungen und TCP_KEEPCNT für
Anzahl der nicht beantworteten keep-alive Meldungen die zum
Verbindung-Abbau führen. Siehe "man 7 tcp" für
Details.
Damit TCP Client und TCP Server eine Leitungsunterbrechung
feststellen können, müssen beide Seiten die keep-alive
Meldungen aktivieren.
Programm Test
Programme kompilieren mit
gcc -Wall -o keepalive_server keepalive_server.c
gcc -Wall -o keepalive_client keepalive_client.c
Im ersten xterm Fenster eingeben für TCP-Server:
./keepalive_server
Im zweiten xterm Fenster eingeben für TCP-Client
./keepalive_client hostname
Übung
Lassen die die beiden Programme auf getrennten Computern laufen.
Was passiert nach einer Leitungsunterbrechung? Zum Vergleich testen
Sie noch einmal mit stream_server und stream_client. Bemerken diese
Programme eine Leitungsunterbrechung? Was passiert wenn
während einer Leitungsunterbrechung eine Nutzdaten Meldung
ausgesendet wird?
Datei keepalive_client.c
// #include und #define wie
bei stream_client.c und zusaetzlich
#include
<netinet/tcp.h> // fuer TCP_KEEP... (Linux
spezifisch)
// do_chat() wie bei
stream_client.c
// Wrapper um
setsockopt()
void mysetsockopt(int fd, int level, int option, int value) {
int rv = setsockopt(fd, level, option, &value, sizeof
value);
if (-1 == rv) {
perror("keepalive_client setsockopt");
exit(1);
}
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "keepalive_client usage:
keepalive_client hostname\n");
exit(1);
}
struct hostent *he;
he = gethostbyname(argv[1]);
if (NULL == he) {
perror("keepalive_client gethostbyname");
exit(1);
}
int fdServer = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == fdServer) {
perror("keepalive_client socket");
exit(1);
}
mysetsockopt(fdServer, SOL_SOCKET, SO_KEEPALIVE, 1);
mysetsockopt(fdServer, IPPROTO_TCP, TCP_KEEPIDLE,
4); // Linux spezifisch
mysetsockopt(fdServer, IPPROTO_TCP, TCP_KEEPINTVL, 4);
// Linux spezifisch
mysetsockopt(fdServer, IPPROTO_TCP, TCP_KEEPCNT,
1); // Linux spezifisch
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof serv_addr);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr = *((struct in_addr *)
he->h_addr);
serv_addr.sin_port = htons(PORT);
int rv = connect(fdServer, (struct sockaddr
*)&serv_addr, sizeof serv_addr);
if (-1 == rv) {
perror("keepalive_client connect");
exit(1);
}
do_chat(STDIN_FILENO, fdServer);
return 0;
}
Datei keepalive_server.c
// #include und #define wie
bei stream_client.c und zusaetzlich
#include
<netinet/tcp.h> // fuer TCP_KEEP... (Linux
spezifisch)
// do_chat() wie bei
stream_client.c
// mysetsockopt() wie bei
keepalive_client.c
int main() {
int fdListen = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == fdListen) {
perror("keepalive_server socket");
exit(1);
}
mysetsockopt(fdListen, SOL_SOCKET, SO_REUSEADDR, 1);
struct sockaddr_in my_addr;
memset(&my_addr, 0, sizeof my_addr);
my_addr.sin_family = AF_INET;
my_addr.sin_addr.s_addr = INADDR_ANY;
my_addr.sin_port = htons(PORT);
int rv = bind(fdListen, (struct sockaddr *) &my_addr,
sizeof my_addr);
if (-1 == rv) {
perror("keepalive_server bind");
exit(1);
}
rv = listen(fdListen, 5);
if (-1 == rv) {
perror("keepalive_server listen");
exit(1);
}
for(;;) {
struct sockaddr_in addr;
socklen_t addrlen = sizeof addr;
int fdClient = accept(fdListen, (struct sockaddr
*)&addr, &addrlen);
if (-1 == fdClient) {
perror("keepalive_server
accept");
continue;
}
printf("keepalive_server conn. from %s\n",
inet_ntoa(addr.sin_addr));
mysetsockopt(fdClient, SOL_SOCKET, SO_KEEPALIVE,
1);
mysetsockopt(fdClient, IPPROTO_TCP,
TCP_KEEPIDLE, 4); // Linux spezifisch
mysetsockopt(fdClient, IPPROTO_TCP,
TCP_KEEPINTVL, 4); // Linux spezifisch
mysetsockopt(fdClient, IPPROTO_TCP, TCP_KEEPCNT,
1); // Linux spezifisch
do_chat(STDIN_FILENO, fdClient);
printf("keepalive_server closed from %s\n",
inet_ntoa(addr.sin_addr));
}
return 0;
}
Nicht-blockierende Ein-/Ausgabe (slow
system-call)
Wie im Programm tcp_client.c schon festgestellt ist connect() ein
langsamer system-call. Der connect() dauert 3 Sekunden wenn der
TCP-Server Computer im Netzwerk nicht läuft. Während
dieser Zeit wird die Event Schleife nicht ausgeführt. Das
Programm reagiert nicht auf Nutzer-Eingaben.
Mit der O_NONBLOCK Option des fcntl() system-calls kann das
Verhalten des connect() von blockierend auf nicht blockierend
geändert werden. Der connect() system-call mit NONBLOCK dauert
nicht länger als ein normaler system-call. Die TCP
Verbindung-Aufbau über das Netzwerk benötigt aber
weiterhin seine Zeit von 3 Sekunden.
Wird connect() mit NONBLOCK verwendet muss die Applikation 3
Sekunden abwartet und dann nachfragen ob der TCP Verbindungsaufbau
erfolgreich war. Dieses Nachfragen erfolgt mit der Option SO_ERROR
des getsockopt() system-calls.
Für die Änderung auf nicht-blockierendes connect() wird
der Abschnitt in keepalive_client.c:
int rv =
connect(fdServer, (struct sockaddr *)&serv_addr, sizeof
serv_addr);
if (-1 == rv) {
perror("keepalive_client connect");
exit(1);
}
ersetzt durch folgenden Abschnitt:
int flags =
fcntl(fdServer, F_GETFL, 0);
if (-1 == flags) {
perror("nonblock_client fcntl F_GETFL");
exit(1);
}
int rv = fcntl(fdServer, F_SETFL, flags | O_NONBLOCK);
if (-1 == rv) {
perror("nonblock_client fcntl F_SETFL");
exit(1);
}
rv = connect(fdServer, (struct sockaddr *)&serv_addr,
sizeof serv_addr);
if (-1 == rv) {
extern int errno;
if (errno != EINPROGRESS)
{ //
Error in Progress wird erwartet
perror("nonblock_client
connect");
exit(1);
}
}
printf("connect() NONBLOCK fertig\n");
sleep(3); // TCP Verbindungsaufbau
durch Betriebssystem abwarten
printf("sleep() fertig\n");
int err = 0;
unsigned len = sizeof err;
getsockopt(fdServer, SOL_SOCKET, SO_ERROR, &err,
&len);
if (err != 0) {
fprintf(stderr, "nonblock_client getsockopt:
%s\n", strerror(err));
exit(1);
}
Übung
Datei keepalive_client.c nach nonblock_client.c ändern. Den
connect() Abschnitt auswechseln. Folgende Header Dateien sind
zusätzlich nötig:
#include
<fcntl.h>
#include
<errno.h>
Programm Test
Programm kompilieren mit
gcc -Wall -o nonblock_client nonblock_client.c
Im ersten xterm Fenster eingeben für TCP-Server:
./keepalive_server
Im zweiten xterm Fenster eingeben für TCP-Client
./nonblock_client localhost
Programm: Zeitauflösung Linux
Uhrzeit
Mit dem system-call time() wird die Uhrzeit mit Zeitauflösung
Sekunde angegeben. Bei dem system-call gettimeofday() ist die
Auflösung Mikro-Sekunde (1.000.000 Mikro-Sekunden sind 1
Sekunde).
Die gettimeofday() Uhrzeit steckt in zwei struct Variablen (tv_sec
und tv_usec). Mit Hilfe des Datentyp long long (64 Bit Integer)
lässt sich die gettimeofday() Uhrzeit in einer Variablen
abbilden.
struct timeval
tv;
gettimeofday(&tv,
NULL);
long long t = 1000000
* tv.tv_sec + tv.tv_usec;
printf("gettimeofday
time is %lld\n", t);
Programm Test
Programm kompilieren mit
gcc -Wall -o gettimeofday gettimeofday.c
Im xterm Fenster eingeben:
./gettimeofday
Übung
Testen Sie gettimeofday auf verschiedenen Computern. Ist die
Zeitauflösung immer gleich?
Datei gettimeofday.c
#include <stdio.h>
#include <sys/time.h>
int main() {
struct timeval tv[11];
gettimeofday(tv + 0, NULL);
int i;
for(i = 1; i < 11; ++i) {
for(;;) {
// eine busy waiting Schleife
gettimeofday(tv + i, NULL);
if (tv[i].tv_sec != tv[i-1].tv_sec
|| tv[i].tv_usec != tv[i-1].tv_usec) {
break;
}
}
}
for(i = 1; i < 11; ++i) {
long long t0 = 1000000 * tv[i-1].tv_sec +
tv[i-1].tv_usec;
long long t1 = 1000000 * tv[i].tv_sec +
tv[i].tv_usec;
printf("gettimeofday Aufloesung = %lld usec\n",
t1 - t0);
}
return 0;
}
Fehlersuche mit
valgrind memory debugger
Ein typischer Fehler im
Programm gettimeofday.c wäre die erste for() Schleife mit dem
Anfangswert 0 zu schreiben:
for(i = 0; i < 11; ++i)
{
Dieser Fehler wird vom C-Compiler
nicht gefunden, auch nicht wenn die Option -fstack-protector-all
benutzt wird. Das Programm läuft auch scheinbar fehlerfrei.
Erst der memory debugger in valgrind findet den Fehler. Für
gute valgrind Fehlerausgaben sollte man das Programm mit den
zusätzlichen Optionen -g und -O0
übersetzen.
gcc -g -O0 -Wall
-o gettimeofday gettimeofday.c
Dann das Programm unter valgrind
memory leak check laufen lassen.
valgrind
--leak-check=yes ./gettimeofday
Die relevante Fehlermeldung
ist
==5564== Conditional
jump or move depends on uninitialised value(s)
==5564== at 0x8048462: main
(gettimeofday.c:25)
In Zeile 25
steht:
if
(tv[i].tv_sec != tv[i-1].tv_sec || tv[i].tv_usec !=
tv[i-1].tv_usec) {
Mit i = 0 ergibt der Ausdruck i-1 den Wert
-1. Und einen tv[-1].tv_sec gibt es nicht. Für weitere
Dokumentation über valgrind siehe http://www.valgrind.org/docs/manual/index.html
Signale
Mit Signalen meldet sich das Betriebssystem Linux bei der
Applikation. Die Taste Strg-C löst das Signal SIGINT aus. Ein
"killall -9 Programmname" löst das Signal SIGKILL aus. Siehe
"man 7 signal" für die Liste der Signale.
Ohne eigene Programmierung führt ein Signal zum Programm-Ende.
Mit dem signal() system-call wird die Reaktion auf Signale
geändert. Die Signale SIGINT (Strg-C von der Tastatur),
SIGTERM (Strg-D von der Tastatur), SIGTSTP (Strg-Z von der
Tastatur) können ignoriert werden. Dazu schreibt man am Anfang
der main() Funktion:
signal(SIGINT,
SIG_IGN); // Strg-C ignorieren
signal(SIGTERM,
SIG_IGN); // Strg-D ignorieren
signal(SIGTSTP,
SIG_IGN); // Strg-Z ignorieren
Die Include Datei für signal() ist
#include
<signal.h>
Übung
Erweitern Sie tcp_server.c zu tcp_servera.c um die obige Signal
Behandlung. Testen Sie das Programm mit Strg-C, Strg-D, Strg-Z. Das
Programm kann nur noch mit "killall -9 tcp_servera" beendet werden.
Das Signal SIGKILL (Signal Nummer 9) kann nicht ignoriert
werden.
EINTR Behandlung bei select()
Das mögliche Auftreten von Signalen ist der Grund für die
spezielle Behandlung des return value (rv) von select(). Der Fehler
EINTR bedeutet "ein nicht-blockiertes Signal ist aufgetreten".
Dieser Fehler ist kein echter Fehler der zum Programm-Ende
führen soll.
int rv =
select(32, &readfds, NULL, NULL, NULL);
if (-1 ==
rv) {
extern int
errno;
if (errno
!= EINTR) {
perror("select");
exit(1);
}
// Signal
erhalten, entsprechender Quelltext ...
}
if (rv
> 0) {
if
(FD_ISSET(STDIN_FILENO, &readfds)) {
// Daten vom Keyboard verfuegbar, entsprechender Quelltext
...
}
}
Achtung: Das Bit-Feld readfds darf nur verwendet werden wenn rv
> 0 ist. Bei -1 == rv ist das Bit-Feld readfds undefiniert.
Siehe select man page.
Programm: Intervall Timer
Ein Intervall Timer (periodischer Aufruf einer Funktion) kann unter
Linux auf verschiedene Arten realisiert werden:
- mit dem time-out Parameter von select()
- setitimer()
Die beste Möglichkeit ist setitimer(). Der Timer muss nur
einmal eingerichtet werden und meldet sich dann immer wieder.
Leider ist die Benutzung von setitimer() nicht ganz einfach. Einige
Bedingungen müssen im Programm erfüllt sein:
- es muss einen SIGALRM Handler geben (dieses Signal wird
ausgelöst)
- die Timer-Funktion ergibt sich aus select() mit return value =
-1 und errno = EINTR
Programm Test
Programm kompilieren mit
gcc -Wall -o setitimer setitimer.c
Im xterm Fenster eingeben:
stty -F /dev/tty -icanon
./setitimer
Das Programm gibt 1000 Punkte aus. Der Timer ist auf knapp 4msec
gestellt, d.h. das Programm benötigt 4000msec = 4sec
für die Ausgabe.
Übung 1
Testen Sie das Programm mit "time ./setitimer". Ändern Sie nun
die Timer-Konstante von 4000-2 (d.h. 3998) auf 4000. Testen
Sie das Programm erneut. Was passiert? Testen Sie weiter. Benutzen
Sie folgende Timer-Konstanten:
10000-2 (d.h. 9998)
10000
1000-2 (d.h. 998)
1000
Was ist die Intervall Timer Zeitauflösung?
Übung 2
Halten Sie die Taste A gedrückt während das Programm
setitimer läuft. Was passiert mit der Ausgabe "dots" und
"chars". Was passiert mit der time Ausgabe?
Übung 3
Die Ausgabe write(STDOUT_FILENO, s, strlen(s)) kann auch in der
alarmhandler() Funktion stehen. Schieben Sie die nötigen zwei
Programmzeilen aus der main() Funktion in die alarmhandler()
Funktion und testen Sie die neue Programmversion setitimer1.c.
Datei setitimer.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>
#include <sys/time.h>
#include <sys/select.h>
void alarmhandler() {
// absichtlich nichts tun
}
int main() {
signal(SIGALRM, alarmhandler);
struct itimerval value;
value.it_interval.tv_sec = value.it_value.tv_sec = 0;
value.it_interval.tv_usec = value.it_value.tv_usec =
4000-2;
setitimer(ITIMER_REAL, &value, NULL);
int chars = 0;
int dots;
for(dots = 0; dots < 1000;) { //
absichtlich kein ++dots hier
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
int rv = select(32, &readfds, NULL, NULL,
NULL);
if (-1 == rv) {
extern int errno;
if (errno != EINTR) {
perror("setitimer
select");
exit(1);
}
// Signal erhalten, d.h. ein
setitimer() Intervall ist abgelaufen
++dots;
// hier kommt Schleifenzaehler hochzaehlen
char s[] = ".";
write(STDOUT_FILENO, s,
strlen(s)); // ungepuffert
ausgeben
}
if (rv > 0) {
if (FD_ISSET(STDIN_FILENO,
&readfds)) { // Daten vom Keyboard verfuegbar
char buf[1];
read(STDIN_FILENO, buf,
sizeof buf); // Puffer leer lesen
++chars;
}
}
}
printf("\ndots = %d chars = %d\n", dots, chars);
return 0;
}
Programm: TCP client mit setitimer()
Die Programme werden langsam größer. Der Quelltext muss
gegliedert werden um nicht die Übersicht und die Wartbarkeit
zu verlieren. Für die Strukturierung ist die Unterteilung des
Quelltextes in Software-Schichten (software layers) sehr hilfreich.
Eine höhere Software-Schicht ist immer etwas abstrakter als
die darunter liegende Softwareschicht.
Eine höhere Softwareschicht bietet oft auch mehr
Funktionalität. In unserem Fall werden anstelle von nur einem
setitimer() Intervall-Timer insgesamt 32 after() Timer
angeboten.
Mit den Funktionen after() und fileevent_readable() wird das
Konzept von events (Ereignissen) und event-handler Funktionen
vertieft. Im Kapitel Signale und im Programm setitimer.c wurden
events (z.B. das Signal SIGINT) und event-handler (z.B. die
Funktion alarmhandler()) schon eingeführt. Event-handler
Funktionen werden auch call-back Funktionen genannt.
Die Namen vwait, after, after cancel, fileevent readable und socket
-async stammen übrigens von der Programmiersprache Tcl/Tk und
sind dort Tcl/Tk Funktionen.
| Höhere Schicht |
Aufgabe |
Niedrigere Schicht |
| vwait_init() |
Daten für Event Schleife initialisieren |
nicht vorhanden |
| after() |
Zeit gesteuerter Funktionsaufruf |
basiert auf setitimer() |
| after_cancel() |
Abbruch eines after() |
nicht vorhanden |
| fileevent_readable() |
Daten sind verfügbar |
basiert auf select() |
| vwait() |
Event Schleife ausführen |
nicht vorhanden |
socket_async()
|
TCP Client Socket
nicht-blockierend öffnen |
gethostbyname(), socket(),
connect()
|
Die Header Datei vwait.h erfüllt nebenbei die Aufgabe der
Dokumentation für die neuen Funktionen. Die Kommentare
können von dem Dokumentation-Programm doxygen ausgewertet
werden. In der Quelltext-Datei vwait_client.c werden die Funktionen
der niedrigen Schicht nicht verwendet. Die Quelltext-Datei vwait.c
realisiert das “vwait Modul” durch eine prozedurale
Abstraktion.
Programm Test
Programm kompilieren mit
gcc -Wall -o vwait_client vwait_client.c vwait.c
Im ersten xterm Fenster eingeben für TCP-Server:
./keepalive_server
Im zweiten xterm Fenster eingeben für TCP-Client
./vwait_client hostname
Übung 1
Ändern Sie SIZE auf den Wert 41 und testen Sie das Programm.
Was passiert wenn mehr als 40 Zeichen eingeben werden? Schalten Sie
die Zeilen-Pufferung des Terminals aus mit
stty -F /dev/tty -icanon
Wie verändert sich das Verhalten bei zu langen Eingaben?
Datei vwait_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/tcp.h> // fuer TCP_KEEP... (Linux
spezifisch)
#include "vwait.h"
#define PORT 55555
#define SIZE 1400
// Callback Funktion: Daten vom Keyboard verfuegbar
void cb_kbd(int fdKbd, void *pServer) {
FILE *fpServer = pServer;
char buf[SIZE];
fgets(buf, sizeof buf, stdin);
fputs(buf,
fpServer);
fflush(fpServer);
}
// Callback Funktion: Daten vom Netzwerk verfuegbar
void cb_Server(int fdServer, void *pServer) {
FILE *fpServer = pServer;
char buf[SIZE];
char *rv = fgets(buf, sizeof buf, fpServer);
if (NULL == rv) {
// Verbindung von Gegenseite geschlossen oder
Fehler
perror("cb_Server fgets");
fclose(fpServer);
fileevent_readable(fdServer, NULL, NULL);
exit(1);
}
printf("%s", buf);
fflush(stdout);
}
// Callback Funktion: Timer ist abgelaufen
void cb_timeout(void *p) {
char s[] = ".";
write(1, s,
strlen(s));
// ungepuffert auf Bildschirm ausgeben
after(1000, cb_timeout, NULL); // neuen
Timer starten
}
void mysetsockopt(int fd, int level, int option, int value) {
int rv = setsockopt(fd, level, option, &value, sizeof
value);
if (-1 == rv) {
perror("vwait_client setsockopt");
exit(1);
}
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "vwait_client usage:
vwait_client hostname\n");
exit(1);
}
struct hostent *he;
he = gethostbyname(argv[1]);
if (NULL == he) {
perror("vwait_client gethostbyname");
exit(1);
}
int fdServer = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == fdServer) {
perror("vwait_client socket");
exit(1);
}
mysetsockopt(fdServer, SOL_SOCKET, SO_KEEPALIVE, 1);
mysetsockopt(fdServer, IPPROTO_TCP, TCP_KEEPIDLE,
4); // Linux spezifisch
mysetsockopt(fdServer, IPPROTO_TCP, TCP_KEEPINTVL, 4);
// Linux spezifisch
mysetsockopt(fdServer, IPPROTO_TCP, TCP_KEEPCNT,
1); // Linux spezifisch
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof serv_addr);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr = *((struct in_addr *)
he->h_addr);
serv_addr.sin_port = htons(PORT);
int rv = connect(fdServer, (struct sockaddr
*)&serv_addr, sizeof serv_addr);
if (-1 == rv) {
perror("vwait_client connect");
exit(1);
}
FILE *fpServer = fdopen(fdServer, "a+");
if(NULL == fpServer) {
perror("vwait_client fdopen");
exit(1);
}
vwait_init();
fileevent_readable(fdServer, cb_Server, fpServer);
fileevent_readable(STDIN_FILENO, cb_kbd, fpServer);
after(1000, cb_timeout, NULL);
vwait();
// Event Schleife ausfuehren
return 0;
}
Datei vwait.h
#ifdef
__cplusplus // bei C++ Compiler definiert
extern "C" {
// C Namensumsetzung (name mangling) benutzen
#endif
/**
* vwait() Datenstrukturen initialisieren. Einmal bei
Programmstart aufrufen
*/
void vwait_init();
/**
* Zeitgesteuerter Funktions-Aufruf
* @param ms die Wartezeit in Millisekunden
* @param proc Zeiger auf eine Funktion mit einem void*
Parameter
* @param param Zeiger auf void * Funktionsparameter
*
* return After Id, -1 bei Fehler
*/
int after(int ms, void (*proc)(void *), void *param);
/**
* Loesche Zeitgesteuerten Funktions-Aufruf
* @param afterId die AfterId von after()
*
* return 0 wenn okay, -1 bei Fehler
*/
int after_cancel(int afterId);
/**
* Filedescriptor Daten vorhanden Eventhandler
* @param fd der Filedescriptor (returnwert von open())
* @param proc Zeiger auf eine Funktion mit zwei Parameter
(int, void *)
* @param param Zeiger auf void * Funktionsparameter
*
* return 0 wenn okay, -1 bei Fehler
*/
int fileevent_readable(int fd, void (*proc)(int, void *), void
*param);
/**
* vwait Eventloop. Hier verschwindet die
Programmkontrolle
*/
void vwait();
/** TCP Client Socket nicht-blockierend oeffnen
* @param host Hostname
* @param port Portnummer
* return -1 bei Fehler, Filedescriptor sonst
*/
int socket_async(const char *host, int port);
#ifdef __cplusplus
}
#endif
Datei vwait.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/tcp.h> // fuer TCP_KEEP... (Linux
spezifisch)
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/time.h>
#include "vwait.h"
#define INTERVAL 100 // in ms
#define MAXFD
32 // maximale Anzahl fd fuer
fileevent_readable()
#define MAXAFTER 32 // maximale
Anzahl wartende after() Aufrufe
/*************************************** */
/* after */
typedef struct {
void (*proc)(void *); // aufzurufende
Funktion
void
*param;
// Parameter fuer aufzurufende Funktion
int
ms;
// Count Down in Millisekunden
int
magic;
// after(), after_cancel() Konsistenz Pruefwert
} AFTERENTRY;
static AFTERENTRY afterarray[MAXAFTER];
int after(int ms, void (*proc)(void *), void *param) {
int i;
for (i = 0; i < MAXAFTER; ++i) {
if (NULL == afterarray[i].proc)
{
// Eintrag ist frei
afterarray[i].proc = proc;
afterarray[i].ms = ms;
afterarray[i].param = param;
++afterarray[i].magic;
if (afterarray[i].magic >=
INT_MAX/MAXAFTER) { // vermeide Ueberlauf
afterarray[i].magic = 1;
}
return MAXAFTER *
afterarray[i].magic + i;
}
}
return
-1;
// kein freier after() Timer vorhanden
}
int after_cancel(int afterId) {
int id = afterId % MAXAFTER;
if (id < 0 || id >= MAXAFTER) {
return -1;
}
int magic = afterId / MAXAFTER;
if (magic != afterarray[id].magic) { // after_cancel()
passt nicht zu after()
return -1;
}
afterarray[id].proc = NULL;
afterarray[id].param = NULL;
afterarray[id].ms = 0;
return 0;
}
/*************************************** */
/* fileevent */
typedef struct {
void (*proc)(int, void *); // aufzurufende
Funktion
void
*param;
// Parameter fuer aufzurufende Funktion
} FILEEVENTENTRY;
static fd_set readfds;
static FILEEVENTENTRY fileeventarray[MAXFD];
int fileevent_readable(int fd, void (*proc)(int, void *), void
*param) {
if (fd < 0 || fd >= MAXFD) {
return -1;
}
if (proc != NULL) {
FD_SET(fd, &readfds);
} else {
FD_CLR(fd, &readfds);
}
fileeventarray[fd].proc = proc;
fileeventarray[fd].param = param;
return 0;
}
void vwait_init() {
FD_ZERO(&readfds);
memset(fileeventarray, 0, sizeof fileeventarray);
memset(afterarray, 0, sizeof afterarray);
}
/*************************************** */
/* vwait */
static void alarmhandler(int sig) {
// absichtlich nichts tun
}
void vwait() {
signal(SIGALRM, alarmhandler);
struct itimerval value;
value.it_interval.tv_sec = value.it_value.tv_sec = 0;
value.it_interval.tv_usec = value.it_value.tv_usec =
INTERVAL * 1000;
setitimer(ITIMER_REAL, &value, NULL);
for(;;) {
fd_set in_out =
readfds; //
select() aendert den Parameter
int rv = select(MAXFD, &in_out, NULL, NULL,
NULL);
if (-1 == rv)
{
// setitimer() abgelaufen oder Fehler
extern int errno;
if (errno != EINTR) {
perror("vwait
select");
exit(1);
}
int i;
for (i = 0; i < MAXAFTER; ++i)
{
if (afterarray[i].proc
!= NULL) {
afterarray[i].ms -= INTERVAL;
if
(afterarray[i].ms <= INTERVAL/2) {
(*afterarray[i].proc)(afterarray[i].param);
afterarray[i].proc = NULL;
}
}
}
}
if (rv > 0)
{
// Daten fuer file descriptors vorhanden
int i;
for (i = 0; i < MAXFD; ++i) {
if (FD_ISSET(i,
&in_out) && (fileeventarray[i].proc != NULL)) {
(*fileeventarray[i].proc)(i, fileeventarray[i].param);
}
}
}
}
}
int socket_async(const char *host, int port) {
struct hostent *he;
he = gethostbyname(host);
if (NULL == he) return -1;
int fdServer = socket(AF_INET, SOCK_STREAM, 0);
if (-1 == fdServer) return -1;
int val = 1;
int rv = setsockopt(fdServer, SOL_SOCKET, SO_KEEPALIVE,
&val, sizeof val);
if (-1 == rv) goto error1;
val = 4;
rv = setsockopt(fdServer, IPPROTO_TCP, TCP_KEEPIDLE,
&val, sizeof val);
if (-1 == rv) goto error1;
val = 4;
rv = setsockopt(fdServer, IPPROTO_TCP, TCP_KEEPINTVL,
&val, sizeof val);
if (-1 == rv) goto error1;
val = 1;
rv = setsockopt(fdServer, IPPROTO_TCP, TCP_KEEPCNT,
&val, sizeof val);
if (-1 == rv) goto error1;
struct sockaddr_in serv_addr;
memset(&serv_addr, 0, sizeof serv_addr);
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr = *((struct in_addr *)
he->h_addr);
serv_addr.sin_port = htons(port);
int flags = fcntl(fdServer, F_GETFL, 0);
if (-1 == flags) goto error1;
rv = fcntl(fdServer, F_SETFL, flags | O_NONBLOCK);
if (-1 == rv) goto error1;
rv = connect(fdServer, (struct sockaddr *)&serv_addr,
sizeof serv_addr);
if (-1 == rv) {
extern int errno;
if (errno != EINPROGRESS) goto
error1; // EINPROGRESS wird erwartet
}
return fdServer;
error1:
close(fdServer);
return -1;
}
State Diagram (Zustands Übergang
Diagramm)
Beim System-Design werden die einzelnen Zustände als Kreise,
Ellipsen oder Rechtecke dargestellt, die
Zustandsübergänge als gerichtete Pfeile zwischen den
Kreisen. Die Initialisierung der State-Machine (der Anfangszustand)
wird oft mit einem Pfeil "aus dem Nichts" mit der Beschriftung
"Start" gekennzeichnet. Das TCP Protokoll benutzt ein
State-Diagram. Das FDDI Protokoll übrigens auch.

TCP State Diagram (http://world.std.com/~franl/tcp-state-diagram.gif)
Die Zustände "Listen", "Established (Verbunden)" usw. lassen
sich mit dem Programm netstat -antp ansehen. Hier die Ausgabe
nachdem tcp_server und tcp_client auf dem Computer gestartet
wurden:
Proto Recv-Q Send-Q Local
Address
Foreign Address
State PID/Program name
tcp
0 0
0.0.0.0:55555
0.0.0.0:*
LISTEN
7303/tcp_server
tcp
0 0
127.0.0.1:32927
127.0.0.1:55555
VERBUNDEN 7304/tcp_client
tcp
0 0
127.0.0.1:55555
127.0.0.1:32927
VERBUNDEN 7303/tcp_server
Die erste Zeile mit State LISTEN zeigt das der tcp_server auf einen
Verbindungsaufbau wartet - auch wenn das Programm nur einen
tcp_client gleichzeitig bedienen kann. Die zweite Zeile ist die
Verbindung aus Sicht des tcp_client. Die dritte Zeile ist die
Verbindung aus Sicht des tcp_server.
Programm: TCP Client mit NONBLOCK, reopen
Im Programm nonblock_client.c wurde nicht-blockierendes connect()
vorgestellt. Erst mit after() zum zeit-gesteuerten Funktionsaufruf
machen nicht-blockierende system-calls richtig Sinn.
Die Applikation führt den nicht-blockierenden system-call aus,
wartet mit after() auf das Ergebnis und benutzt dann die
erfolgreich geöffnete TCP-Verbindung.
Wie bei dem vwait Modul wird wieder das "Räderwerk" hinter
einfachen Zugriffsfunktionen versteckt (abstrahiert). Das
Modul-Interface wird in tcp.h definiert und umfasst:
| tcp_open() |
Öffne TCP Verbindung auf TCP-Client Seite |
| tcp_puts() |
Schreibe C-String buf in Richtung TCP Server |
| tcp_flush() |
Sofort aussenden (flush buffer) |
| tcp_gets() |
Lese C-String bis Zeilenende in buf mit Größe size
vom TCP-Server |
| tcp_close() |
Schließe TCP Verbindung auf TCP-Client Seite |
Die Funktionen sind nach den Stream-IO Funktionen fopen(), fputs(),
fflush(), fgets() und fclose() gestaltet. Die Funktion tcp_gets()
blockiert und sollte deshalb nur innerhalb eines mit
fileevent_readable() eingerichteten Event-Handlers benutzt
werden.
Programm Test
Programm kompilieren mit
gcc -Wall -o single_client single_client.c vwait.c tcp.c
Im ersten xterm Fenster eingeben für TCP-Server:
./keepalive_server
Im zweiten xterm Fenster eingeben für TCP-Client
./single_client hostname portnummer
State Diagram single_client
Das Programm single_client erfüllt natürlich das TCP
state diagram. Darüber hinaus hat das Programm noch sein
eigenes State Diagram.
Nach dem Start geht die durch das Modul tcp.c realisierte state
machine in den Zustand connect1. In diesem Zustand wird der erste
Teil des TCP connect() ausgeführt.
Nach 3 Sekunden time out erfolgt ein Übergang ist den Zustand
connect2. In diesem Zustand wird geprüft ob der TCP connect()
erfolgreich war. Wenn erfolgreich erfolgt ein Übergang zum
Zustand transfer statt, sonst erfolgt ein Übergang zum Zustand
connect1.
In dem Zustand transfer können Daten mit tcp_puts() versendet
werden und Daten mit tcp_gets() empfangen werden. Treten Fehler im
Zustand transfer erfolgt ein Übergang zum Zustand reopen.
Im Zustand reopen wird die Verbindung geschlossen. Nach 1 Sekunde
time-out erfolgt ein Übergang zu Zustand connect1, die state
machine ist wieder im Anfangszustand.
Die state machine als Diagramm:

Übung 1
Die Zustände werden mit Hilfe der tcp_xxx() Funktionen
realisiert. Machen Sie die Zustände deutlich durch printf()
Ausgaben am Anfang der Funktionen. Benutzen Sie folgende Texte:
| Am Anfang von |
Ausgabe von |
| tcp_connect1() |
Zustand connect1 |
| tcp_connect2() |
Zustand connect2 |
| tcp_puts() |
Zustand transfer (puts) |
| tcp_gets() |
Zustand transfer (gets) |
| tcp_reopen() |
Zustand reopen |
Übung 2
Entfernen Sie die Funktion tcp_reopen() aus tcp.c. Das Programm
Verhalten soll gleich bleiben.
Datei single_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "vwait.h"
#include "tcp.h"
#define SIZE 1400
// Callback Funktion: Daten vom Keyboard verfuegbar
void cb_Kbd(int fdKbd, void *dummy) {
char buf[SIZE];
fgets(buf, sizeof buf, stdin);
int rv = tcp_puts(buf);
if (-1 == rv) {
fprintf(stderr, "cb_Kbd tcp_puts failed\n");
return;
}
tcp_flush();
}
// Callback Funktion: Daten vom Netzwerk verfuegbar
void cb_Server(int fdServer, void *dummy) {
char buf[SIZE];
char *rv = tcp_gets(buf, sizeof buf);
if (NULL == rv) {
fprintf(stderr, "cb_Server tcp_gets
failed\n");
return;
}
printf("%s", buf);
fflush(stdout);
}
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "single_client usage:
single_client host port\n");
exit(1);
}
const char *host = argv[1];
int port = atoi(argv[2]);
vwait_init();
tcp_open(host, port, cb_Server);
fileevent_readable(STDIN_FILENO, cb_Kbd, NULL);
vwait();
// Event Schleife ausfuehren
return 0;
}
Datei tcp.h
/**
* Oeffne TCP Verbindung auf TCP-Client Seite mit Retry
* @param host_ Hostname passend fuer gethostbyname()
* @param port_ Portnummer
* @param callback Callback-Funktion passend fuer
fileevent_readable()
*/
void tcp_open(const char *host_, int port_, void (*callback)(int,
void *));
/**
* Schreibe C-String buf in Richtung TCP Server
* @param buf Zeiger auf 0-terminierten C-String
*
* return -1 bei Fehler, 0 sonst (wie bei fputs)
*/
int tcp_puts(const char *buf);
/**
* Sofort aussenden (flush buffer)
*
* return -1 bei Fehler, 0 sonst (wie bei fflush)
*/
int tcp_flush();
/**
* Lese C-String bis Zeilenende in buf mit Groesse size vom
TCP-Server
* @param buf Zeiger auf Puffer
* @param size Groesse des Puffers
*
* return NULL bei Fehler, buf sonst (wie bei fgets)
*/
char *tcp_gets(char *buf, int size);
/**
* Schliesse TCP Verbindung auf TCP-Client Seite
*
* return -1 bei Fehler, 0 sonst (wie bei fclose)
*/
int tcp_close();
Datei tcp.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <netdb.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/tcp.h> // fuer TCP_KEEP... (Linux
spezifisch)
#include <errno.h>
#include <fcntl.h>
#include "vwait.h"
#include "tcp.h"
static const char *host;
static int port;
static int fdServer;
static FILE *fpServer = NULL;
static void (*callback)(int, void *); // tcp_gets() Callback
Funktion
static void tcp_connect1(void *dummy);
static void tcp_connect2(void *dummy);
void tcp_open(const char *host_, int port_, void (*callback_)(int,
void *)) {
host = host_;
port = port_;
callback = callback_;
tcp_connect1(NULL);
}
// TCP Client connect Teil 1: connect() NONBLOCK
static void tcp_connect1(void *dummy) {
fdServer = socket_async(host, port);
if (fdServer < 0) {
perror("tcp_connect1 socket_async");
after(1000, tcp_connect1, NULL);
return;
}
after(3000, tcp_connect2, NULL);
}
// TCP Client connect Teil 2: Pruefen ob Verbindungsaufbau
erfolgreich war
static void tcp_connect2(void *dummy) {
int err = 0;
unsigned len = sizeof err;
getsockopt(fdServer, SOL_SOCKET, SO_ERROR, &err,
&len);
if (err != 0) {
fprintf(stderr, "tcp_connect2 getsockopt: %s\n",
strerror(err));
goto error1;
}
fpServer = fdopen(fdServer, "a+");
if(NULL == fpServer) {
perror("tcp_connect2 fdopen");
goto error1;
}
fileevent_readable(fdServer, callback, fpServer);
fprintf(stderr, "tcp_connect2 success host = %s port =
%d\n", host, port);
return;
error1:
close(fdServer);
after(1000, tcp_connect1, NULL);
return;
}
int tcp_puts(const char *buf) {
if (NULL == fpServer) return -1;
return fputs(buf, fpServer);
}
int tcp_flush() {
if (NULL == fpServer) return -1;
return fflush(fpServer);
}
// TCP Client nach Fehler aufraeumen
static void tcp_reopen() {
tcp_close();
after(1000, tcp_connect1, NULL);
}
char *tcp_gets(char *buf, int size) {
char *rv = fgets(buf, size, fpServer);
if (NULL == rv) { //
Verbindung von Gegenseite geschlossen oder Fehler
tcp_reopen();
buf[0] = '\0';
}
return rv;
}
int tcp_close() {
int rv = fclose(fpServer);
fpServer = NULL;
fileevent_readable(fdServer, NULL, NULL);
fdServer = -1;
return rv;
}
Go To Statement Considered Harmful
Im Jahr 1968 schrieb Edsger W. Dijkstra sein berühmtes
"A
case against the GO TO Statement". In der Funktionen
socket_async() und tcp_connect2() wird goto verwendet. Auch das
unstrukturierte goto kann der strukturierten Programmierung dienen.
Das goto error1 ist ein Sprung zum Fehler-Aufräum Quelltext
(error recovery code) der Funktion. Dieses goto ist mehr
strukturiert als die Wiederholung des Fehler-Aufräum
Quelltextes an mehreren Stellen in der Funktion.
Programm: TCP Client für redundante
TCP-Server
Das letzte Programm der TCP Client Serie fügt noch eine kleine
Erweiterung hinzu. Der TCP Client versucht wechselweise zu zwei
unterschiedlichen TCP Servern Verbindung aufzubauen. Diese Methode
wird bei einigen redundanten Systemen eingesetzt.
Die Änderungen im Quelltext sind minimal. Die Funktion
tcp_open2() erhält die IP Information für zwei TCP
Server. Die Funktion tcp_connect1() tauscht die TCP Server IP
Informationen aus damit einmal der eine Server und im nächsten
Durchlauf der andere Server zum Verbindungsaufbau benutzt wird.
Programm Test
Programm kompilieren mit
gcc -Wall -o dual_client dual_client.c vwait.c tcp2.c
Im ersten xterm Fenster eingeben für TCP-Server:
./keepalive_server1 55555
Im zweiten xterm Fenster eingeben für TCP-Server:
./keepalive_server1 55556
Im dritten xterm Fenster eingeben für TCP-Client
./dual_client localhost 55555 localhost 55556
Beispiel Sitzung
Die Benutzer Eingaben waren:
abcd
Nun Server auf Port 55555 beenden
efgh
Nun auch Server auf Port 55556 beenden
./dual_client localhost 55555
localhost 55556
tcp_connect2 success host =
localhost port = 55555
abcd
Nun Server auf Port 55555
beenden
cb_Server tcp_gets
failed
tcp_connect2 success host =
localhost port = 55556
efgh
Nun auch Server auf Port
55556 beenden
cb_Server tcp_gets
failed
tcp_connect2 getsockopt:
Connection refused
tcp_connect2 getsockopt:
Connection refused
tcp_connect2 getsockopt:
Connection refused
tcp_connect2 getsockopt:
Connection refused
Datei dual_client.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "vwait.h"
#include "tcp2.h"
#define SIZE 1400
// Callback Funktion: Daten
vom Keyboard verfuegbar
// cb_Kbd() wie bei
single_client.c
// Callback Funktion: Daten
vom Netzwerk verfuegbar
// cb_Server() wie bei
single_client.c
int main(int argc, char
*argv[]) {
if (argc != 5) {
fprintf(stderr, "dual_client usage: dual_client
host port host2 port2\n");
exit(1);
}
const char *host = argv[1];
int port = atoi(argv[2]);
const char *host2 = argv[3];
int port2 = atoi(argv[4]);
vwait_init();
tcp_open2(host, port, host2, port2, cb_Server);
fileevent_readable(STDIN_FILENO, cb_Kbd, NULL);
vwait();
// Event Schleife ausfuehren
return 0;
}
Datei tcp2.h
/**
* Oeffne TCP Verbindung auf TCP-Client Seite fuer redundante
TCP-Server
* @param host_ Hostname passend fuer gethostbyname()
* @param port_ Portnummer
* @param host2_ Hostname alternativer TCP-Server
* @param port2_ Portnummer alternativer TCP-Server
* @param callback Callback-Funktion passend fuer
fileevent_readable()
*/
void tcp_open2(const char *host_, int port_, const char *host2_,
int port2_,
void (*callback_)(int, void *));
// Rest wie bei
tcp.h
Datei tcp2.c
// zusaetzliche Variablen
gegenueber tcp.c
static const char *host2 =
""; // Hostname
alternativer TCP Server
static int port2 =
-1;
// Portnummer alternativer TCP Server
// geaenderte Funktion
gegenueber tcp.c
void tcp_open(const char
*host_, int port_, void (*callback_)(int, void *)) {
host = host2 = host_;
port = port2 = port_;
callback = callback_;
tcp_connect1(NULL);
}
// neue Funktion
gegenueber tcp.c
static void tcp_swapServer()
{
const char *hostTmp = host; // Dreier-Tausch
host = host2;
host2 = hostTmp;
int portTmp =
port;
// Dreier-Tausch
port = port2;
port2 = portTmp;
}
// neue Funktion gegenueber
tcp.c
void tcp_open2(const char
*host_, int port_, const char *host2_, int port2_,
void (*callback_)(int, void *)) {
host = host_;
port = port_;
host2 = host2_;
port2 = port2_;
callback = callback_;
tcp_swapServer();
// weil tcp_connect1() auch einen swap macht
tcp_connect1(NULL);
}
// geaenderte Funktion
gegenueber tcp.c
static void tcp_connect1(void
*dummy) {
tcp_swapServer();
fdServerTmp = socket_async(host, port);
if (fdServerTmp < 0) {
perror("tcp_connect1 socket_async");
after(1000, tcp_connect1, NULL);
return;
}
fpServerTmp = fdopen(fdServerTmp, "a+");
if(NULL == fpServerTmp) {
perror("tcp_connect1 fdopen");
after(1000, tcp_connect1, NULL);
return;
}
after(3000, tcp_connect2, NULL);
}
Ausblick: TCP Client für redundante
TCP-Server in C++
Die C Lösung dual_client.c ist schon sehr ausgefeilt. Leider
versagt die C Lösung wenn gleichzeitig mit mehreren
TCP-Servern kommuniziert werden soll. Durch eine C++ Klasse ist der
nötige Schritt zur Verallgemeinerung möglich.
Das Modul vwait bleibt in C. Es gibt nur einen setitimer() und nur
einen aktiven select() pro Linux Programm, hier kann nichts
verallgemeinert werden. Zwischen dem C Modul vwait und der C++
Klasse TCP2 gibt es einen Bruch im Programmiermodell. Um diesen
Bruch zur überwinden werden die Helper Funktionen
tcp_connect1() und tcp_connect2() benutzt. Die Helper Funktionen
nehmen einen void Zeiger aus dem C Modul entgegen und führen
den Aufruf einer C++ Klassenfunktion aus.
Programm Test
Programm kompilieren mit
gcc -Wall -c vwait.c
g++ -Wall -o dual_clienta dual_clienta.cpp tcp2.cpp vwait.o
Im ersten xterm Fenster eingeben für TCP-Server:
./keepalive_server1 55555
Im zweiten xterm Fenster eingeben für TCP-Server:
./keepalive_server1 55556
Im dritten xterm Fenster eingeben für TCP-Client
./dual_clienta localhost 55555 localhost 55556
Datei dual_clienta.cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "vwait.h"
#include "tcp2.hpp"
#define SIZE 1400
// Callback Funktion: Daten vom Keyboard verfuegbar
void cb_Kbd(int fdKbd, void *p) {
TCP2 *tcp = (TCP2 *)p;
char buf[SIZE];
fgets(buf, sizeof buf, stdin);
int rv = tcp->puts(buf);
if (-1 == rv) {
fprintf(stderr, "cb_Kbd tcp->puts
failed\n");
return;
}
tcp->flush();
}
// Callback Funktion: Daten vom Netzwerk verfuegbar
void cb_Server(int fdServer, void *p) {
TCP2 *tcp = (TCP2 *)p;
char buf[SIZE];
char *rv = tcp->gets(buf, sizeof buf);
if (NULL == rv) {
fprintf(stderr, "cb_Server tcp->gets
failed\n");
return;
}
printf("%s", buf);
fflush(stdout);
}
int main(int argc, char *argv[]) {
if (argc != 5) {
fprintf(stderr, "dual_clienta usage:
dual_clienta host port host2 port2\n");
exit(1);
}
const char *host = argv[1];
int port = atoi(argv[2]);
const char *host2 = argv[3];
int port2 = atoi(argv[4]);
vwait_init();
TCP2 tcp;
tcp.open2(host, port, host2, port2, cb_Server);
fileevent_readable(STDIN_FILENO, cb_Kbd, &tcp);
vwait();
// Event Schleife ausfuehren
return 0;
}
Datei tcp2.hpp
class TCP2 {
const char *host;
int port;
const char
*host2;
// Hostname alternativer TCP Server
int
port2;
// Portnummer alternativer TCP Server
int fdServer;
FILE *fpServer;
void (*callback)(int, void *); // tcp_gets() Callback
Funktion
void swapServer();
void reopen();
public:
void
connect1();
// public wegen C Helper Funktionen
void
connect2();
// public wegen C Helper Funktionen
TCP2();
/**
* Oeffne TCP Verbindung auf TCP-Client Seite fuer
redundante TCP-Server
* @param host_ Hostname passend fuer
gethostbyname()
* @param port_ Portnummer
* @param host2_ Hostname alternativer TCP-Server
* @param port2_ Portnummer alternativer TCP-Server
* @param callback Callback-Funktion passend fuer
fileevent_readable()
*/
void open2(const char *host_, int port_, const char *host2_,
int port2_,
void (*callback_)(int, void *));
/**
* Oeffne TCP Verbindung auf TCP-Client Seite mit
reopen
* @param host_ Hostname passend fuer
gethostbyname()
* @param port_ Portnummer
* @param callback Callback-Funktion passend fuer
fileevent_readable()
*/
void open(const char *host_, int port_, void
(*callback)(int, void *));
/**
* Schreibe C-String buf in Richtung TCP Server
(aehnlich fputs)
* @param buf Zeiger auf 0-terminierten C-String
*
* return -1 bei Fehler, 0 sonst (wie bei fputs)
*/
int puts(const char *buf);
/**
* Sofort aussenden (flush buffer)
*
* return -1 bei Fehler, 0 sonst (wie bei fflush)
*/
int flush();
/**
* Lese C-String bis Zeilenende in buf mit Groesse size
vom TCP-Server
* @param buf Zeiger auf Puffer
* @param size Groesse des Puffers
*
* return NULL bei Fehler, buf sonst (wie bei
fgets)
*/
char *gets(char *buf, int size);
/**
* Schliesse TCP Verbindung auf TCP-Client Seite
*
* return -1 bei Fehler, 0 sonst (wie bei fclose)
*/
int close();
};
Datei tcp2.cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include "vwait.h"
#include "tcp2.hpp"
// Helper Funktion um C void Zeiger auf C++ Klassenfunktion Aufruf
umzusetzen
static void tcp_connect1(void *p) {
TCP2 *tcp = (TCP2 *)p;
tcp->connect1();
}
// Helper Funktion um C void Zeiger auf C++ Klassenfunktion Aufruf
umzusetzen
static void tcp_connect2(void *p) {
TCP2 *tcp = (TCP2 *)p;
tcp->connect2();
}
TCP2::TCP2() {
fpServer = NULL;
}
void TCP2::open(const char *host_, int port_, void
(*callback_)(int, void *)) {
host = host2 = host_;
port = port2 = port_;
callback = callback_;
connect1();
}
void TCP2::swapServer() {
const char *hostTmp = host; // Dreier-Tausch
host = host2;
host2 = hostTmp;
int portTmp =
port;
// Dreier-Tausch
port = port2;
port2 = portTmp;
}
void TCP2::open2(const char *host_, int port_, const char *host2_,
int port2_,
void (*callback_)(int, void *)) {
host = host_;
port = port_;
host2 = host2_;
port2 = port2_;
callback = callback_;
swapServer(); //
weil TCP2::connect1() auch einen swap macht
connect1();
}
// TCP Client connect Teil 1: connect() NONBLOCK
void TCP2::connect1() {
swapServer();
fdServer = socket_async(host, port);
if (fdServer < 0) {
perror("TCP2::connect1 socket_async");
after(1000, tcp_connect1, this);
return;
}
after(3000, tcp_connect2, this);
}
// TCP Client connect Teil 2: Pruefen ob Verbindungsaufbau
erfolgreich war
void TCP2::connect2() {
int err = 0;
unsigned len = sizeof err;
getsockopt(fdServer, SOL_SOCKET, SO_ERROR, &err,
&len);
if (err != 0) {
fprintf(stderr, "TCP2::connect2 getsockopt:
%s\n", strerror(err));
goto error1;
}
fpServer = fdopen(fdServer, "a+");
if(NULL == fpServer) {
perror("TCP2::connect2 fdopen");
goto error1;
}
fileevent_readable(fdServer, callback, this);
fprintf(stderr, "TCP2::connect2 success host = %s port =
%d\n", host, port);
return;
error1:
::close(fdServer);
// system-call close()
after(1000, tcp_connect1, this);
return;
}
int TCP2::puts(const char *buf) {
if (NULL == fpServer) return -1;
return fputs(buf, fpServer);
}
int TCP2::flush() {
if (NULL == fpServer) return -1;
return fflush(fpServer);
}
// TCP Client nach Fehler aufraeumen
void TCP2::reopen() {
close();
after(1000, tcp_connect1, this);
}
char *TCP2::gets(char *buf, int size) {
char *rv = fgets(buf, size, fpServer);
if (NULL == rv) { //
Verbindung von Gegenseite geschlossen oder Fehler
reopen();
buf[0] = '\0';
}
return rv;
}
int TCP2::close() {
int rv = fclose(fpServer);
fpServer = NULL;
fileevent_readable(fdServer, NULL, NULL);
return rv;
}
TJSON Grammatik
Bis jetzt wurde der wohl geordnete Zustand von kleinen
Datenelementen (ein C-String, ein unsigned char, eine Uhrzeit)
betrachtet. In diesem Beispiel wird eine kleine Grammatik und ein
kleiner Parser vorgestellt.
Die Grammatik ist eine LL(1)-Grammatik. Die Programmiersprache
PASCAL benutzt eine LL(1) Grammatik. Die Grammatik einer
Programmiersprache definiert was ein Quelltext ohne Syntax-Fehler
ist.
Der Parser ist ein Programm welches den Quelltext einliest und nach
den Regeln der Grammatik in seine Bestandteile zerlegt. Typische
Bestandteile sind Sonderzeichen wie < oder =,
Schlüsselwörter wie "begin" und "end" und die vom
Programmierer verwendeten Namen für Variablen oder
Prozeduren.
Für die Datenübertragung werden gerne kleine
LL(1)-Grammatiken verwendet. Für eine korrekte LL(1)-Grammatik
lässt sich leicht ein korrekter Parser programmieren. Das
Informatik Fachgebiet zum Thema Grammatiken und Parser ist der
Compilerbau.
Grammatik in Strukturierter Sprache
Strukturierte Sprache ist "Deutsch klar und präzise".
Strukturierte Sprache ist schlecht geeignet die Grammatik zu
definieren, aber gut geeignet die Grammatik zu erklären.
Meldungsformat
Die Meldungen werden in 7-bit ASCII (ANSI X3.4-1968) kodiert. Jede
Meldung besteht aus einem
- Start of Message Zeichen: { (Geschweifte Klammer auf)
- dem Meldungsinhalt
- End of Message Zeichen: }\r\n (Geschweifte Klammer zu,
Carrige Return, Line Feed)
Meldungsinhalt
Der Meldungsinhalt besteht aus einem oder mehreren
Schlüsselwort/Werte Paaren. Ein einzelnes
Schlüsselwort/Wert Paar besteht aus:
- Signal Name (Schlüsselwort) in doppelten
Anführungszeichen "
- Zuweisungszeichen : (Doppelpunkt)
- Signal Wert, entweder
-
- eine Zahl in Dezimalschreibweise
- oder eine Zeichenkette in doppelten Anführungszeichen
"
Zwischen aufeinander folgenden Schlüsselwort/Werte Paaren
steht das Trennzeichen , (Komma).
Meldungsbeispiel
{"TAG":"LANB","DATE":"16.08.2007","TIME":"11:05:07","SEQ":12,"RWY":1,"CLD":0,"LA1":1,"LA2":0,"CAT1":1,"CATB":0,"CAT2":0,"CAT3":0,"PTA":0}\r\n
Syntax-Diagramm
Das Syntax-Diagramm stellt die Grammatik graphisch dar.


Die Bilder stammen von der JSON Homepage http://www.json.org
Grammatik in EBNF Schreibweise
Die Grammatik mit Hilfe einer Grammatik-Beschreibung-Sprache zu
definieren ist präzise. Hier wird der Coco/R Parser Generator
verwendet.
/* Tiny JSON - subset of JSON
(RFC4627) */
/* ATG Datei fuer Coco/R LL(1) Parser Generator */
COMPILER tjson
/* Regulaere Ausdruecke (Scanner) */
CHARACTERS
stringChar = ANY - '\"' - '\r' - '\n'
.
digit =
"123456789" .
TOKENS
STRING = '\"' { stringChar } '\"' .
CONST = '0' | ( digit { '0' | digit} ) .
/* LL(1) Grammatik (Parser) */
PRODUCTIONS
tjson = '{' [ pair { ',' pair } ] '}' '\r' '\n' .
pair = STRING ':' ( STRING | CONST ) .
END tjson.
Der aufmerksame Leser bemerkt die kleinen Unterschiede zwischen den
verschiedenen Beschreibungen der Grammatik. Es wird aber immer die
gleiche Grammatik definiert.
TJSON Parser Skelett
Aus einer Grammatik kann ein Parser-Generator einen C-Quelltext
erzeugen. Dieses Parser Skelett ist der Ausgangspunkt für den
kompletten Parser. Das Parser Skelett Programm kann schon
feststellen ob ein Quelltext wohl formatiert ist.
void Expect(int
n)
{
if (lookahead ==
n)
GetToken();
else
{
SynErr(n);
}
}
pair()
{
Expect(TJSON_STRING);
Expect(':');
if (lookahead ==
TJSON_STRING) {
GetToken();
} else if (lookahead
== TJSON_CONST) {
GetToken();
}
else
SynErr(256);
}
tjson()
// hier startet der
Parser
{
Expect('{');
if (lookahead ==
TJSON_STRING) {
pair();
while
(lookahead == ',') {
GetToken();
pair();
}
}
Expect('}');
Expect('\r');
Expect('\n');
// hierher kommt der
Parser nur wenn der Quelltext wohl formatiert war
}
| lookahead |
Variable welche das nächste Symbol (Token) der Eingabe
enthält . |
| GetToken() |
Funktion welche die Variable lookahead mit dem nächsten
Symbol der Eingabe lädt. |
| Expect() |
Funktion welche das erwartete Symbol mit dem gelieferten Symbol
vergleicht und bei Erfolg GetToken() aufruft. |
| SynErr() |
Funktion welche eine Fehlermeldung ausgibt. |
Programm: TJSON Parser mit
Fehlerbehandlung
Der TJSON Parser wird als C++ Programm vorgestellt. Es werden nur
die bisher vorgestellten C++ Erweiterungen Klasse (verallgemeinerte
Zugriffsfunktionen) und Exceptions (Fehler Ausnahme) benutzt. Der
TJSON Parser läßt sich auch als C Programm realisieren -
aber bitte glauben Sie mir das es einfacher ist ein bisschen C++ zu
lernen um den Quelltext zu verstehen als sich mit der
Hässlichkeit der C Version auseinander zu setzen.
Programm Test
Programm kompilieren mit
g++ -o tjson_parse_test tjson_parse_test.cpp tjson_parse.cpp
Im xterm Fenster eingeben:
./tjson_parse_test
Datei tjson_parse.hpp
#ifndef TJSON_PARSE_HPP
#define TJSON_STRINGLEN 20
#define TJSON_VALUELEN TJSON_STRINGLEN
#define TJSON_STRING 257
#define TJSON_NUMBER 258
#define TJSON_ERR_STRING_TOO_LONG -1
#define TJSON_ERR_NUMBER_TOO_LONG -2
#define TJSON_ERR_UNKNOWN_CHAR -3
#define TJSON_ERR_OPEN_EXPECTED -4
#define TJSON_ERR_CLOSE_EXPECTED -5
#define TJSON_ERR_COLON_EXPECTED -6
#define TJSON_ERR_COMMA_EXPECTED -7
#define TJSON_ERR_CR_EXPECTED -8
#define TJSON_ERR_LF_EXPECTED -9
#define TJSON_ERR_STRING_EXPECTED -10
#define TJSON_ERR_NUMBER_EXPECTED -11
#define
TJSON_ERR_SCANNER
-12
#define TJSON_ERR_INVALID_PAIR -13
#define TJSON_ERR_BAD_STRING -14
class TJSON {
int
laChar;
// Scanner Lookahead Zeichen
char yytext[TJSON_STRINGLEN]; // string oder
number
int
laToken;
// Parser Lookahead Symbol
char token[TJSON_STRINGLEN]; // enthaelt
string oder number
char string[TJSON_STRINGLEN]; //
Zwischenspeicher fuer call-back Funktionen
const char
*input;
// Class Interface Input Zeiger
void *param;
int (*parse_str)(void *, const char *, const char *);
// Callback Funktion
int (*parse_int)(void *, const char *, const char *);
// Callback Funktion
void GetChar();
int yylex();
void GetToken();
void Expect(int n);
void pair();
void parser();
public:
/** Parse String input nach TJSON LL(1) Grammatik
* @param input_ TJSON Meldung als ASCII C-String
* @param parse_str_ call-back Funktion. Aufruf wenn
String:String Paar
*
Return wert < 0 beendet Parsing
* @param parse_int_ call-back Funktion. Aufruf wenn
String:Integer Paar
*
Return wert < 0 beendet Parsing
* @param param_ Zeiger auf void *
Funktionsparameter
*
* return 0 bei Parsing erfolgreich, <0 bei
Fehler
*/
int parse(const char *input_,
int (*parse_str_)(void *, const char *, const char
*),
int (*parse_int_)(void *, const char *, const char
*),
void *param_);
};
#define TJSON_PARSE_HPP
#endif
Datei tjson_parse.cpp
#include <cctype>
#include <cstring>
#include "tjson_parse.hpp"
/* ***************************************************** */
/* Scanner */
// Lese neues Zeichen aus dem Eingabe Puffer
void TJSON::GetChar() {
laChar = *input;
if (laChar > 0) { // nicht ueber Stringende
hinauslesen
input++;
}
}
// Scanner Funktion
int TJSON::yylex() {
if (index("{:,}\r\n", laChar))
{ // Ein Zeichen lange Symbole
char ch = laChar;
GetChar();
return ch;
}
if ('\"' == laChar)
{
// TJSON String Konstante
int i = 0;
GetChar();
while (laChar != '\"') {
if ('\r' == laChar || '\n' == laChar
|| '\0' == laChar) {
throw
TJSON_ERR_BAD_STRING;
}
yytext[i++] = laChar;
GetChar();
if (i+1 >= TJSON_STRINGLEN) {
throw
TJSON_ERR_STRING_TOO_LONG;
}
}
GetChar();
yytext[i] = '\0';
strcpy(token, yytext);
return TJSON_STRING;
}
if (isdigit(laChar))
{
// TJSON Integer Konstante
int i = 0;
yytext[i++] = laChar;
GetChar();
while (isdigit(laChar)) {
yytext[i++] = laChar;
GetChar();
if (i+1 >= TJSON_VALUELEN) {
throw
TJSON_ERR_NUMBER_TOO_LONG;
}
}
yytext[i] = '\0';
strcpy(token, yytext);
return TJSON_NUMBER;
}
throw
TJSON_ERR_UNKNOWN_CHAR;
// Unbekanntes Zeichen
}
/* ***************************************************** */
/* Top Down Parser */
void TJSON::GetToken() {
laToken = yylex();
}
void TJSON::Expect(int n)
{
if (laToken == n)
GetToken();
else {
switch (n) {
case '{': throw
TJSON_ERR_OPEN_EXPECTED; break;
case '}': throw
TJSON_ERR_CLOSE_EXPECTED; break;
case ':': throw
TJSON_ERR_COLON_EXPECTED; break;
case ',': throw
TJSON_ERR_COMMA_EXPECTED; break;
case '\r': throw
TJSON_ERR_CR_EXPECTED; break;
case '\n': throw
TJSON_ERR_LF_EXPECTED; break;
case TJSON_STRING: throw
TJSON_ERR_STRING_EXPECTED; break;
case TJSON_NUMBER: throw
TJSON_ERR_NUMBER_EXPECTED; break;
default: throw
TJSON_ERR_SCANNER; break;
}
}
}
void TJSON::pair()
{
Expect(TJSON_STRING);
strcpy(string, token);
Expect(':');
if (TJSON_STRING == laToken) {
GetToken();
int rv = (*parse_str)(param, string, token);
if (rv < 0) throw rv;
} else if (TJSON_NUMBER == laToken) {
GetToken();
int rv = (*parse_int)(param, string, token);
if (rv < 0) throw rv;
} else
throw TJSON_ERR_INVALID_PAIR;
}
void TJSON::parser()
{
Expect('{');
if (TJSON_STRING == laToken) {
pair();
while (',' == laToken) {
GetToken();
pair();
}
}
Expect('}');
Expect('\r');
Expect('\n');
}
/* ***************************************************** */
/* Interface */
int TJSON::parse(const char *input_,
int (*parse_str_)(void *, const char *, const char *),
int (*parse_int_)(void *, const char *, const char *),
void *param_) {
param = param_;
input = input_;
parse_str = parse_str_;
parse_int = parse_int_;
try {
GetChar(); // Init
Scanner
GetToken(); // Init
Parser
parser();
return 0;
}
catch (int err) {
return err;
}
}
Datei tjson_parse_test.cpp
#include <cstdio>
#include "tjson_parse.hpp"
// Parser call-back. Aufruf bei String:String Paaren
int cb_parse_str(void *p, const char *key, const char *val) {
printf("\tcb_parse_str %s %s\n", key, val);
return 0;
}
// Parser call-back. Aufruf bei String:Integer Paaren
int cb_parse_int(void *p, const char *key, const char *val) {
printf("\tcb_parse_int %s %s\n", key, val);
return 0;
}
int main() {
const char *testinput[] = {
"{\"TAG\":\"LANB\",\"RWY\":1}\r\n",
"{\"TAG\":\"LANffffffffffffffffB\",\"RWY\":1}\r\n",
"{\"TAG\":\"LANB\",\"RWY\":19999999999999999}\r\n",
"{\"TAG\":\"LANB\",.\"RWY\":1}\r\n",
"\"TAG\":\"LANB\",\"RWY\":1}\r\n",
"{\"TAG\":\"LANB\",\"RWY\":1\r\n",
"{\"TAG\":\"LANB\",\"RWY\"1}\r\n",
"{\"TAG\":\"LANB\":\"RWY\":1}\r\n",
"{\"TAG\":\"LANB\",\"RWY\":1}\n",
"{\"TAG\":\"LANB\",\"RWY\":1}\r",
"{\"TAG\":\"LANB\",2:1}\r\n",
"{\"TAG\":\"LANB\",\"RWY\":0x11}\r\n",
"{\"TAG\"::\"LANB\",\"RWY\":1}\r\n",
"{{\"TAG\":\"LANB\",\"RWY\":1}\r\n",
"{\"TAG\"\":\"LANB\",\"RWY\":1}\r\n",
"{\"TAG\":\"LANB\",,\"RWY\":1}\r\n",
"{\"TAG\":\"LANB\",\"RWY\":1}}\r\n",
"{\"TAG\":\"LANB\",\"RWY\":1}\r\r\n",
"\n{\"TAG\":\"LANB\",\"RWY\":1}\r\n",
"{\"TAG:\"LANB\",\"RWY\":1}\r\n",
"{\"TAG\":\"LANB,\"RWY\":1}\r\n",
"{\"TAG\":\"LANB\",\"RWY\":1",
"{\"TAG\":\"LANB\",\"RWY",
"{\"TAG\":\"LANB\",\"RWY:1}\r\n",
"{\"TAG\r\":\"LANB\",\"RWY\":1}\r\n",
"{\"TAG\":\"LANB\n\",\"RWY\":1}\r\n",
"{\"TAG\":\"LANB\",\"RWY\":1\"}\r\n",
NULL // testinput vector Ende
};
int i;
for (i = 0; testinput[i] != NULL; ++i) {
printf("Input: %s", testinput[i]);
TJSON tjson;
int rv = tjson.parse(testinput[i], cb_parse_str,
cb_parse_int, NULL);
printf("TJSON::parse() returns %d\n\n", rv);
}
return 0;
}
Das Parser Interface benutzt call-back Funktionen. Butler W.
Lampson (erhielt 1992 den ACM Turing Award) empfiehlt dieses
Vorgehen in seinem Artikel Hints
for computer system design von 1983.
Programm: Datenstrom nach Datensatz
Umwandlung
Der Parser liest die Daten in der externen ASCII Darstellung
(representation). Für die weitere Verarbeitung im C Programm
ist eine internen Darstellung als struct oder class besser
geeignet. Mit Hilfe der Parser call-back Funktionen wird dieser
letzte Schritt der Datenstrom nach Datensatz Umwandlung
ausgeführt.
Programm Test
Programm kompilieren mit
g++ -o tjson_parse_test2 tjson_parse_test2.cpp tjson_parse.cpp
string_s.c
Im xterm Fenster eingeben:
./tjson_parse_test2
Datei tjson_parse_test2.cpp
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include "tjson_parse.hpp"
#include "string_s.h"
typedef struct {
char tag[10];
char date[11];
int rwy;
}
LIGHTS;
// Datensatz
// Parser call-back. Aufruf bei String:String Paaren
int cb_parse_str(void *p, const char *key, const char *val) {
LIGHTS *lights = (LIGHTS *)p;
if (0 == strcmp("TAG", key)) {
strcpy_s(lights->tag, sizeof lights->tag,
val);
return 0;
}
if (0 == strcmp("DATE", key)) {
strcpy_s(lights->date, sizeof
lights->date, val);
return 0;
}
return -101;
}
// Parser call-back. Aufruf bei String:Integer Paaren
int cb_parse_int(void *p, const char *key, const char *val) {
LIGHTS *lights = (LIGHTS *)p;
if (0 == strcmp("RWY", key)) {
lights->rwy = atoi(val);
return 0;
}
return -102;
}
int main() {
const char *testinput[] = {
"{\"TAG\":\"LANB\",\"DATE\":\"20.01.2008\",\"RWY\":1}\r\n",
"{\"MAG\":\"LANB\",\"DATE\":\"20.01.2008\",\"RWY\":1}\r\n",
"{\"TAG\":\"LANB\",\"DATE\":\"20.01.2008\",\"rWY\":1}\r\n",
NULL // testinput vector Ende
};
int i;
for (i = 0; testinput[i] != NULL; ++i) {
printf("Input: %s", testinput[i]);
LIGHTS lights = { "", "", -1};
TJSON tjson;
int rv = tjson.parse(testinput[i], cb_parse_str,
cb_parse_int, &lights);
if (0 == rv) {
printf("TJSON::parse() found tag=%s,
date=%s, rwy=%d\n",
lights.tag, lights.date,
lights.rwy);
}
printf("TJSON::parse() returns %d\n\n", rv);
}
return 0;
}
Prozesspriorität
Die Prozesspriorität legt fest wie wichtig ein Prozess
für den Computer sein soll. Wichtigere Prozesse werden
bevorzugt bedient.
Alle Benutzer-Prozesse laufen mit der Prozesspriorität 0.
Für den Scheduler (Prozessumschalter) im Linux Kernel sind
dadurch alle Benutzer-Prozesse gleich. Prozesse welche vom
Superuser gestartet werden können eine bessere
Prozesspriorität erhalten.
Mit dem system-call setpriority() kann die Prozesspriorität
gesetzt werden. Dabei bedeutet 19 die unwichtigste Priorität
und -20 die wichtigste Priorität. Einige
Betriebssystem-Prozesse (Daemons) arbeiten mit der Priorität
19 (ksoftirqd), andere Daemons arbeiten mit -2 (ipw3945d) bis -5
(kthreadd).
Ein bevorzugter Benutzer-Prozess sollte deshalb eine
Prozesspriorität von -1 benutzen.
Übung 1
Ändern Sie setitimer.c nach setitimer2.c. Bringen Sie am
Anfang von main() den setpriority() Aufruf unter. Die nötige
Include Datei ist.
#include
<sys/resource.h>
int rv =
setpriority(PRIO_PROCESS, 0, -1);
if (-1 == rv)
{
perror("setitimer2 setpriority");
}
Testen Sie das Programm mit dem Aufruf
./setitimer2
und mit dem Aufruf
sudo ./setitimer2
Was ist der Unterschied?
Übung 2
Für eine Zeitmessung im Programm erweitern Sie setitimer2.c
nach setitimer3.c. Vor der for() Schleife schreiben Sie:
struct timeval tv0;
gettimeofday(&tv0,
NULL);
Nach der for() Schleife schreiben Sie:
struct timeval tv1;
gettimeofday(&tv1,
NULL);
long long t0 = 1000000
* tv0.tv_sec + tv0.tv_usec;
long long t1 = 1000000
* tv1.tv_sec + tv1.tv_usec;
printf("\nLaufzeit =
%lld usec\n", t1 - t0);
Testen Sie wieder der Programm mit beiden Aufrufen:
./setitimer3
sudo ./setitimer3
Gibt es einen Unterschied?
Übung 3
Versuchen Sie den Test auch wenn die CPU stark belastet ist.
Für die CPU Belastung benutzen Sie in einem zweiten xterm
Fenster folgendes Kommando zur Berechnung der Fakultät von
123456 mit dem bc "Taschenrechner":
bc <fac.bc
Bemerkung: Für Computer mit 2 CPU-Cores muss zweimal bc
aufgerufen werden für volle .
Die Datei fac.bc hat den Inhalt
# fac.bc
# BC Skript zum Berechnen der
Fakultaet
define f (x)
{
if (x <= 1) return
(1); # der Sonderfall f(1) = 1
return (x *
f(x-1)); # der Normalfall rekursive
Berechnung
}
f(123456);
Ist fehlerfreie Software möglich?
Die Antwort lautet ja, unter gewissen Randbedingungen.
Softwarequalität entsteht durch Erkennen der Sonderfälle
und korrekte Behandlung der Sonderfälle. Bei den vorgestellten
Programmen sind zwei Arten von Sonderfällen nicht
berücksichtigt worden:
- write() system-call liefert Fehler
- IP fragmentation bei read()
Diese Sonderfälle sind absichtlich nicht aus-programmiert
worden. Es werden auch keine Tests für diese Sonderfälle
angegeben. Die Programme werden beim Auftreten dieser
Sonderfälle Fehler zeigen gemäß dem Sinnspruch:
Alles was getestet wird funktioniert, alles was nicht getestet wird
funktioniert nicht.
Eine zweite Quelle für versteckte Fehler sind die Brüche
im Programmiermodell. Das Betriebssystem Linux ist in C
programmiert. Die Schnittstelle zum Betriebssystem sind C
system-calls. Wird die Applikation in C programmiert gibt es keinen
Bruch zwischen Applikation-Programmiersprache und
Betriebssystem-Programmiersprache. Schon die Verwendung der string
Klasse der Programmiersprache C++ schafft einen solchen Bruch. Ein
Dateiname als Variable der Klasse string muss für einen
Betriebssystem Aufruf in einen C-String umgesetzt werden. Die
Umsetzung in die andere Richtung ist auch nötig, z.B. beim
Abfragen der Dateinamen in einem Verzeichnis über den
Betriebssystem Aufruf readdir().
Die Datenkommunikation zwischen zwei Computern geht ebenfalls mit
einem Bruch im Programmiermodell einher. Ein einfaches Beispiel
sind die htons() und ntohs() Aufrufe um die Port-Nummer von Host
Byte Order (CPU eigenen Integer Format) nach Network Byte Order
(von IP verlangtes Integer Format) umzuwandeln.
Richtig deutlich wird der Bruch im Programmiermodell bei
Lösungen wie CORBA. Hier gibt es eine
eigene Programmiersprache IDL zum Beschreiben der Datensätze.
Ein IDL Compiler erzeugt aus der Datensatzbeschreibung einen
Quelltext für die in der Applikation verwendeten
Programmiersprache damit der Applikationsprogrammierer sich nicht
mit den Details der Umwandlung von Datensätzen zwischen
interner und externer Darstellung auseinander setzen muss. Leider
muss sich der Applikationsprogrammierer mit den Details des IDL
Compilers und dem vom IDL Compiler produzierten Quelltext herum
plagen. Für manche Programmierer ist das schlimmer als das
eigentliche Problem!
Die hier vorgestellte TJSON Grammatik leidet auch unter dem Problem
des Unterschiedes der externen Darstellung der Daten durch einen
ASCII String und der sinnvollen internen Darstellung der Daten
durch eine struct oder class. Für TJSON gibt es keinen IDL
Compiler, der Programmierer muss die entsprechenden Funktionen
für die Umwandlung selbst schreiben.
Neuer als CORBA ist Web Service. Die
Idee hinter Web Service ist älter. Es werden HTTP Meldungen
zwischen Service-Nutzer-Computer und Service-Anbieter-Computer
ausgetauscht. Durch genaue Spezifikation (SOAP, SAML, Semantic Web
Services, ...) der Nutzdaten soll Web Service die
Integrationsfähigkeit von Computersystemen auf eine
höhere Entwicklungsstufe bringen.
Wenn alle nötigen Sonderfälle aus-programmiert werden und
wenn die Brüche an der Schnittstelle Applikation zu
Betriebssystem und Applikation zum Netzwerk und über das
Netzwerk zur nächsten Applikation sinnvoll behandelt werden
dann entsteht fehlerfreie Software mit noch immer einer
Einschränkung.
Die letzte Einschränkung ergibt sich aus der
Sonderfall-Behandlung. Die vorgestellten Methoden truncation
(Abschneidung) und saturation (Sättigung) sind sinnvoll. Diese
Methoden lösen aber nicht das grundlegende Problem: Die im
Programm vorgesehene Speichergröße reicht für das
Datum nicht aus. Eine Programmiersprache mit dynamischen
Variablen-Größen ist nur scheinbar die Lösung.
Irgendwann ist der komplette Arbeitsspeicher aufgebraucht und dann
hat auch eine solche Applikation Probleme.
Zum Abschluss bleibt nur folgender Rat: Die verwendete
Programmiersprache ist zweitrangig. Wichtiger ist eine robuste
Programmierung im Kleinen wie im Großen. Die relevanten
Sonderfälle müssen während dem System Design erkannt
werden und müssen konsequent aus-programmiert werden. Für
die Wartbarkeit des Systems ist die Untergliederung der Applikation
in sinnvolle Software-Schichten (software layers,
Abstraktion-Ebenen, Module, Klassen) sehr wichtig, vielleicht die
wichtigste Entscheidung überhaupt die während dem System
Design getroffen wird.
Für die Programmierung in C++ ist
RAII (Resourcenbelegung ist
Initialisierung) eine sehr wichtige Programmiertechnik.
Was die theoretische Informatik noch nicht
weiss
Wenn die theoretische Informatik für ein Problem keine
Lösung kennt, dann kann ein System für dieses Problem
keine fehlerfreie Software-Lösung liefern. Dieser Zusammenhang
zwischen Theorie und Praxis sollte einleuchten. Leider bemerkt Tony
Hoare in seiner Turing
Award Rede 1980 treffend: "Almost anything in software can be
implemented, sold, and even used given enough determination. There
is nothing a mere scientist can say that will stand against the
flood of a hundred million dollars."
Das Problem der letzten Meldung
Im TCP state diagram ist die ACK Meldung die letzte Meldung beim
TCP Verbindungs-Abbau. Sie bringt den ACK empfangenden Computer vom
Zustand LAST_ACK in den Zustand CLOSED. Fehlt diese letzte Meldung
aufgrund eines kurzzeitigen Netzwerk-Ausfalles, so hat der eine
Computer den TCP Verbindungs-Abbau komplett durchgeführt, der
andere Computer aber nicht.
Das Problem der letzten Meldung wird üblicherweise mit einem
Time-out entschärft. Der system-call setsockopt() mit dem
Parameter SO_REUSEADDR ändert das Time-out Verhalten des
TCP-Stacks.
Das Split-Network Problem
Im Fault
Tolerant CORBA Dokument 01-09-29 der OMG.org wird in aller
Einfachheit gesagt: "Network partitioning faults separate the hosts
of the system into two or more sets, the hosts of each set being
able to operate and to communicate within that set but not with
hosts of different sets. The current state-of-the-art does not
provide an adequate solution to network partitioning faults."
Natürlich leidet nicht nur Fault Tolerant CORBA unter dem
Split-Network Problem. Die Lösung ist üblicherweise ein
Eingriff durch das Wartungspersonal. Die Applikationen in einem der
beiden Split-Networks werden komplett gestoppt. Dann werden die
beiden Split-Networks wieder vereint. Im letzten Schritt werden die
gestoppten Applikationen neu gestartet.
Historie von C
Im Jahr 1978 erschien das Buch "The C Programming Language" von
Brian W. Kernighan und Dennis M. Ritchie. Das Betriebssystem UNIX
und die Programmiersprache C sind eigentlich ungewollte
Entwicklungen die 1970 begonnen haben. Eigentlich wollte Bell Labs
(AT&T) das Betriebssystem Multics mit der Programmiersprache
PL/1 einsetzen. Aber 1969 ist Bell Labs aus der Multics Entwicklung
ausgestiegen. Multics und PL/1 sind Riesen, UNIX und C sind Zwerge.
Heute leben die Zwerge noch prächtig und die Riesen sind schon
lange tot und vergessen.
Im Jahr 1983 erschien das Berkeley
sockets application programming interface (socket-API) als Teil
von 4.2BSD UNIX. Damit hat TCP/IP und das weltweite Internet seinen
Siegeszug begonnen.
Im Jahr 1985 erschien "The C++ Programming Language" von Bjarne
Stroustrup. Mit C++ wurde aus dem Zwerg C ein Riese. Wie bei
anderen Riesen-Programmiersprachen wie Algol 68 oder PL/1 kann der
Programmierer in der Komplexität der Programmiersprache C++
grandios untergehen.
Im Jahr 1989 erschien der ANSI-C Standard. Dadurch wurde der
Sprachumfang von C und der C-Bibliothek genauer definiert und die
Funktionsprototypen offiziell in den C Sprachumfang
aufgenommen.
Im Jahr 2008 ist C ungefähr 35 Jahre alt. Heute ist C out und
Programmiersprachen wie Java sind in. Leider machen die Java
Jünger die gleiche Erfahrung wie die Ada, PL/1 oder Algol
Programmierer vor ihnen: In jeder Programmiersprache kann man
fehlerhafte Programme schreiben. Ein Array Range Check und ein
Garbage Collector fangen vielleicht einige Fehler, aber nicht alle.
Deshalb geht die Suche nach der perfekten Programmiersprache
weiter. Bis jetzt hat nur die nutzlose Programmiersprache SIMPLE
dieses Ziel erreicht.
C war für seine Zeit und ist auch heute noch ein wichtiger
Meilenstein auf dieser Suche nach dem heiligen Gral der
Programmiersprachen.
Danksagung
Die Danksagung geht an die Personen welche die Werkzeuge geschaffen
haben mit denen dieses Dokument erzeugt wurde:
- gcc Compiler von Richard
Stallman und allen anderen Leuten der FSF
- Linux Betriebsystem von Linus Torvalds
und allen anderen Linux Kernel Leuten
- Openoffice Textverarbeitung von Marco
Börries und allen anderen Leuten von
Staroffice/Openoffice
- Seamonkey Composer von Marc Andreessen
und allen anderen Leuten von Mosaic, Netscape, Mozilla
- und alle anderen die bis jetzt noch nicht erwähnt wurden
für ihre Werkzeuge (man, nedit, Coco/R, indent, tidy,
valgrind, strace, lsof ...)
Weiterführende Literatur
TCP Programmierung in C:
W. Richard Stevens, Programmieren von UNIX-Netzwerken, 2.Auflage,
2000, Carl Hanser Verlag
Jürgen Wolf, Linux-UNIX-Programmierung, 2.Auflage, 2005,
Galileo Computing
C Programmiersprache:
Jürgen Wolf, C von A bis Z, 2. Auflage, 2006, Galileo
Computing
Kernighan, Ritchie, Programmieren in C, 2. Ausgabe, 1990,
Prentice-Hall
C++ Programmiersprache:
Jürgen Wolf, C++ von A bis Z, 2006, Galileo Computing
Bjarne Stroustrup, Die C++ Programmiersprache, 2000, Addison
Wesley
Die Bücher von Jürgen Wolf sind als Open-Books im
Internet lesbar bei
http://www.pronix.de
http://www.galileocomputing.de/openbook/c_von_a_bis_z
Über der Autor
Prozessdatenverarbeitung unter C hat für den Autor begonnen
mit dem Lattice C
Compiler. Das ist nun schon einige Jahre her. Heute sind gcc
und g++ unter Linux die Werkzeuge. Nach 20 Jahren PDV unter C ist
der Autor fest entschlossen die nächsten 20 Jahre weiterhin
PDV unter C zu machen. Natürlich sind die Systeme von 2007
anders als die Systeme von 1987 und das C heute ist ein C++. Aber
das Message
Sequence Chart und das State Diagram sind immer noch die besten
Werkzeuge des System Designers. "Nichts ist praktischer als eine
gute Theorie" sagte der Professor und ging.