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:
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.