TCP/IP-Sockets: Die Funktionen

Willemers Informatik-Ecke

Kommunikationsendpunkt: socket und close

Um mit Sockets zu arbeiten, muss erst eine Verbindung eröffnet werden. Wie man sehen wird, gibt es einige Analogien zu Dateizugriffen. Im Fall des Aufrufes socket wäre dies ein open bei Dateien.

#include 

int IDMySocket, IDPartnerSocket;

  IDMySocket = socket(AF_INET, SOCK_STREAM, 0);
  if (IDMySocket) close(IDMySocket);

Jeder eröffnete Socket muss auch wieder geschlossen werden. Dies ist an sich eine Binsenweisheit. Eine Nachlässigkeit an dieser Stelle kann sich bitter rächen, da insbesondere bei Serverprozessen Verbindungen sehr oft eröffnet werden und das Fehlen weiterer Sockets meist zum Stillstand des Servers führt.

Das Schließen des Sockets erfolgt unter UNIX mit dem close-Aufruf. Dies funktioniert bei anderen Betriebssystemen im Normalfall nicht, da die Integration der Sockets ins System nicht so eng ist. Meist werden Namen wie closesocket, socketclose oder soclose verwendet.

Serveraufrufe: bind, listen und accept

Der Serverprozess muss 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 er den Socket und eine Struktur sockaddr_in, die diesen Port beschreibt.

Der listen-Call gibt an, wieviele Anfragen gepuffert werden können. In fast allen Programmen wird eine 5 verwendet.

Der accept wartet auf eine Anfrage eines Clients. Der Aufruf liefert als Rückgabewert die Socket-ID des Partners. Des weiteren wird per Parameter die sockaddr_in des Partners geliefert.

struct sockaddr_in AdrMySock, AdrPartnerSocket;
...

  AdrMySock.sin_family = AF_INET;
  AdrMySock.sin_addr.s_addr = INADDR_ANY; /* akzept. jeden */
  AdrMySock.sin_port = PortNr;	/* wird per getservbyname bestimmt */
  bind(IDMySocket, &AdrMySock, sizeof(AdrMySock));
  listen(IDMySock, 5);
  do {
    IDPartnerSocket = accept(IDMySocket, &AdrPartnerSocket, &len);

Nicht zu vergessen: die IDPartnerSocket muss nach Ende der Kommunikation geschlossen werden, ansonsten gehen dem System nach einiger Zeit die Sockets aus.

Clientaufruf: connect

Sobald der Server steht, kann der Client Verbindung zum well known port des Servers aufnehmen. Der entsprechende Aufruf lautet connect. Die Verbindung erfolgt einerseits zu einem Rechner, andererseits zu einem festgelegten Port.

Der Server-Computer wird durch seine IP-Nummer festgelegt. Diese ist bekanntlich ein 4-Byte-Wert und steht in der sockaddr_in-Struktur im Element sin_addr.

  struct sockaddr_in AdrSock;
  AdrSock.sin_addr = HostID;
  AdrSock.sin_port = PortNr;
  connect(IDSocket, (struct sockaddr *)&AdrSock, sizeof(AdrSock));

Datenaustausch: send und recv

Mit diesen beiden Aufrufen werden Daten über die bestehenden Verbindungen transportiert. Unter UNIX können dafür auch die Dateiaufrufe read und write verwendet werden. Sofern nicht Funktionen sowohl mit Dateien als auch mit Sockets arbeiten sollen, empfielt es sich aber, bei send und recv zu bleiben. Erstens erkennt man bereits am Aufruf, dass es Netzverbindungen sind, zweitens hat man Vorteile beim Portieren auf andere Plattformen.

Die recv-Funktion liefert als Rückgabewert die Größe des empfangenen Speicherbereichs. Die recv-Funktion liefert die Sendung in Happen von maximal 1KB. Wurden größere Pakete verschickt, müssen sie häppchenweise gelesen werden. Das Senden ist nicht beschränkt.

Da der Rückgabewert nichts über die Grösse des tatsächlich gesendeten Pakets aussagt, muss dies vom Programm geregelt werden. Wenn die Pakete nicht immer gleicher Größe sind, wird die Paketlänge meist in den ersten Bytes des ersten Paketes kodiert.

Namensauflösung

Computer und Dienste werden unter TCP/IP eigentlich mit Nummern angesprochen. Allerdings gibt es für beides Mechanismen zur Namensauflösung. Damit sie auch im Programm Anwendung finden, ruft man entsprechende Funktionen auf.

struct hostent *RechnerID;
struct servent *Service;

  RechnerID = gethostbyname("server");  /* Bestimme den Rechner namens server */
  Service = getservbyname("hilfe","tcp");  /* Bestimme den Port für hilfe */

Das wichtigste Element der servent-Struktur ist das Feld s_port. Hierin befindet sich die Nummer des Ports, wie sie von der Funktion connect verwendet wird.

Das wichtigste Element der hostent-Struktur ist das Feld h_addr_list. Hierin befindet sich das Array der IP-Nummern des Rechners. Das Makro h_addr liefert die Nummer, wie sie in älteren Versionen üblich war. Das Feld h_length liefert die Größe einer IP-Nummer.

Zahlendreher ntoh und hton

Die Bytefolge ist auf den verschiedenen Computern unterschiedlich definiert. So besteht ein short aus zwei Byte. Auf einer Maschine mit Intel-CPU kommt dabei der niederwerte Byte zuerst, während es auf einem 68000 genau umgekehrt ist. In einem heterogenen Netz muss es dafür einen Standard geben. Unter TCP/IP ist das höherwertige Byte zuerst (Big Endian). Um Zahlen der Maschine in die Netzform 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. Um long-Variablen zu bearbeiten, gibt es die analogen Makros htonl und ntohl.

Um beispielsweise den Port des POP3 (110) numerisch in die sock_add_in-Struktur zu drücken, würde man hton verwenden. Selbstverständlich ist es eigentlich sauberer, an dieser Stelle getservbyname zu verwenden, wodurch sich die Notwendigkeit von hton erledigen würde.

struct sockaddr_in AdrSock;
   AdrSock.sin_port = hton(110);


Homepage (C) Copyright 2000 Arnold Willemer