Client-Server Socketprogrammierung

Willemers Informatik-Ecke


Weitere Informationen zur UNIX- und Linux-Programmierung finden Sie im umfassenden Handbuch UNIX.


Spendieren Sie ein Getränk!

2016-02-10
Im Gegensatz zu den anderen Abschnitten der UNIX Programmierung soll die Socketprogrammierung am Beispiel eines Client Socket Paares erläutert werden. Dabei soll zunächst die Struktur erläutert werden und dann in die Details gegangen werden.

Sockets werden wie Dateien behandelt

Die Socketprogrammierung ist die Grundlage der Programmierung verteilter Anwendungen unter TCP/IP in kommerziellen Client Server Architekturen als auch bei Internetanwendungen. Ein Socket (engl. Steckdose) ist ein Verbindungsendpunkt, der vom Programm wie eine gewöhnliche Datei mit read() und write() beschrieben und gelesen werden kann. Ein Socket wird auch mit close() geschlossen. Er wird allerdings nicht mit open() eröffnet, sondern mit dem Aufruf socket(). Auf der folgenden Abbildung sehen Sie auf der linken Seite den Ablauf eines typischen Servers und auf der rechten einen entsprechenden Client. Die Pfeile dazwischen zeigen auf die Synchronisationspunkte. Die Pfeilrichtung soll zeigen, woher die Auflösung des Wartens an dieser Stelle kommt.

Start des Servers

Der Serverprozess muss von außen eindeutig angesprochen werden können. Dazu bindet er sich mit dem Aufruf bind() an einen festen Socket, den so genannten well known port, über den er erreichbar ist. Die Nummer des Ports wird in der Datei /etc/services mit einem Servicenamen verbunden. Im Programm kann der Servicename durch den Aufruf von getservbyname() wieder in eine Nummer umgewandelt werden. Dann bereitet der Server mit listen() den accept() vor. Der Aufruf von accept() blockiert den Prozess bis eine Anfrage kommt. Direkt danach wird der Server read() oder alternativ recv() aufrufen, um den Inhalt der Anfrage zu lesen. Er verarbeitet die Anfrage und sendet die Antwort an den derzeit wartenden Client. Anschließend schleift der Server zum accept(), um auf weitere Anfragen zu warten.

Start des Client

Der Client braucht keinen festen Port. Er holt sich einen normalen Socket, dem vom System eine freie Nummer zugeteilt wird. Der Server erfährt die Nummer des Clients aus der Anfrage und kann ihm unter diesem Port antworten. Im nächsten Schritt ruft der Client connect() auf, um eine Verbindung mit dem Server aufzunehmen, der in den Parametern beschrieben wird. Sobald die Verbindung da ist, sendet der Client seine Anfrage per write() oder alternativ send() und wartet per read() oder recv() auf die Antwort des Servers. Nach dem Erhalt der Daten schließt der Client seine Verbindung.

Übersicht über die Systemaufrufe

Die Tabelle fasst die Systemaufrufe, die die Socketprogrammierung betreffen, zusammen.
Aufruf Zweck
socket Anforderung eines Kommunikationsendpunktes
bind Lege die Portnummer fest
listen Festlegen der Pufferzahl für Anfragen
accept Auf Anfragen warten
connect Verbindung anfordern
send Senden von Daten
recv Empfangen von Daten
close Schließen des Sockets

read oder recv

Werden die Funktionen so geschrieben, dass sie alternativ Dateien oder Sockets bearbeiten können müssen, sollte man zum Senden und Empfangen read() und write() verwenden. Sind die Funktionen aber nur für den Einsatz im Netz, empfielt sich die Verwendung von recv() und send(), da andere Betriebssysteme diese enge Bindung zwischen Datei und Socket nicht kennen und die Benutzung von read() und write() auf Sockets nicht erlauben. Verwendet man recv() und send() ist die Portabilität der Programme höher.

Kommunikationsendpunkt: socket und close

Um mit Sockets zu arbeiten, muss er zuerst geöffnet werden. Die Funktion socket() legt den Socket an, die Funktion close() schließt ihn. Der Aufruf von socket() liefert die Nummer des neuen Socket als Rückgabe. Falls etwas schiefgelaufen ist, liefert der Aufruf -1.

