In diesem Abschnitt werden die einzelnen Phasen des verbindungsorientierten Dienstes näher erläutert anhand eines Programmbeispiels für folgende einfache Client/Server-Anwendung:
Client und Server führen die lokale Verwaltung durch.
Zwischen Client und Server wird eine Verbindung aufgebaut.
Der Server überträgt eine Datei an den Client. Der Client empfängt die Datei vom Server und gibt sie auf seiner Standardausgabe aus.
Client und Server beenden die Verbindung.
Das Programmbeispiel wird in einzelnen Programmausschnitten vorgestellt, wobei jeweils zwei Programmausschnitte eine Phase des verbindungsorientierten Dienstes erläutern. Ein Programmausschnitt übernimmt dabei die Rolle des Clients und der andere die Rolle des Servers.
Der in den Beispielen des vorliegenden Abschnitts verwendete Programmcode ist vollständig und zusammenhängend dargestellt in den Abschnitten "Client im verbindungsorientierten Dienst" und "Server im verbindungsorientierten Dienst".
Lokale Verwaltung am Beispiel des Client/Server-Modells
Bevor Client und Server eine Kommunikationsverbindung aufbauen können, müssen sie zuerst mit t_open() je einen lokalen Kanal zum Transportanbieter einrichten. Danach muss jeder von beiden mit t_bind() eine lokale Adresse bekannt geben, unter der er über seinen zugeordneten Transportendpunkt erreichbar ist.
Die verschiedenen Dienste, die die Transportschnittstelle anbietet, erhält der Benutzer mit dem Aufruf von t_open().
Die Dienste sind wie folgt aufgebaut: | |
Adresse | maximale Größe einer Adresse |
Optionen | maximale Anzahl Bytes für protokollspezifische Optionen, die der Benutzer mit dem Transportanbieter austauschen kann |
tsdu | maximale Nachrichtengröße, die im verbindungsorientierten oder verbindungslosen Dienst übertragen werden kann |
etsdu | maximale Anzahl von Bytes für Vorrangdaten, die über eine Verbindung gesendet werden können |
Verbindungsaufbau | maximale Anzahl von Bytes für Benutzerdaten, die beim Verbindungsaufbau ausgetauscht werden können |
Verbindungsabbau | maximale Anzahl von Bytes der Benutzerdaten, die beim Verbindungsabbau übertragen werden können |
Diensttyp | Typ des Dienstes, der vom Transportanbieter unterstützt wird |
Drei Diensttypen sind definiert: | |
T_COTS | Der Transportanbieter unterstützt den verbindungsorientierten Dienst, erlaubt aber keinen geordneten Verbindungsabbau. Die Verbindung kann nur abgebrochen werden. |
T_COTS_ORD | Der Transportanbieter unterstützt den verbindungsorientierten Dienst und bietet die Möglichkeit eines geordneten Verbindungsabbaus (Standardfall bei XTI(POSIX) im verbindungsorientierten Dienst). |
T_CLTS | Der Transportanbieter unterstützt den verbindungslosen Dienst. |
Mit t_open() erhält der Benutzer die voreingestellten Leistungsmerkmale des Transportendpunkts. Wenn es sich um dynamische Leistungsmerkmale handelt, können sich diese Merkmale nachträglich noch ändern. Mit t_getinfo() kann sich der Benutzer über die aktuellen Leistungsmerkmale des Transportendpunkts informieren.
Wenn ein Benutzer einen Transportendpunkt eingerichtet hat, muss er dem Transportanbieter die Adresse übergeben, unter der er über diesen Transportendpunkt zu erreichen ist. Wie bereits beschrieben, übergibt der Benutzer mit t_bind() dem Transportanbieter die Adresse des Transportendpunkts. Bei Server-Stationen sorgt t_bind() außerdem dafür, dass ankommende Verbindungsanforderungen vom Transportanbieter bearbeitet und dem Transportendpunkt übergeben werden können.
Während der Einrichtung des Transportendpunkts ist noch eine weitere Funktion verfügbar: Mit t_optmgmt() kann der Benutzer Leistungsmerkmale verändern. Von jedem Transportprotokoll wird erwartet, dass es seine eigene Menge von veränderbaren Leistungsmerkmalen bereitstellt. Dies können zum Beispiel Parameter sein, die die Dienstqualität beeinflussen. Auf Grund der protokollspezifischen Natur dieser Parameter werden nur Anwendungen für eine spezielle Protokollumgebung diese Möglichkeit nutzen.
Die lokalen Verwaltungsaufgaben werden nachfolgend jeweils am Beispiel eines Clients und eines Servers gezeigt. Die beiden Beispiele enthalten die Definitionen und die Aufrufe.
Lokale Verwaltung durch den Client
#include <xti.h> #include <stdio.h> #include <fcntl.h> #include <netinet/in.h> #include <sys/socket.h> #define SRV_ADDR 0x7F000001 #define SRV_PORT 8888 main() { int fd; int nbytes; int flags = 0; char buf[1024]; struct t_call *sndcall; struct sockaddr_in *sin; if ((fd = t_open("/dev/tcp", O_RDWR, NULL)) < 0) { t_error("t_open() gescheitert"); exit(1); } if (t_bind(fd, NULL, NULL) < 0) { t_error("t_bind() nicht erfolgreich"); exit(2); }
Der erste Parameter von t_open() ist der Pfadname des Gerätes, das den geforderten Transportdienst bereitstellt. Im vorliegenden Beispiel ist /dev/tcp eine Gerätedatei; /dev/tcp stellt ein verbindungsorientiertes Transportprotokoll zur Verfügung. Dieses Transportprotokoll wird durch den zweiten Parameter für Schreib-/Lesezugriffe geöffnet. Den dritten Parameter kann der Benutzer verwenden, um sich über Leistungsmerkmale zu informieren. Diese Information wird für das Erstellen protokollunabhängiger Programme benötigt. Um das Beispiel einfach zu halten, wird auf diese Information nicht zurückgegriffen.
Client und Server nehmen an, dass der Transportanbieter folgende Leistungsmerkmale besitzt:
Der Transportanbieter unterstützt den Diensttyp T_COTS_ORD; im Beispiel wird T_COTS_ORD für den geordneten Verbindungsabbau verwendet.
Benutzerdaten können während des Verbindungsaufbaus oder Verbindungsabbaus nicht ausgetauscht werden.
Der Transportanbieter unterstützt keine protokollspezifischen Leistungsmerkmale.
Da diese Leistungsmerkmale nicht vom Benutzer benötigt werden, wird beim Aufruf von t_open() als dritter Parameter NULL übergeben. Falls der Benutzer einen anderen Diensttyp als T_COTS_ORD benötigt, muss eine andere Gerätedatei geöffnet werden. Ein Beispiel für T_CLTS ist im Abschnitt "Verbindungsloser Dienst am Beispiel eines Auftragssystems" aufgeführt.
Als Rückgabewert liefert t_open() einen Integer-Wert, der in allen weiteren Aufrufen des Transportanbieters zur Identifizierung des mit t_open() eingerichteten Transportendpunkts benötigt wird. Dieser Integer-Wert ist ein Dateideskriptor.
Nachdem der Transportendpunkt eingerichtet ist, ruft der Benutzer t_bind() auf, um dem Transportendpunkt eine Adresse zuzuweisen. Der erste Parameter von t_bind() kennzeichnet den Transportendpunkt, der zweite Parameter beschreibt die Adresse, die an den Transportendpunkt gebunden werden soll. Der dritte Parameter enthält bei der Rückkehr von t_bind() die tatsächlich gebundene Adresse.
Im Gegensatz zur Adresse eines Server-Transportendpunkts, die von allen Clients beim Zugriff auf den Server benötigt wird, muss die Adresse eines Clients nicht allgemein bekannt sein. Da kein anderer Prozess versuchen wird, auf die Adresse eines Clients zuzugreifen, kümmert sich ein Client normalerweise nicht um seine Adresse. Dies wird im obigen Beispiel gezeigt, wo beim t_bind()-Aufruf als zweiter und dritter Parameter NULL übergeben wird. Wenn der zweite Parameter NULL ist, ordnet der Transportanbieter eine Adresse zu. Der dritte Parameter NULL bedeutet, dass der Client sich nicht für die vom Transportanbieter zugeordnete Adresse „interessiert“.
Wenn entweder t_open() oder t_bind() nicht erfolgreich ist, wird t_error() aufgerufen, um eine entsprechende Fehlermeldung auf stderr auszugeben. Wenn irgendeine Transportanbieterfunktion nicht erfolgreich sein sollte, enthält die globale Integer-Variable t_errno einen entsprechenden Wert, der den Fehler genauer anzeigt. Eine Reihe solcher Fehlerwerte sowie die Variable t_errno selbst sind in <xti.h> für die Transportanbieter definiert. t_error() gibt eine Fehlermeldung entsprechend dem Wert von t_errno aus. Diese Funktion arbeitet analog zur Funktion perror(), die eine Fehlermeldung entsprechend dem Wert von errno ausgibt. Wenn es sich bei dem Fehler im Transportanbieter um einen Systemfehler handelt, erhält t_errno den Wert TSYSERR und errno wird auf den entsprechenden Systemfehlerwert gesetzt.
Lokale Verwaltung durch den Server
Der Server in diesem Beispiel muss ähnlich vorgehen, bevor mit der Kommunikation begonnen werden kann. Der Server muss einen Transportendpunkt einrichten, der ständig auf Verbindungsanforderungen wartet.
Die notwendigen Definitionen und Aufrufe sehen wie folgt aus:
#include <xti.h> #include <stropts.h> #include <fcntl.h> #include <stdio.h> #include <signal.h> #include <netinet.in.h> #include <sys/socket.h> #define FILENAME "/etc/services" #define DISCONNECT -1 #define SRV_ADDR 0x7F000001 #define SRV_PORT 8888 int conn_fd; /* Für den Dateideskriptor der Verbindung */ main() { int listen_fd; /* Dateideskriptor für * Verbindungsanforderung */ struct t_bind *bind; struct t_call *call; struct sockaddr_in *sin; if ((listen_fd = t_open("/dev/tcp", O_RDWR, NULL)) < 0) { t_error("Aufruf t_open() für listen_fd gescheitert."); exit(1); } if ((bind = (struct t_bind *)t_alloc(listen_fd, T_BIND, T_ALL)) == NULL) { t_error("t_alloc() für Struktur t_bind gescheitert."); exit(2); } bind->qlen = 1; bind->addr.len=sizeof(struct sockaddr_in); sin=(struct sockaddr_in *)bind->addr.buf; sin->sin_family=AF_INET; sin->sin_port=htons(SRV_PORT); sin->sin_addr.s_addr=htonl(SRV_ADDR); if (t_bind(listen_fd, bind, bind) < 0) { t_error("t_bind() für listen_fd gescheitert."); exit(3); }
Analog zum Client ruft auch der Server t_open() auf, um eine Verbindung zum gewünschten Transportanbieter aufzubauen, d.h. der Server richtet einen Transportendpunkt (listen_fd) ein. Diesen Transportendpunkt listen_fd wird der Server später bei Aufruf der Funktion t_listen() verwenden, um auf Verbindungsanforderungen zu warten.
Bevor der Server mit der Funktion t_bind() eine Adresse an den Transportendpunkt listen_fd binden kann, muss der Server diese Adresse bereitstellen. Diese Adresse wird mit dem zweiten Parameter (bind) beim Aufruf von t_bind() übergeben.
Der Parameter bind ist ein Zeiger auf ein Objekt vom Datentyp struct t_bind. Alle Strukturen und Konstanten des Transportanbieters sind in <xti.h> deklariert bzw. definiert.
Die Struktur t_bind ist in <xti.h> wie folgt deklariert:
struct t_bind { struct netbuf addr; unsigned qlen; };
bind->qlen gibt die maximale Anzahl erlaubter Verbindungsanforderungen an. Wenn der Wert von bind->qlen größer als 0 ist, können ankommende Verbindungsanforderungen mit diesem Transportendpunkt bearbeitet werden. Ankommende Verbindungsanforderungen für die in bind->addr bereitgestellte Adresse stellt der Server dann in eine Warteschlange. Außerdem gibt bind->qlen die maximale Anzahl von Anforderungen an, die der Server gleichzeitig bearbeiten kann. Der Server muss auf jede Anforderung antworten, indem er sie annimmt oder zurückweist. Eine Verbindungsanforderung heißt anstehend, wenn der Server sie noch nicht beantwortet hat.
Oft wird ein Server erst eine Verbindungsanforderung vollständig bearbeiten und dann die nächste. In diesem Fall ist 1 der richtige Wert für qlen. Wenn ein Server mehrere Aufträge gleichzeitig bearbeiten möchte, spezifiziert bind->qlen die maximale Anzahl Aufträge, die gleichzeitig bearbeitet werden können.
Da der Server im vorliegenden Beispiel eine Verbindungsanforderung nach der anderen verarbeitet, muss bind->qlen der Wert 1 zugewiesen werden. Das Beispiel eines Servers, der mehrere Anforderungen gleichzeitig bearbeitet, wird im Abschnitt "Gleichzeitige Verwaltung mehrerer Verbindungen und ereignisgesteuerter Betrieb" vorgestellt.
addr hat den Datentyp struct netbuf und beschreibt die anzubindende Adresse.
Die Struktur netbuf ist in <xti.h> wie folgt deklariert:
struct netbuf { unsigned int maxlen; unsigned int len; char *buf; };
buf ist ein Zeiger auf einen Datenpuffer, len gibt die Anzahl Bytes im Puffer an, und maxlen gibt die maximale Anzahl Bytes an, die in den Puffer geschrieben werden können. Letztere Angabe wird nur benötigt, wenn Daten vom Transportanbieter zum Benutzer transportiert werden.
Durch den Aufruf von t_alloc() wird dynamisch Speicher für ein t_bind-Objekt angelegt. Der erste Parameter von t_alloc() nennt den Dateideskriptor, der den Transportendpunkt identifiziert. Der zweite Parameter spezifiziert die anzulegende Transportanbieterstruktur, d.h. im vorliegenden Fall t_bind. Der dritte Parameter gibt an, welche Komponenten dieser Struktur angelegt werden sollen. T_ALL bedeutet, dass für alle Komponenten der Struktur Speicher angelegt werden soll. Im obigen Beispiel wird dadurch der addr-Puffer angelegt. Die Größe dieses Puffers wird vom Transportanbieter bestimmt, der eine maximale Adresslänge festlegt. Diese Länge steht in der Komponente maxlen der Struktur netbuf. Die Verwendung von t_alloc() stellt die Kompatibilität mit zukünftigen Versionen des Transportanbieters sicher.
Bei Objekten des Typs struct t_bind werden die Daten als Adresse interpretiert. Allgemein wird angenommen, dass die Struktur einer Adresse von Protokoll zu Protokoll verschieden ist. Die Struktur netbuf ist so aufgebaut, dass jedes Protokoll unterstützt werden kann.
Anschließend wird die Adressinformation dem neu angelegten t_bind-Objekt zugewiesen. Im Beispiel wird die Adresse selbst dabei entsprechend der Adressstruktur der Internet-Kommunikationsdomäne strukturiert (siehe struct sockaddr_in im Abschnitt "Adress-Struktur sockaddr_in der Adressfamilie AF_INET").
Die so erzeugte Adresse bindet der Server nun mit der Funktion t_bind() an den Transportendpunkt listen_fd. Nach erfolgreichem Aufruf von t_bind() kann der Server von jedem Client über diese Adresse angesprochen werden. Der Transportanbieter stellt ankommende Verbindungsanforderungen in eine Warteschlange und leitet damit die nächste Phase des Verbindungsaufbauprotokolls ein, den eigentlichen Verbindungsaufbau.
Verbindungsaufbau am Beispiel des Client/Server-Modells
Der Verbindungsaufbau verdeutlicht den Unterschied zwischen Client und Server. Der Transportanbieter stellt für beide jeweils spezielle Funktionen zur Verfügung. Der Client ruft t_connect() auf, um eine Verbindung anzufordern, während der Server mit t_listen() auf Verbindungsanforderungen wartet. Der Server kann mit der Funktion t_accept() eine Verbindung annehmen oder sie mit t_snddis() ablehnen. Der Client wird über die Entscheidung des Transportanbieters informiert, wenn die Funktion t_connect() beendet ist.
Verbindungsanforderung durch den Client
Um mit dem Client/Server-Beispiel fortzufahren, sind aus Sicht des Clients für einen Verbindungsaufbau folgende Schritte notwendig:
if ((sndcall = (struct t_call *)t_alloc(fd, T_CALL, T_ADDR)) == NULL) { t_error("t_alloc() gescheitert"); exit(3); } sndcall->addr.len=sizeof(struct sockaddr_in); sin=(struct sockaddr_in *)sndcall->addr.buf; sin->sin_family=AF_INET; sin->sin_port=htons(SRV_PORT); sin->sin_addr.s_addr=htonl(SRV_ADDR); if (t_connect(fd, sndcall, NULL) < 0) { t_error("t_connect() für fd gescheitert"); exit(4); }
Bevor der Client mit t_connect() eine Verbindungsanforderung an den Server schicken kann, muss der Client die Adresse des Servers spezifizieren. Diese Adresse wird dann als zweiter Parameter (sndcall) beim Aufruf von t_connect() übergeben.
Der Parameter sndcall ist ein Zeiger auf ein Objekt vom Datentyp struct t_call.
Die Struktur t_call ist in <xti.h> wie folgt deklariert:
struct t_call { struct netbuf addr; struct netbuf opt; struct netbuf udata; int sequence; };
t_alloc() wird im Beispiel verwendet, um ein t_call-Objekt dynamisch anzulegen. Im Beispiel auf der vorigen Seite werden keine Leistungsmerkmale oder Benutzerdaten angegeben. Nur die Server-Adresse wird verwendet. Als dritter Parameter von t_alloc() wird T_ADDR gewählt, um für die Adressinformation einen entsprechenden Puffer anzulegen.
Nach erfolgreicher Ausführung von t_alloc() legt der Server die Länge der Server-Adresse sowie die Server-Adresse selbst im von t_alloc() reservierten Speicherbereich ab. Die Serveradresse wird dabei entsprechend der Adressstruktur der Internet-Kommunikationsdomäne strukturiert (siehe struct sockaddr_in im Abschnitt "Adress-Struktur sockaddr_in der Adressfamilie AF_INET").
Der Aufruf t_connect() schickt eine Verbindungsanforderung zum Server. Der erste Parameter des Aufrufs ist der Transportendpunkt, über den die Verbindung aufgebaut werden soll. Mit dem zweiten Parameter (sndcall) wird die Adresse des gewünschten Servers übergeben. Der dritte Parameter ist ebenfalls ein Zeiger auf ein Objekt vom Typ struct t_call. Dieser Parameter von t_connect() wird benutzt, um Informationen über die errichtete Verbindung zu erhalten. Da diese Information hier nicht benötigt wird, wird im Beispiel als dritter Parameter NULL übergeben. Wenn t_connect() erfolgreich ist, wird die Verbindung aufgebaut. Falls der Server die Verbindungsanforderung zurückweist, wird t_errno auf den Wert TLOOK gesetzt.
Der Fehler TLOOK hat eine besondere Bedeutung für die Transportschnittstelle: TLOOK informiert den Benutzer, wenn eine Funktion der Schnittstelle durch ein unerwartetes asynchrones Ereignis am gegebenen Transportendpunkt unterbrochen wurde. TLOOK zeigt daher nicht einen Fehler in der Schnittstelle an, sondern nur, dass die aufgerufene Funktion auf Grund des anstehenden Ereignisses nicht ausgeführt wird. Welche Ereignisse der Transportschnittstelle definiert sind, ist im Abschnitt "Zustände und Zustandsübergänge" beschrieben.
Mit der Funktion t_look() kann der Benutzer feststellen, welches Ereignis aufgetreten ist, wenn ein Fehler TLOOK gemeldet wird. Wenn im Beispiel auf der vorigen Seite die Verbindungsanforderung abgelehnt wird, erhält der Client eine Nachricht über den Abbruch. Das Programm wird in diesem Fall beendet.
Verbindungsannahme durch den Server
Wenn der Client mit t_connect() eine Verbindung anfordert, wird am Transportendpunkt des Servers ein entsprechendes Ereignis gesetzt. Im Folgenden wird gezeigt, welche Schritte für die Behandlung dieses Ereignisses erforderlich sind. Der Server nimmt für jeden Client den Auftrag an und erzeugt einen neuen Prozess, um die Verbindung zu verwalten.
if ((call = (struct t_call *)t_alloc(listen_fd, T_CALL, T_ADDR)) == NULL){ t_error("t_alloc() für t_call Struktur gescheitert"); exit(5); } while (1) { if (t_listen(listen_fd, call) < 0) { t_error("t_listen für listen_fd gescheitert"); exit(6); } if ((conn_fd = accept_call(listen_fd, call)) != DISCONNECT) run_service(listen_fd); } }
Der Server legt mit t_alloc() ein Objekt vom Typ struct t_call an, das von t_listen() benötigt wird. Der dritte Parameter von t_alloc(), T_ADDR, bewirkt, dass der Puffer für die Adresse des Clients angelegt wird.
Der Wert von maxlen in einem netbuf-Objekt gibt die aktuelle Länge des angelegten Puffers an.
Der Server läuft in einer Endlosschleife und bearbeitet pro Schleifendurchlauf eine ankommende Verbindungsanforderung. Dabei geht der Server wie folgt vor:
Der Server ruft die Funktion t_listen() auf, um auf Verbindungsanforderungen zu warten, die auf dem Transportendpunkt listen_fd ankommen. Die Transportadresse des Senders einer Verbindungsanforderung wird von t_listen() im t_call-Objekt gespeichert, auf das die Zeigervariable call zeigt.
Wenn keine Verbindungsanforderung ansteht, blockiert die Funktion t_listen() den Prozess so lange, bis eine Verbindungsanforderung eintrifft.Wenn eine Verbindungsanforderung eintrifft, ruft der Server die benutzerdefinierte Funktion accept_call() auf, um die Verbindung zu bestätigen. accept_call() nimmt die Verbindungsanforderung auf einem neuen Transportendpunkt entgegen und liefert den zugehörigen Dateideskriptor als Ergebnis. Dieser Dateideskriptor wird in der globalen Variablen conn_fd gespeichert. Da die Verbindung auf einem neuen Transportendpunkt aufgebaut wird, kann der Server auf dem alten Transportendpunkt neue Anforderungen erwarten. Die Funktion accept_call() ist auf der folgenden Seite näher beschrieben.
Wenn die Verbindungsannahme erfolgreich war, erzeugt die Funktion run_service() einen neuen Prozess, um die Verbindung zu verwalten. Die benutzerdefinierte Funktion run_service() ist im Abschnitt "Verbindungsorientiertes Client/Server-Modell" näher beschrieben.
Die Transportschnittstelle unterstützt einen asynchronen Modus. Der asynchrone Modus wird beschrieben im Abschnitt "Weiterführende Konzepte von XTI(POSIX)".
Die Funktion accept_call(), die der Server aufruft, um eine Verbindungsanforderung anzunehmen, ist wie folgt definiert:
accept_call(listen_fd, call) int listen_fd; struct t_call *call; { int resfd; struct t_call *refuse_call; if ((resfd = t_open("/dev/tcp", O_RDWR, NULL)) < 0) { t_error(„t_open() Aufruf für accept gescheitert"); exit(7); } while (t_accept(listen_fd, resfd, call) < 0) { if (t_errno == TLOOK) { if (t_look(listen_fd) == T_DISCONNECT) { /* Verbindungsabbruch */ if (t_rcvdis(listen_fd, NULL) < 0) { t_error("t_rcvdis() gescheitert für listen_fd"); exit(9); } if (t_close(resfd) < 0) { t_error("t_close gescheitert für antwortenden fd"); exit(10); } /* Aufruf beenden und auf weiteren Aufruf warten */ return(DISCONNECT); } else { /* neues T_LISTEN; Ereignis löschen */ if ((refuse_call = (struct t_call *)t_alloc(listen_fd,T_CALL,0)) == NULL) { t_error("t_alloc() für refuse_call gescheitert"); exit(11); } if (t_listen(listen_fd, refuse_call) < 0) { t_error("t_listen() für refuse_call gescheitert"); exit(12); } if (t_snddis(listen_fd, refuse_call) < 0) { t_error("t_snddis() für refuse_call gescheitert"); exit(13); } if (t_free((char *)refuse_call, T_CALL) < 0) { t_error("t_free() für refuse_call gescheitert"); exit(14); } } } else { t_error("t_accept() gescheitert"); exit(15); } } return(resfd); }
Der Aufruf von accept_call() benötigt zwei Parameter:
listen_fd gibt den Transportendpunkt an, an dem die Verbindungsanforderung angekommen ist.
call ist ein Zeiger auf ein Objekt vom Datentyp struct t_call, das alle Informationen für diese Anforderungen enthält.
Die Funktion t_call() erzeugt zuerst einen weiteren Transportendpunkt. Der neu erzeugte Transportendpunkt resfd wird benutzt, um die Verbindungsanforderung anzunehmen.
Die Funktion t_accept() nimmt die Verbindungsanforderung an. Der erste Parameter der Funktion t_accept() gibt den Transportendpunkt an, an dem die Anforderung empfangen wurde, der zweite Parameter gibt den Transportendpunkt an, an dem die Anforderung bestätigt werden soll.
Eine Anforderung kann an demselben Transportendpunkt bestätigt werden, an dem sie empfangen wurde. In diesem Fall können andere Clients für die Dauer dieser Verbindung keine Anforderungen stellen.
Der dritte Parameter von t_accept() zeigt auf das t_call-Objekt der aktuell bearbeiteten Verbindungsanforderung. Dieses Objekt sollte die Adresse des rufenden Clients und die laufende Nummer des t_listen()-Aufrufs enthalten. Der Wert von call->sequence ist von Bedeutung, falls der Server mehrere Verbindungen verwaltet. Ein entsprechendes Beispiel finden Sie im Abschnitt "Ereignisgesteuerter Server".
Um das vorliegende Beispiel einfach zu halten, beendet der Server das Programm, wenn der Aufruf t_open() scheitert. exit(2) schließt den Transportendpunkt, der listen_fd zugeordnet ist. Der Transportanbieter sendet damit dem Client eine Nachricht, dass die Verbindung abgebrochen wurde und der Verbindungsaufbau nicht erfolgreich war. Der Aufruf t_connect() scheitert, und t_errno wird auf TLOOK gesetzt.
Die Ausführung von t_accept() kann scheitern, falls ein asynchrones Ereignis am empfangenden Transportendpunkt eintrifft, bevor die Verbindung angenommen ist. t_errno wird dann auf TLOOK gesetzt. Die Tabelle "Ereignisse beim Fehler TLOOK" im Abschnitt "Zustände und Zustandsübergänge" zeigt, dass genau eines der beiden folgenden Ereignisse eintreffen kann:
Eine Abbruchbenachrichtigung für die zuvor gemeldete Verbindungsanforderung ist eingetroffen, d.h. der Client, der die Verbindungsanforderung gesendet hat, möchte die Verbindung abbrechen.
Wenn ein Abbruchwunsch eintrifft, muss der Server sofort durch einen t_rcvdis()-Aufruf den Grund des Auftrags analysieren. Die Funktion t_rcvdis() hat als Parameter einen Zeiger auf ein Objekt vom Datentyp t_discon (siehe "t_rcvdis() - Ursache eines Verbindungsabbaus abfragen"). Das t_discon-Objekt wird benötigt, um die Abbruchbedingung zu speichern. Im vorliegenen Beispiel wird der Grund für den Abbruch nicht abgefragt; daher ist der Parameter auf NULL gesetzt. Nach Empfang der Abbruchbedingung schließt accept_call() den Transportendpunkt und liefert ein DISCONNECT als Ergebnis. Dies informiert den Server, dass die Verbindung vom Client geschlossen worden ist.
Während der Ausführung von t_accept() ist eine neue Verbindungsanforderung eingetroffen.
Im vorliegenden Beispiel weist der Server diese Verbindungsanforderung zurück, um die aktuell bearbeitete Verbindungsanforderung ungestört annehmen zu können. Im Einzelnen verfährt der Server dabei wie folgt:
Mit t_alloc() legt der Server ein neues Objekt vom Typ struct t_call an.
Anschließend nimmt der Server die neue Verbindungsanforderung mit t_listen() entgegen. t_listen() liefert im Feld refuse_call->sequence ein eindeutiges Kennzeichen für die neue Verbindungsanforderung zurück.
Mit t_snddis() weist der Server die neue Verbindungsanforderung zurück.
Nach Freigabe des durch refuse_call referenzierten t_call-Objekts mit t_free() wiederholt der Server den t_accept()-Aufruf.
Die Transportverbindung ist mit dem neu erzeugten Transportendpunkt erstellt worden. Der Empfangsendpunkt kann dadurch neue Verbindungsanforderungen behandeln.
Datenübertragung am Beispiel des Client/Server-Modells
Wenn die Verbindung einmal hergestellt ist, können Client und Server mit dem Datenaustausch beginnen. Hierfür verwenden sie die Funktionen t_snd() und t_rcv(). Von diesem Zeitpunkt an unterscheidet der Transportanbieter nicht mehr zwischen Client und Server. Jeder Benutzer kann Daten senden, Daten empfangen und die Verbindung beenden. Der Transportanbieter bietet eine gesicherte, die Sendereihenfolge erhaltende Übertragung der Daten über eine bestehende Verbindung.
Im Beispiel überträgt der Server eine Datei zum Client über die bestehende Transportverbindung.
Senden der Daten durch den Server
Der Server organisiert die Datenübertragung, indem er einen neuen Prozess erzeugt, der die Daten zum Client schickt. Der Vaterprozess wartet auf weitere Verbindungsanforderungen, während der Sohnprozess die Daten überträgt.
Die Funktion run_service() wird aufgerufen, um diesen Sohnprozess zu erzeugen. Der folgende Ausschnitt aus der Definition von run_service() veranschaulicht dieses Vorgehen:
run_service(listen_fd) int listen_fd; { int nbytes; FILE *logfp; /* Zeiger auf die Protokolldatei */ char buf[1024]; switch (fork()) { case -1: perror("fork gescheitert"); exit(20); break; default: /* Vaterprozess */ /* Schliessen von conn_fd und beenden der Funktion */ if (t_close(conn_fd) < 0) { t_error("t_close() gescheitert für conn_fd"); exit(21); } return; case 0: /* child */ /* schließen von listen_fd und übertragen der Datei */ if (t_close(listen_fd) < 0) { t_error("t_close() gescheitert für listen_fd"); exit(22); } if (t_look(conn_fd) != 0) { /* ist Verbindungsabbruch da? */ fprintf(stderr, "t_look: nicht erwartetes Ereignis \n"); exit(25); } while ((nbytes = fread(buf, 1, 1024, logfp)) > 0) { if (t_snd(conn_fd, buf, nbytes, 0) < 0) { t_error("t_snd() gescheitert"); exit(26); } }
Nach dem fork() kehrt der Vaterprozess wieder zur Hauptschleife zurück und wartet auf neue Verbindungsanforderungen.
Währenddessen verwaltet der Sohnprozess die neu aufgebaute Verbindung. Falls der Aufruf fork() scheitert, schließt exit() die aufgebaute Verbindung und sendet eine Abbruchmeldung an den Client. Dadurch scheitert dann der Aufruf t_connect() des Client.
Der Sohnprozess liest 1024 byte der Protokolldatei und sendet die Daten mit dem t_snd()-Aufruf an den Client. buf zeigt auf den Anfang des Datenpuffers, und nbytes gibt die Anzahl der zu übertragenden Zeichen an.
Wenn der Benutzer dem Transportanbieter zu viele Daten zur Übertragung zur Verfügung stellt, kann der Transportanbieter die Annahme verweigern, um die Flusskontrolle sicherzustellen. In diesem Fall wird der Aufruf t_snd() blockiert, bis die Flusskontrolle wieder freigegeben ist und mit der Übertragung fortgefahren werden kann. Der t_snd()-Aufruf wird dann nicht beendet, bevor dem Transportanbieter so viele Zeichen übergeben worden sind, wie der Wert der Variablen nbytes angibt.
Die Funktion t_snd() kontrolliert nicht, ob ein Abbruchwunsch ankam, bevor die Daten an den Transportanbieter übergeben werden. Bedingt durch den Datenverkehr in nur einer Richtung, ist es dem Benutzer außerdem nicht möglich, ankommende Ereignisse zu behandeln. Wenn zum Beispiel die Verbindung unterbrochen wird, sollte der Benutzer informiert werden, dass Daten verloren gehen könnten. Der Benutzer kann t_look() aufrufen, um vor jedem t_snd()-Aufruf zu prüfen, ob es ankommende Ereignisse gab.
Empfang der Daten durch den Client
Im Beispiel überträgt der Server über die bestehende Transportverbindung eine Datei zum Client. Der Client empfängt die Datei und gibt sie auf der Standardausgabe aus. Um die Daten zu empfangen, verwendet der Client folgendes Programmstück:
while ((nbytes = t_rcv(fd, buf, 1024, &flags)) != -1) if (fwrite(buf, 1, nbytes, stdout) == 0) { fprintf(stderr, "fwrite gescheitert \n"); exit(5); } }
Der Client ruft die Funktion t_rcv() auf, um die ankommenden Daten zu empfangen. Wenn keine Daten verfügbar sind, wird der Prozess durch den Aufruf t_rcv() solange blockiert, bis Daten verfügbar sind. Dann liefert t_rcv() die Anzahl der Bytes zurück, die im Empfangspuffer buf bereitstehen (maximal 1024). Der Client schreibt dann die empfangenen Daten auf die Standardausgabe. Die Datenübertragung wird beendet, wenn der Aufruf t_rcv() scheitert. Das ist dann der Fall, wenn ein Wunsch nach Verbindungsabbau empfangen wird. Eine Erklärung hierzu finden Sie auf der folgenden Seite.
Falls der Aufruf fwrite() scheitert, wird das Programm beendet und der Transportendpunkt geschlossen. Das Schließen eines Transportendpunkts (durch exit() oder t_close()) in der Datenübertragungsphase bewirkt den Abbruch der Verbindung; der Kommunikationspartner erhält eine Abbruchnachricht.
Verbindungsabbau am Beispiel des Client/Server-Modells
Wie bereits erwähnt, gibt es zwei unterschiedliche Formen des Verbindungsabbaus, die vom Transportanbieter unterstützt werden können:
Der Verbindungsabbruch beendet eine Verbindung sofort. Dies kann zu Datenverlust führen, falls noch nicht alle Daten den Empfänger erreicht haben.
Mit dem Aufruf der Funktion t_snddis() kann jeder Benutzer einen solchen Abbruch erreichen. Falls innerhalb des Transportanbieters Probleme auftreten, kann auch der Transportanbieter einen Verbindungsabbruch erzeugen.
Wenn die Abbruchnachricht den Empfänger erreicht, muss dieser die Funktion t_rcvdis() aufrufen, um die Nachricht zu empfangen. t_rcvdis() liefert als Ergebnis einen Wert zurück, der den Grund für den Verbindungsabbruch angibt. Dieser Wert ist abhängig vom verwendeten Transportanbieter und sollte bei protokollunabhängigen Programmen nicht interpretiert werden.
Der geordnete Verbindungsabbau beendet eine Verbindung erst dann, wenn alle Daten übertragen worden sind.
Jeder Transportanbieter muss die erste Variante, d.h. den Verbindungsabbruch, unterstützen. Im Beispiel wird unterstellt, dass der Transportanbieter außerdem den geordneten Verbindungsabbau gestattet.
Verbindungsabbau durch den Server
Wenn alle Daten übertragen sind, kann der Server den geordneten Abbau der Verbindung wie folgt einleiten:
if (t_sndrel(conn_fd) < 0) { t_error("t_sndrel() gescheitert"); exit(27); }
Die Verbindung wird erst abgebaut, wenn beide Benutzer einen Abbruchwunsch gesendet und von der Gegenseite eine Bestätigung erhalten haben (siehe Abschnitt "Phasen des verbindungsorientierten Dienstes").
Verbindungsabbau durch den Client
Der Verbindungsabbau findet aus der Sicht des Clients in der gleichen Art und Weise statt wie aus der Sicht des Servers. Wie bereits erwähnt, empfängt der Client Daten, bis der Aufruf t_rcv() scheitert. Wenn der Server entweder t_snddis() oder t_sndrel() aufruft, scheitert der Aufruf t_rcv(), und t_errno wird auf T_LOOK gesetzt. Der Client behandelt diese Situation wie folgt:
if ((t_errno == TLOOK) && (t_look(fd) == T_ORDREL)) { if (t_rcvrel(fd) < 0) { t_error("t_rcvrel() gescheitert"); exit(6); } if (t_sndrel(fd) < 0) { t_error("t_sndrel() gescheitert"); exit(7); } exit(0); } t_error("t_rcv() gescheitert"); exit(8); }
Wenn am Transportendpunkt des Clients ein Ereignis auftritt, überprüft der Client, ob der erwartete Auftrag zum geordneten Verbindungsabbau angekommen ist. Ist dies der Fall, so ruft der Client t_rcvrel() auf, um die Anforderung zu erhalten. Danach ruft der Client t_sndrel() auf. Dies zeigt dem Server an, dass auch der Client bereit ist, die Verbindung abzubauen. An diesem Punkt wird das Client-Programm beendet, wodurch auch der Transportendpunkt geschlossen wird.
Falls der Transportanbieter den eben beschriebenen geordneten Verbindungsabbau nicht unterstützt, müssen die Benutzer den abbruchartigen Verbindungsabbau verwenden. Dabei müssen die Benutzer selbst dafür sorgen, dass durch den Verbindungsabbau keine Daten verloren gehen. Zum Beispiel kann eine bestimmte Byte-Kombination anzeigen, dass die Verbindung beendet werden soll. Es gibt viele Möglichkeiten, Datenverlusten vorzubeugen. Jede Anwendung und jedes höhere Protokoll muss über einen entsprechenden Mechanismus verfügen, der sich der gegebenen Transportumgebung anpasst.