|
Die Socket-NetzwerkschnittstelleAnfang der 80er Jahre wurde mit 4.2BSD in UNIX-Systemen die sogenannte "Socket"-Schnittstellefür die Kommunikation zwischen Prozessen eingeführt. Ein "Socket" ist dabei der Name für einen Endpunkt einer Kommunikationsverbindung. Seine Schnittstelle ist im wesentlichen konzipiert für:- verbindungsorientierte Kommunikation, aufsetzend auf TCP (SOCK_STREAM)
- verbindungslose Kommunikation, aufsetzend auf UDP (SOCK_DGRAM)
- direkten Zugriff auf die IP-Schicht (SOCK_RAW)
- Kommunikation zwischen lokalen Prozessen (AF_UNIX)
- Kommunikation zwischen im TCP/IP-Netz verteilten Prozessen (AF_INET)
- Kommunikation für andere Protokoll- und Adressierungsfamilien
Dabei gibt es normalerweise ein Programm, das Anfragen von anderen Programmenentgegennimmt und sie beantwortet (ein sog. Server-Socket) und beliebigviele andere Programme, die ihre Anfragen an das Server-Socket schickenund mit den Antworten weiterarbeiten (die sog. Client-Sockets). Das ganzeSystem ist auch bekannt als Client-Server-Programmierung.Ein sehr typisches Beispiel sind Webserver: der Webserverwartet auf Anfragen von Browsern (oder User Agents o.ä.) und gibtWebseiten zurück. Die Browser arbeiten dann mit diesen Webseiten weiter,indem sie sie anzeigen und Operationen darauf erlauben (Anzeigen des Quelltextesetc.).Die Socket-Schnittstelle ist zwar von keiner Institution genormt, stellt aber denIndustriestandard dar. Wichtige Gründe sind u.a.:
- leicht verständliches Konzept
- leicht zu handhaben und zu programmieren
- fügt sich harmonisch in die UNIX-Welt ein
- verfügbar für viele Systeme, z.B. die Windows Sockets,
WinSock von Microsoft und anderen Firmen aus der PC-Welt definiert und kompatibel
zu den UNIX-sockets.
Um mit einem bestimmten Dienst (Programm) auf einem anderen Rechner
zu kommunizieren, reicht es allerdings nicht, einfach den anderen Rechner
als solchen anzusprechen. Es ist vielmehr jedes Server-Programm
auf einer bestimmten Port-Nummer zu erreichen. Jeder Client muß dem
entfernten Rechner diese Port-Nummer mitteilen, damit dieser die Anfrage dem
richtigen Programm zuleiten kann.
Die Netz-Ein- und Ausgabe wurde an die Datei-Ein- und Ausgabe angelehnt und
etliche Ein- und Ausgabe-Systemaufrufe lassen sich auf Dateien und Sockets
anwenden. Es gibt jedoch einige Unterschiede:
- Die typische Client-Server-Beziehung ist unsymmetrisch. Zur Einrichtung
einer Netzverbindung muß ein Programm seine Rolle kennen (Client oder Server).
- Eine Netzverbindung kann verbindungsorientiert oder verbindungslos sein.
Der erste Fall ähnelt einer Datei-E/A, der zweite Fall kennt kein "open"
oder dergleichen.
- Namen spielen bei Netzverbindungen eine größere Rolle. So kann ein
Kindprozeß einen vom Elternprozeß übergebenen Dateideskriptor
verwenden, ohne den Dateinamen zu kennen.
- Es gibt mehr Parameter zur Spezifizierung einer Netzverbindung als bei der
Date-E/A. Eine Verbindung wird durch fünf Parameter beschrieben:
- Protokoll
- lokale Adresse
- lokaler Prozeß
- ferne Adresse
- ferner Prozeß
- Das Netz-Interface muß mehrere Kommunikationsprotokolle unterstützen
und daher allgemeingütiger gehalten werden.
Verbindungsorientierte und verbindungslose Kommunikation
- Die "verbindungsorientierte Kommunikation" geht davon aus, daß vor dem
eigentlichen Datentransfer vom Client-Prozess eine logische Verbindung aufgebaut
wird, über die dann in der Folge beliebig
viele Datenpakete hin und her geschickt werden können - so lange bis
einer der beiden Partner die Verbindung wieder abbricht ("hangup"). Bei
dieser Art der Kommunikation wird Empfänger- und Absenderadresse nur
beim Verbindungsaufbau angegeben - danach erfolgt die Kommunikation über
Verbindungsnummern, die vom Betriebssystem zugeteilt werden. Die Telefonie
funktioniert nach diesem Prinzip.
Diese Form der Kommunikation eignet sich besser für Anwendungen, bei denen
große Datenmengen übertragen werden. Der Verbindungsaufbau dauert
zwar etwas, die eigentlichen Daten werden allerdings vollständig
und in der richtigen Reihenfolge von der Quelle zum Ziel übermittelt.
- Bei der "verbindungslosen Kommunikation" gibt es - wie der Name sagt
- keine bestehende Verbindung zwischen den Partnern. Die Datenpakete werden
einzeln mit den vollständigen Adressangaben versehen und auf den Weg
gebracht. Im täglichen Leben entspricht dies der Kommunikation mittels
Briefen.
Bei diesem Modell - man spricht dabei auch von "Datagrammen" - wird vom
Betriebssystem die korrekte Reihenfolge der einzelnen Datenpakete nicht garantiert.
Es ist noch nicht mal gewährleistet, daß ein Paket überhaupt
ankommt. Da jedoch der zeitaufwendige Verbindungsaufbau entfällt,
ist es in Fällen, bei denen es mehr auf Schnelligkeit als auf Sicherheit
ankommt, oft die bessere Wahl. Eventuell nötige Reihenfolge-Überprüfungen
bzw. Zeitüberwachngen müssen dann aber vom Anwendungsprozess
selbst vorgenommen werden.
Sockets
Zur Kommunikation zwischen Prozessen, die auch auf verschiedenen Rechnern
ablaufen können, wurde mit den Sockets im BSD-Unix ein leistungsfähiger
Mechanismus der Datenübertragung definiert. Sockets sind heute Grundlage der
meisten höheren Datenübertragungsprotokolle und in fast allen
Betriebssystemen realisiert.
Sie stellen die Schnittstelle zwischen Anwendungsprogramm und den
Betriebssystemroutinen zur Datenkommunikation dar. Dabei besteht der Vorteil für
den Benutzer darin, daß einem Socket ein Dateideskriptor zugeordnet wird,
über den das Anwendungsprogramm fast genauso kommunizieren kann, wie
über Pipes oder normale Dateien. Im Gegensatz zu einer Pipe, die grundsätzlich
nur in einer Richtung betrieben werden kann, ist ein Socket-Deskriptor jedoch
bidirektional - wie eine zum Lesen und Schreiben geöffnete Datei.
Sockets sind, wie die Client-Server-Beziehung, unsymmetrisch: Einer der beiden
beteiligten Prozesse ist "Server", der andere "Client". Der Server (Diensterbringer)
wartet darauf, daß irgendein Client (Kunde) mit ihm Kontakt aufnehmen
möchte. Der Client ist der aktive Part und veranlasst den Beginn der
Kommunikation.
Über Sockets kann der Datenaustausch auf zweierlei Art erfolgen:
-
Datenströme (Streams): Zwischen Client und Server wird eine Verbindung aufgebaut,
die einzelnen Datenpakete werden gesichert und in korrekter Reihenfolge übertragen
und zum Schluß wird die Verbindung wieder abgebaut. Dies entspricht dem Zyklus
"open" - "read"/"write" - "close" bei einer normalen Datei. Bei einer Verbindung
über IP wird dafür TCP benutzt.
-
Einzelpakete (Datagrams): Datagramme werden gleichsam als "Pakete" mit
Absender- und Empfängeradresse verschickt. Das entsprechende Internet-Protokoll
heißt UDP. Es wird keine Verbindung zwischen den beiden Prozessen aufgebaut,
weshalb UDP wesentlich schneller ist.
Allerdings gibt es keine Garantie für das Ankommen des Paketes bei
der Gegenseite und keine Gewähr für die Einhaltung der richtigen
Reihenfolge.
Sockets sind noch über verschiedenen "Domänen" definiert: Es gibt neben der
"Internet-Domäne" noch weitere Domänen, z. B.die "Unix-Domäne" für
die Kommunikation zwischen reinen Unix-Prozessen. Thema der Vorlesung ist aber
ausschließlich die Internet-Domäne.
Die Systemaufrufe im Überblick
Einige wichtige Socket-Primitive bzw. -Systemcalls sind:
- Erstellen eines Socket:
s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)
zum Eröffnen einer verbindungsorientierten Kommunikation im Internet
- Verbindungsaufbau:
bind(s, address, addresslength)
zum Verbinden des Sockets mit der lokalen Endpunktadresse
cs = connect(s, address, addresslength)
zum Verbindungsaufbau über einen virtuellen Kanal zum entfernten Prozess
- Kommunikation:
nwrite = write(s, buffer, nbytes) (bzw. send(...)) und
nread = read(s, buffer, maxbytes) (bzw. recv(...)
zum Schreiben bzw. Lesen auf/vom virtuellen Kanal
- Verbindungsabbau:
close(s)
Die folgende Tabelle gibt einen Überblick über die wichtigsten
Socket-Systemcalls in Client- und Serverprogrammen:
Phase |
Client |
Server |
Endpunkt erzeugen |
socket() |
socket() |
Binden einer Adresse |
bind() |
bind() |
Verbindung aufbauen |
connect() |
|
Warteschlange festlegen |
|
listen() |
Warten auf Verbindung |
|
accept() |
Daten senden |
write() send() sendto() sendmsg() |
write() send() sendto() sendmsg() |
Daten empfangen |
read() recv() recvfrom() recvmsg() |
read() recv() recvfrom() recvmsg() |
Verbindung schließen |
shutdown() |
shutdown() |
Endpunkt abbauen |
close() |
close() |
Ereignisse annehmen |
select() |
select() |
Verschiedenes |
getpeername() getsockname() getsockopt() setsockopt() |
getpeername() getsockname() getsockopt() setsockopt() |
Für die Kommunikation bei verbindungslosen, d.h. UDP-basierten Socketanwendungen
sind die speziellen send()- und receive()-Systemcalls empfehlenswert,
während bei TCP-Verbindugen daneben die Standard-Systemcalls read()
und write() einsetzbar sind.
Eine TCP/IP-Verbindung ist, wie wir gesehen haben, durch eine Client-Server-Architektur
geprägt und damit asymetrisch. Vor der Programmierung muß die Verbindung
stehen. Das betrifft einmal die Verbindung zwischen den Rechnern, als auch jene zwischen
den Prozessen. Die Adressierung der Rechner erfolgt per Hostname, der vom
System auf die IP-Nummer umgesetzt wird.
Vom Client aus muß nicht nur der richtige Rechner, sondern auch der richtige
Serverprozeß angesprochen werden können. Dazu bindet sich der Serverprozeß
an einen festen Port, über den er erreichbar ist. Damit die
Nummer des Ports mit einem Namen versehen werden kann, verwendet man
die Datei /etc/services. Im Programm wird die Servicenummer durch den
Aufruf der Systemfunktion getservbyname() bestimmt.
Der Client braucht keinen festen Port. Er erbittet sich auf der lokalen
Maschine eine freie Nummer und ruft damit den Port des Servers. Der Server
erfährt die Nummer des Clients aus der Anfrage und kann ihm unter diesem
Port antworten. Das Szenario zwischen Server und Client sieht wie folgt aus:
Betrachten wir beispielhaft einmal das Listing eines ganz einfachen
Servers in der Programmiersprache C. Die einzelnen Systemaufrufe werden
weiter unten genauer behandelt, das Listing soll zunächst nur einen
Überblick des Ablaufs geben:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netdb.h>
#define MAXPUF 1023
main()
{
int MySocket, ForeignSocket;
struct sockaddr_in AdrMySock, AdrPartnerSocket;
struct servent *Service;
int AdrLen;
char Puffer[MAXPUF];
int MsgLen;
/* Socket einrichten */
MySocket = socket(AF_INET, SOCK_STREAM, 0);
/* Socket an Port-Nummer binden */
AdrMySock.sin_family = AF_INET; /* Internet-Protokolle */
AdrMySock.sin_addr.s_addr = INADDR_ANY; /* akzept. jeden Client-Host */
Service = getservbyname("echo","tcp"); /* bestimme Port */
AdrMySock.sin_port = Service->s_port; /* (Get Service by Name) */
bind(MySocket, &AdrMySock, sizeof(AdrMySock));
/* Empfangsbereitschaft signalisieren und warten */
listen(MySocket, 5);
for (;;) /* forever */
{
/* Verbindungswunsch vom Client annehmen */
ForeignSocket = accept(MySocket, &AdrPartnerSocket, &AdrLen);
/* Datenaustausch zwischen Server und Client */
MsgLen = recv(ForeignSocket, Puffer, MAXPUF, 0); /* String empfangen */
send(ForeignSocket, Puffer, MsgLen, 0); /* und zuruecksenden */
/* Verbindung beenden und wieder auf Client warten */
close(ForeignSocket);
}
}
Dieser Server bearbeitet jede Anfrage, die über den Port
"echo" 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, selbst
wenn es kein Multitasking beherrscht.
Es gibt zwei Variablen pro Socket. Die eine ist wie bei Dateizugriffen
ein einfaches Handle (MySocket), die andere hält die Adresse der
Verbindung (AdrSock), also die IP-Nummer des Rechners und die Nummer des
verwendeten Ports. Der Server erlaubt Verbindungen von jedem Rechner aus,
weil die Konstante INADDR_ANY benutzt wird.
Der zugehörige Client gibt dagegen die Adresse des anzusprechenden Servers an.
Das Programm sieht wie folgt aus:
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netdb.h>
#define MAXPUF 1023
main()
{
int MySocket; /* Socket-Handle */
struct sockaddr_in AdrSock; /* Socketstruktur */
int len; /* Die Laenge der Socketstruktur */
struct hostent *RechnerID; /* ferner Rechner */
struct servent *Service; /* Dienst auf dem fernen Rechner */
char Puffer[MAXPUF] = "Wir erschrecken zu guten Zwecken!";
MySocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
/* Bestimme den Zielrechner */
RechnerID = gethostbyname("server");
bcopy(RechnerID->h_addr,&AdrSock.sin_addr,RechnerID->h_length);
/* Bestimme den Port */
Service = getservbyname("echo","tcp");
AdrSock.sin_port = Service->s_port;
connect(MySocket, (struct sockaddr *)&AdrSock, sizeof(AdrSock));
send(MySocket, Puffer, MAXPUF, 0); /* String senden */
recv(MySocket, Puffer, MAXPUF, 0); /* und wieder empfangen */
printf("%s\n", Puffer); /* ausgeben */
close(MySocket);
}
Die recv-Funktion liefert als Rückgabewert die Größe
des versandten Speicherbereichs (max. 1 KByte, siehe unten).
Grundsätzlich liefern fast alle Netz-Funktionen bei Fehlern den Wert 0
zurück. In den obigen Beispiel-Listing fehlt jegliche Fehlerbehandlung,
damit das Prinzip übersichtlich dargestellt werden kann. Im "richtigen"
Programm ist eine umfassende Fehlerbehandlung unumgänglich.
Das Server-Programm hat noch einen Nachteil. Nach dem Start des Servers ist
die Konsole oder das Shell-Fenster für weitere Zwecke blockiert. Auch
fehlt eine korrekte Möglichkeit, den Server zu beenden. Bei einem Abbruch
des Programms wird es dem Betriebssystem überlassen, den Socket zu schließen.
Zweckmässigerweise wird vom Programm ein Dämon erzeugt, der per
fork-Aufruf in den Hintergrund gestellt.
|
|
|