#include <sys/socket.h>

int IDMySocket;

  IDMySocket = socket(AF_INET, SOCK_STREAM, 0);
  ...
  if (IDMySocket>0) 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 statuslosen Serverprozessen Verbindungen sehr oft eröffnet werden und das Fehlen weiterer Sockets zum Stillstand des kompletten Systems führt.

Socket schließen unter UNIX mit close

Das Schließen des Sockets erfolgt unter UNIX mit dem Aufruf von close(). Dies funktioniert bei anderen Betriebssystemen im Normalfall nicht, da die Analogie der Sockets zu Dateien dort nicht existiert. Meist werden von den TCP/IP Bibliotheken Namen wie closesocket, socketclose oder soclose verwendet.

Serveraufrufe: bind, listen und accept

bind

Der Serverprozess muss von außen erreichbar sein. Dazu bekommt er einen so genannten well known port. Diese Nummer ist also den Clientprozessen wohlbekannt. Um einen Socket an diese Nummer zu binden, wird der Aufruf bind() verwendet. Als Parameter verwendet er den Socket und eine Struktur sockaddr_in, die diesen Port beschreibt. Ist alles in Ordnung, liefert bind() als Rückgabewert eine 0, im Fehlerfall eine -1.

listen()

Der Aufruf listen() gibt an, wieviele Anfragen gepuffert werden können, während der Server nicht im accept steht. In fast allen Programmen wird eine 5 als Parameter verwendet, da dies das Maximum einiger BSD-Systeme ist. Auch listen() liefert als Rückgabewert eine 0, wenn alles glatt lief, ansonsten eine -1.

accept()

accept() wartet auf eine Anfrage eines Clients. Der Aufruf liefert als Rückgabewert den Socket, mit dem der Server im weiteren die Daten mit dem Client austauscht. Er verwendet zum Senden also nicht den gebundenen Socket. Im Fehlerfall liefert accept() eine -1.

