Die Steckdose für das Internet (3): Grundlagen des Socket-lnterface

Ein Socket enthält Informationen über die beiden Endpunkte einer Kommunikation. Im Falle des Internets ist das jeweils die Kombination aus IP-Adresse und Portnummer für den eigenen sowie den entfernten Rechner.

Die Frage ist nun, wie mit einem Socket umzugehen ist, um Daten über das Internet zu transportieren.

Prinzipiell arbeiten Sie mit Sockels wie mit den gewohnten Dateien, d.h. Sie können sie öffnen und schließen sowie daraus lesen und hineinschreiben. Sie legen ein Socket mit der Funktion

int socket(int af, int type, int protocol);

an. Für den ersten Parameter wird im Falle des Internets immer die Konstante AF_INET angegeben. Es folgt der Typ des Sockels: SOCK_STREAM oder SOCK_DGRAM. Diese beiden Konstanten beslimmen, ob man einen Sockel zur Kommunikalion über einen Stream (=TCP) oder über Datagramme (=UDP) erzeugen will. Daraus leitet sich dann das zu verwendende Protokoll für das Socket-Interface automatisch ab, so dass als dritter Parameter immer IPPROTO_IP übergeben werden kann. Zurückgeliefert wird ein Socket-Handle, mit dem Sie wie bei Dateioperationen den Socket für alle anderen Funktionen referenzieren.

Fest verbunden

Lassen Sie uns zunächst die Kommunikation via TCP betrachten. Der nächste Schritt nach dem Aufruf von socket() ist die Verbindungsaufnahme zu einem anderen Rechner mit der Funktion

*int connect(int s, const void addr, int addrlen);

Sie erwartet als ersten Parameter das Sockel-Handle und als zweiler die Zieladresse in Form der aus dem vorangegangen Teil bekannten sockaddr_in-Struktur mit der IP-Adresse und der Porlnummer des entfernten Rechners. Der letzte Parameter gibt die Größe der übergebene Struktur an und ist immer sizeof(sockaddr_in). Die Funktion connect() stellt den lokalen Anschluß des Sockels ein, indem sie selbständig eine gerade freie Portnummer ab 1024 zuweist. Ist die TCP-Verbindung zustande gekommen (Rückgabe E_OK), kann Ihr Programm beginnen, Daten an den entfernten Rechner zu senden oder von ihm zu empfangen. Gesendel wird grundsätzlich mit der Funktion

*int send(int s, const void msg, int len, int flags);

Parameter sind das Sockel-Handle, ein Zeiger auf die Nachricht und die Länge (in Bytes) der Nachricht. Der letzte Parameter wird normalerweise auf 0 gesetzt. Die Funktion liefert die Anzahl der übernommenen Bytes oder einen negativen Wert, der einen Fehler signalisiert, zurück. Da das Socket-Interface zunächst die Daten in einen internen Puffer kopiert, muss bei Rückkehr der Funktion die Nachricht noch nicht gesendel sein. Der Empfang wird über die Funktion

*long recv(int s, void buf, long len, int flags);

realisiert, wobei wieder das Socket-Handle sowie die Adresse des Empfangspuffers und dessen Länge übergeben werden. Der letzte Parameter wird in der Regel auf 0 gesetzt. Alternativ können Sie ihn auch auf MSG_PEEK setzen, wenn Daten zwar in den Empfangspuffer kopiert werden, aber nicht aus dem internen Empfangspuffer des Socket-Interface gelöscht werden sollen. Die Nachricht steht dann auch beim nächsten Aufruf erneut zur Verfügung. Die Rückgabe der Funktion ist die Anzahl der aus dem internen Empfangspuffer kopierten, und damit die zum Zeitpunkt des Aufrufs der Funktion von dem entfernten Rechner empfangenen, Bytes. Sie wartet also nicht, bis eine der Größe des übergebenen Puffers entsprechende Anzahl von Zeichen vorliegen. Sind es dagegen mehr, wird Ihr Puffer komplett gefüllt, und die noch verbleibenden Daten erhalten Sie erst beim nächsten Aufruf von recv(). Im Gegensatz zu send() kehrt recv() aber auch erst dann zurück, wenn Daten im internen Empfangspuffer vorliegen.

Telegramme

Für die Kommunikation via UDP ist die Vorgehensweise nach dem Anlegen des Socket mittels socket() etwas anders. Während bei TCP connect() den lokalen Anschluß im Socket einstellt, müssen sie hier mit der Funktion

*int bind(int s, const void addr, int addrlen);

selbst Hand anlegen. Sie erwartet neben dem Socket-Handle einen Zeiger auf die uns mittlerweile wohlbekannte sockaddr_in-Struktur, allerdings diesmal mit der IP-Adresse und Portnummer des lokalen Rechners. Wird als Portnummer ein Wert kleiner 0 eingetragen, weist das Socket-Interface dem Socket selbständig eine Portnummer ab 1024 zu.

Über den so lokal gebundenen Socket lassen sich dann Daten mit einer erweiterten Form von send() - nämlich sendto() - versenden:

**int sendto(int s, const void msg, int len, int flags, void to, int tolen);

Als zusätzlichen Parameter erwartet sendto() eine sockaddr_in-Struktur für das Ziel der Daten. Das Funktionsergebnis entspricht der Anzahl der gesendeten Bytes. Es liegt in der Natur von UDP, dass Ihre Daten beim entfernten Rechner nicht unbedingt in dieser Anzahl und vor allem in der selben Form ankommen. Für fehlerfreie Kommunikation via UDP müssen Sie selbst geeignete Maßnahmen ergreifen...

