![]() |
Internet-TechnologieProf. Jürgen Plate |
TCP/IP wird nicht nur verwendet, um mit anderen Rechnern Kontakt aufzunehmen. Bei Systemen, die nicht über einen Netzwerkanschluß vewrfügen, wird eine Schnittstelle gebraucht, die eine Netzwerkschnittselle emuliert. Dies ist das loopback-Interface, weil es wie eine Schleife auf den Rechner selbst zurückführt. Notwendig ist das loopback-Interface, weil verschiedene Dienste (X-Window, Drucksysteme etc.) netwerkorientiert arbeiten. Für die Progammierung ist es ideal, weil Client und Server auf demselben Rechner laufen können.
In diesem Skript wird gleich mit der Programmierung losgelegt. Grundlagen über TCP/IP, Nameserver usw. finden Sie im Netzwerk-Skript.
Die Socket-Schnittstelle ist damit die Grundlage der Programmierung verteilter Anwendungen unter TCP/IP. Ein Socket ("Steckdose") ist ein also Verbindungsendpunkt, der vom Programm wie eine gewöhnliche Datei beschrieben und gelesen werden kann. Anfang der 80er Jahre wurde mit 4.2 BSD in UNIX-Systemen die sogenannte "Socket"-Schnittstelle, das "Socket Application Programming Interface" (Socket API) für die Kommunikation zwischen Prozessen eingeführt. Es basiert nach wie vor auf dem Berkley Socket API, das für die Programmiersprache C entwickelt wurde. Ein "Socket" ist dabei der Name für einen Endpunkt einer Kommunikationsverbindung. Seine Schnittstelle ist im wesentlichen konzipiert für:
In der Berkley Socket API wird der Socket als Datei aufgefasst. Er wird somit im Betriebssystem über einen Filediscriptor identifiziert und auf ähnliche Weise wie reguläre Dateien genutzt. Neben den grundlegenden Funktionen für Lesen und Scheiben, dem Empfangen und Senden von Daten, stellt der Socket die Funktionalitäten der im Rechner verfügbaren Transportschichten bereit. Nach der Initialisierungsphase, in der Adressen, Protokolle und Optionen festgelegt werden kann die Kommunikation sogar über die klassischen IO-Funktionen read() und write() erfolgen. Die von einem Benutzerprogramm in einen Socket geschriebenen Daten werden vom Betriebssystem mit Headerdaten versehen und über das Netz gesendet. Analog werden über das Netz empfangene Daten vom Betriebssystem ohne Headerdaten im zugehörigen Socket abgelegt. Von dort können sie vom Benutzerprogramm ausgelesen werden. Folglich operiert ein Benutzerprogramm stets nur auf den Nutzdaten (Payload) der Netzwerkkommunikation.
Die Socket-Schnittstelle ist zwar von keiner Institution genormt, stellt aber den Industriestandard dar. Wichtige Gründe sind u.a.:
Um mit einem bestimmten Dienst (Programm) auf einem anderen Rechner zu kommunizieren, reicht es allerdings nicht, einfach den anderen Rechner als solchen anzusprechen. Es ist vielmehr jedes Server-Programm auf einer bestimmten Port-Nummer zu erreichen. Jeder Client muß dem entfernten Rechner diese Port-Nummer mitteilen, damit dieser die Anfrage dem richtigen Programm zuleiten kann.
Die Netz-Ein- und Ausgabe wurde an die Datei-Ein- und Ausgabe angelehnt und etliche Ein- und Ausgabe-Systemaufrufe lassen sich auf Dateien und Sockets anwenden. Es gibt jedoch einige Unterschiede:
Über Sockets kann der Datenaustausch auf zweierlei Art erfolgen:
Sockets sind noch über verschiedenen "Domänen" definiert: Es gibt neben der "Internet-Domäne" noch weitere Domänen, z. B. die "Unix-Domäne" für die Kommunikation zwischen reinen Unix-Prozessen. So kennen die meisten Systeme:
/* Supported address families. */ #define AF_UNIX 1 /* Unix domain sockets */ #define AF_LOCAL 1 /* POSIX name for AF_UNIX */ #define AF_INET 2 /* Internet IP Protocol */ #define AF_IPX 4 /* Novell IPX */ #define AF_APPLETALK 5 /* AppleTalk DDP */ #define AF_INET6 10 /* IP version 6 */ #define AF_IRDA 23 /* IRDA sockets */ #define AF_BLUETOOTH 31 /* Bluetooth sockets */Thema dieses Skripts ist aber ausschließlich die Internet-Domäne. Zur Übergabe der Parameter einer Socketverbindung dienen zwei Strukturen, sockaddr und für die Internet-Protokolle sockaddr_in. Erstere ist so konzipiert, dass sie unabhängig von der verwendeten Adressfamilie ist:
struct sockaddr { sa_family_t sa_family; /* address family, AF_xxx */ char sa_data[14]; /* 14 bytes of protocol address */ };Für Intermet-Anwendungen existiert mit sockaddr_in eine spezielle Struktur, die es erlaubt, IP-Adresse und Portnummer getrennt einzutragen. Im Speicher sind diese beiden Strukturen kompatibel, es reicht also eine einfache Typumwandlung, um die gewünschten Informationen zu übergeben:
struct sockaddr_in { sa_family_t sin_family; /* Address family */ unsigned short int sin_port; /* Port number */ struct in_addr sin_addr; /* Internet address */ unsigned char pad[8]; /* Pad to size of `struct sockaddr'. */ };Alle Zahlenwerte müssen in Network Byteorder (Big-Endian) vorliegen. Ein Beispiel für eine Zuweisung an eine Variable my_addr vom Typ struct sockaddr_in könnte lauten:
my_addr.sin_family = AF_INET; /* Umwandlung in Network Byteorder mit htons() */ my_addr.sin_port = htons(25); /* Umwandeln in 32-Bit-Zahl in Network-Byteorder */ my_addr.sin_addr.s_addr = inet_addr("10.27.210.232");
Die folgende Tabelle gibt einen Überblick über die wichtigsten Socket-Systemcalls in Client- und Serverprogrammen:
Phase | Client | Server |
---|---|---|
Endpunkt erzeugen | socket() | socket() |
Binden einer Adresse | bind() | bind() |
Verbindung aufbauen | connect() | |
Warteschlange festlegen | listen() | |
Warten auf Verbindung | accept() | |
Daten senden | write() send() sendto() sendmsg() |
write() send() sendto() sendmsg() |
Daten empfangen | read() recv() recvfrom() recvmsg() |
read() recv() recvfrom() recvmsg() |
Verbindung schließen | shutdown() | shutdown() |
Endpunkt abbauen | close() | close() |
Ereignisse annehmen | select() | select() |
Verschiedenes | getpeername() getsockname() getsockopt() setsockopt() |
getpeername() getsockname() getsockopt() setsockopt() |
Für die Kommunikation bei verbindungslosen, d.h. UDP-basierten Socketanwendungen sind die speziellen send()- und receive()-Systemcalls empfehlenswert, während bei TCP-Verbindugen daneben die Standard-Systemcalls read() und write() einsetzbar sind.
Eine TCP/IP-Verbindung ist, wie wir gesehen haben, durch eine Client-Server-Architektur geprägt und damit asymmetrisch. Vor der Kommunikation muß die Verbindung stehen. Das betrifft einmal die Verbindung zwischen den Rechnern, als auch jene zwischen den Prozessen. Die Adressierung der Rechner erfolgt per Hostname, der vom System auf die IP-Nummer umgesetzt wird.
Vom Client aus muß nicht nur der richtige Rechner, sondern auch der richtige Serverprozeß angesprochen werden können. Dazu bindet sich der Serverprozeß an einen festen Port, über den er erreichbar ist. Damit die Nummer des Ports mit einem Namen versehen werden kann, verwendet man die Datei /etc/services. Im Programm wird die Servicenummer durch den Aufruf der Systemfunktion getservbyname() bestimmt.
Für bekannte Dienste werden bestimmte Portnummern von der IANA (Internet Assigned Number Authority) festgelegt. Portnummern sind in drei Kategorien eingeteilt:
Der Client braucht normalerweise keinen festen Port. Er erbittet sich auf der lokalen Maschine eine freie Nummer und ruft damit den Port des Servers. Der Server erfährt die Nummer des Clients aus der Anfrage und kann ihm unter diesem Port antworten. Das Szenario zwischen Server und Client sieht wie folgt aus:
Betrachten wir beispielhaft einmal das Listing eines ganz einfachen Servers in der Programmiersprache C. Die einzelnen Systemaufrufe werden weiter unten genauer behandelt, das Listing soll zunächst nur einen Überblick des Ablaufs geben:
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <netdb.h> #define MAXPUF 1023 main() { int MySocket, ForeignSocket; struct sockaddr_in AdrMySock, AdrPartnerSocket; struct servent *Service; int AdrLen; char Puffer[MAXPUF]; int MsgLen; /* Socket einrichten */ MySocket = socket(AF_INET, SOCK_STREAM, 0); /* Socket an Port-Nummer binden */ memset(&AdrMySock, 0, sizeof (AdrMySock)); AdrMySock.sin_family = AF_INET; /* Internet-Protokolle */ AdrMySock.sin_addr.s_addr = INADDR_ANY; /* akzept. jeden Client-Host */ Service = getservbyname("echo","tcp"); /* bestimme Port */ AdrMySock.sin_port = Service->s_port; /* (Get Service by Name) */ bind(MySocket, &AdrMySock, sizeof(AdrMySock)); /* Empfangsbereitschaft signalisieren und warten */ listen(MySocket, 5); for (;;) /* forever */ { /* Verbindungswunsch vom Client annehmen */ ForeignSocket = accept(MySocket, &AdrPartnerSocket, &AdrLen); /* Datenaustausch zwischen Server und Client */ MsgLen = recv(ForeignSocket, Puffer, MAXPUF, 0); /* String empfangen */ send(ForeignSocket, Puffer, MsgLen, 0); /* und zuruecksenden */ /* Verbindung beenden und wieder auf Client warten */ close(ForeignSocket); } }Dieser Server bearbeitet jede Anfrage, die über den Port "echo" an ihn gestellt wird. Nach jeder Anfrage wird die Verbindung wieder gelöst und ein anderer Client kann anfragen. Ein solcher Server dürfte auf jedem Betriebssystem arbeiten können, das TCP/IP unterstützt, selbst wenn es kein Multitasking beherrscht.
Es gibt zwei Variablen pro Socket. Die eine ist wie bei Dateizugriffen ein einfaches Handle (MySocket), die andere hält die Adresse der Verbindung (AdrSock), also die IP-Nummer des Rechners und die Nummer des verwendeten Ports. Der Server erlaubt Verbindungen von jedem Rechner aus, weil die Konstante INADDR_ANY benutzt wird.
Der zugehörige Client gibt dagegen die Adresse des anzusprechenden Servers an. Das Programm sieht wie folgt aus:
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netinet/tcp.h> #include <netdb.h> #define MAXPUF 1023 main() { int MySocket; /* Socket-Handle */ struct sockaddr_in AdrSock; /* Socketstruktur */ int len; /* Die Laenge der Socketstruktur */ struct hostent *RechnerID; /* ferner Rechner */ struct servent *Service; /* Dienst auf dem fernen Rechner */ char Puffer[MAXPUF] = "Wir erschrecken zu guten Zwecken!"; MySocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); memset(&AdrSock, 0, sizeof (AdrSock)); /* Bestimme den Zielrechner */ RechnerID = gethostbyname("server"); bcopy(RechnerID->h_addr,&AdrSock.sin_addr.s_addr,RechnerID->h_length); /* Bestimme den Port */ Service = getservbyname("echo","tcp"); AdrSock.sin_port = Service->s_port; connect(MySocket, (struct sockaddr *)&AdrSock, sizeof(AdrSock)); send(MySocket, Puffer, MAXPUF, 0); /* String senden */ recv(MySocket, Puffer, MAXPUF, 0); /* und wieder empfangen */ printf("%s\n", Puffer); /* ausgeben */ close(MySocket); }Die recv-Funktion liefert als Rückgabewert die Größe des versandten Speicherbereichs (max. 1 KByte, siehe unten).
Grundsätzlich liefern fast alle Netz-Funktionen bei Fehlern den Wert 0 zurück. In den obigen Beispiel-Listing fehlt jegliche Fehlerbehandlung, damit das Prinzip übersichtlich dargestellt werden kann. Im "richtigen" Programm ist eine umfassende Fehlerbehandlung unumgänglich.
Das Server-Programm hat noch einen Nachteil. Nach dem Start des Servers ist die Konsole oder das Shell-Fenster für weitere Zwecke blockiert. Auch fehlt eine korrekte Möglichkeit, den Server zu beenden. Bei einem Abbruch des Programms wird es dem Betriebssystem überlassen, den Socket zu schließen. Zweckmässigerweise wird vom Programm ein Dämon erzeugt, der per fork-Aufruf in den Hintergrund gestellt.
Server und Client können auch unterschiedliche Prozessor-Architekturen haben. Die Speicherung von Zahlen können als Big-Endian oder als Little-Endian erfolgen. Um aus einer lokal verwendeten Byte-Reihenfolge (Host Byte Order) eine Network-Byte-Order-Reihenfolge oder umgekehrt zu erstellen, stehen die vier Funktionen htonl(), htons(), ntohl() und ntohs() zur Verfügung (siehe auch Zahlenformat: ntoh und hton).
Grundlage dieses Themenkreises bildet die
Einführung der C-Systemaufrufe zur Behandlung von Prozessen.
Dort können Sie alles genau nachlesen. Hier soll nur ein Beispiel für die
Programmierung eines Signal-Handlers für die Taste [Strg]-[C] und die
Alarm-Funktion gezeigt werden:
Für die Anwendung bei Timeouts würde man beispielsweise beim Empfang eines
Blocks jedesmal den Wecker für einen geeigneten Zeitraum aufziehen. Der
Signalhandler wird nicht aktiviert, solange alles wunschgemäß läuft.
Tritt ein längerer Timeout auf, sorgt der Signalhandler dann für ein
geordnetes Ende des Programms.
1.2 Behandlung von Signalen und Timeouts
Mitunter kommt es vor, dass eine Netzverbindund unterbrochen wird. In solchen Fällen
stellt sich die Frage, wie das Programm verfahren soll. Wie lange soll auf Daten
von der Gegenstation gewartet werden? Wie oft soll ein Verbindungsaufbau wiederholt
werden, wenn der Kontakt nicht zustande kommt? Wie kann mit anderen Prozessen
kommuniziert werden? Und so weiter?
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
void propeller(void)
{
/* Aktivitaetsanzeige in Spalte 1 der aktuellen Zeile */
static char sign[] = {'|','/','-','\\'};
static int pos = 0;
putchar('\r');
putchar(sign[pos]);
fflush(stdout); /* sonst sieht man nix */
pos = (pos + 1) % 4;
}
void tick (int dummy) /* den Wecker wieder aufziehen */
{ alarm(1); }
void beenden(int dummy) /* STRG-C behandeln */
{
printf("\nHasta la vista, baby!\n");
exit(0);
}
int main(void)
{
/* Signalhandler aktivieren */
signal(SIGINT,beenden);
signal(SIGALRM,tick);
alarm(1); /* Wecker aufziehen */
for (;;)
{
pause(); /* auf Signal warten */
propeller(); /* Anzeigen, dass sich was tut */
};
return(0); /* never reached */
}
So kann z. B. beim Drücken von [Strg]-[C] das Programm alle Dateien
und Sockets ordentlich schließen bevor es beendet wird.
int inet_aton(const char *ptr, struct in_addr *inp);
Hiermit wird die IP-Adressen-Zeichenkette ptr eine 32-Bit-Adresse konvertiert. Der Wert dieser 32-Bit-Adresse befindet sich anschließend in in_addr der Strukture struct sockaddr_in. Zur Erinnerung hier nochmals die Strukturen:
struct sockaddr_in { /* Adressfamilie normalerweise AF_INET */ short int sin_family; /* Port der Verbindung */ unsigned short int sin_port; /* Adresse, zu der verbunden werden soll */ struct in_addr sin_addr; /* Fuelldaten, um auf 14 Bytes zu kommen */ unsigned char sin_zero[8]; }; struct in_addr { unsigned long int sin_addr; }in_addr ist in der Headerdatei <netinet/in.h> definiert.
Bei einem Fehler gibt die Funktion inet_aton() den Wert 0, bei Erfolg einen Wert ungleich 0 zurück.
Für die Zukunft wichtig ist die Funktion inet_pton(), die im Gegensatz zu inet_aton() nicht nur IPv4-Adressen umwandelt:
int inet_pton(int af, const char *src, void *dst);
Für af wird die Adressfamilie (ähnlich wie bei socket()) angegeben, src enthält die Adresse als String und dst ist ein Zeiger auf die Zielstruktur, die je nach Adressfamilie variiert. Zum Beispiel:
struct in_addr addr; ... inet_pton(AF_INET, "127.0.0.1", &addr);
Soll dagegen aus einer 32-Bit-Darstellung der IP-Adresse wieder ein "dotted quad" String entstehen, steht die Funktion inet_ntoa() zur Verfügung:
char *inet_ntoa(struct in_addr ip);
Damit wird die übergebene 32-Bit-Adresse ip, die als Network-Byteorder vorliegen muss, in einen String konvertiert. Der String wird als Rückgabewert der Funktionen in einem statischen Puffer abgelegt. Bei einem Fehler wird NULL zurückgegeben.
Die Alternative ist das Gegenstück zu inet_pton(), inet_ntop():
const char *inet_ntop( int af, const void *src, char *dst, socklen_t cnt );
af gibt wieder die Adressfamilie an, src enthält die 32-Bit-Darstellung der IP-Adresse und dst ist ein Zeiger auf einen String, in dem der konvertierte Wert von cnt Bytes länge kopiert wird. Zum Beispiel:
char buf[16]; ... inet_ntop(AF_INET, &addr, buf, 16);
Das folgende Beispiel zeigt die Anwendung von inet_aton() und inet_ntoa():
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <netdb.h> int main (int argc, char **argv) { char *ip_addr; struct in_addr ip; char *ip_str; if (argc != 2) { printf ("Usage: %s IP-Adresse\n", argv[0]); return -1; } ip_addr = argv[1]; if (inet_aton (ip_addr, &ip) == 0) return -1; printf ("IP-Adresse (argv[1]) : %s\n", ip_addr); printf ("IP-Adresse als 32-Bit Wert: %08X\n", ip.s_addr); ip_str = inet_ntoa (ip); if (ip_str == NULL) return -1; printf ("IP-Adresse als String : %s\n", ip_str); return 0; }
Um aus einer IP-Adresse in Form eines Strings die Netzwerkadresse zu extrahieren, dient die Funktion inet_network():
in_addr_t inet_network(const char * ptr);
Bei der Netzadresse wird bekanntlich der Hostanteil zu 0. Bei Erfolg gibt die Funktion die Netzadresse in Host-Byteorder zurück. Bei einem Fehler wird -1 zurückgegeben.
Um aus einer nummerischen 32-Bit-IP-Adresse eine Netzadresse zu erhalten, wird die Funktion inet_netof() verwendet:
in_addr_t inet_netof(struct in_addr in);
Aus der nummerischen 32-Bit-Darstellung der IP-Adresse, die als Parameter angegeben wird, wird die Netzadresse extrahiert. Diese Adresse wird bei Erfolg als Rückgabewert von inet_netof() in Host-Byteorder zurückgegeben. Bei einem Fehler wird -1 zurückgegeben.
Um den Host-Anteil einer nummerischen 32-Bit-IP-Adresse zu ermitteln, steht die Funktion inet_lnaof() zur Verfügung:
in_addr_t inet_lnaof(struct in_addr in);
Damit ermitteln Sie die Adresse des Host-Anteils (z. B. ist bei 192.168.0.23 der Host-Anteil 0.0.0.23). Die Funktion gibt diesen Wert in Form eines 32-Bit-Wertes in Host-Byteorder zurück. Bei Fehler wird -1 zurückgegeben.
Mit der Funktion inet_makeaddr() wird aus der Host- und Netzadresse eine IP-Adresse zusammengesetzt.
struct in_addr inet_makeaddr(int net, int host);
Damit wird aus der Netzadresse net und der Host-Adresse host eine vollständige 32-Bit-IP-Adresse erzeugt und zurückgeliefert. Die Werte net und host müssen in Host-Byteorder übergeben werden.
Normalerweise sind der gewünschte Dienst und der Name des Hosts bekannt, der bezüglich des Dienstes angesprochen werden soll. Daher zuerst ein Blick auf den Host.
#include <netdb.h> ... struct hostent *gethostbyname (char *hostname); ...Die gethostbyname-Funktion gibt einen Zeiger auf eine hostent-Struktur zurück:
struct hostent { char *h_name; /* official name of host */ char **h_aliases; /* alias list */ int h_addrtype; /* host address type */ int h_length; /* length of address */ char **h_addr_list; /* a NULL terminates the list */ }; #define h_addr h_addr_list[0]; /* first address in list */Gegenwärtig enthält das Feld h_addrtype immer den Wert A_INET und analog das Feld h_length immer den Wert 4 (ist gleich der Länge der Internet-Adressse). Bei Internet-Adressen besteht die Matrix der Zeiger h_addr_list[0], h_addr_list [1], ... nicht aus Zeigern auf Zeichen, sondern aus Zeigern auf Strukturen vom Typ in_addr. Die hostent-Struktur ist sehr allgemein gehalten, wobei momentan vieles davon noch nicht verwendet wird.
/* Print the "hostent" information for every host whose name is * specified on the command line. (nach Stevens) */ #include <stdio.h> #include <sys/types.h> #include <netdb.h> /* for struct hostent */ #include <sys/socket.h> /* for AF-INET */ #include <netinet/in.h> /* for struct in_addr */ #include <arpa/inet.h> /* for inet_ntoa() */ void pr_inet(char **listptr, int length); int main(int argc, char **argv) { char *ptr; struct hostent *hostptr; while (--argc > 0) { ptr = *++argv; if ((hostptr = gethostbyname(ptr)) == NULL) { printf("gethostbyname error for host %s\n",ptr); continue; } printf ("official host name: %s\n", hostptr->h_name); /* go through the list of aliases */ while ((ptr = *(hostptr->h_aliases)) != NULL) { printf(" alias: %s\n", ptr); hostptr->h_aliases++; } printf(" addr type = %d, addr length = %d\n", hostptr->h_addrtype, hostptr->h_length); switch (hostptr->h_addrtype) { case AF_INET: pr_inet(hostptr->h_addr_list, hostptr->h_length); break; default: printf("unknown address type\n"); break; } } return 0; } void pr_inet(char **listptr, int length) /* Go through a list of internet addresses, printing each one in dotted-decimal notation. */ { struct in_addr *ptr; while ( (ptr = (struct in_addr *) *listptr++) != NULL) printf (" Internet address: %s\n", inet_ntoa(*ptr)); }Es gibt auch den Fall, daß ein Server die Internet-Adresse des Clients weiß, aber dessen Namen wissen möchte. Die Funktion gethostbyaddr erledigt in diesem Fall die Konvertierung von Adresse zu Namen:
#include <netdb.h> ... struct hostent *gethostbyaddr (char *Addr, int Len, int Type); ...Der Addr-Parameter ist ein Zeiger auf eine sockaddr_in-Struktur, welche die Internet-Adresse enthält. Len ist die Größe dieser Struktur. Type muß mit AF_INET angegeben werden. Ähnlich wie bei der gethostbyname-Funktion gibt es auch hier viel Allgemeingültiges, von dem jedoch nicht viel verwendet wird.
Den eigenen Hostnamen erhät man mit der Funktion gethostname(), die zwei Parameter besitzt:
int gethostname(char *hostname, int len);Der Parameter hostname nimmt den nullterminierten Hostnamen auf. len spezifiziert dabei die maximale Länge des char-Arrays. Ist der Hostname länger, wird er auf len Zeichen gekappt. In diesem Fall kann es sein, dass kein Nullbyte als Terminierung vorhanden ist. Kann der Hostname nicht ermittelt werden, gibt die Funktion -1 zurück, sonst 0.
Um die eigene IP-Adresse zu ermitteln, kann man hostname() und gethostbyname() kombinieren:
#define INTERFACE eth0 ... char *GetLocalIP(char IPaddress[]) { /* Mike Niedermayr */ int sock_fd; struct ifreq ifr; sock_fd = socket(AF_INET, SOCK_DGRAM, 0); if (sock_fd == -1) { sprintf(IPaddress, "unknown"); return IPaddress; } else { strcpy(ifr.ifr_name, "INTERFACE"); if (ioctl(sock_fd, SIOCGIFADDR, &ifr) == -1) { sprintf(IPaddress, "unknown"); } else { sprintf(IPaddress, "%s", inet_ntoa(((struct sockaddr_in *) (&ifr.ifr_addr))->sin_addr)); } close(sock_fd); return IPaddress; } }
Die Funktion getservbyname sucht nach einem Dienst - letztendlich nach einem Port:
#include <netdb.h> ... struct servent *getservbyname(char *Servicename, char *Protname); ...Diese Funktion gibt einen Zeiger auf folgende Struktur zurück:
struct servent { char *s_name; /* official service name */ char **s_aliases; /* alias list */ int s_port; /* port number, network byte order */ char *s_proto; /* protocol to use */ }Die Information für diese Funktion wird der Datei /etc/services entnommen. In dieser Datei wird eine Suche nach dem geforderten Service (Servicename) gestartet. Ist auch ein Protokoll angegeben (d. h. Protname != NULL), dann muß der entsprechende Eintrag für dieses Protokoll in der Datei vorliegen. Es gibt einige Internet-Dienste, die entweder von TCP oder UDP unterstützt werden (z. B. der Echodienst), und andere, die nur ein Protokoll unterstützen (FTP erfordert beispielsweise TCP). Das Hauptaugenmerk innerhalb der servent-Struktur liegt auf der Internet-Portnummer. Zu beachten ist, daß diese Struktur Integer-Portnummern handhaben kann, sogar Intenet-Portnummern in 16 bit-Größe. Beispiel:
struct hostent *RechnerID; struct servent *Service; ... RechnerID = gethostbyname("server"); /* Bestimme den Rechner */ Service = getservbyname("echo","tcp"); /* Bestimme den Port */ ...Das wichtigste Element der servent-Struktur ist das Feld s_port. es enthält die Nummer des Ports, wie sie von der Funktion connect verwendet wird.
Nichtprivilegierte Programme (d. h. Programme ohne Root-Rechte) dürfen keine Server-Sockets auf Ports kleiner 1024 öffnen. So wird ein minimaler Schutz davor gewährleistet, daß irgend welche Programme normaler Anwender Ports kidnappen oder auf Ports eigene Services hochfahren, die die Maschine normalerweise nicht bieten würde. Andererseits ist es aus Sicherheitsaspekten nicht sinnvoll, wenn alle Serverprozesse mit root-Privilegien laufen. Die Lösung des Problems ist einfach: sobald man die Server-Sockets gebunden hat, kann man mit setreuid(2) die Sonderprivilegien gegen "normale" Userprivilegien tauschen. Alternativ kann man Beispielsweise sicherheitsrelevante setuid-Programme in einem chroot(2)-Gefängnis ablaufen lassen, oder das Programm in zwei Prozesse aufteilen, so daß nicht alles mit root-Rechten laufen muß.
Wenn man kurz nachdem ein Programm eine Server-Socket geschlossen hat versucht, einen neuen Socket an denselben Port wie den alten Server-Socket zu binden, erhält man einen "Address already in use"-Fehler. Der Grund dafür ist, daß möglicherweise im Netz noch Pakete herumgeistern, die für den alten Socket bestimmt sind und es deshalb sinnvoll ist, erst einmal zu warten, bis sich das Netz beruhigt hat. Wenn man eine Socket sofort an einen Port binden will, verwendet man die "Reuse"-Option.
1.5 Programmbeispiele
Jetzt sind alle Werkzeuge für das Programmieren von TCP-Servern und -Clients vorhanden.
Die folgenden Beispiele verzichten teilweise auf Fehlerbehandlung, damit der eigentliche
Programmfluss deutlicher zutage tritt. Für produktive Anwendungen sind sie ohne Fehlerbehandlung
etc. nicht geeignet. Alle Beispielprogramme können Sie auch direkt als
C-Quelldateien herunterladen.
Für viele Versuche steht per Default bei jedem Linux-System ein Programm zur Verfügung, der gute, alte Telnet-Client. Als ersten Parameter wird der Rechnername (z. B. localhost) und als zweiter Parameter der Port angegeben. Aber man kann natürlich seinen speziellen Client selbst programmieren.
Zum Übersetzen unter Linux genügt folgende Programmzeile:
gcc -Wall -o <Binärdatei> <Quelldatei.c>Zu beachten ist noch, dass die Reihenfolge bei den Include-Anweisungen eine Rolle spielt, so muss z. B. #include <sys/types.h> - sofern verwendet - vor #include <sys/socket.h> stehen.
#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 <netdb.h> #include <arpa/inet.h> /* Port fuer die Requests */ #define PORT 7777 int main(void) { struct sockaddr_in my_addr; struct sockaddr_in remote_addr; int size; int s; int remote_s; /* Die Socket erzeugen */ s = socket(AF_INET, SOCK_STREAM, 0); if (s < 0) { fprintf(stderr, "Error: Socket\n"); return -1; } memset(&my_addr, 0, sizeof (my_addr)); my_addr.sin_family = AF_INET; my_addr.sin_port = htons(PORT); my_addr.sin_addr.s_addr = INADDR_ANY; /* An jedem Device warten */ if (bind(s, (struct sockaddr *)&my_addr, sizeof(my_addr))==-1) { fprintf(stderr, "Error: bind\n"); return -1; } /* Warteschlange einrichten */ if (listen(s, 1) == -1) { fprintf(stderr, "Error: listen\n"); return -1; } size = sizeof(remote_addr); /* Auf eine eingehende Verbindung warten */ remote_s = accept(s, (struct sockaddr *)&remote_addr, (socklen_t*)&size); if (remote_s < 0) { fprintf(stderr, "Error: accept\n"); return -1; } /* Infos ausgeben */ printf("\nConnect von: %s\n", inet_ntoa(remote_addr.sin_addr)); printf("sende Daten...\n"); size = send(remote_s, "Hello World",11,0); if (size == -1) { fprintf(stderr, "error while sending\n"); } else { printf("%d Bytes sent\n", size); } printf("closing sockets\n"); /* Sockets wieder freigeben */ close(remote_s); close(s); printf("terminating\n"); return 0; }Für die ersten Versuche ist das ganz nett, aber nach jeder Kommunikation muss der Server neu gestartet werden - nicht so toll. Zudem wird der Port nicht vom Kernel nicht sofort frei gegeben, sondern erst nach etwa einer halben Minute. Aber immerhin wissen wir nun, dass das Konzept funktioniert. Statt fprintf(stderr, "...") hätte ich übrigens auch perror("...") nehmen können.
Da der Server nun mehrere Anfragen bedienen soll, gibt es nach dem Vorspann eine Endlosschleife, innerhalb der dann accept() und do_dialog aufgerufen werden. Ist die Kommunikation mit dem Client beendet, wird lediglich der mit accept() kreierte Client-Socket (newsockfd) geschlossen. Der Server-Socket (sockfd) bleibt bestehen und so kann der Server die nächste Anfrage bearbeiten. In der Kommunikationsroutine darf diesmal auch der Client Daten senden, die der Server am Bildschirm ausgibt und seinerseits mit "OK" beantwortet - ein erster, zarter Ansatz für ein Protokoll. Beachten Sie jedoch, dass der Server nur immer eine Anfrage auf einmal verarbeiten kann.
#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> /* Port fuer die Requests */ #define PORT 7777 /* Puffergroesse */ #define BUFSIZE 16384 void do_dialog (int sock); void err_exit(char *message); int main(int argc, char *argv[]) { int sockfd, newsockfd, c_len; struct sockaddr_in serv_addr, c_addr; /* Socket erzeugen */ sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) err_exit("ERROR opening socket"); /* Socket-Struktur initialisieren */ memset(&serv_addr, 0, sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(PORT); /* Port an die Host-Adresse binden */ if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) err_exit("ERROR on binding"); /* Auf Verbindungsanfragen warten */ listen(sockfd,5); for(;;) { printf("Server bereit ...\n"); /* Verbindung akzeptieren */ c_len = sizeof(c_addr); newsockfd = accept(sockfd, (struct sockaddr *)&c_addr, (socklen_t *)&c_len); if (newsockfd < 0) err_exit("ERROR on accept"); /* Verbindung steht -> Kommunizieren */ printf("Connect von: %s\n", inet_ntoa(c_addr.sin_addr)); do_dialog(newsockfd); close(newsockfd); } return 0; } /* Kommunikation ueber den Socket durchfuehren */ void do_dialog (int sock) { int n; char buffer[BUFSIZE]; memset(buffer,0,BUFSIZE); n = read(sock,buffer,BUFSIZE); if (n < 0) err_exit("ERROR reading from socket"); printf("Message: %s\n",buffer); n = write(sock,"OK\n",3); if (n < 0) err_exit("ERROR writing to socket"); } /* Fehlermeldung ausgeben und exit */ void err_exit(char *message) { perror(message); exit(1); }
Nach dem Verbindungsaufbau mit accept() wird nun geforkt. Hier kommt ein weiteres Konzept von Unix/Linux zum tragen: Jeder Kindprozess "erbt" alles Wichtige von seinen Eltern. In diesem Fall wird auch die offene TCP-Verbindung vererbt und der Kindprozess kann nun mit dem Client kommunizieren, während der Elternprozess schon wieder für den nächsten Verbindungswunsch bereit steht. der Kindprozess schließt deshalb auch den Server-(Eltern-)Socket (den er nicht braucht) und führt den Dialog mit dem Client durch. Der Elternprozess schließt seinerseits den Client-Socket und kann nun wieder mit accept() auf Anfragen warten.
Ein Problem mit den Kindern bei Unix ist, dass sie zu Zombies werden wenn sie sterben. Solche Zombies entstehen, wenn Kindprozesse sich beenden und es den Elternprozess "nicht interessiert". Ein ordentlicher Elternprozess wartet darauf, dass ein Kind stirbt (eine schrecklich nette Familie!). Dazu ruft er wait() auf. Doch wait() ist eine blockierende Funktion, was bedeuten würde, dass der Server so lange warten muss, bis der Kindprozess beendet ist. Also lässt er doch wieder nur eine Anfrage zu, oder? Zum Glück nicht, denn die Alternative waitpid() ist nicht blockierend. Und da der Server ja dauernd läuft, kann er weitermachen und alles klappt wie gewünscht.
#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> #include <sys/wait.h> /* Port fuer die Requests */ #define PORT 7777 /* Puffergroesse */ #define BUFSIZE 16384 void do_dialog (int sock); void err_exit(char *message); int main(int argc, char *argv[]) { int sockfd, newsockfd, c_len, status; struct sockaddr_in serv_addr, c_addr; pid_t pid, wpid; /* Socket erzeugen */ sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) err_exit("ERROR opening socket"); /* Socket-Struktur initialisieren */ memset(&serv_addr, 0, sizeof (serv_addr)); serv_addr.sin_family = AF_INET; serv_addr.sin_addr.s_addr = INADDR_ANY; serv_addr.sin_port = htons(PORT); /* Port an die Host-Adresse binden */ if (bind(sockfd, (struct sockaddr *) &serv_addr, sizeof(serv_addr)) < 0) err_exit("ERROR on binding"); /* Auf Verbindungsanfragen warten */ listen(sockfd,5); c_len = sizeof(c_addr); for(;;) { printf("Server bereit ...\n"); /* Verbindung akzeptieren */ newsockfd = accept(sockfd, (struct sockaddr *) &c_addr, (socklen_t *)&c_len); if (newsockfd < 0) err_exit("ERROR on accept"); /* Verbindung steht */ printf("Connect von: %s\n", inet_ntoa(c_addr.sin_addr)); /* Kindprocess erzeugen (erbt die akt. Verbindung) */ pid = fork(); if (pid < 0) err_exit("ERROR on fork"); else if (pid == 0) { /* Das ist der Kindprozess */ close(sockfd); /* Eltern-Socket schliessen */ /* Kommunizieren */ do_dialog(newsockfd); exit(0); } else { /* Dies ist der Elternprozess */ close(newsockfd); /* Kind-Socket schliessen */ printf("Parent PID = %d, Child PID = %d\n", getpid(), pid); waitpid(pid, &status, WNOHANG); printf("Kindprozess-Exitstatus: %d\n", status); } } /* end of while */ } /* Kommunikation ueber den Socket durchfuehren */ void do_dialog (int sock) { int n; char buffer[BUFSIZE]; memset(buffer,0,BUFSIZE); n = read(sock,buffer,BUFSIZE); if (n < 0) err_exit("ERROR reading from socket"); printf("Message: %s\n",buffer); n = write(sock,"OK\n",3); if (n < 0) err_exit("ERROR writing to socket"); } /* Fehlermeldung ausgeben und exit */ void err_exit(char *message) { perror(message); exit(1); }
#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> #include <netdb.h> /* Port fuer die Requests */ #define PORT 7777 /* Puffergroesse */ #define BUFSIZE 16384 int main(int argc, char **argv) { struct sockaddr_in host_addr; int size; int sock; struct hostent *host; char buffer[BUFSIZE]; if (2 != argc) { fprintf(stderr, "Angabe des Servers fehlt\n"); return -1; } host = gethostbyname(argv[1]); if (host == NULL) { fprintf(stderr, "Unbekannter Host %s\n",argv[1]); return -1; } /* Socket erzeugen */ sock = socket(AF_INET, SOCK_STREAM, 0); if (sock < 0) { fprintf(stderr, "Error: socket\n"); return -1; } /* Socket an das Ziel binden */ memset(&host_addr, 0, sizeof (host_addr)); host_addr.sin_family = AF_INET; host_addr.sin_addr = *((struct in_addr *)host->h_addr); host_addr.sin_port = htons(PORT); /* Verbindung aufbauen */ if (connect(sock, (struct sockaddr *)&host_addr, sizeof(host_addr)) == -1) { fprintf(stderr, "Error: connect\n"); return -1; } /* Daten empfangen */ size = recv(sock, buffer, BUFSIZE, 0); if (size == -1) { fprintf(stderr, "reading data failed\n"); return -1; } printf("%d Bytes: %s\n",size,buffer); /* Socket wieder freigeben */ close(sock); return 0; }Bei diesem sehr einfachen Client wird ein Problem der Netzwerkprogrammierung offenbar: Auch wenn beim Lesen mit recv() keine Daten empfangen wurden (size == 0), kann daraus nicht geschlossen werden, dass der Server schon alle Daten gesendet hat - es könnte ja auch eine längere Latenz im Netz schuld sein. Deshalb müssen immer Vereinbarungen darüber getroffen werden, wie die Kommunikation zwischen Server und Client ablaufen soll (Protokoll). Zumindest eine End-of-Data-Markierung ist notwendig. Im obigen Fall wird nur ein Datenblock empfangen. Wie der Empfang von mehreren Datenblöcken programmiert werden kann, zeigt der HTTP-Client weiter unten.
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <unistd.h> #include <arpa/inet.h> #define DEFAULTPORT 13 #define LINEMAX 1000 int main(int argc, char *argv[]) { int server, client; socklen_t len; struct sockaddr_in server_addr; struct sockaddr_in client_addr; time_t t; struct tm *tm; char timemsg[LINEMAX]; int portnumber; /* Rest der struct nullsetzen */ memset(&(server_addr.sin_zero), '\0', 8); /* Kommandozeile einlesen */ switch (argc) { case 1: /* Keine Argumente - default port */ portnumber = DEFAULTPORT; break; case 2: /* Portnummer von der Kommandozeile */ portnumber = atol(argv[1]); if (portnumber <= 0 || portnumber > 65535) { perror("Falsche Portnummer (muss zwischen 0 und 65535 liegen)"); return 1; } break; default: fprintf(stderr, "Usage: %s [portnumber] (0 < port_number < 65535)\nBeispiel: %s 1024\n", argv[0], argv[0]); return 1; } /* Set protocol to "0" to have socket() choose the correct protocol. */ if ((server = socket(PF_INET, SOCK_STREAM, 0)) < 0) { perror("Fehler beim Anlegen des Socket"); return 2; } /* Server-Adress-Struktur vorbereiten */ memset( &server_addr, 0, sizeof (server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(portnumber); server_addr.sin_addr.s_addr = htonl(INADDR_ANY); /* Listen to an interface with a specific IP (requires <arpa/inet.h>). */ /* inet_aton("127.0.0.1", &(server_addr.sin_addr)); */ if (bind(server, (struct sockaddr *)&server_addr, sizeof server_addr) < 0) { perror("Fehler bei bind (Portnummer < 1024? Dann musst Du root sein)"); return 3; } /* Daemonisieren */ switch (fork()) { case 0: break; case -1: perror("Fork fehlgeschlagen"); return 4; break; default: close(server); return 0; break; } listen(server, 5); /* warten auf Anfragen */ for (;;) { len = sizeof(client_addr); if ((client = accept(server, (struct sockaddr *)&client_addr, &len)) < 0) { perror("Accept fehlgeschlagen"); return 4; } fprintf(stderr, "Connect von %s\n",inet_ntoa(client_addr.sin_addr)); /* Zeit bestimmen */ t = time(NULL); tm = localtime(&t); sprintf(timemsg, "%.4i-%.2i-%.2i %.2i:%.2i:%.2i %s\n", tm->tm_year + 1900, tm->tm_mon + 1, tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, tm->tm_zone); /* Uhrzeit zum Client senden */ if ((send(client, timemsg, strlen(timemsg), 0)) < 0) { perror("Senden fehlgeschlagen"); return 6; } if (close(client) < 0) { perror("daytimed-tcp close"); return 7; } } }
Der zugehörige Client erlaubt ebenfalls die Angabe der Portnummer. Der abzufragende Host wird als Domainname angegeben - das Programm erfragt intern die IP-Adresse über gethostbyname(). Die vom Host abgefragte Zeit- und Datumsinfo wird auf der Standardausgabe ausgegeben.
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <netdb.h> #define MAXDATASIZE 100 #define DEFAULTPORT 13 int main(int argc, char *argv[]) { int server; int numbytes; struct sockaddr_in server_addr; struct hostent *host; char buf[MAXDATASIZE]; int portnumber; /* Kommandozeile einlesen */ switch (argc) { case 3: /* Host und Portnummer von der Kommandozeile */ portnumber = atol(argv[2]); if (portnumber <= 0 || portnumber > 65535) { perror("Falsche Portnummer (muss zwischen 0 und 65535 liegen)"); return 1; } host = gethostbyname(argv[1]); if (host == NULL) { perror("Unbekannter Host"); return 1; } break; default: fprintf(stderr, "Usage: %s [host] [port] (0 < port < 65535)\nBeispiel: %s localhost 1024\n", argv[0], argv[0]); return 1; } /* Set protocol to "0" to have socket() choose the correct protocol. */ if ((server = socket(PF_INET, SOCK_STREAM, 0)) < 0) { perror("Fehler beim Anlegen des Socket"); return 2; } memset(&server_addr, 0, sizeof (server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_port = htons(portnumber); server_addr.sin_addr = *((struct in_addr *)host->h_addr); if (connect(server, (struct sockaddr *)&server_addr, sizeof server_addr) < 0) { perror("Connect fehlgeschlagen"); close(server); return 3; } /* Receive the message with recv(). */ /* (The message was sent in the server applikation the server with send().) */ if ((numbytes = recv(server, buf, MAXDATASIZE-1, 0)) == -1) { perror("Empfangsfehler"); close(server); return 4; } /* Null-terminate the message so we can print it as a string. */ buf[numbytes] = '\0'; printf("%s",buf); close(server); return 0; }
Beim Client werden als Kommandozeilenparameter werden der Servername und der Dateiname übergeben. Das Programm versucht dann, diese Datei vom Server zu laden, indem es den Dateinamen sendet. Es gibt diese dann auf der Standardausgabe aus.
Deshalb entfallen auch listen() und accept(). Der folgende UDP-Server wartet auf eine Nachricht vom Client, gibt diese aus und beendet sich. Das Paket wird also komplett auf einmal übertragen, was bedeutet, dass es mit einem einzigen Leseaufruf gelesen werden kann. Trotzdem hat auch ein UDP-Paket eine maximale Größe, abhängig vom Transportweg und der Hardware (Ethernet: 1500 Bytes). Der Server ruft lediglich die Funktion recvfrom() auf und wartet darauf, dass irgendein Client Daten schickt.
#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> /* Port fuer die Requests */ #define PORT 7777 /* Puffergroesse */ #define BUFSIZE 4096 int main(void) { int sockfd; /* unsere Socket */ struct sockaddr_in my_addr, remote_addr; /* 2 Adressen */ int remote_addr_size = sizeof(remote_addr); /* fuer recvfrom() */ char buf[BUFSIZE]; /* Datenpuffer */ if ((sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) { fprintf(stderr, "Error: socket()\n"); exit(1); } memset(&my_addr, 0, sizeof (my_addr)); my_addr.sin_family = AF_INET; my_addr.sin_addr.s_addr = htonl(INADDR_ANY); my_addr.sin_port = htons(PORT); if (bind(sockfd, (struct sockaddr*)&my_addr, sizeof(my_addr)) < 0) { fprintf(stderr, "Error: bind()\n"); close(sockfd); exit(1); } if (recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&remote_addr, (socklen_t*)&remote_addr_size) > 0) { printf("Getting Data from %s\n", inet_ntoa(remote_addr.sin_addr)); printf("Data : %s\n", buf); } close(sockfd); return(0); }Den Server so umzubauen, dass er sich nicht beendet, sondern auf weitere Client-Anfragen zu warten, geht genauso wie bei den TCP-Servern. Um die Empfangsroutine herum kommt wieder eine Endlosschleife:
#include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <netdb.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> /* Port fuer die Requests */ #define PORT 7777 /* Puffergroesse */ #define BUFSIZE 4096 int main (int argc, char **argv) { int sock, client, rc, len; struct sockaddr_in cliAddr, servAddr; char buf[BUFSIZE]; const int y = 1; /* fuer setsockopt */ /* Socket erzeugen */ sock = socket (AF_INET, SOCK_DGRAM, 0); if (sock < 0) { fprintf(stderr, "Kann Socket nicht öffnen\n"); exit(1); } /* Lokalen Server Port binden */ memset(&servAddr, 0, sizeof (servAddr)); servAddr.sin_family = AF_INET; servAddr.sin_addr.s_addr = htonl (INADDR_ANY); servAddr.sin_port = htons (PORT); /* sofortiges Wiederverwenden des Ports erlauben */ setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &y, sizeof(int)); rc = bind(sock, (struct sockaddr *) &servAddr, sizeof (servAddr)); if (rc < 0) { fprintf (stderr, "Kann Port nicht binden\n"); exit (1); } printf ("Warte auf Daten ...\n"); /* Serverschleife */ for (;;) { /* Puffer initialisieren */ memset (buf, 0, BUFSIZE); /* Nachrichten empfangen */ len = sizeof (cliAddr); client = recvfrom (sock, buf, BUFSIZE, 0, (struct sockaddr *) &cliAddr, (socklen_t*)&len ); if (client < 0) { fprintf(stderr, "Kann keine Daten empfangen ...\n"); continue; } /* Erhaltene Nachricht ausgeben */ printf("Getting Data from %s, UDP-Port %u\n", inet_ntoa(cliAddr.sin_addr), ntohs(cliAddr.sin_port)); printf("Data: %s\n", buf); } return (0); }Jeder ankommende Datenblock wird getrennt vom vorhergehenden betrachtet, zusammengehörige Daten können nur anhand der IP-Adresse des Absenders detektiert werden. Auf die gleiche Weise kann der Server nach dem Connect eines Clients auch Daten versenden, anstatt sie zu empfangen.
#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> #include <netdb.h> /* Port fuer die Requests */ #define PORT 7777 /* Puffergroesse */ #define BUFSIZE 4096 int main(int argc, char **argv) { int sockfd; struct sockaddr_in remote_addr; struct hostent *host_addr; if (argc != 3) { fprintf(stderr, "Usage: %s <HOST> <MESSAGE>\n", argv[0]); } if ((host_addr = gethostbyname(argv[1])) == NULL) { fprintf(stderr, "Cannot resolv hostname: %s\n", argv[1]); exit(1); } if ((sockfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP)) < 0) { fprintf(stderr, "Error: socket()\n"); exit(1); } memset(&remote_addr, 0, sizeof (remote_addr)); remote_addr.sin_family = AF_INET; remote_addr.sin_addr = *((struct in_addr *) host_addr->h_addr); remote_addr.sin_port = htons(PORT); if (sendto(sockfd, argv[2], strlen(argv[2]) + 1, 0, (struct sockaddr *)&remote_addr, sizeof(remote_addr)) > 0) { printf("Message sent\n"); } else { fprintf(stderr, "Error while sending data\n"); } close(sockfd); return(0); }
Der Server kann dem Client dann gegebenenfalls noch mit sendto() antworten, da der Client mit seinem sendto() auch seine Adresse mitgeschickt hat. Der Client kann die Daten vom Server mit recvfrom() einlesen.
Diese Funktion liest zunächst die Anfrage des Browsers in einen Puffer. Im Fall eines GET besteht der Request nur aus dem Header, der neben der Zeile mit dem Request noch etliche Zeilen mit Angaben zum Client/Browser enthält (auf diese Weise erfährt ein Webserver auch mehr über den User. Auch Cookies sind im Header enthalten.) Das Aufdröseln des Pufferinhalts in einzelne Zeilen kann mittels strtok() erfolgen, wobei als Delimiter der Zeilenwechsel "\r\n" angegeben wird:
/* ptr ist ein Hilfszeiger, der mit ptr = buffer initialisiert ist */ ptr = strtok(buffer,delimiter); while ((ptr != NULL))// && !eoh) { printf ("Headerzeile: %s\n", ptr); /* GET-Request? Dann URL extrahieren */ sscanf(ptr, "GET %1023s HTTP/", url); /* naechste Zeile nehmen */ ptr = strtok(NULL,delimiter); }Ausnahmsweise mache ich hier mal keinen weiten Bogen um die Funktion sscanf(), denn diese Funktion kann Zeichenmuster in der Eingabezeile erkennen. In diesem Fall suche ich nach dem Muster "GET <Dateipfad> HTTP/". Für den Dateipfad steht der Platzhalter "%1023s", der für einen String von maximal 1023 Zeichen steht. Passt das Muster auf die aktuelle Zeile, packt mir sscanf() brav die Dateiangabe in die Variable url, in allen anderen Fällen wird die Zeile ignoriert. Gegebenenfalls muss noch ein an der URL hängendes '\r' beseitigt werden.
Bei der URL selbst, genauer bei der Dateiangabe des Browsers, muss auch noch unterschieden werden:
/* Implementierung eines einfachen HTTP-Servers, * der ausschließlich GET-Requests bearbeiten kann */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <fcntl.h> #include <errno.h> #include <sys/stat.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <arpa/inet.h> /* Port fuer die HTTP-Requests */ #define HTTP_PORT 8080 static void serv_request(int client_fd, char* rootpath); static void err_exit(char *error_message); int main( int argc, char **argv) { struct sockaddr_in server, client; int sock, clientd; int len; /* Die "Document Root" wird per Kommadozeilenargument * uebergeben */ if (2 != argc) err_exit("Angabe von document_root fehlt\n"); /* Erzeuge das Socket */ sock = socket( PF_INET, SOCK_STREAM, 0); if (sock < 0) err_exit("Kann Socket nicht anlegen"); /* Erzeuge die Socketadresse des Servers */ memset(&server, 0, sizeof (server)); server.sin_family = AF_INET; server.sin_addr.s_addr = htonl(INADDR_ANY); server.sin_port = htons(HTTP_PORT); /* Binde Port an die Serveradresse */ if (bind(sock, (struct sockaddr*)&server, sizeof( server)) < 0) err_exit("Kann Socket nicht an Port binden"); listen(sock, 5); /* Bearbeite die Verbindungswuensche von Clients * in einer Endlosschleife */ for (;;) { printf("Server wartet auf Anfrage ...\n"); len = sizeof(client); clientd = accept(sock, (struct sockaddr*)&client, (socklen_t*)&len); if (clientd < 0) err_exit("accept failed"); printf("client address: %s\n", inet_ntoa(client.sin_addr)); /* Bearbeite den HTTP-Request */ serv_request(clientd, argv[1]); /* Schließe Verbindung */ close(clientd); } } /* Bearbeite den ankommenden HTTP-Request */ static void serv_request(int client_fd, char* rootpath) { char delimiter[] = "\r\n"; /* Zeilentrenner im Puffer */ char buffer[16384]; /* Empfangspuffer */ char *ptr = NULL; /* Hilfspointer */ struct stat info; /* fileinfo */ char url[1024]; /* URL */ char path[1024]; /* Dateipfad */ int count; /* Bytezaehler */ int fd; /* Filedesc. fuer Ausgabedatei */ int eoh = 0; /* End of Header Flag */ *url = '\0'; /* HTTP-Request einlesen */ count = recv(client_fd, buffer, sizeof(buffer), 0); eoh = (count > 0)? 0 : 1; buffer[count] = '\0'; /* Stringterminator setzen */ ptr = strtok(buffer,delimiter); while ((ptr != NULL))// && !eoh) { printf ("Headerzeile: %s\n", ptr); /* GET-Request? Dann URL extrahieren */ sscanf(ptr, "GET %1023s HTTP/", url); /* naechste Zeile nehmen */ ptr = strtok(NULL,delimiter); } if (url[strlen(url)-1] == '\r') url[strlen(url)-1] = '\0'; if (strlen(url) > 1) { /* URL im Header gefunden */ printf( "--- Request: GET %s ", url); sprintf(path, "%s/%s", rootpath, url); printf( "--- Path: %s ", path); } else { /* als default "index.html" nehmen */ printf( "--- Request: GET /index.html "); sprintf(path, "%s/index.html", rootpath); printf( "--- Path: %s ", path); } if (stat(path, &info) == 0 && S_ISDIR(info.st_mode)) { /* bei Directory "/index.html" anhaengen */ sprintf(path, "%s/%s/index.html", rootpath, url); printf( "--- Path: %s ", path); } /* gewuenschte Datei oeffnen */ fd = open(path, O_RDONLY); if (fd > 0) { /* Datei vorhanden, also ausliefern */ sprintf(buffer, "HTTP/1.0 200 OK\nContent-Type: text/html\n\n"); send(client_fd, buffer, strlen(buffer), 0); do { count = read(fd, buffer, sizeof(buffer)); send(client_fd, buffer, count, 0); } while (count > 0); close(fd); } else { /* Datei nicht vorhanden - Fehler senden */ sprintf(buffer, "HTTP/1.0 404 Not Found\n\n"); send(client_fd, buffer, strlen(buffer), 0); } printf(" --- done!\n"); } /* Funktion gibt aufgetretenen Fehler aus und * beendet das Programm */ static void err_exit(char *error_message) { fprintf(stderr, "%s: %s\n", error_message, strerror(errno)); exit(EXIT_FAILURE); }
Der folgende HTTP-Client ist auch ein Muster an Einfachheit. Er entspricht im Grund dem Muster-TCP-Client oben. Die empfangenen Daten werden einfach auf die Standardausgabe kopiert. Das hat den Vorteil (oder auch Nachteil), dass der Header auch zu sehen ist. Zwischen Header und Body (der eigentlichen Webseite) befindet sich eine Leerzeile. Wer will, kann das Programm ja dahingehen erweitern, dass Header und Body getrennt werden (oder man schaltet per Pipe passende Unix-Kommandos dahinter).
/* GET-Request via HTTP an einen Webserver */ #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdlib.h> #include <string.h> /* HTTP-Port */ #define PORT 80 /* Groesse des Puffers */ #define BUFSIZE 8192 /* Filehandle-Nummer der Standard-Ausgabe */ #define STD_OUT 1 static void err_exit(char *error_message); int main( int argc, char **argv) { struct sockaddr_in server; struct hostent *host_info; char buffer[BUFSIZE]; int sock; int count; if (argc != 3) err_exit("usage: httpget <server> <path/file>\n"); /* Erzeuge das Socket */ sock = socket(PF_INET, SOCK_STREAM, 0); if (sock < 0) err_exit("failed to create socket"); memset(&server, 0, sizeof (server)); server.sin_family = AF_INET; server.sin_port = htons(PORT); /* Wandle den Servernamen in eine IP-Adresse um */ host_info = gethostbyname( argv[1]); if (host_info == NULL) err_exit("unknown server"); memcpy((char *)&server.sin_addr, host_info->h_addr, host_info->h_length); /* Baue die Verbindung zum Server auf */ if (connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0) err_exit("can't connect to server"); /* Sende den HTTP-GET-Request */ sprintf(buffer, "GET %s HTTP/1.0\r\n\r\n", argv[2]); send(sock, buffer, strlen( buffer), 0); /* Hole die Serverantwort und gib sie auf Konsole aus */ do { count = recv(sock, buffer, sizeof(buffer), 0); write(STD_OUT, buffer, count); } while (count > 0); /* Schliesse Verbindung und Socket */ close(sock); return(EXIT_SUCCESS); } /* Funktion gibt aufgetretenen Fehler aus und * beendet das Programm */ static void err_exit(char *error_message) { fprintf(stderr, "%s: %s\n", error_message, strerror(errno)); exit(EXIT_FAILURE); }
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <unistd.h> #include <netdb.h> int main(int argc, char *argv[]) { struct sockaddr_in addr; struct servent *serv; int sock, i; unsigned long begin, end, curr; if (argc < 3) { fprintf(stderr, "usage: %s <begin> <end>\n", argv[0]); exit(1); } /* numerische IP-Adressen ermitteln */ begin = ntohl(inet_addr(argv[1])); end = ntohl(inet_addr(argv[2])); /* Anfangadresse < Endadresse sicherstellen */ if (begin > end) { curr = end; end = begin; begin = curr; } memset(&addr, 0, sizeof (addr)); /* Adressbereich abscannen */ for (curr = begin; curr <= end; curr++) { addr.sin_addr.s_addr = htonl(curr); printf("%s:\n", inet_ntoa(addr.sin_addr)); /* well known ports abscannen */ for (i = 0; i < 1024; i++) { sock = socket(PF_INET, SOCK_STREAM, 0); if (sock == -1) { perror("socket() failed"); exit(1); } addr.sin_addr.s_addr = htonl(curr); addr.sin_port = htons(i); addr.sin_family = AF_INET; /* aktuelle Portnummer in Spalte 1 anzeigen */ printf("--> %4i\r", i); fflush(stdout); if (!connect(sock, (struct sockaddr*)&addr, sizeof(addr))) { /* Namen des Services zum Port ermitteln */ serv = getservbyport(addr.sin_port, "tcp"); if (serv) printf("%i (%s) open\n", i, serv->s_name); else printf("%i (unknown) open\n", i); } close(sock); } puts("\r \n"); } return 0; }
Bitnummer | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Variable IBM: | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 |
Variable OBM: | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Variable EBM: | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Mit dem Aufruf
int i = select(8, &IBM, &OBM, &EBM, NULL);wird ohne Zeitbeschränkung darauf gewartet, daß entweder auf den "Kanälen" 0 oder 4 Eingabedaten zur Verfügung stehen, oder daß auf "Kanal" 7 ein Schreiben möglich ist (oder ein Fehler auftrat). Nach dem Verlassen der Funktion mit Rückgabewert 1 (nur noch 1 Bit gesetzt) sehen die Variablen wie folgt aus:
Bitnummer | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|
Variable IBM: | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
Variable OBM: | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Variable EBM: | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Die C-Bibliothek stellt Makros zur Verfügung, die das Setzen, Löschen und Abfragen von Bits in den bei select() benutzten Bitmuster erleichtern. Die Deklaration ist oben schon aufgelistet. Hier einige Beispiele:
Abschließend noch ein Beispiel: Ein Programm soll auf Eingaben von der Tastatur warten, aber alle 3 Sekunden den Benutzer zur Eingabe auffordern, wenn er nicht reagiert.
#include <unistd.h> #include <stdio.h> #include <sys/types.h> #include <sys/time.h> fd_set EBM; struct timeval Zeit; char buffer[1000]; int main() { do { printf("\nGibs mir:"); fflush(stdout); /* Ausgabepuffer leeren */ FD_ZERO(&EBM); /* Eingabebitmuster = 0 */ FD_SET(0,&EBM); /* Bit 0 setzen */ Zeit.tv_sec=3; /* Timeout = 3 Sekunden */ Zeit.tv_usec=0; } while (!select(1, &EBM, NULL, NULL, &Zeit)); /* Wenn select() mit 0 zurückkommt, ist die Uhr abgelaufen andernfalls steht eine Eingabe an */ fgets(buffer,1000,stdin); printf("Eingabe war: %s\n",buffer); }Der Aufruf fflush(stdout) wird in diesem Beispiel eingesetzt, damit die Eingabeaufforderung sofort auf dem Bildschirm erscheint.
Noch ein Beispiel: select() erlaubt es beispielsweise, ein Programm zu schreiben, das auf einem Port wartet und alle engehenden Daten an einen anderen Port weitergibt. Fertig ist der Proxy-Server! Angenommen man hat einen Rechner der per Modem eine Verbindung zum Internet aufgebaut hat als Gateway für ein lokales Netz dient (mittels IP-Masquerading). Nur der Gateway ist von aussen sichtbar, die Rechner des lokalen Netzes jedoch dahinter versteckt. Nehmen wir weiter an, daß auf einem der lokalen Rechner ein Web-Server läuft, der nach aussen Daten anbieten soll. Man braucht also ein Programm, das die Anfragen die an Port 80 des Gateway gelangen, zum Web-Server weitergereicht werden sollen (was einen einfachen Portforwarder aus dem Rennen wirft). Um das zu verwirklichen, braucht man also ein Programm das zwei Sockets geöffnet hat: einen zum Benutzer ausserhalb des Netzes, und einen zweiten der zum Web-Server führt. Das Programm muß erkennen, auf welchem Socket gerade etwas ankommt und diese Daten dann über den anderen Socket schicken. Eine Lösungsmöglichkeit wäre es, einen Firewall zu installieren. Mit select() geht es aber auch.
Das folgende Programmfragment zeigt, wie es geht.
int data_interchange(int src, int dest) { /* Implementierung der Polling-Methode + select() um * Systemressourcen zu sparen */ char buffer[BUFFER_SIZE]; int src_sent, src_recvd, dest_sent, dest_recvd, max, total, i; fd_set rfds; struct timeval tv; if (src > dest) max = src; else max = dest; total = 0; fcntl(src, F_SETFL, O_NONBLOCK); fcntl(dest, F_SETFL, O_NONBLOCK); for (;;) { FD_SET(src, &rfds); FD_SET(dest, &rfds); tv.tv_sec = 300; tv.tv_usec = 0; select(max + 1, &rfds, NULL, NULL, &tv); src_recvd = recv(src, buffer, sizeof(buffer), 0); dest_recvd = recv(dest, buffer, sizeof(buffer), 0); if (src_recvd > 0) send(dest, buffer, src_recvd, 0); if (dest_recvd > 0) send(src, buffer, dest_recvd, 0); if ((src_recvd == 0) || (dest_recvd == 0)) break; } return 0; }Wie man sieht, wartet select() darauf, daß von einem der beiden Sockets gelesen werden kann. Ist dies der Fall, wird gelesen. Man hätte auch mit FD_ISSET() testen können, von welchem Socket gelesen werden kann, doch so haben wir gleich noch ein Beispiel für nicht-blockierende Ein-/Ausgabe. Durch den Aufruf von fcntl() mit dem Attribut O_NONBLOCK blockiert ein recv()-Aufruf nicht, bis Daten eingetroffen sind, sondern kehrt sofort zurück. Die beiden if-Abfragen überprüfen, von welchem der Sockets eingetroffen sind (Wert > 0). Falls von keinem der beiden Sockets Daten kommen, ist ein Timeout aufgetreten. Das bedeutet, daß der Server-Prozeß nach fünf Minuten Inaktivität automatisch endet.
Wie groß Sie Ihre Puffer für Empfang und Senden machen, hängt vom Anwendungsfall ab. Allerdings ist ein Byte-Puffer genauso sinnlos wie ein überdimensional großes Pufferarray. Puffergrößen von 512 oder 1024 Byte sind oft zu finden. Im Netzwerk ist nur interessant, dass ein ankommendes Paketim Prinzip nur so gross sein kann, wie es die MTU (Maximum Transfer Unit) des Netzes erlaubt. Das wären z. B. 1500 Bytes für Ethernet. Es können jedoch auch größere Datagramme ankommen, wenn die sendende Einrichtung die MTU nicht beachtet, und es zur Fragmentierung von Datagrammen kommt. Insofern sind 4096 - 8192 Byte Puffergröße keine schlechte Wahl. Sie sollten auch beachten, dass Funktionen wie recv() die Bytes abliefern, die bis zum Funktionsaufruf empfangen wurden - unabhängig davon, ob da noch was kommt. Wie bei allen bekannten Internetanwendungen wie SMTP, POP3, IMAP, HTTP etc. werden bzw. wurden Protokolle auf eine höheren Ebene definiert, die fast immer aus einen Frage- und Antwort-Spiel bestehen. Damit läuft die Kommunikation nicht nur in geregelten Bahnen ab, sondern ist auch leichter zu debuggen. Näheres zu einigen höheren Protokollen finden Sie im Netzwerk-Skript.
Bei den bisherigen Beispielen habe ich mich elegant um die Verarbeitung von etwas größeren Datenmengen gedrückt. Entweder es genügte ein einmaliges Lesen von der Schnittstelle, um alle Daten des Absenders im Puffer zu haben oder ich habe den Empfangspuffer einfach mittels write() in Richtung Standardausgabe weitergeleitet - nach dem Motto "Soll sich doch ein anderes Programm um die Weiterverarbeitung kümmern". Aus diesem Grund soll hier das Splitten des Puffers in einzelne Zeilen etwas näher betrachtet werden. Normalerweise wäre es ein Zufall, wenn ein Zeilenende genau mit dem Pufferende zusammenfällt, und man den String per Standardfunktion splitten kann. Die Regel ist, dass ein Puffer den Anfang und der nächgste Puffer das Ende einer Zeile enthält. Darum soll jetzt die Arbeit mit mehreren aufeinanderfolgenden Datenblöcken näher betrachtet werden:
Die erste Möglichkeit, die sich anbietet, ist das zeichenweise Lesen von der Netzwerk-Schnittstelle (das Betriebssystem puffert ja die Pakete für uns). Sobald dann ein Newline ('\n') auftritt, wird die Funktion verlassen und gibt den String zurück. Es versteht sich von selbst, dass vom aufrufenden Programm ein Zeichen-Array zur Verfügung gestell werden muss und dass man dessen Länge nicht überschreitet. Die folgende Funktion get_line() liest von einen vorhergeöffneten Socket genau eine Zeile ein (der Unterstrich in get_line() ist notwendig, weil getline() eine Bibliotheksfunktion ist). Als Parameter werden neben dem Socket das Array und dessen maximale Länge übergeben:
int get_line(int fd, char *buffer, unsigned int len) { /* read a '\n' terminated line from socket fd into buffer * of size len. The line in the buffer is terminated * with '\0'. It returns -1 in case of error and -2 if the * capacity of the buffer is exceeded. * It returns 0 if EOF is encountered before reading '\n'. */ int numbytes = 0; int ret; char buf; buf = '\0'; while ((numbytes <= len) && (buf != '\n')) { ret = recv(fd, &buf, 1, 0);/* read a single byte */ if (ret == 0) break; /* nothing more to read */ if (ret < 0) return -1; /* error or disconnect */ buffer[numbytes] = buf; /* store byte */ numbytes++; } if (buf != '\n') return -2; /* numbytes > len */ buffer[numbytes-1] = '\0'; /* overwrite '\n' */ return numbytes; }Nachteil dieser Lösung ist die Geschwindigkeit bzw. deren Fehlen. Durch die vielen recv()-Aufrufe ist die Funktion ziemlich langsam. Besser wäre eine Lösung, bei der ein Datenpaket komplett eingelesen (z. B. bei Ethernet 1500 Bytes) und dann Zeile für Zeile ans aufrufende Programm weitergereicht wird. Genau das macht die folgende Funktion, bei der die Parameter die gleiche Aufgabe haben wie oben. Diese Funktion hat einen internen Puffer, der mittels recv() gefüllt wird und dessen Inhalt Stück für Stück bei jedem Aufruf weitergegeben wird. Dazu verwendet die Funktion die statischen Variablen bufptr, count und mybuf, deren Werte erhalten bleiben und bei jedem Aufruf wieder zur Verfügung stehen. Werden mit recv() mehrere Zeilen gelesen, bleibt der jeweilige Rest in mybuf erhalten und wird beim nächsten Aufruf der Funktion verarbeitet:
int readline(int fd, char *buffer, unsigned int len) { /* read a '\n' terminated line from socket fd into buffer * bufptr of size len. The line in the buffer is terminated * with '\0'. It returns -1 in case of error or -2 if the * capacity of the buffer is exceeded. * It returns 0 if EOF is encountered before reading '\n'. * Notice also that this routine reads up to '\n' and overwrites * it with '\0'. Thus if the line is really terminated with * "\r\n", the '\r' will remain unchanged. */ static char *bufptr; static int count = 0; static char mybuf[1500]; char *bufx = buffer; char c; while (--len > 0) /* repeat until end of line */ { /* or end of external buffer */ count--; if (count <= 0) /* internal buffer empty --> read data */ { count = recv(fd, mybuf, sizeof(mybuf), 0); if (count < 0) return -1;/* error or disconnect */ if (count == 0) return 0; /* nothing to read - so reset */ bufptr = mybuf; /* internal buffer pointer */ } c = *bufptr++; /* get c from internal buffer */ if (c == '\n') { *buffer = '\0'; /* terminate string and exit */ return buffer - bufx; } else { *buffer++ = c; /* put c into external buffer */ } } return -2; /* external buffer to short */ }Beim Senden von Daten sollte man eigentlich nicht zwischenpuffern, sondern jede Zeile sofort auf die Reise schicken - schließlich wartet der Empfänger darauf.
Auch bei binären Strukturen sollte man die komplette Struktur vor dem Senden in eine Zeichenkette konvertieren. Bei komplexen Strukturen würde sich XML als Beschreibungssprache anbieten, insbesondere da es für die XML-Konvertierung fertige Bibliotheken gibt. Auch auf Empfangseite müssen selbstverständlich ebenfalls bestimmte Vorkehrungen getroffen werden. Zusätzlich müssen Sie nationale Umlaute berücksichtigen. Durch die Umwandlung in Strings wird zwar etwas Bandbreite verschwendet, aber dafür sind die übertragenen Informationen lesbar und man kann ggf. einen Telnet-Client für das Debugging einsetzen.
Pufferüberläufe waren (Buffer Overflows) in der Geschichte der C-Programmierung schon immer eine der häufigsten Fehlerursache, weil sie so einfach zu programmieren sind und weil man sie sehr gerne übersieht. Bei einem Pufferüberlauf werden im Grunde ganz einfach mehr Daten in einen Puffer geschrieben, als die Puffergrösse zuläßt. Wenn die Daten, die überlappen, von einer bestimmten Beschaffenheit sind, dann kann der Angreifer damit unter Umständen Schadcode einschleusen und ausführen lassen. Die Socket-Funktionen (z. B. recv()) besitzen alle einen Parameter, der die Datenmenge im Zielpuffer begrenzt. Wenn man hier den richtigen Wert verwendet, kann eigentlich nichts passieren. Wenn man Text erwartet, ist es günstig hier ein Byte weniger anzugeben, um den String im Puffer anschließend mit '\0' zu terminieren.
Potentiell anfällige Funktionen sind z. B. strcpy(), strcat() und sprintf(), die keinen Parameter zur Längenbegrenzung besitzen. Sie sollten deshalb nur dort verwendet werden, wo ganz sicher nichts passieren kann. In C90 (ANSI-C) gab es bereits teilweise Abhilfe in Form von strncpy() und strncat(), die eine Grössenbeschränkung besitzen. Neu in C99 hinzugekommen ist snprintf(). Diese Funktionen sollten bevorzugt verwendet werden. Man darf übrigens einem Puffer wirklich nur so weit vertrauen, wie man ihn selbst mit Inhalt gefüllt hat - insbesondere, was Null-Terminierung von Strings angeht.
Zeilen werden in der Netzwerkwelt zumeist mit Carriage Return und Linefeed (\r\n) abgeschlossen. Das kann zum seltsamen Verhalten des Programms führen, wenn man beispielsweise nur das Linefeed (Newline) abschneidet, oder mit einem Server kommunizieren will, und der einfach so tut, wie er soll.
Der inetd vereinfacht zudem das Schreiben von Server-Daemonen, da etliche
Start-Details bereits durch den inetd selbst abgehandelt werden. Der
Nachteil besteht darin, daß der inetd für jede Anfrage
sowohl ein fork als auch ein exec ausführen muß,
um den aktuellen Serverprozeß zu starten. Der Ablauf entspricht in etwa
folgendem Schema:
Der inetd und die von ihm gestarteten Server stüten sich auf
folgenden Dateien.
Das Hauptproblem beim Einsatz des inetd ist die Reaktionszeit auf
Dienstanforderungen. Das ist bei Diensten wie Telnet oder FTP unproblematisch,
kann aber z.B. bei HTTP-Servern sehr kritisch werden, da ja HTTP für jede
einzelne Datein eines Webdokuments eine Verbindung aufbaut, demnach für jede
einzelne Datei der httpd vom inetd neu gestartet werden
muß. Daher werden zeitkritischen Dienste und Server als Standalone-Server
betrieben.
Im inetd selbst sind keinerlei Sicherheitsmechanismen implementiert.
Deswegen wurde ein Filter geschaffen, der vom inetd anstelle des für
den Port zuständigen Dienstes gestartet wird, der sogenannte TCP-Wrapper
tcpd. Er erhält den Programmnamen des Dienstes als Argument.
Der tcpd arbeitet für Server und Client transparent, er wird durch den
zu startenden Dienst ersetzt. Zuvor protokolliert und überprüft er die
Zulässigkeit des Zugriffs.
Falls also in inetd.conf an der vorletzten Position zusätzlich noch der
tcp-Wrapper (tcpd) aufgerufen wird (also beispielsweise statt
/etc/ftpd nun /etc/tcpd /etc/ftpd), wird vom tcpd
vor dem Programmaufruf geprüft, ob der entsprechende Host überhaupt das
Recht besitzt, Serverdienste in Anspruch zu nehmen. Dazu werden die Dateien
/etc/hosts.allow und /etc/hosts.deny herangezogen.
Schließlich kann ein optionales Kommando angegeben werden, das immer dann
ausgeführt wird, wenn diese Zeile für eine Anforderung zutrifft. Falls auf
diese Möglichkeit zurückgegriffen wird, dann meist für ein detailliertes
Protokoll. Im Argument des Kommandos können einige Sonderzeichen verwendet werden:
Die Konfiguration kann mit dem Kommando tcpdchk überprüft werden
(Unstimmigkeiten in inetd.conf, Syntaxfehler in hosts.deny / hosts.allow,
unbekannte Rechnernamen).
In Gegensatz zu einem "normalen" Programm, das an ein Terminal gebunden ist, löst sich der
Daemon nach dem Start vom Terminal und läuft unabhängig im Hintergrund weiter. Normalerweise
wird der Prozess auch vom init-Prozess adoptiert. An die Stelle der Terminalausgabe tritt
oft ein Logfile; der Daemon kann aber auch bequemerweise den Syslog-Mechanismus nutzen.
Um einen Daemon zu erzeugen, werde die Funktionen fork(), setsid() und
chdir() benötigt. Mittels chdir() bekommt der Daemon sein Standardverzeichnis
zugewiesen. Hier können "/" oder "/tmp" verwendet wewrden, wenn man kein spezielles Verzeichnis
vorsehen will. Auch müssen die Standarddateien stdin, stdout und stderr
geschlossen werden,damit sich der Daemon vom Terminal löst. Normelerweise werden sie nicht
einfach geschlossen, sondern auf "/dev/null" umgeleitet. Das folgende Beispielprogramm zeigt,
wie man den Übergang zum Daemon programmiert, macht aber sonst nichts Sinnvolles.
Auch sind im Zusammenhang mit Daemon-Prozessen die Funktionen setuid() (Set User Id)
und setgid() (Set Group Id) interessant, um die Rechte einzuschränken. Wenn man seinen
Daemon nämlich beim Bootvorgang mittels einer Startdatei hochfährt, läuft er natürlich mit
"root"-Identität - kann also im Fehlerfall größtmöglichen Schaden anrichten. Daher ist es
sinnvoll, möglichst bald eine weniger privilegierte Identität anzunehmen. Das alles und
noch einiges mehr ist im Programm dummy_daemon.c ausfühlicher
skizziert. Bei den Stellen im Programm, die noch geändert bzw. ergänzt werden müssen,
beginnen die Kommentare mit /***.
1.9 Der Internet-Superserver
Rein theoretisch müßte jeder Daemon bei Systemstart hochgefahren werden,
für die eine Anforderung eines entfernten Rechner (Host) auftreten könnte.
Dies würde aber die Zahl der laufenden Prozesse unnötig in die Höhe
treiben und Systemresourcen verbrauchen. Deshalb wurde der Daemon inetd,
der Internet-Superserver, entwickelt. Er "lauscht" auf alle Diensteanforderungen,
die an dem von ihm überwachten Ports eingehen. Tritt eine solche Anforderung auf,
prüft der Daemon die Zugriffsberechtigung (exakt: Die Kontrolle wird an den
TCP-Warapper tcpd übergeben und dieser macht weiter) und startet
im positivem Fall den entsprechenden Daemon, der dann die Anforderungen des
Clients bearbeitet. Die Konfigurationsdatei ist /etc/inetd.conf.
In ihr sind alle Dienste und die entsprechenden Dämonen mit Parametern
verzeichnet.
Programmname Sockettyp Protokoll Flags User Programmpfad Programmargumente
Beispiel:
...
ftp stream tcp nowait root /usr/sbin/in.ftpd in.ftpd
telnet stream tcp nowait root /usr/sbin/in.telnetd in.telnetd
...
shell stream tcp nowait root /usr/sbin/in.rshd in.rshd
login stream tcp nowait root /usr/sbin/in.rlogind in.rlogind
exec stream tcp nowait root /usr/sbin/in.rexecd in.rexecd
...
#tftp dgram udp wait root /usr/sbin/in.tftpd in.tftpd -s /tftpboot
...
Nach Änderungen der Konfiguration (z.B. Ein- und Ausschalten des
TFTP-Dienstes durch Entfernen bzw. Hinzufügen des Kommentarzeichens)
muß der inetd nicht neu gestartet werden, sondern kann durch das
Versenden des Signal SIGHUP angewiesen werden, sich neu zu konfigurieren.
Programmname Port/Protokoll
Programm muß der gleiche Name sein wie er in /etc/inetd.conf
an erster Position angegeben wurde. Port/Protokoll bezeichnet den Port, auf
dem gelauscht werden soll, und das dazugehörigen Protokoll, z. B.:
37 telnet/tcp
Eine Zeile in den beiden Dateien hat folgenden Aufbau:
Hier verzeichnete Hosts, Domains oder Dienste werden nicht zugelassen.
Will man nur einige Bösewichte aussperren, wird man diese in dieser Datei
eintragen uns alles andere zulassen, z.B.:
# Host sperren
192.168.0.2: ALL
# Domain sperren
.boese.org: ALL
Sicherer ist es jedoch, in dieser Datei alles zu sperren, was nicht ausdrücklich
erlaubt ist:
ALL: ALL
Hier verzeichnete Hosts, Domains oder Dienste werden zugelassen. Man kann wahlweise
Dienste sperren oder Hosts und Domains nach IP-Nummer oder Namen. Als Beispiel
eine aktuelle Datei:
# See tcpd(8) and hosts_access(5) for a description.
sshd: ALL : ALLOW
proftpd: ALL : ALLOW
sendmail: ALL : ALLOW
popper: ALL : ALLOW
in.telnetd: localhost : ALLOW
bcpd: e-technik.fh-muenchen.de, ariacenter.rz.fh-muenchen.de : ALLOW
Die Daemons für SSH, FTP, SMTP und POP sind für alle offen, Telnet
ist nur am lokalen Rechner erlaubt (Das ":ALLOW" kann man auch weglassen) und der
Backup-CLient (bcpd) darf nur innerhalb des Fachbereichs und vom
Backupserver des Rechenzentrums angesprochen werden.
Dienst : Rechner : [Kommando]
An Stelle von Dienst kann entweder ein Programmname stehen oder das
Schlüsselwort ALL, falls die Zeile alle Dienste betreffen soll. Einem
ALL kann ein EXCEPT Dienstname folgen, dann sind alle Dienste mit
Ausnahme der benannten gemeint. Per Komma getrennt, lassen sich mehrere Dienste angeben.
Die möglichen Einträge sind Rechnernamen, IP-Adressen oder die folgenden
Schlüsselworte:
%a liefert die IP des rufenden Rechners
%c gibt den Namen des Nutzers und Rechners zurück (sofern ermittelbar)
%d Name des gewünschten Dienstes
%h Name des zugreifenden Rechners oder dessen IP
%n Name des zugreifenden Rechners (oder unknown oder paranoia)
%p Prozessnummer des Dienstes
%s Name des Serves in Verbindung mit dem Rechnernamen
%u Name des Nutzers auf Clientseite, sofern er ermittelt werden kann
1.10 Einen Daemon beschwören
Unter einem Daemon versteht man bei UNIX einen Prozess, der im Hintergrund arbeitet. Bei
Wondows heisst so etwas "Dienst". Es ist oft sinnvoll, Server-Prozesse als Daemon einzurichten.
Programmnamen für Daemons enden in der Regel auf 'd' (sshd, httpd, inetd etc.).
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/fcntl.h>
#include <unistd.h>
int start_daemon(void)
/* macht das ganze daemon-Zeugs */
{
if (chdir ("/tmp") < 0) /* Basisverzeichnis des Daemons */
return -1;
if (setsid ( ) < 0) /* Erzeugen einer neuen Prozessgruppe */
return -1;
umask(0); /* unmask the file mode */
close(STDIN_FILENO); /* Standarddateien schliessen */
close(STDOUT_FILENO);
close(STDERR_FILENO);
/* Standarddateien auf /dev/null umleiten */
open("/dev/null", O_RDWR); /* stdin */
dup(STDOUT_FILENO); /* stdout */
dup(STDERR_FILENO); /* stderr */
/* hier kommt dann die grosse Daemon-Magie */
for(;;)
{
sleep(10);
}
}
int main(void)
{
pid_t pid; /* Prozess-Id des Kindes */
if ((pid = fork()) < 0)
{
perror("fork() ging schief");
return 1;
}
if (pid == 0) /* dies ist der Kindprozess */
{
if (start_daemon() < 0)
{
perror("start_daemon() ging schief");
return 1;
}
}
else
printf("Daemonprozess gestartet, ID %i\n", pid);
return 0;
}
Siehe auch
Linux Daemon Howto.
1.11 Server ganz ohne Programmieren
Schon vor Jahren hat der Programmierer R. Tudorica gezeigt, wie man mit dem Programm Netcat
(nc) und einigen Zeilen Shellscript mal schnell einen Server aufsetzen kann:
#!/bin/bash
while :
do
{ echo -e 'HTTP/1.1 200 OK\r\n' ; cat <Pfad/Datei> } \
| nc -l 8000
done
Wird das Script auf dem eigenen Server gestartet, kann jeder andere, für den der Server
erreichbar ist, mit dem Browser über die URL http://<hostname>:8000
die Datei herunterladen. Das kann man auf dem Server sogar beobachten, da Netcat seine
Status-Infos auf der Standardausgabe ausgibt.
Zum Inhaltsverzeichnis
Zum nächsten Abschnitt
Copyright © Hochschule München, FK 04, Prof. Jürgen Plate
Letzte Aktualisierung: