71. Systemprogrammierung VI: Sockets

Ein Socket dient der Kommunikation zwischen Prozessen. Anders als bei Pipes müssen die verschiedenen Prozesse aber nicht viel gemein haben, insbesondere nicht die Maschine, auf der sie laufen. Netzwerkkommunikation läuft fast immer über Sockets.

Dieses Feld ist weit – wir wollen nur mal sehen, wie man eine Netzwerkverbindung öffnet und damit spielt. Zunächst brauchen wir eine Funktion, die Hostnamen in Internetadressen “auflöst”:

void init_sockaddr(struct sockaddr_in *name,
  char *hostname, unsigned short port)
{
  struct hostent *hostinfo;

  name->sin_family = AF_INET;
  name->sin_port = htons(port);
  if (!(hostinfo = gethostbyname(hostname))) {
    perror(hostname); exit(1);
  }
  name->sin_addr = *(struct in_addr*)hostinfo->h_addr;
}

Die Idee dahinter ist, dass Menschen lieber Namen wie tucana.cl.uni-heidelberg.de haben als Zahlen wie 147.142.207.26. Der Rechner aber braucht die Nummern, um mit dem Rechner sprechen zu können. Für diese “Auflösung” sorgt im Internet ein kompliziertes System namens DNS, auf anderen Netzwerken kann das auch etwas anderes sein, und auch die Adressen können dort anders aussehen als die 32-bit-Zahlen, die das Internet-Protokoll (Version 4) verwendet – schon in der Version 6 des Internet-Protokolls werden 128 bit lange Zahlen verwendet werden.

Wir beschäftigen uns hier nur mit dem “alten” Internet (es ist nach wie vor schwierig, IPv6-Netze zu finden). In diesem Rahmen ist so eine Adresse in einer struct sockaddr_in untergebracht, und die Funktion oben füllt so eine Struktur, die drei Felder hat: sin_family, in der ein Symbol für den “Namensraum” steht, hier nämlich AF_INET (eben das Internet); sin_port, eine Zahl, die einen “Port” auf dem Zielrechner symbolisiert, quasi einen von vielen virtuellen Netzeingängen, die ein Rechner haben kann; und schließlich sin_addr, eben die IP-Nummer des Zielrechners, etwas wie 147.142.207.26.

Zum Setzen des Ports: IP sieht vor, dass jeder Rechner 65536 Ports haben kann – das sollten auch so viele sein, weil jede Netzwerkverbindung in der Regel auf einem eigenen Port läuft. Jedenfalls braucht man zwei Bytes, um das darzustellen, und damit hat man das Problem der Endianness (vgl. Folie “Pointer I”) – am Internet müssen Rechner verschiedener Endianness miteinander reden, und die Bytefolge 00 50 hex würde von den einen als 80 dez, von den anderen als 32768 dez interpretiert werden. Um dem vorzubeugen, reden Rechner am Netz immer in “Network Byte Order”. Die Funktion htons (“Host to Net for Shorts”) besorgt gerade die Wandlung der Endianness der aktuellen Maschine zur Network Byte Order.

Die Funktion gethostbyname nimmt einen String wie tucana.cl.uni-heidelberg.de und gibt einen struct hostent* zurück. Diese hat ein Feld (h_addr), in dem die gesuchte numerische IP-Adresse steht, die wir mit einem passenden Cast in die sockaddr_in-Struktur schreiben können. Damit wissen wir, wohin wir uns verbinden wollen.

Die eigentlichen Sockets müssen zunächst mit der Funktion socket erzeugt und dann – im Beispiel – mit dem Zielrechner verbunden werden. Die Flexibilität der Sockets bringt mit sich, dass dabei viele Parameter im Spiel sind.

Im folgenden Programm ist unser (leicht modifiziertes) select-Beispielprogramm mit Sockets kombiniert. Das Wesentliche steht dabei in der Main-Funktion. Sockets haben dabei wie File Descriptors den Typ int, und wir erzeugen uns zunächst einen Socket. Als Argument übergeben wir PF_INET, was, analog zum AF_INET bei der Namensauflösung, bedeutet, dass dieser Socket ins Internet gehen soll – es wäre auch beispielsweise PF_LOCAL für Sockets auf der lokalen Maschine denkbar, dann müssten aber auch andere “Adressen” (in dem Fall dann ein Pfad im Dateisystem) angegeben werden.

SOCK STREAM bedeutet hier, dass wir einen “abgesicherten Strom” haben möchten, der sich ähnlich wie eine Datei verhält. Auf der Netzwerkseite entspricht das dem TCP (Transmission Control Protocol), das richtige Verbindungen zwischen Rechnern macht – alternativ käme hier etwa SOCK_DGRAM in Frage, das nur einzelne Pakete (“Datagramme”) verschickt und sich nicht weiter kümmert, ob sie auch ankommen; das entsprechende Protokoll auf der Netzwerkseite heißt dann UDP (User Datagram Protocol) und wird beispielsweise gern für Netzwerkdateisysteme wie NFS verwendet.

Das letzte Argument von socket würde die Auswahl verschiedener Protokolle für SOCK_STREAM oder SOCK_DGRAM erlauben – im Internet ist es aber unüblich, etwas von den beiden oben erwähnten Protokollen abweichendes zu verwenden, so dass die Null hier eine sichere Wahl ist.

Die eigentliche Verbindung öffenen wir mit connect, wenn wir aus dem ersten Argument eine numerische Adresse gemacht haben. Die Funktion connect nimmt dazu den Socket, die von sockaddr_in* auf das generischere sockaddr* zurückgecastete Adresse und die Größe der Daten, auf die dieser Pointer zeigt. Danach überlassen wir die Arbeit einer Funktion, die sich eng an unser select-Beispiel anlehnt.

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <netdb.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

#define CP_SZ 80
#define HTTP_PORT 80

int copyChars(int srcfd, int targfd)
{
  static char buffer[CP_SZ];
  int bytesRead;

  if ((bytesRead=read(srcfd, buffer, CP_SZ))==-1) {
    if (errno!=EAGAIN) perror("read");
  } else {
    if (bytesRead==0) {
      exit(0);
    }
    if (write(targfd, buffer, bytesRead)==-1) {
      perror("write");
    }
  }
  return 1;
}


int communicate(int socket)
{
  int srcwr=fileno(stdout), srcrd=fileno(stdin);
  fd_set inSet;

  fcntl(srcrd, O_NONBLOCK);
  fcntl(socket, O_NONBLOCK);
  FD_ZERO(&inSet);
  while (1) {
    FD_SET(socket, &inSet); FD_SET(srcrd, &inSet);
    select(FD_SETSIZE, &inSet, NULL, NULL, NULL);
    if (FD_ISSET(socket, &inSet)) copyChars(socket, srcwr);
    if (FD_ISSET(srcrd, &inSet)) copyChars(srcrd, socket);
    }
  }
  return 0;
}


void init_sockaddr(struct sockaddr_in *name,
  char *hostname, unsigned short port)
{
  struct hostent *hostinfo;

  name->sin_family = AF_INET;
  name->sin_port = htons(port);
  if (!(hostinfo = gethostbyname(hostname))) {
    perror(hostname); exit(1);
  }
  name->sin_addr = *(struct in_addr*)hostinfo->h_addr;
}


int main(int argc, char **argv)
{
  int sock;
  struct sockaddr_in servername;

  if (argc!=2) return 1;
  if ((sock = socket(PF_INET, SOCK_STREAM, 0))<0) {
    perror("open socket"); return 1;
  }
  init_sockaddr(&servername, argv[1], HTTP_PORT);
  if (0>connect(sock, (struct sockaddr*)&servername,
   sizeof(servername))) {
    perror("connect"); return 1;
  }
  communicate(sock);
  close(sock);
  return 0;
}

Mit diesem Programm lassen sich schon ganz lustige Dinge tun, wenn man die Netzwerkprotokolle kenn, die wiederum auf TCP aufbauen. Im Beispiel verbinden wir fest mit Port 80, auf dem normalerweise ein Webserver lauscht (in der Tat heißt die Funktion, mit der man einen Server aufsetzt listen und wird in Servern statt connect verwendet). Im folgenden Beispiel lasse ich mir die Homepage des Instituts geben:

examples> socketdemo www.cl.uni-heidelberg.de
GET / HTTP/1.0

HTTP/1.1 200 OK
Date: Tue, 28 Jan 2003 13:36:58 GMT
Server: Apache/1.3.26 (Unix) Debian GNU/Linux
Connection: close
Content-Type: text/html

<HEAD>
<LINK href="http://www.cl.uni-heidelberg.de/style.css"
...

– in HTTP fragt erwartet der Server zunächst eine Zeile mit einer Anfrage, dann ggf. noch einige Key-Value-Paare (in denen der Client zum Beispiel angeben kann, welche Sprachen er gerne hätte) und dann eine Leerzeile, woraufhin der Server anwortet, wiederum mit einer Kopfzeile, in der das Ergebnis der Operation verkündet wird, einigen Key-Value-Paaren, in denen z.B. verraten wird, welche Sorte von Daten jetzt kommen (hier ist das HTML-Text) und schließlich nach einer Leerzeile wieder die Nutzdaten. Viele Protokolle im Netz sind syntaktisch ähnlich einfach gestrickt. Einen Server oder Client für die Protokolle zu schreiben, ist aber dennoch nicht ganz einfach, weil die Syntax eben nicht alles ist.


Markus Demleitner

Copyright Notice