Ende der Durchsage

Ist die Kommunikation abgeschlossen, wird der jeweilige Socket geschlossen. Bei TCP wird der Socket zuerst noch für das Senden und Empfangen von Daten mit der Funktion

int shutdown(int s, int how);

gesperrt ( how=2). Falls how 0 ist, wird nur Empfang gesperrt, bei l nur das Senden. Schließlich wird der Socket mit

int sclose(int s);

geschlossen. Analog zum Schließen einer Datei wird auch das Socket-Handle ungültig.

typedef struct {
char    *s_name;         /* Offizieller Name des Dienstes */
char    **s_aliases;  /* Zeiger auf Array mit alternativen Namen */
int     s_port;           /* Portnummer */
char    *s_proto;        /* Protokoll */ }servent;

Ein Beispiel

Ich möchte Ihnen die Anwendung der oben angesprochenen Funktionen an einem der vielen Dienste, die üblicherweise von einem unter Unix laufenden Rechner zur Verfügung gestellt werden, demonstrieren. Diese Standarddienste sind üblicherweise in den berüchtigten RFCs (Request for Comments) definiert. Hier wollen wir uns den Echo-Service (RFC 862) näher anschauen. Diesen Service gibt es in zwei Varianten, je nachdem ob der Service über TCP oder UDP genutzt wird. Im Falle von TCP wartet der Server, bis eine Verbindung zu einem Client zustande gekommen ist und sendet anschließend bis zum Schließen der Verbindung alle empfangenen Daten an den Client zurück. Bei UDP entfällt naturgemäß das Warten auf die Verbindung. Der Server sendet ein empfangenes Datagramm schlicht an den Client zurück.

Bei Nutzung des Echo-Service ist die verwendete Portnummer durch die RFC gewissermaßen festgelegt. Anders sieht es aus, wenn Sie etwa für ein Intranet selbst einen Dienst entwickeln. Würden Sie nach Entwicklung der entsprechenden Programme die Portnummer aus irgendwelchen Gründen ändern wollen, müßten Sie jedesmal alle Programme neu compi-lieren. Praktikabler ist es, die zu einem bestimmten Dienst gehörige Portnummer in einer Datei zu definieren, die dann wesentlich einfacher an neue Verhältnisse anpaßbar ist. Das Sockel-Interface benutzt dafür die Datei \etc\services. Die zur Ermittlung der Portnummer vorgesehene Funktion

**servent *getservbyname(const char name, const char proto);

ist gethostname() recht ähnlich. Beim Aufruf wird ihr die Adresse einer Zeichenfolge mit dem Namen des Dienstes "echo" und den Namen des Protokolls "tcp" oder "udp" übergeben.

Die Rückgabe ist ein Zeiger auf eine servent-Struktur (siehe Listing 1).

In der Komponente s_port; steht dann auch der von uns gesuchte Port. Falls der Dienst nicht bekannt ist, ist die Rückgabe ein Nullzeiger.

Zurück zu unserem Beispiel: Nach der IConnect-spezifischen Initialisierung des Socket-Interface wird die Portnummer des Echo-Services mit der gerade vorgestellten Funktion getservbyname() ermittelt. Anschließend wird das DNS bemüht, um die IP-Adresse des beim Aufruf übergebenen Hosts zu bekommen. Existiert zum Host eine IP-Adresse, wird mit socket() entweder ein Socket für TCP oder UDP angelegt.

Weiter geht es bei TCP mit dem Versuch, eine Verbindung zu dem Host mittels connect() aufzubauen. Gelingt das, wird eine Nachricht mittels send() in den Socket geschrieben und anschließend mit recvQ auf die Antwort gewartet. Abschließend wird die Verbindung mittels shutdownQ heruntergefahren und der Socket mit sclose() geschlossen.

Das UDP-Beispiel stellt nach dem Anlegen des Socket zuerst einmal mit bind() den lokalen Anschluß ein. Hierfür wird die eigenen IP-Adresse mit gethostid() ermittelt. Die Zuteilung der Portnummer wird dem Sockel-Interface überlassen. Mit sendto() wird die Nachricht in den Socket geschrieben und danach mit recv() wiederum auf die Antwort gewartet. Zu guter Letzt wird der Socket mit scloseQ wieder geschlossen.

Am Ende diesen Teils kennen Sie nun bereits alle grundlegenden Socket-Funktionen, die für die Entwicklung von Internet-Anwendungen - wie etwa eMail-TNews-Frontends, Telnet-Clients oder auch eines WWW-Browsers - notwendig sind.

In der nächsten Folge wenden wir uns der Gegenstelle zu und werden uns damit beschäftigen, wie ein Programm an einem bestimmten Port lauscht, um von außen eine Verbindung entgegenzunehmen. Diese Funktionalität wird natürlich vor allem - aber nicht nur - von Programmen benötigt, die für andere einen Dienst zur Verfügung stellen. Bis dahin wünsche ich viel Spaß bei den ersten Programmierversuchen zum Thema Internet.


Jürgen Koneczny
Aus: ST-Computer 03 / 1999, Seite 42

Links

Copyright-Bestimmungen: siehe Über diese Seite