struct sockaddr_in AdrMySock, AdrPartnerSocket;
...

  AdrMySock.sin_family = AF_INET;
  AdrMySock.sin_addr.s_addr = INADDR_ANY; /* akzept. jeden */
  AdrMySock.sin_port = PortNr; /* 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, obwohl sie nicht explizit geöffnet wurde.

Clientaufruf: connect

connect schlägt die Verbindung zum Server

Sobald der Server durchgestartet ist, kann der Client Verbindung zum well known port des Servers aufnehmen. Der entsprechende Aufruf lautet connect(). 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. Man erhält diese Nummer normalerweise durch einen Aufruf von gethostbyname(). Der zweite Bestandteil ist der Zielport. Diesen ermittelt man durch den Aufruf von getservbyname(). Die Umwandlung von Namen in Nummern wird später näher behandelt.

struct sockaddr_in AdrSock;

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

connect() liefert als Rückgabe eine 0, wenn alles funktioniert und im Fehlerfall eine -1.

Datenaustausch: send und recv

Mit den beiden Aufrufen send() und recv() 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 leichter, dass es Netzverbindungen sind, zweitens hat man Vorteile beim Portieren auf andere Plattformen. Auf anderen Plattformen arbeiten read() und write() ausschließlich auf Dateien. Um aus einem read(), einen recv() zu machen, muss man lediglich einen weiteren Parameter 0 hinten anhängen. Analoges gilt für write() und send(). Die Funktion recv() liefert als Rückgabewert die Größe des empfangenen Speicherbereichs oder eine -1 im Fehlerfall. 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 meist die Paketlänge in den ersten Bytes des ersten Paketes kodiert. Bei der Übertragung mit Zeichenketten ergibt sich die Länge aus dem Zeilenende.

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 die Funktionen gethostbyname() zur Ermittlung einer IP-Adresse anhand des Hostnamen und getservbyname() zur Ermittlung der Servicenummer anhand des Servicenamens auf.

struct hostent *RechnerID;
struct servent *Service;

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

gethostbyname()

gethostbyname() erhält als Parameter einfach die Zeichenkette mit dem Namen des gesuchten Servers und liefert die IP-Nummer in Form eines Zeigers auf eine Struktur hostent. 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.

getservbyname()

Die Funktion getservbyname() liefert für die beiden Zeichenketten, die den Dienst beschreiben einen Zeiger auf eine Struktur namens servent. 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.

Zahlendreher ntoh und hton

Die Bytefolge ist auf den verschiedenen Computern unterschiedlich definiert. So besteht eine Variable vom Typ short aus zwei Byte. Auf einer Maschine mit Intel-CPU kommt dabei das 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\gpFussnote{Der Legende nach stammt diese Bezeichnung aus dem Buch >>Gullivers Reisen<<, in dem sich zwei Völker darüber zerstreiten, ob man das Ei am dicken Ende (big end) oder am dünnen Ende (little end) zuerst aufmacht.}). 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) in die sock_add_in-Struktur zu schreiben, würde man hton verwenden. Wird an dieser Stelle getservbyname verwendet, erledigt sich die Notwendigkeit von hton.

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

Rahmenprogramm eines Client-Server Paars

Der Server beantwortet in einer Endlosschleife Clientanfragen. Bevor er in diese Endlosschleife kommt, muss er seinen Dienst anmelden. Er blockiert erstmals beim accept(), der durch den connect() des Clients freigegeben wird. recv() führt zwar kurzfristig zum Blockieren des Prozesses, aber da der Client sofort seine Anfrage senden wird, ist dies nur von kurzer Dauer. Er sendet die Antwort und wendet sich dem nächsten Anfrager zu.
#include <sys/types.h>
#ifdef WIN32
#include <winsock.h>
// link with Ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netdb.h>
#include <sys/signal.h>
#include <unistd.h>
#define closesocket close
#endif

#define MAXPUF 1024
#define WELLKNOWNPORT 5000

#include <iostream>

int main()
{
#ifdef WIN32
    // Windows muss da erst initialisieren!
    WSADATA wsaData;
    int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed: %d\n", iResult);
        return 1;
    }
#endif

    char Puffer[MAXPUF];
    int IDMySocket = socket(AF_INET, SOCK_STREAM, 0);
    if (IDMySocket == 0)
    {
        std::cout << "socket liefert 0 "  << std::endl;
        return 1;
    }
    // Socket-Parameter festlegen
    struct sockaddr_in AdrMySock = { 0 };
    AdrMySock.sin_family = AF_INET;
    AdrMySock.sin_addr.s_addr = INADDR_ANY; // akzept. jeden
    AdrMySock.sin_port = htons(WELLKNOWNPORT);
    // Die Partner-Socket-Struktur wird bei accept ermittelt
    struct sockaddr_in AdrPartnerSocket;
    // int AdrLen = sizeof(struct sockaddr);
    socklen_t AdrLen = sizeof(struct sockaddr);

    // Das Casting auf (sockaddr*) ist bei Windows erforderlich
    int error = bind(IDMySocket, (sockaddr*)&AdrMySock, sizeof(AdrMySock));
    if (error<0)
    {
        std::cout << "bind: " << error << std::endl;
    }
    error = listen(IDMySocket, 5);
    if (error<0)
    {
        std::cout << "listen: " << error << std::endl;
    }
    do {
        // Das Casting auf (sockaddr*) ist bei Windows erforderlich
        int IDPartnerSocket = accept(IDMySocket, (sockaddr*)&AdrPartnerSocket, &AdrLen);
        int MsgLen = recv(IDPartnerSocket, Puffer, MAXPUF, 0);
        if (MsgLen>0)
        {
            // tu was mit den Daten
            Puffer[0] = 'A'; // nur zum Test
        }
        send(IDPartnerSocket, Puffer, MsgLen, 0);
        closesocket(IDPartnerSocket); // Wichtig! Sonst gehen die Sockets aus
    } while (1); // bis zum St. Nimmerlein
    closesocket(IDMySocket);
}

Das Listing wurde ein klein wenig aktualisiert, weil sich ein paar Kleinigkeiten am Syntax von C und in den includes geändert haben. Die Ausgaben (std::cout, std::endl, include <iostream>) sind zwar C++. Der Rest ist aber noch reines C. Da einige Leute auch unter Windows programmieren müssen, sind die Besonderheiten von Windows mit ifdef WIN32 eingebaut.

Sequentielles Abarbeiten

Dieser Server bearbeitet nacheinander jede Anfrage, die über den Port >>hilfe<< 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. Der zugehörige Client bereitet die Verbindung in der Variablen AdrSocket vor und ruft damit die Funktion connect() auf. Diese blockiert bis der Server auf der anderen Seite den accept() aufgerufen hat. Der Client fährt fort, indem er seine Anfrage sendet. Der Sendevorgang blockiert nie, dafür aber der anschließende Empfang der Antwort. Sobald der Server seine Antwort gesendet hat, kann der Client sich beenden.
#include <sys/types.h>
#ifdef WIN32
#undef UNICODE
#include <ws2tcpip.h> // getaddinfo
#include <winsock2.h>
// link with Ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netdb.h>
#include <unistd.h>
#define closesocket close
#include <string.h> // memcpy
#endif

#include <iostream>

#define MAXPUF 1024
#define WELLKNOWNPORT 5000
#define ZIELSERVER "127.0.0.1"

int main()
{
    struct sockaddr_in AdrSock;

    // struct servent *Service; // für Zugriff auf die /etc/services
    char Puffer[MAXPUF];

    int error;
#ifdef WIN32
    // Windows muss da erst initialisieren!
    WSADATA wsaData;
    int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != 0) {
        printf("WSAStartup failed: %d\n", iResult);
        return 1;
    }
    AdrSock.sin_addr.s_addr = inet_addr(ZIELSERVER);
#else
    struct hostent *RechnerID;
    RechnerID = gethostbyname(ZIELSERVER);
    if (RechnerID == 0)
    {
        std::cout << "Rechner " << ZIELSERVER << " unbekannt" << std::endl;
        return 1;
    }
    // bcopy(src, dest, len) -> memcpy(dest, src, len)
    memcpy(&AdrSock.sin_addr, RechnerID->h_addr, RechnerID->h_length);
#endif

    // Bestimme Port per Name aus der /etc/services
    //Service = getservbyname("servicename", "tcp");
    //AdrSock.sin_port = Service->s_port;
    // ... oder direkt ...
    AdrSock.sin_port = htons(WELLKNOWNPORT);
    AdrSock.sin_family = AF_INET;

    int IDSocket = socket(AF_INET, SOCK_STREAM, 0);
    if (IDSocket <= 0)
    {
        std::cout << "Socketfehler - IDSocket: " << IDSocket << std::endl;
    }
    error = connect(IDSocket, (struct sockaddr *)&AdrSock, sizeof(AdrSock));
    if (error<0)
    {
        std::cout << "connect-Fehler" << std::endl;
    }
    error = send(IDSocket, "Uhu", 4, 0);
    if (error<0)
    {
        std::cout << "send-Fehler" << std::endl;
    }
    int len = recv(IDSocket, Puffer, MAXPUF, 0);
    std::cout << Puffer[0] << "/" << len << std::endl;
    closesocket(IDSocket);
}

Variablen

Es gibt zwei Variablen pro Socket. Die eine ist wie bei Dateizugriffen ein einfaches Handle (hier mit ID gekennzeichnet), die andere hält die Adresse der Verbindung, also die Internet-Nummer des Rechners und die Nummer des Ports. Der Server legt die Nummer des Rechners nicht fest, indem die Konstante INADDR_ANY benutzt wird. Der Client dagegen gibt die Adresse des anzusprechenden Servers an. Die Funktion recv() liefert als Rückgabewert die Größe des versendeten Speicherbereichs. Die Funktion recv() liest die Sendung in Paketen von maximal 1KB. Wurden größere Pakete verschickt, müssen sie häppchenweise gelesen werden. Das Senden ist nicht beschränkt.

Parallelität

Der Server wird ergänzt, damit er die Vorteile einer Multitaskingumgebung nutzen und mehrere Anfragen parallel abarbeiten kann. Dazu muss an passender Stelle ein fork() eingebaut werden:

  do {
    IDPartnerSocket = accept(IDMySocket,
                             &AdrPartnerSocket, &len);
    if (fork()==0) {
      MsgLen = recv(IDPartnerSocket, Puffer, MAXPUF, 0);

      /* tu was mit den Daten */

      send(IDPartnerSocket, Puffer, MsgLen, 0);
      close(IDPartnerSocket);
      /* Sohn toetet sich selbst */
      exit(0);
    } /* if fork.. */
    close(IDPartnerSocket); /* der Vater schliesst Verbindung */
  } while(1);

