Jedes Netzwerkinterface im TCP/IP hat eine eigenständige Nummer. Diese ist 4 Byte lang. Man pflegt diese Nummer der besseren Lesbarkeit halber durch Punkte zu trennen. Eine typische Rechnernummer könnte beispielsweise 192.11.109.14 lauten. Da aber die Zeiten eigentlich vorbei sein sollten, in denen man in der EDV alles mit Nummern bezeichnete, kann man auch einen Namen statt der Nummer verwenden. So könnte der Rechner auch anton heißen. Die Umsetzung vom Namen zur Nummer wird in größeren Netzen durch einen Nameserver erreicht. In kleineren Netzen wird dies auf dem lokalen Rechner durch Einträge in der Datei /etc/hosts aufgelöst. Eine typische Zeile dieser Datei lautet:
192.11.109.14 anton supernovaDer Rechner 192.11.109.14 kann unter dem Namen anton oder supernova angesprochen werden.
Die Bezeichnung des Rechners ist aber nur ein Teil der Miete. Auf einem PC mit nur einem laufenden Programm würde diese Adresse vielleicht reichen. Aber auf einer UNIX-Maschine mit vielen gleichzeitig laufenden Programmen muß angegeben werden, welcher Prozeß die Nachricht empfangen soll. Zu diesem Zweck werden Ports verwendet. Für jeden Dienst gibt es einen Port, der eine eindeutige Nummer hat. An diesem Port hängt ein Server, der Anfragen dieses Dienstes empfängt und beantwortet. Die Portnummern unter 5000 sind für Systemdienste reserviert. Darüeber kann man eine Nummer für den eigenen Server verwenden. Auch hier gilt, daß man der Portnummer einen Namen zuorden kann und sollte. Die Auflösung der Namen erfolgt in der Datei /etc/services und im Programm durch den Aufruf von getservbyname(). Der Port für den Server-Socket wird in der Datei /etc/services eingetragen. Auf jedem Rechner im Netz muß hier der gleiche Eintrag stehen:
hilfe 6004/tcphilfe ist die Bezeichnung des Dienstes, 6004 ist eine beliebige Zahl, die lediglich nicht von anderen Diensten benutzt werden darf und größer als 5000 sein sollte. Das tcp ist die Kennung für das Protokoll. Hier gibt es noch die Möglichkeit, daß udp für ein Protokoll tieferer Ebene eingetragen ist. Man spricht von einem well-known-port, da die Clients wissen müssen, auf welchem Port sie ihren Server finden können. 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.
Aufruf | Zweck |
---|---|
socket | Anforderung eines Kommunikationsendpunktes |
bind | Lege die Portnummer fest |
listen | Festlegen der Pufferzahl für Anfragen |
accept | Akzeptanz von Anfragen signalisieren |
connect | Verbindung anfordern |
send | Senden von Daten |
recv | Empfangen von Daten |
close | Schließen des Sockets |
Das Szenario zwischen Server und Client sieht wie folgt aus:
Server | Client | Aktion |
---|---|---|
socket | Server fordert Socket an | |
bind | Server legt seinen Port fest | |
listen | Server gibt die Verbindungszahl fest | |
Schleife-Anfang | ||
socket | Client fordert Socket an | |
accept | Server ist bereit zum Empfang | |
connect | Client meldet Anforderung | |
recv | Server wartet auf Daten | |
send | Client sendet Daten | |
Server empfängt und bearbeitet die Anfrage | ||
recv | Client wartet auf die Antwort | |
send | Server sendet Antwort zurück | |
close | Client hat empfangen und löst seine Verbindung | |
Schleife-Ende | ||
close | Server meldet sich endgültig ab |
Betrachten wir erst einmal das Listing des Servers:
#includeDieser 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, selbst wenn es kein Multitasking beherrscht. Der zugehörige Client könnte wie folgt aussehen:#include #include #include #include #include #define MAXPUF 1023 main() { int IDMySocket, IDPartnerSocket; struct sockaddr_in AdrMySock, AdrPartnerSocket; int AdrLen; char Puffer[MAXPUF]; int MsgLen; IDMySocket = socket(AF_INET, SOCK_STREAM, 0); /* Socket an Port-Nummer binden */ AdrMySock.sin_family = AF_INET; AdrMySock.sin_addr.s_addr = INADDR_ANY; /* akzept. jeden */ /* Bestimme Port */ Service = getservbyname("hilfe","tcp"); AdrSock.sin_port = Service->s_port; bind(IDMySocket, &AdrMySock, sizeof(AdrMySock)); listen(IDMySock, 5); do { IDPartnerSocket = accept(IDMySocket, &AdrPartnerSocket, &len); MsgLen = recv(IDPartnerSocket, Puffer, MAXPUF, 0); /* tu was mit dem Kram */ send(IDPartnerSocket, Puffer, MsgLen, 0); close(IDPartnerSocket); } while(1); /* bis zum St. Nimmerlein */ }
#includeEs 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 recv-Funktion liefert als Rückgabewert die Größe des versandten 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. Grundsätzlich liefern fast alle Funktionen eine 0 bei Fehlern zurück. Ich habe mir diese Abfragen im Listing verkniffen. Im ``richtigen'' Programm müssen die natürlich drin sein.#include #include #include #include #define MAXPUF 1023 main() { int IDSocket; struct sockaddr_in AdrSock; int len; /* Die Laenge der Socketstruktur */ struct hostent *RechnerID; struct servent *Service; char Puffer[MAXPUF]; IDSocket = socket(AF_INET, SOCK_STREAM, 0); /* Bestimme den Zielrechner */ RechnerID = gethostbyname(ßerver"); bcopy(RechnerID->h_addr,&AdrSock.sin_addr,RechnerID->h_length); /* Bestimme den Port */ Service = getservbyname("hilfe","tcp"); AdrSock.sin_port = Service->s_port; connect(IDSocket, (struct sockaddr *)&AdrSock, sizeof(AdrSock)); send(IDSocket, Puffer, MAXPUF, 0); recv(IDSocket, Puffer, MAXPUF, 0); close(IDSocket); }
if (fork()!=0) exit(0); /* toete den Vater, Sohn ueberlebt */Der fork-Mechanismus erschlägt 2 Fliegen mit einer Klappe: Der Vater ist Sohn der Benutzershell. Wenn der Benutzer sich abmeldet, erhält der Server ein Signal, daß er sich verabschieden soll. Das soll aber vermieden werden. Wird der Vater getötet, wird der Sohn zum Waisen und wird daraufhin vom init-Prozeß adoptiert. Aufgrund dessen geht der Prozeß ohne explizite Aufforderung in den Hintergrund.
do { IDPartnerSocket = accept(IDMySocket, &AdrPartnerSocket, &len); if (fork()==0) { MsgLen = recv(IDPartnerSocket, Puffer, MAXPUF, 0); /* tu was mit dem Kram */ send(IDPartnerSocket, Puffer, MsgLen, 0); close(IDPartnerSocket); /* Sohn toetet sich selbst */ exit(0); } /* if fork.. */ close(IDPartnerSocket); /* der Vater schliesst die Verbindung */ } while(1);Der fork-Mechanismus erschlägt auf einfache Weise die Bearbeitung mehrerer paralleler Anfragen. Der Prozeß erzeugt einen Sohn, der auch den Socket erbt, über den die Verbindung zum Client erhalten bleibt.
signal(SIGCLD, SIG_IGN); /* verhindere Zombies */Es gibt feine Unterschiede zwischen den Unix-Version:\footnote{Vielen Dank an Thomas Weidenfeller Maus F}
void server_proc::sig_child() ///////////////////////////////////////////////////////////////////////// // 4.3 BSD SIGCLD-Handler, der dann benutzt wird, wenn ein Server-Prozess // nicht am Exit-Status seiner Childs interessiert ist ///////////////////////////////////////////////////////////////////////// { #ifdef BSD union wait status; while(wait3(&status, WNOHANG, (struct rusage *)NULL) > 0) // // Warten wenn es noch Childs mit dem Status // "stopped" oder "terminated" gibt // ; #endif }
Homepage | (C) Copyright 1999 Arnold Willemer |