![]() |
Internet-TechnologieProf. Jürgen Plate |
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:
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 asymetrisch. Vor der Programmierung 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.
Der Client braucht 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 */
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);
/* 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.
Die Systemaufrufe fork(), exec() und wait() haben mit der Generierung von Kindprozessen zu tun und erlauben die Synchronisation zwischen Eltern- und Kindprozessen. An dieser Stelle wird nur soweit darauf eingegangen, wie es zum Verständnis der folgenden Abschnitte nötig ist.

#!/usr/bin/perl -w
# Kindprozess starten
$chld_pid=fork();
if ($chld_pid < 0)
{
die "Fork fehlgeschlagen: $!\n";
}
if($chld_pid == 0) # I am the child.
{
print "CHILD: Here I am.\n";
sleep 1;
print "CHILD: terminating.\n";
exit(1); # Exit-Status 1
}
else # I am the parent
{
print "PARENT: Kind wurde erzeugt. Warte...\n";
waitpid($chld_pid,0);
print "PARENT: Kind terminiert. Exit status: $?\n";
}
waitpid wartet darauf, daß der Kind-Prozeß mit der
angegebenen Prozeß-ID terminiert und nimmt den Rückgabewert
entgegen (in Perl in der Variablen $?). Die Null ist ein
Flag-Byte; hier kann man angeben, ob waitpid auch für
gestoppte Kindprozesse zurückkehren soll, oder ob waitpid
einfach nur nachsehen soll, ob der Kindprozeß mit der angegebenen PID
terminiert hat, ohne zu warten, falls das nicht der Fall war. Daneben
gibt's noch die Funktion wait, die wartet, bis irgend ein
Kindprozeß terminiert, und dann dessen PID zurückgibt.
In welchem Teil sich das Programm befinden, kann wir anhand des Rückgabewerts von fork() festgestellt werden. Beim Kindprozeß ist dieser Null, beim Elternprozeß die PID des Kind-Prozesses. Beide Prozesse haben zunächst denselben Eingabe- und Ausgabekanal, und teilen sich auch alle anderen Filedeskriptoren. Wenn sie nun beide wahllos auf den Ausgabekanal schreiben, werden die beiden Ausgaben einfach durcheinandergemischt. Wenn sie beide von der Eingabe lesen, gewinnt der Schnellere, wenn eine neue Eingabe ansteht. Um nun wirklich kommunizieren zu können, müssen vor dem fork() ein Paar (oder auch mehrere) von zusätzlichen Kanälen geschaffen werde, von denen einer benutzt wird, damit der Elternprozeß Daten an den Kindprozeß senden kann, und ein anderer, damit der Kindprozeß Daten an den Elternprozeß senden kann. Es gibt hierfür zwei verschiedene Systemfunktionen. Die erste heißt pipe() und erzeugt ein Paar von zusammengehörigen Filedeskriptoren, wobei auf dem ersten gelesen und auf dem zweiten geschrieben wird. Der zweite heißt socketpair() und erzeugt zwei Sockets für den gleichen Zweck.
Der fork-Mechanismus löst auf einfache Weise das Problem der Bearbeitung mehrerer paralleler Anfragen. Der Prozeß erzeugt einen Sohn, der auch den Socket erbt, über den die Verbindung zum Client erhalten bleibt. Die Endlosschleife des C-Serverprogramms muß dazu geändert werden:
for (;;)
{
ForeignSocket = accept(MySocket, &AdrPartnerSocket, &len);
if (fork() == 0) /* Das ist der Kindprozess */
{
MsgLen = recv(ForeignSocket, Puffer, MAXPUF, 0);
send(ForeignSocket, Puffer, MsgLen, 0);
close(ForeignSocket);
exit(0); /* Kindprozess wird beendet */
}
close(ForeignSocket); /* der Elternprozess schliesst die Verbindung */
}
Vorteile:
#define SIGHUP 1 // "Auflegen" - z.B. bei einer Terminalleitung #define SIGINT 2 // Interrupt - z.B. Ctrl-C #define SIGILL 4 // Falscher Befehlscode #define SIGBUS 7 // Busfehler #define SIGKILL 9 // "Töten" eines Prozesses #define SIGSEGV 11 // Fehlerhafter Speicherzugriff #define SIGALRM 14 // Timer-Signal #define SIGCHLD 17 // "Vater, eines deiner Kinder ist tot" #define SIGCONT 18 // Prozeß fortsetzen (aus Zustand "stopped") #define SIGSTOP 19 // Prozeß anhalten -> Zustand "stopped"Das Senden eines Signales an einen Prozeß entspricht im Wesentlichen dem Setzen des entsprechenden Bits in einem dafür vorgesehenen Speicherwort des Prozeßkontrollblockes. Der Prozeß kann zu jedem beliebigen Zeitpunkt festlegen, ob beim Empfang eines bestimmten Signales
Was macht man, wenn beispielsweise 25 Kindprozesse aktiv sind, und sich im Prinzip jeder jederzeit beenden kann, man aber nicht die Übersicht verlieren will? Wenn der Kindprozeß stirbt, schickt er dem Elternprozeß ein Signal, SIGCHLD. Solange der Elternprozeß dieses Signal nicht annimmt, kann der Kindprozeß nicht aus der Prozeßtabelle entfernt werden, obwohl es nicht mehr aktiv ist. Solche Prozesse nennt man "Zombie-Prozesse". Erst wenn der Elternprozeß mit waitpid() oder wait() das Signal des Kindes beachtet und dessen Rückgabewert entgegengenommen hat, wird das Kind aus der Prozeßtabelle entfernt. Stirbt hingegen der Elternprozeß und verwaist das Kind, so erbt der Prozeß mit Prozeß-ID 1 - in aller Regel init - diesen Prozeß. Die PPID wird entsprechend abgeändert.
Wenn man für SIGCHLD einen Signalhandler setzt, kann das Problem ganz einfach gelöst werden. Dazu müssen wir uns aber zuerst mit Signalen beschäftigen. Signale sind die wohl einfachste Form der Prozeßkommunikation. Jeder Prozeß kann seinen Kindern und auch allen anderen Prozessen desselben Anwenders Signale schicken. Ein Prozeß mit Root-Rechten kann jedem Prozeß Signale schicken. Wann immer ein Prozeß vom Scheduler aktiviert wird oder vom Aufruf einer Systemfunktion zurückkehrt, wird nachgesehen, ob irgendwelche Signale angekommen sind, und gegebenenfalls die hierfür eingetragenen Signalhandler aktiviert. Man kann alle Signale mit Ausnahme von SIGKILL und SIGSTOP ignorieren. Signalhandler, laufen unter besonderen Bedingungen, weshalb sie so klein und einfach wie möglich gehalten werden sollten.
Sehen wir uns hierzu das folgende Perl-Programm an:
#!/usr/bin/perl -w
my $count = 0;
$SIG{INT} = sub
{
$count++;
warn "Oops! Das ist schon die Unterbrechung $count\n";
};
while ($count < 5)
{
print "Ratzepuehh!\n";
sleep(3);
}
Das Programm gibt alle drei Sekunden "Ratzepuehh!" aus und schläft dann
weiter. Immer wenn die Taste Control-C gedrückt wird, löst dies
einen Interrupt aus. Für diesen Interrupt (Signal INT) wurde ein
Signalhandler installiert, der eine Warnung ausgibt und die ANzahl der Unterbrechungen
hochzählt. Nach mehr als fünf Umterbrechungen beendet sich der Prozess.
Der magische Hash %SIG enthält zu jedem Signal eine Subroutine,
die aufgerufen wird, wenn dieses Signal ankommt. Mit der Anweisung
$SIG{INT} = sub { ... } setzen wir einen eigenen Signalhandler für
das Signal INT. Für die Aktionen eines Signalhandlers bieten sich
folgende Möglichkeiten:
| $SIG{INT} = 'IGNORE'; | Ignoriert SIGINT |
| $SIG{INT} = 'DEFAULT'; | Setzt die Default-Action für SIGINT |
| $SIG{INT} = \&catcher; | führt den Code in sub catcher aus |
| $SIG{INT} = sub { $counter++; }; | führt den Code der anonymen sub aus |
Signale sind asynchrone Ereignisse. Das laufende Programm wird unterbrochen und die Anweisungen im Signalhandler werden ausgeführt. Je nachdem wo sich Ihr Programm im Code gerade befindet, wenn ein Signal eintritt, können unterschiedliche Ereignisse auftreten. Perl ist nicht reentrant, zumindest nicht im Bereich der Low-Level-Systemzugriffe. Wenn ein Signal auftaucht, während Perl seine interne Datenstruktur ändert (z.B. malloc) ist ein Absturz die Regel. Auch deshalb sollten Signalhandler so kurz und einfach wie möglich sein. Probieren wir ein Beispiel (in Perl) mit mehreren Prozessen:
#!/usr/bin/perl -w
$|=1;
my ($i, $pid, $time);
my %child_pids = (); # Hash fuer Prozessnummern
# Signalhandler fuer Childs
$SIG{CHLD} = sub
{
my $pid=wait();
print "Terminated: $pid\n";
delete $child_pids{$pid};
};
# Machen wir mal 10 Kinder
for($i = 0; $i < 10; $i++)
{
$pid = fork();
if($pid == 0) # KIND
{
sleep rand(20);
exit(0);
}
else # ELTERN
{
print "$pid wurde gestartet\n";
$child_pids{$pid} = 1; # merken
}
}
# Warten, bis alle Kinder terminiert sind
$time = 0;
while(0 + keys(%child_pids))
{
print "TIME: $time\n";
sleep 1;
$time++;
}
Der Signalhandler wird jedesmal beim Terminieren eines Kindes aufgerufen, da
der Erlternprozeß ein SIGCHLD-Signal erhält. Der Aufruf
von wait() beseitigt dann alle Spuren des Kindes (wobei hier auch
der Rückgabewert ignoriert wird). wait() wartet ja auf das
Ende eines Kindprozesses und liefert dessen ID zurück.
Man sollte erwarten, daß dieses Programm korrekt arbeitet. Es kommt je nach
Rechner häufiger oder seltener vor, daß der Elternprozeß nicht
mitbekommt, daß ein Kind terminiert ist, und am Schluß ewig wartet.
Das hängt damit zusammen, daß
#!/usr/bin/perl -w
use POSIX ":sys_wait_h";
$|=1;
my ($i, $pid, $time);
my %child_pids = ();
$SIG{CHLD} = sub
{
my($pid);
foreach $pid (keys(%child_pids))
{
if(waitpid($pid,WNOHANG))
{
print "Terminated: $pid\n";
delete $child_pids{$pid};
}
}
};
# Machen wir mal 10 Kinder
for($i = 0; $i < 10; $i++)
{
$pid = fork();
if($pid == 0) # KIND
{
sleep rand(20);
exit(0);
}
else # ELTERN
{
print "$pid wurde gestartet\n";
$child_pids{$pid} = 1; # merken
}
}
# Warten, bis alle Kinder terminiert sind
$time = 0;
while(0 + keys(%child_pids))
{
print "TIME: $time\n";
sleep 1;
$time++;
}
Ein grundsätzliches Problem mit Kindprozessen ist, daß man stets damit
rechnen muß, daß Eltern- oder Kindprozeß aus unterschiedlichsten
Gründen verstirbt, und sei es nur, weil der Anwender ihm ein SIGKILL
geschickt hat. Für einen Elternprozeß ist es relativ einfach, verstorbene
Kinder auszumachen. Verwaiste Kinder werden hingegen nicht per Signal benachrichtigt.
BSD und POSIX-konforme Systeme verfügen über verläßliche Signale. Manche Systeme, z. B. (ältere) System V verfügen über keine zuverlässige Bibliothek zur Signalbehandlung. Für solche Systeme (und ggf. aus Portabilitätsgründen) müssen Sie die Signalhandler nach jedem Auftreten des Signals neu installieren.
#!/bin/perl
# globale Variablen initalisieren
my $sig = '';
# ALLE Signale erhalten einen Signalhandler
@sigs = keys %SIG;
for (@sigs)
{ $SIG{$_} = \&catcher; }
# Signalhandler
sub catcher
{
$sig = shift;
print STDERR "SIGNAL $sig \n";
# reinstall handler
$SIG{$sig} = \&catcher;
}
Für Neugierige gibt es eine Einführung der C-Systemaufrufe zur Behandlung von Prozessen als Hintergrundinformation.
Mit Sockets läßt sich der Austausch von Nachrichten zwischen Prozessen
verbindungsorientiert oder mit Datagrammen recht einfach programmieren. Durch Angabe
eines Sockettyps wird die Art der Kommunikation festgelegt: SOCK STREAM: verbindungsorientiert,
SOCK DGRAM: Datagramm (daneben gibt es noch weitere Typen).
Die kommunizierenden Prozesse können auf demselben Rechner ablaufen oder auf vernetzten
Maschinen. Die Programmierschnittstelle unterstützt verschiedene Protokollfamilien und,
daran gekoppelt, verschiedene Adressierungsarten. Ein Beispiel für eine Adressfamilie ist
AF_UNIX. Sie definiert einen Adressierungsmechanismus für die rechnerinterne Kommunikation
zwischen UNIX-Prozessen. Als Adressobjekte werden Pfade im Dateisystem verwendet, genau wie
bei Pipes. Welche Adressfamilien unterstützt werden, hängt davon ab, welche
Netzwerkprotokolle das Betriebssystem beherrscht. UNIX-Systeme werden zumindest AF_UNIX und
AF_INET unterstützen.
Der socket()-Systemaufruf liefert einen Integerwert zurück, der
einem Dateidescriptor ähnelt. Dieser Wert wird daher "Socketdeskriptor" oder
"sockfd" genannt. Im Fehlerfall hat er den Wert -1. Beispiel:
Jeder eröffnete Socket muß auch wieder geschlossen werden. Eine
Nachlässigkeit an dieser Stelle kann sich bitter rächen, da
insbesondere bei Serverprozessen Verbindungen sehr oft eröffnet werden
und die Systemresourcen für Netzverbindungen irgendwann zur Neige gehen,
was meist zum Stillstand des Servers führt.
Das Schließen des Sockets erfolgt unter UNIX mit dem close-Aufruf.
struct sockaddr ist eine allgemeingültige Datenstruktur, die
für verschieden Protokollfamilien existiert. Bei Verwendung der
Internet-Protokollfamilie kann sie vom Anwenderprogramm überlagert werden
durch eine Struktur sockaddr_in, die ausschließlich für
IP geeignet ist. Unter der Annahme, daß der Anwender eine Variable
vom Typ "sockaddr_in" in der Form struct sockaddr_in adresse;
deklariert hat, enthält "adresse" u. a. die folgenden Komponenten:
Falls die listen()-Warteschlange voll ist, werden weitere Verbindungswünsche
von Clients abgewiesen. Ein Server für Datagramme (UDP) braucht listen() nicht
aufzurufen, da er keine Verbindungen zu Clients einrichtet.
accept generiert (bei einem concurrent server) automatisch einen neuen
Socketdescriptor für die aktuelle Verbindung.
Der Rückgabewert von accept() ist also ein neuer Dateideskriptor, über
den in der Folge die Kommunikation mit dem Client erfolgt (z.B. mit read()
und write()). Der als erster Parameter
angegebene Deskriptor bleibt für weitere Verbindungswünsche reserviert.
Zum Beispiel:
HostID stellt eine 32-Bit-Ganzzahl dar. Im Normalfall liegt die
Host-Adresse natürlich nicht Ganzzahl vor, sondern als Zeichenkette der
Form "www.netzmafia.de" oder "192.168.234.77".
Zu korrekten Umwandlung in eine ganze Zahl, die in Netzwerk-Anordnung
vorliegen muss, stehen in der C-Bibliothek zwei Routinen zur Verfügung,
gethostbyname() und inet_addr(), die weiter unten besprochen werden.
Das folgende Beispiel zeigt den kompletten Verbindungsaufbau eines Clients:
Alle Funktionen liefern als Rückgabewert die Größe der empfangenen bzw.
gesendeten Datenmenge. Die recv-Funktion liefert die Sendung in Blöcken
von maximal 1 KByte Größe. Wurden größere Pakete verschickt,
müssen sie stückweise gelesen werden. Das Senden ist nicht beschränkt.
Da der Rückgabewert nichts über die Grösse des tatsächlich
gesendeten Pakets aussagt, muß dies vom Programm geregelt werden. Wenn die Pakete
nicht immer gleiche Größe besitzen, wird die Paketlänge meist in den
ersten Bytes des ersten Paketes kodiert.
Für den Normalfall (Flags gleich Null) kann statt send() auch die
Systemfunktion write verwendet werden. Zum Beispiel kann statt
Der Rückgabewert von recv() gibt Auskunft über die
tatsächliche Anzahl empfangener Bytes. Ist dieser Wert -1, handelt es sich
um einen Fehler, beim Wert 0 wurde die Verbindung von der Gegenseite geschlossen.
Andernfalls ist der Wert immer größer 0 und kleiner gleich dem Parameter
NBytes.
Die Funktionen sendto() und revcfrom() dienen dazu, Daten
auf einer UDP-Verbindung zu senden und zu empfangen.
Die ersten vier Parameter haben die gleiche Bedeutung wie bei send()
und recv(). Beim vorletzten Parameter von sendto() muss
man einen Pointer auf eine Variable vom Typ struct sockaddr_in, in der
festgehalten ist, wohin das Paket genau gesendet werden soll. Der letzte Parameter
gibt die genaue Länge der vorangegangenen Variablen an. In der Funktion
recvfrom() dient der Parameter From als Platzhalter, in den bei
einem empfangenen Paket Informationen über den Sender gespeichert und an das
Programm zurückgegeben werden.
Wenn Sie für eine UDP-Verbindung vorher connect()aufgerufen haben,
so können Sie einfach send() und recv() verwenden.
Bei Rechnerarchitekturen, bei denen der Speicher nach der Host-Order ausgewertet wird
(das niederwertige Byte also vor dem höherwertigen im Speicher steht), ist es notwendig,
alle Werte mit dem Type LONG oder WORD vor der Übergabe an den Treiber in die
Network-Order zu konvertieren. Um Zahlen der Maschine in die
passende Form für das Netz zu bringen und die Programme portabel zu halten, gibt
es die Makros ntoh() (Net to Host) und hton() (Host to Net). Beide
wirken auf short-Variablen. Für long-Variablen gibt es die
analog funktionierenden Makros htonl() und ntohl(). Vorsicht ist
auch bei Vergleichen geboten: Sie liefern in Network- und Host-Order nicht das gleiche
Ergebnis!
Das Socket-Interface stellt hier eine Reihe von Konvertierungs-Funktionen zur Verfügung.
Die Funktionen inet_addr() und inet_ntoa() weichen etwas von den
anderen ab. Sie wandeln eine Internetadresse, die als String im "dotted quad"-Format
vorliegt, in einen 32-Bit-Wert und umgekehrt.
Um beispielsweise den Port des POP3-Dienstes (110) numerisch an die Struktur
sock_add_in zu übergeben, würde man hton verwenden
(eigentlich sollte man dazu getservbyname verwenden):
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.
Die Funktion getservbyname sucht nach einem Dienst - letztendlich
nach einem Port:
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.
3.3 TCP/IP-Sockets: Die Funktionen
Da die Programmierung von Client und Server primär in Perl stattfinden wird,
erfolgt die Vorstellung der Systemfunktionen recht kurz. Die Ähnlichkeit
der später besprochenen Perl-Funktionen und -Methoden mit den C-Systemaufrufen
ist jedoch nicht rein zufällig.
Kommunikationsendpunkt: socket
Um mit Sockets zu arbeiten, muß zuerst eine Verbindung geöffnet
werden. Hier gibt es Analogien zu Dateizugriffen. Als erstes muß also
ein Socket vom Betriebssystem angefordert werden. Dies geschieht mit dem
socket()-Systemaufruf. Der Aufruf entspricht einem fopen
bei Dateien. Die Funktion socket() hat drei Parameter:
int socket(int Family, int Sockettype, int Protocol);
Wir werden nur mit AF_INET zu tun haben.
#include <sys/types.h>
#include <sys/socket.h>
int MySocket, ForeignSocket;
...
MySocket = socket(AF_INET, SOCK_STREAM, IPPPROTO_TCP);
...
close(MySocket);
Nach Aufruf von "socket" ist der Socket jedoch noch nicht betriebsbereit.
Es muss jetzt noch festgelegt werden, für welchen Port (d.h. für
welches Protokoll der Anwendungsebene) der Socket zuständig sein soll,
ob es sich um einen Server- oder Client-Socket handeln soll, etc.
Socket einrichten: bind
Der Serverprozess muß von außen erreichbar sein. Dazu bekommt
er einen sogenannten well known port. Diese Nummer ist also
den Clientprozessen bekannt. Um einen Socket an diese Nummer zu
binden, wird der bind-Aufruf verwendet. Als Parameter verwendet
bind den Socket und eine Struktur sockaddr_in, die diesen
Port beschreibt.
int bind (int sockfd, struct sockaddr *Myaddr, int Addrlen);
Mit dem Aufruf wird ein Speicherbereich bereitgestellt, der zur Festlegung der
Protokoll-Familie und der Portnummer vorgesehen ist. Bei einem Server-Socket
erfolgt damit die Zuordnung zu dem gewünschten Port - er erklärt sich damit
zuständig für ein bestimmtes Anwender-Protokoll. Der Aufruf von
"bind" ist sowohl bei Datenströmen als auch bei Datagrammen erforderlich.
Der Parameter "sockfd" ist ein Dateideskriptor, der mit einem vorangegangenen
"socket"-Aufruf erzeugt wurde.
Der zweite Parameter ist ein Zeiger auf eine protokollspezifische Adresse und
der dritte Parameter gibt die Größe der Adreßstruktur an.
bind wird in drei Fällen angewendet:
bind füllt also im oben angeführten Fünfertupel die
Felder "lokale Adresse" und "lokaler Prozeß".
adresse.sin_family /* vorzeichenlose 16bit-Ganzzahl (Protokoll-Familie) */
adresse.sin_port /* vorzeichenlose 16bit-Ganzzahl (Portnummer) */
adresse.sin_addr.s_addr /* vorzeichenlose 32bit-Ganzzahl (Internetadresse) */
In die Komponente sin_family wird die Konstante AF_INET (2) eingetragen.
In die Komponente sin_port ist die Portnummer einzutragen - allerdings
in sog. "Netzwerk-Anordnung": Portnummer und Internetadresse sind Zahlen,
die über das Netz verschickt werden und demnach unabhängig von
der internen Zahlendarstellung des jeweiligen Rechners sein müssen
(siehe später).
struct sockaddr_in adresse;
adresse.sin_family = AF_INET; /* Internet-Protokoll-Familie */
adresse.sin_port = htons(80); /* Port festlegen */
adresse.sin_addr.s_addr = 0; /* Internetadresse irrelevant */
int ergebnis = bind(descriptor,(struct sockaddr *)&adresse,sizeof(adresse));
Der "Typecast"-Operator (struct sockaddr *) ist erforderlich, wenn der
Compiler auf strenge Typprüfung eingestellt ist. Die Funktion bind()
erwartet ja einen Zeiger vom Typ struct sockaddr *. Beispiel:
.
.
.
s = socket(AF_INET, SOCK_STREAM, 0);
if (s < 0)
{
fprintf(stderr, "Error: Socket\n");
return -1;
}
my_addr.sin_family = AF_INET;
my_addr.sin_port = htons(4711); /* Portnummer */
my_addr.sin_addr.s_addr = INADDR_ANY; /* An jedem Device warten */
if (bind(s, (struct sockaddr *)&my_addr, sizeof(my_addr)) < 0)
{
close(s);
fprintf(stderr, "Error: bind\n");
return -1;
}
.
.
.
Warteschlange festlegen: listen
Der listen-Aufruf gibt an, wieviele Anfragen gepuffert werden
können. In fast allen Programmen wird hier ein Wert von 5 für
backlog verwendet (der derzeitige Höchstwert).
int listen(int sockfd, int backlog);
listen folgt normalerweise nach socket und bind
und unmittelbar vor accept.
Verbindungswunsch entgegennehmen: accept
Der accept-Aufruf wartet auf eine Anfrage eines Clients.
Der Aufruf von accept() liefert als Rückgabewert die Socket-ID des Partners.
Des weiteren wird per Parameter in einer Variablen der Struktur sockaddr_in
die Adresse des Partners geliefert.
int accept(int sockfd, struct sockaddr_in *Peer, int *Addrlen);
accept nimmt die erste Anforderung von der Warteschlange und generiert einen
weiteren Socket mit der gleichen Eigenschaft wie sockfd.
Der Parameter Peer verweist auf einen Speicherbereich, dessen Inhalt
beim Aufruf undefiniert sein kann. In diesen trägt accept() die Internetadresse
des Absenders eines eintreffenden Verbindungswunsches ein (die Adresse
des Clients). Auf diese Datenstruktur kann genauso zugegriffen werden,
wie dies bereits bei bind() erklärt wurde. Peer und Addrlen
liefern also die Felder "ferne Adresse" und "ferner Prozeß" des Fünfertupels.
Der Parameter "Addrlen" ist die Adresse eines Variablen-Parameters: Vor dem Aufruf muß
dort die maximale Länge des Speicherbereiches stehen, auf den der Parameter Peer
zeigt. Nach dem Aufruf enthält er die Anzahl Bytes, die das Betriebssystem
tatsächlich dort eingetragen hat.
#include <sys/types.h>
#include <sys/socket.h>
int MySocket, ForeignSocket, Partnerlen;
struct sockaddr_in AdrMySock, AdrPartnerSocket;
...
MySocket = socket(AF_INET, SOCK_STREAM, IPPPROTO_TCP);
...
AdrMySock.sin_family = AF_INET;
AdrMySock.sin_addr.s_addr = INADDR_ANY; /* akzept. jeden */
AdrMySock.sin_port = PortNr; /* wird per getservbyname bestimmt */
bind(MySocket, &AdrMySock, sizeof(AdrMySock));
listen(MySock, 5);
for(;;)
{
ForeignSocket = accept(MySocket, &AdrPartnerSocket, &Partnerlen);
...
close(ForeignSocket);
}
...
Nicht vergessen: ForeignSocket muß nach Ende der Kommunikation
geschlossen werden, sonst gehen dem System nach einiger Zeit die Sockets aus.
Clientaufruf: connect
Sobald der Server läuft, kann der Client Verbindung zum well known port
des Servers aufnehmen. Der entsprechende Aufruf lautet connect.
int connect(int sockfd, struct sockaddr_in *ServAddr, int Addrlen);
Der Parameter sockfd ist natürlich wieder der Socket-Deskriptor.
Die weiteren Parameter entsprechen jenen von bind. Für die meisten
verbindungsorientierten Protokolle richtet connect eine Verbindung
vom lokalen zum fernen Rechner ein.
Allerdings muß diesmal in der Datenstruktur, auf die ServAddr zeigt,
die Internetadresse des gewünschten Servers eingetragen werden. Die Verbindung
erfolgt zu dem angegebenen Rechner. Weiterhin ist ein Port anzugeben. Der Ziel-Server
wird durch seine IP-Nummer festgelegt. Diese steht in der Struktur sockaddr_in
im Element sin_addr.
Beispiel:
#include <sys/types.h>
#include <sys/socket.h>
struct sockaddr_in AdrSock;
...
AdrSock.sin_family = AF_INET;
AdrSock.sin_addr = HostID;
AdrSock.sin_port = htons(PortNr);
connect(MySocket, (struct sockaddr *)&AdrSock, sizeof(AdrSock));
...
#include <sys/types.h>
#include <sys/socket.h>
struct sockaddr_in server;
/* Deklaration des Sockets */
int descr = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
/* Datenstruktur "server" vorbereiten */
server.sin_family = AF_INET; /* Internet-Protokoll-Familie */
server.sin_port = htons(80); /* Port 80 festlegen (HTTP) */
/* 1.Versuch die Internetadresse zu ermitteln, Form: "a.b.c.d" */
server.sin_addr.s_addr = inet_addr(server_name);
if (server.sin_addr.s_addr == -1) /* keine korr. Punktnotation */
{
/* 2. Versuch: symbolisch */
struct hostent *host = gethostbyname(server_name);
if (host != NULL)
{
server.sin_addr.s_addr = *((unsigned long*)host->h_addr_list[0]);
}
else
{
printf("Internetadresse nicht gefunden\n");
exit(1);
}
}
/* jetzt kann verbunden werden */
int i = connect(descr, (struct sockaddr *)&server, sizeof(server));
if (i == 0)
{ /* jetzt steht die Verbindung! */
...
}
Datenaustausch: send, sendto, recv und recvfrom
Mit diesen Aufrufen werden Daten über die bestehenden Verbindungen transportiert.
Unter UNIX könnten dafür auch die Dateiaufrufe read und
write verwendet werden.
int send(int sockfd, char *Buffer, int NBytes, int Flags);
int sendto(int sockfd, char *Buffer, int NBytes, int Flags,
struct sockaddr_in *To, int AddrLen);
int recv(int sockfd, char *Buffer, int NBytes, int Flags);
int recvfrom(int sockfd, char *Buffer, int NBytes, int Flags,
struct sockaddr_in *From, int AddrLen);
Die ersten drei Parameter dieser vier Systemaufrunfe sind den ersten drei Parametern von
read und write ähnlich.
Der Parameter sockfd identifiziert wieder den gewünschten Socket,
Buffer ist ein Zeiger auf einen beliebigen Speicherpuffer, NBytes
bestimmt die Anzahl der zu übertragenden Bytes und Flags hat im Normalfall
den Wert Null oder er stellt das das Resultat einer Oder-Verknüpfung mit einer der
folgenden Konstanten dar:
Wird Flags beispielsweise auf den Wert 1 gesetzt (MSG_OOB), dann
soll die Übertragung "out of band" erfolgen. Bei dieser Übertragung
werden nach Möglichkeit bisher bereits abgeschickte Daten überholt.
Es handelt sich dann beispielsweise um hochpriore Informationen, wie beispielsweise
das Abbruchsignal Crtl-C beim Telnet-Protokoll.
send(sock,"Hello World",11,0);
auch wie folgt programmiert werden:
write(sock,"Hello World",11);
Darüberhinaus besteht natürlich die Möglichkeit, eine Datei
für Standard-Ein/Ausgabe über dem betreffenden Deskriptor zu definieren:
FILE *f = fdopen(descr,"rw");
wodurch nun auch mit Routinen der "stdio"-Bibliothek auf den Socket
zugegriffen werden kann, z. B.:
fprintf(f,"Hello World");
Dies ist insbesondere wichtig, wenn die Standardeingabe oder Standardausgabe
eines beliebigen Programmes auf einen Socket umgeleitet werden soll.
Bei Verwendung des Flags MSG_PEEK während des Empfangs werden die
Daten zwar zum Anwenderprogramm übertragen, sie verbleiben jedoch auch noch
in der Empfangswarteschlange, so daß sie mit einem nachfolgenden
recv()-Aufruf nochmals gelesen werden können.
Ruft der Empfänger die recv()-Funktion mit NBytes > 0
auf und stehen im Empfangspuffer bereits Daten bereit (aber weniger als erwartet -
beispielsweise weil der Rest noch nicht angekommen ist), dann kehrt die Funktion
trotzdem sofort zurück und übergibt die tatsächliche Anzahl
der übertragenen Bytes. Erfordert es die Logik des Anwenderprogrammes,
daß vor einer Fortsetzung die Gesamtzahl der erwarteten Bytes eingetroffen
ist, so muß der Aufruf von recv() so lange wiederholt werden, bis
alle Daten eingetroffen sind. Die Daten müssen vom Empfänger in geeigneter
Form zusammengesetzt werden.
Wird recv() mit Flags = 0 aufgerufen, kann stattdessen
die Systemfunktion read() verwendet werden.
Socket schließen: close
Eine bidirektionale Socket-Verbindung kann mit dem Aufruf
int shutdown(int sockfd, int how);
geschlossen werden. Dabei legt der Parameter how fest, ob künftig
keine Daten mehr empfangen werden sollen (how=0), keine mehr gesendet werden
(how=1), oder beides (how=2). Wird statt shutdown() die Systemfunktion
close() benutzt, dann entspricht dies einem shutdown(sock,2).
int close(int sockfd);
Zahlenformat: ntoh und hton
Portnummer und Internetadresse sind Zahlen, die über das Netz verschickt werden
und demnach unabhängig von der internen Zahlendarstellung des jeweiligen Rechners
sein müssen.
Die Reihenfolge der Bytes eines Datenwortes ist auf den verschiedenen Computern
unterschiedlich definiert. So besteht eine Variable vom Typ short aus zwei
Byte. Auf einer Maschine mit Intel-Architektur kommt dabei das niederwerte Byte
zuerst ("little endian"), während es auf einem 68000-Prozessor oder einer Sun
genau umgekehrt ist.
Aus diesem Grund wurde eine eindeutige Netzwerk-Anordnung der
zu übertragenden Bytes definiert (höherwertige Bytes zuerst!).