Parallelverarbeitung mit wenig Aufwand

Man sieht, mit welch geringer Änderung ein multitaskingfähiger Server zu realisieren ist. Beim Aufruf von fork() wird von dem Prozess ein Kindprozess gebildet, der alle Ressourcen des Vaters besitzt. So kann er die Verbindung mit dem Anfrager weiter bearbeiten. Er tritt vollkommen an die Stelle des Vaters, der seinerseits die Verbindung schließen kann und auf eine neue Anfrage wartet. Sobald diese eintrifft, wird wieder ein Kind generiert, der gegebenenfalls parallel zum anderen Kind arbeitet, falls jenes noch nicht fertig ist.

Statusloser Server

Der Server ist so, wie er nun vorliegt, ein statusloser Server (stateless server). Das bedeutet, er kann sich den Stand einer Kommunikation nicht merken. Fragt derselbe Client noch einmal an, wird er ihn anonym wie eine völlig neue Anfrage behandeln. In dieser Art arbeitet ein Webserver. Jede Anfrage ist für ihn neu. Andere Server, beispielsweise POP3-Server, halten die Verbindung mit ihrem Client solange aufrecht, bis beide ein Ende der Verbindung vereinbaren. In solch einem Fall würde eine Schleife im Sohnprozess über recv(), Verarbeitung und send() laufen, bis ein definiertes Ende der Kommunikation stattfindet. Natürlich würde dann auch der Client eine Schleife haben, die erst bei Einigung über den Verbindungsabbau beendet wird.

