|
TCP/IP-Sockets: Die FunktionenDa die Programmierung von Client und Server primär in Perl stattfinden wird,erfolgt die Vorstellung der Systemfunktionen recht kurz. Die Ähnlichkeitder später besprochenen Perl-Funktionen und -Methoden mit den C-Systemaufrufeninst jedoch nicht rein zufällig.Kommunikationsendpunkt: socketUm mit Sockets zu arbeiten, muß zuerst eine Verbindung geöffnetwerden. Hier gibt es Analogien zu Dateizugriffen. Der Aufruf socket entspricht einemfopen bei Dateien. Die Funktion socket() hat drei Parameter:int socket(int Family, int Sockettype, int Protocol); - Family legt die Protokoll-Familie fest:
- AF_UNIX : UNIX-interne Protokolle
- AF_INET : Internet-Protokolle
- AF_NS : Xerox-NS-Protokolle
- AF_IMPLINK: IMP-Link-Schicht
Wir werden nur mit AF_INET zu tun haben. - Sockettype legt den Typ des Sockets (und damit auch teilweise das Protokoll) fest (in Klammern die Anwendung für die Familie AF_INET):
- SOCK_STREAM : Stream-Socket (TCP)
- SOCK_DGRAM : Datagramm-Socket (UDP)
- SOCK_RAW : Raw-Socket (IP)
- SOCK_SEQPACKET: Paket-Socket
- SOCK_RDM : Nachrichten-Socket
- Protocol legt das genaue Protokoll fest. Für AF_INET
sind die möglichen Werte:
- IPPROTO_UDP : UDP-Protokoll (SOCK_DGRAM)
- IPPROTO_TCP : TCP-Protokoll (SOCK_STREAM)
- IPPROTO_ICMP: ICMP-Protokoll (SOCK_RAW)
- IPPROTO_RAW : Raw-IP-Protokoll (SOCK_RAW)
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:
#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.
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.
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:
- Server registrieren ihre eigene Adresse innerhalb des Systems.
- Ein Client kann eine spezifische Adresse selbst speichern.
- Ein verbindungsloser Client muß vom System eine individuelle
Adresse anfordern, damit er eine gültige Adresse für die
Rückantwort hat.
bind füllt also im oben angeführten Fünfertupel die
Felder "lokale Adresse" und "lokaler Prozeß".
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:
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 *.
Warteschlange festlegen: listen
Der listen-Aufruf gibt an, wieviele Anfragen gepuffert werden
können. In fast allen Programmen wird hier ein Wert von 5 verwendet
(der derzeitige Höchstwert).
int listen(int sockfd, int backlog);
listen folgt normalerweise nach socket und bind
und unmittelbar vor accept.
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.
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.
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:
#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(IDMySock, 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));
...
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:
#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:
- MSG_OOB : Sende/empfange Out-of-Band-Daten
- MSG_PEEK : Peek auf ankommende Nachricht (recv, recvfrom)
- MSG_DONTROUTE: Leite Routing um (send, sendto)
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.
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
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.
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.
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!).
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!
- unsigned long int htonl(unsigned long int hostlong);
wandelt eine lange Ganzzahl (32bit) von der Rechner-Anordnung ("host
order") in die Netzwerk-Anordnung um.
- unsigned short int htons(unsigned short int hostshort);
wandelt eine kurze Ganzzahl (16bit) von der Rechner-Anordnung
("host order") in die Netzwerk-Anordnung um.
- unsigned long int ntohl(unsigned long int netlong);
wandelt eine lange Ganzzahl (32bit) von der Netzwerk-Anordnung
("host order") in die Rechner-Anordnung um.
- unsigned short int ntohs(unsigned short int netshort);
wandelt einekurze Ganzzahl (16bit) von der Netzwerk-Anordnung
("host order") in die Rechner-Anordnung um.
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):
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.
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.
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.
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.
|
|
|