struct sockaddr_in AdrSock;
...
AdrSock.sin_port = hton(110);
...
Byte-Operationen
In den verschiedenen Socket-Adreßstrukturen existieren unterschiedliche
Byte-Felder, die alle behandelt werden müssen. Einige dieser Felder sind,
wie auch immer, keine C-Integer-Felder, so daß hier andere Techniken
angewandt werden müssen, um mit ihnen allen gleich operieren zu können.
BSD definiert die folgenden drei Routinen, die auf benutzerdefinierten
Byte-Strings basieren. Darunter ist zu verstehen, daß es sich um
keine Standard-Strings in C handelt, die bekanntermaßen mit einem Nullbyte
abgeschlossen werden, sondern die benutzerdefinierten Byte-Strings können
innerhalb des Strings durchaus Nullbytes besitzen. Deshalb muß die Länge
des Strings den Funktionen als Parameter mitgegeben werden.
bcopy (char *Src, char *Dest, int NBytes);
Kopiert NBytes vom Ursprung (SRC) zum Ziel (Dest). Achtung: Parameterreihenfolge
anders als bei strcpy.
bzero (char *Dest, int NBytes);
Schreibt NBytes Null-Bytes an das angegebene Ziel.
int bcmp (char *Ptrl, char *Ptr2, int NBytes);
vergleicht zwei Byte-Strings. der Rückgabewert ist gleich Null, wenn beide
Byte-Strings gleich sind, sonst ungleich Null (also auch anders als bei
strcmp).
Namensauflösung
Computer und Dienste werden unter TCP/IP immer über die IP-Nummern angesprochen.
Für den Menschen ist jedoch ein (Domain-)Name bequemer. Allerdings gibt es für
beides Mechanismen zur Namensauflösung. Im Programm ruft man entsprechende
Funktionen auf.
#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.
Das wichtigste Element der hostent-Struktur ist das Feld h_addr_list,
das in einem Array die IP-Nummer des Rechners enthält. Das Makro h_addr
liefert die Nummer, wie sie in früheren Versionen üblich war. Das Feld
h_length liefert die Größe einer IP-Nummer.
Ein Host kann mehr als einen Namen tragen, denn ein universell einsetzbarer Host
kann mehr als eine Internet-Schnittstelle besitzen, jede mit einer eindeutigen
IP-Adresse. Das folgende Beispiel zeigt die Verwendung der
gethostbyname-Funktion.
/* 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.
#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.

Beispiele:
| 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.
Mit den Perl-Modul IO::Socket::INET kann man relativ einfach Socketverbindungen programmieren. Zum Einbinden reicht der Perl-Befehl use IO::Socket. Die Generierung eines konkreten Sockets geschieht über den Konstruktor von IO::Socket::INET, dessen Argumente darüber entscheiden, ob es ein Server- oder ein Client-Socket wird.
Beispiel für das Einrichten eines Client-seitigen Socket:
my $socket = IO::Socket::INET -> new(PeerAddr => $remote_host,
PeerPort => $remote_port,
Proto => "tcp",
Type => SOCK_STREAM)
or die "Couldn't connect to $remote_host:$remote_port: $@\n";
Beispiel für das Einrichten eines Server-seitigen Socket:
$server = IO::Socket::INET -> new(LocalPort => $server_port,
Type => SOCK_STREAM,
Reuse => 1,
Listen => SOMAXCONN)
or die "Couldn't be a tcp server on port $server_port : $@\n";
Beim Server-Socket braucht man keine Rechneradresse angeben, das ist ja
automatisch die Adresse des Rechners, auf der das Socket läuft; nur
einen Port muß man festlegen.
Wird der Server-Socket nicht ordnungsgemäß geschlossen, so kann
normalerweise derselbe Port nicht sofort wieder benutzt, sondern auf einen
Timeout gewartet werden. Dies kann man allerdings durch Setzen des Parameters
Reuse umgehen, was insbesondere für die Phase der Programmentwicklung
nützlich ist, wo man den Server öfter mal abbricht.
'Listen' gibt an, bis zu wie viele Anfragen in eine Warteschleife
aufgenommen werden sollen. Ist diese Warteschleife voll, so wird die Anfrage
nicht bearbeitet und der Client erhält eine entsprechende Meldung.
SOMAXCONN ist eine Systemkonstante die angibt, wie viele Anfragen
das System maximal in der Warteschleife zulässt.
Auf der Client-Seite ist das Öffnen des Socket praktisch schon alles. Man kann $socket jetzt wie ein Filehandle benutzen, d.h. hineinschreiben, auslesen und es schließen, mit denselben Befehlen, die man auch für Filehandles benutzt. Hier ein Beispiel für ein Client-Programm, das einen Socket einrichtet, in eine Meldung hineinschreibt, die Antwort ausliest und den Socket wieder schließt:
#!/usr/bin/perl -w
use IO::Socket;
use strict;
my $remote_host = "atlas.ee.fhm.edu";
my $remote_port = 2000;
my $socket = IO::Socket::INET->new(PeerAddr => $remote_host,
PeerPort => $remote_port,
Proto => "tcp",
Type => SOCK_STREAM)
or die "Couldn't connect to $remote_host:$remote_port: $@\n";
print $socket "Hallo\n";
my $answer = <$socket>;
print "Receiving: $answer\n";
close($socket);
Der Server wartet gewöhnlich in einer Endlos-Schleife auf hereinkommende
Anfragen. Die Methode dafür ist accept. Solange
keine Anfrage erfolgt, bleibt das Programm beim 'accept'-Aufruf stehen.
Kommt dann eine Anfrage, gibt 'accept' die neue Verbindung zum anfragenden
Client zurück und die Anfrage kann bearbeitet werden. Hier ein Beispiel:
#!/usr/bin/perl -w
use IO::Socket;
use strict;
my $server_port = 2000;
my $server = IO::Socket::INET->new(LocalPort => $server_port,
Type => SOCK_STREAM,
Reuse => 1,
Listen => SOMAXCONN )
or die "Couldn't be a tcp server on port $server_port : $@\n";
while (my $client = $server->accept())
{
# $client is the new connection
my $request = <$client>;
chomp($request);
print "Request: $request\n";
print $client "Selber $request\n";
close($client);
print "\nWaiting for the next connection ...\n\n";
}
close($server);
Beide Programme verlassen sich darauf, daß die zu erwartende Nachricht nur
aus einer Zeile besteht. Sonst müsste man wie bei einem Filehandle auch
eine Schleife über <client> laufen lassen.
$| = 1;
Um andere Handles genauso zu behandeln, bedient man sich eines Tricks. Man wechselt das Handle und STDOUT, so daß die Zuweisung auf das Datei- oder Socket-Handle wirkt. Danach wird wieder die ursprüngliche Zuordnung hergestellt. Dazu wird das Filehandle von STDOUT zwischengespeichert. Den Wechsel erreicht man mit select():
my $old_fh = select(SOCK); # Ungepufferte Ausgabe $| = 1; # fuer SOCK einstellen select($old_fh);
Zum vorhergehenden Abschnitt |
Zum Inhaltsverzeichnis |
Zum nächsten Abschnitt |