Zombies vereiteln

Wie im Zusammenhang mit den Signalen gezeigt, sollte die Entstehung von Zombies verhindert werden. Zombies entstehen, wenn ein Sohn endet, aber der Vater nicht auf ihn wartet. Dadurch bleibt ein Eintrag in der Prozesstabelle mit dem Exitwert des Sohnes. Der Aufwand ist denkbar gering:

signal(SIGCLD, SIG_IGN);

Mehrere Sockets parallel abfragen

Ein anderes Thema der Socketprogrammierung ist die parallele Bearbeitung mehrerer Ports durch einen einzigen Prozess. Ein Beispiel für ein solches Programm ist der inetd, der so genannte Internetdämon, der für alle in der inetd.conf aufgeführten Ports die Anfragen entgegen nimmt und den benötigten Server aufruft.

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int MaxHandle}, fd_set *Lesen,
           fd_set *Schreiben},  fd_set *OutOfBand,
           struct timeval *TimeOut);

Zentrale Objekte sind in diesem Zusammenhang die Dateideskriptorenarrays. Von diesen nimmt select() drei an. Da gibt es je einen Satz zum Lesen, zum Schreiben oder um Ausnahmen, wie Nachrichten außerhalb der Reihe zu beobachten. Will man nicht alle Kategorien beobachten, gibt man bei den uninteressanten Parametern NULL an. Sollen die Socket a und b zum Lesen beobachtet werden, dann muss ein fd_set angelegt und damit gefüllt werden:

fd_set lesesockets;

  FD_ZERO(&lesesockets);
  FD_SET(a, &lesesockets);
  FD_SET(b, &lesesockets);

  maxHandle = max(a, b) + 1;

  select(maxHandle, &lesesockets, NULL, NULL, NULL);

select() blockiert so lange, bis auf einem der Sockets Daten ankommen. Soll ein Timeout definiert werden, also eine Zeitspanne, nach der aufgegeben werden soll, so muss der letzte Parameter besetzt werden.

struct timeval myTime;
  ...
  myTime.tv_sec = 0;
  myTime.tv_usec = 100;

  select(maxHandle, &lesesockets, NULL, NULL, &myTime);

Ersatz für sleep

Die Fähigkeit von select(), einen Timeout im Millisekundenbereich festzulegen, wird manchmal dazu missbraucht, um einen Prozess für kurze Zeit schlafen zu legen. Die eigentlich dafür zuständige Funktion sleep() legt einen Prozess für mindestens eine Sekunde schlafen, was manchmal zu viel ist.

IPv6 aus Programmierersicht

Die kommende Generation der IP-Adressen, die nun 16 statt 4 Byte als Adressen verwenden, haben natürlich auch Konsequenzen in der Programmierung. Dabei gibt es neue Konstanten, Strukturen und Funktionen, die anstelle der alten Versionen eingesetzt werden müssen. An den Konzepten, wie Client- und Serverprogramme strukturiert sind, ändert sich nichts. Insofern sind die Administratoren von den Neuerungen stärker betroffen als die Programmierer. Es sind die neue Adressfamilie AF_INET6 statt AF_INET definiert. Statt der Struktur in_addr gibt es die Struktur in6_addr, die wie folgt definiert wird:

struct in6_addr {
    uint8_t s6_addr[16];

Durch diese Änderung ist dann auch die Struktur sockaddr_in betroffen, die nun als sockaddr_in6 folgendermaßen definiert ist:

struct sockadd_in6 {
    sa_family_t        sin6_family;
    in_port_t          sin6_port;
    uint32_t           sin6_flowinfo;
    struct in6_address sin6_addr;
}

Die Umsetzung zwischen Hostname zu IP-Nummer wird nicht mehr mit den Funktionen gethostbyname() bzw. gethostbyaddr(), sondern durch die neuen Funktionen getnodebyname() bzw. getnodebyaddr() erledigt.\gpFussnote{vgl. Santifaller, Michael: TCP/IP und ONC/NFS - Internetworking mit UNIX. Addison-Wesley, 1998. S. 365}

Client-Server aus Sicht der Performance

Das Ziel einer guten Client Server Architektur besteht darin, die Leistung, die im lokalen Rechner ansonsten brach läge, für Dinge zu nutzen, die nicht zentral ablaufen müssen und damit den zentralen Rechner zu entlasten.

Das Modell Terminal

Betrachtet man eine Software, die von mehreren Anwendern gleichzeitig benutzt werden soll, ergeben sich drei Architekturmodelle. Die klassische Variante stellt jedem Teilnehmer ein Terminal zur Verfügung und der Zentralrechner führt alle Anforderungen auf seinem zentralen Prozessor und seiner Platte aus. Dazu gehört auch die Benutzerführung, beispielsweise das Aufbauen der Masken. Damit belastet auch das Navigieren im Programm auf der Suche nach der richtigen Maske die Allgemeinheit.

Das Modell Plattenserver

Bei der Lösung mit einem Plattenserver wird die Prozessorlast auf die Arbeitsplätze verteilt. Da der Server keine Eigenintelligenz mitbringen muss, reicht normalerweise ein handelsüblicher PC. Man übersieht allerdings leicht, dass alle Plattenzugriffe auch das Netz belasten, das typischerweise langsamer als der Plattenzugriff ist. Soll beispielsweise nach einem bestimmten Kunden gesucht werden, erfolgt bei einem sortierten oder indizierten Datenbestand eine binäre Suche. Das bedeutet, der erste Zugriff erfolgt auf den mittleren Datensatz. Ist der gefundene Name im Alphabet höher, so wird die untere Hälfte halbiert, ansonsten die obere Hälfte. Auf diese Weise findet man mit 10 Zugriffen den richtigen Satz in 1024 Sätzen. Für 2048 Sätze braucht man 11 Zugriffe, für 4096 12 und so weiter. Diese Zugriffe laufen aber alle über das Netz und bei einer hohen Netzbelastung wird das Gesamtverhalten in erhebliche Mitleidenschaft gezogen.

Suche nach dem Teilungspunkt

Eine Client-Server Lösung kann an einer beliebigen Stelle geteilt werden. Man wird den Teilungspunkt oberhalb der binären Suche ansetzen, so dass die Zugriffe der binären Suche lokal auf dem Server stattfinden und so das Netz nicht belasten. Auf der anderen Seite wird man die Benutzersteuerung auf dem lokalen Arbeitsplatz belassen, so dass nur schlanke Pakete mit Anfragen an den Server gerichtet werden.

Diese Seite basiert auf Inhalten aus dem Buch Arnold Willemer: UNIX. Das umfassende Handbuch
Verlagsrechte bei galileo computing


Homepage (C) Copyright 2002, 2016 Arnold Willemer