Fachpraktikum 6 von 6: Netzwerkprogrammierung
Transcription
Fachpraktikum 6 von 6: Netzwerkprogrammierung
Institut für Technische Informatik und Kommunikationsnetze Fachpraktikum 6 von 6: Netzwerkprogrammierung Testatbedingungen: Zusatztestat: Lösen der Aufgaben 1 bis 2, sowie eine aus Aufgabe 3 Selbstständiges lösen der Aufgabe 4 Hinweis: Jeder arbeitet für sich, Betreuer/Nachbar fragen erlaubt Abgabe an Betreuer: Programm(e) auf Konsole ausführen und zeigen, Vorhandene Fragen aus Text beantworten, Eigenen Quelltext erklären Communication Systems Group ETH Zurich Ariane Keller <[email protected]> Daniel Borkmann <[email protected]> Inhaltsverzeichnis 1 Einleitung 2 Aufgaben 2.1 Einfacher Chat Client/Server . . . . . . . 2.2 Chat Client/Server mit Multiplexing . . . 2.3 Weitere Server Funktionen . . . . . . . . 2.4 (Optional:) Implementieren von Channels 1 1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1 3 4 5 Einleitung Ziel dieses Praktikums ist es, zu lernen wie Netzwerkprogrammierung mittels Sockets funktioniert. Dabei wird zunächst eine einfache Verbindung zwischen einem Client und einem Server aufgebaut. Das erworbene Wissen wird dann dazu verwendet, einen ChatClient zu bauen und den vorhandenen Chat-Server um nützliche Funktionen zu erweitern. Sockets1 sind Kommunikationskanäle, die ganz allgemein zur Interprozess-Kommunikation verwendet werden. Eine Anwendung davon ist die Netzwerkkommunikation mittels TCP oder UDP. Benutzt werden Sockets dabei ähnlich wie Datei-Deskriptoren. Ein Socket hat zwei Endpunkte, die jeweils über das Tupel von Ip-Adresse und Port definiert sind. Zwei Computer können also untereinander durchaus mehrere Sockets gleichzeitig offen halten. Ein Client könnte z.B. mit einem Server gleichzeitig auf Port 80 eine Verbindung für HTTP benutzen und auf Port 110 Emails abrufen. Dies würde dann über zwei verschiedene Sockets geschehen. Eine typische Verbindung über Sockets läuft wie folgt ab: 1. Der Server öffnet den Socket und legt einen Port (TCP/UDP) fest auf welchem er Verbindungen akzeptieren möchte. Ein Web-Server wartet z.B. auf Port 80 auf einkommende Verbindungen. 2. Der Client baut eine Verbindung zum Server und dem Port auf. Normalerweise wird dem clientseitigen Ende des Sockets dabei automatisch ein zufälliger freier Port zwischen 1024 und 65535 zugewiesen. 3. Der Server akzeptiert die Verbindung. 4. Client und Server wechseln sich mit schreiben und lesen von Daten ab. 5. Die Verbindung wird geschlossen. Nutzen Sie bei Bedarf die Manpages zur Netzwerkkommunikation als Referenz: socket(2), close(2), bind(2), listen(2), accept(2), connect(2), htons(3), read(2), write(2) 2 Aufgaben 2.1 Einfacher Chat Client/Server Zunächst wollen wir eine einfache Verbindung zwischen einem Server und einem Client herstellen. Sobald der Server gestartet ist, können Clients auf den Server verbinden. Nach erfolgtem Verbindungsaufbau verhalten sich Client und Server wie folgt: 1 siehe auch http://de.wikipedia.org/wiki/Socket 1 1. Der Server schickt eine Begrüssungsnachricht an den Client und fragt nach dessen Name. 2. Die Begrüssungsnachricht wird von Client gelesen und ausgegeben. Der Client wartet auf die Eingabe des Namens und schickt diesen dann an den Server. 3. Der Server nimmt den Namen zur Kenntnis und fragt nach einer Nachricht. 4. Der Client erwartet wiederum eine Eingabe und schickt die Nachricht an den Server. 5. Der Server bedankt sich, verabschiedet sich und beendet die Verbindung. Aufgaben 1-3 sind eine Schritt-für-Schritt Arbeitsanleitung um den Server und Client zum Laufen zu bringen. Das Grundgerüst für den Server und Client ist in chat-simple.c zu finden. 0. Grundlagen von Sockets Falls Sie die Manpages oder andere Howtos zum Thema Sockets noch nicht gelesen haben, verschaffen Sie sich jetzt einen kurzen Überblick. 1. Server: Festlegen des Ports und Verbindungen annehmen Im Server fehlt der Code für die Festlegung des Ports und das Annehmen eingehender Verbindungen. Sämtliche notwendigen Datenstrukturen stehen bereits zur Verfügung (lfd (Listen Socket), saddr (Adress-Struktur vom Server), caddr (Adress-Struktur vom Client) und clen (Länge der Adress-Struktur vom Client)) und sind korrekt initialisiert. (a) Fügen Sie zunächst an der bezeichneten Stelle im Sourcecode den Code für die Festlegung des Ports ein. (b) Nun fügen Sie den Befehl zur Annahme eingehender Verbindungen ein. Der zurückgegebene Socket muss der Variablen nfd (Socket vom neuen Client) zugewiesen werden. 2. Client: Verbindung aufbauen Im Client fehlt an der bezeichneten Stelle der Befehl für den Aufbau der Verbindung. Fügen Sie diesen Aufruf ein und kompilieren Sie Client und Server neu (gcc -Wall -O2 chat-simple.c -o chat-simple). 3. Verbindung herstellen Starten Sie den Server in einer Shell mit dem Kommando ./chat-simple server <port>. Der Server benötigt als Parameter den Port (zwischen 1024 und 65535) auf welchem er hören soll. Wurde der Server korrekt gestartet, wartet er auf Clients, die eine Verbindung aufbauen möchten. Starten Sie in einer neuen Shell den Client (./chat-simple client <server-ip/name> <port>) mit dem Hostnamen (oder der IP Adresse) des Servers und dem Port als Parameter. Ein Server auf derselben Maschine kann über ’localhost’ bzw. ’127.0.0.1’ erreicht werden. Wenn alles funktioniert hat, sollten Sie folgenden Output für den Client sehen: Connection successful. Server says: Hi! Here’s a simple server. What’s your name? > Bob Server says: Hi Bob. What’s your message? > Have a nice day! Server says: Thanks, bye! 2 4. Zum Nachbarn verbinden Versuchen Sie mit Ihrem Client auf den Server eines benachbarten Kollegen zu verbinden. Verwenden Sie dazu entweder dessen IP Adresse (/sbin/ifconfig) oder dessen Name (er hat die Form tardis-cX und steht an den Maschinen angeschrieben). 5. Gleichzeitige Verbindungen Was passiert wenn Sie mit mehreren Clients auf denselben Server verbinden? Wie erklären Sie sich das und was könnte man dagegen unternehmen? 6. Warum ist die Fehlerbehandlung beim Server innerhalb der while-Schleife anders als ausserhalb der Schleife? 7. Was bedeutet der setsockopt(2) Systemruf mit der Option SO REUSEADDR? 2.2 Chat Client/Server mit Multiplexing Unser einfacher Server eignet sich nicht sonderlich gut als Basis für einen Chat-Server, da er nicht mehrere Clients gleichzeitig bedienen kann. Aus diesem Grund stellen wir einen rudimentären Chat-Server zur Verfügung (chat.c, Funktion server main), der I/OMultiplexing beherrscht. Grundlage für dieses Multiplexing ist die Funktion select(2) oder auch poll(2). Mittels select(2) lässt sich auf mehreren Sockets gleichzeitig abfragen, ob Daten zum Lesen bereitliegen oder ob Daten ohne zu blockieren geschrieben werden können. Der Server kann dann jene Sockets bedienen, die bereit sind und danach erneut abfragen. So wird der Server nicht von einem einzigen Client blockiert und kann gleichzeitig viele Clients bedienen. Der Chat-Server hat eine ganz einfache Funktionsweise. Jede Nachricht, die er auf einem Socket erhält, verschickt er zusammen mit einer Herkunftsangabe an alle Clients, die zum Server verbunden sind in der Form From <IP/Alias>: <Nachricht>. 1. Alt trifft neu Kompilieren (gcc -Wall -O2 chat.c -o chat -lcurses) und starten Sie den Chat-Server mittels ./chat server <port>. Verbinden Sie sich nun mit mehreren unserer einfachen Clients (./chat-simple client <ip> <port>) auf den Chat-Server. Was ist der Unterschied zum vorhergehenden Server? 2. Der Chat-Client Unser einfacher Client ist etwas überfordert mit dem Chat-Server. Da dieser nun sämtliche Antworten aller Clients schickt und nicht mehr nur die eigenen, fällt die Reihenfolge und Darstellung zum Teil etwas chaotisch aus. Ein grosses Manko ist auch der Umstand, dass jeweils nur nach dem Abschicken einer Antwort gelesen wird. Während der User am schreiben ist, empfängt der Client keine Daten vom Server. Dem wollen wir nun mit einem richtigen Chat-Client Abhilfe verschaffen, indem wir den Bildschirm in zwei Teile aufteilen: Einen Schreibbereich und ein Messageboard wo alle Nachrichten dargestellt werden. Ebenfalls soll der Chat-Client während des Schreibens auf Nachrichten vom Server prüfen. In der Datei chat.c (Funktion client main) steht ein Gerüst bereit, dass ein textuelles User Interface zur Verfügung stellt und die Benutzereingabe verarbeitet. Implementieren Sie im Chat-Client das Verbinden zum Server. In fd soll am Ende ein gültiger Socket-Deskriptor zum Server stehen. Sie können sich dabei an dem Verbindungsaufbau beim einfachen Client orientieren (chat-simple.c). Kompilieren Sie den Chat-Client neu und starten Sie ihn (./chat client <ip> <port>). Der Client kann jetzt bereits Daten an den Server schicken, es fehlt ihm 3 nur noch die Methode um Nachrichten vom Server zu lesen. Mit F9 können Sie den Client beenden. 3. Lesen mit poll Bei der Verarbeitung der Benutzereingabe (while-Schleife in der client mainMethode) gibt es drei Fälle: (a) Es wird ein normales Zeichen eingegeben. Dieses wird in einen Buffer geschrieben und im Schreibbereich angezeigt (client echo character). (b) Es wird die Return-Taste gedrückt. In diesem Fall wird der Buffer mittels write(2) an den Chat-Server geschickt, danach wird der Schreibbereich und der Buffer gelöscht. (c) Es wird nichts eingegeben (case ERR:). In diesem Fall soll mittels poll überprüft werden, ob Daten vom Server angekommen sind. Ist dies der Fall, sollen die Daten mit read(2) gelesen und ins Messageboard geschrieben werden. Implementieren Sie nun Fall c). Dafür benötigen Sie den Befehl poll(2)2 definiert in <sys/poll.h>, welcher auf Events eines File Descriptors (in unserem Fall des Sockets) wartet. Erstellen Sie ein struct pollfd gefüllt mit dem Socket auf dem Sie lesen wollen und bestimmen Sie mittels events, dass Sie an zu lesenden Daten interessiert sind. Rufen Sie dann poll(2) auf. Stehen Daten an, verwenden Sie den read(2) Befehl um die Daten einzulesen (siehe chat-simple.c). Der Buffer zum Einlesen der Daten liegt in der Variable rbuff mit der zu verwendenen Länge rlen bereits vor. Verwenden Sie die Methode client post message(rbuff), um die gelesenen Nachrichten schliesslich ins Messageboard zu schreiben. Am Ende können sich nun mehrere Clients zu Ihrem Server verbinden und untereinander chatten (z.B. mit Ihrem Nachbarn). Der Server blockiert nun auch nicht mehr die einzelnen Verbindungen. 2.3 Weitere Server Funktionen Als richtige Power-User vermissen wir in unserem Server noch ein paar praktische Funktionen, wie etwa eine Liste aller anwesenden Teilnehmer, die Möglichkeit ein Alias zu wählen oder private Nachrichten zu verschicken. Diese Funktionen wollen wir nun auf dem Server nachrüsten. Hinweis: Mit der Funktion server write fd queue können unter Angabe an den zu sendenden Socketdeskriptor fd zu sendende Strings im Sendepuffer konkateniert werden. Anschliessend kann ein write(2) vom Sendepuffer per server schedule write getriggert werden. Siehe auch server process broadcast als Beispiel. Der Empfangspuffer vom gegebenen Socketdeskriptor fd befindet sich in eset.clients[fd].inbuff und hat den Typ uint8 t. 1. Liste aller Anwesenden Im Client soll es möglich sein mittels Eingabe von ’who’ eine Liste aller verbundenen Chat-Clients zu erhalten. Da diese Informationen nur auf dem Server vorhanden sind, müssen wir auch dort ansetzen. Der Client schickt die Eingabe wie eine normale Chat-Nachricht an den Server. Dieser soll nun, wenn das ’who’ Kommando gegeben 2 Sämtliche Details zu poll finden Sie in dessen Manpage. 4 wurde, nicht einfach die Nachricht an alle Clients weiterschicken, sondern dem anfragenden Client die gewünschte Liste zuschicken. Falls Sie die Alias-Teilaufgabe implementiert haben, so nutzen Sie auch vorhandene Aliase zur Anzeige anstatt der IP-Adressen. Orientieren Sie sich am Code für den Broadcast (Methode server process broadcast) und implementieren Sie die Methode server process who. 2. Wählen eines Alias Damit die Privatsphäre der Chatter besser gewahrt bleibt, soll der Server nun die Möglichkeit anbieten, unter einem Alias aufzutreten. Implementieren Sie auf dem Server das Kommando alias <name>, mittels dessen sich ein Client ein Alias vergeben kann. Hat ein Client ein Alias gewählt, soll hinter dem From ... seiner Nachrichten nicht mehr seine IP-Adresse sondern sein Alias stehen. In der Struktur struct client gibt es dafür das Flag has alias und das Characterarray alias, indem der Alias gespeichert werden soll. Implementieren Sie nun analog zum ’who’ Kommando das ’alias’ Kommando in der Methode server process alias. Ein Alias kann pro Session nur einmalig gesetzt werden. Ferner muss geprüft werden, dass das Alias nicht schon vergeben ist und dass es keine Whitespaces (z.B. Leerzeichen, Tab) enthält. Falls das Alias gesetzt werden kann, so ist dem Nutzer eine Erfolgsmeldung zu senden beziehungsweise eine Fehlermeldung bei Fehlschlag. 3. Private Nachrichten Es wäre toll, wenn man auch private Nachrichten verschicken könnte. Es ist schliesslich nicht immer alles für die Öffentlichkeit gedacht. Der Syntax für eine private Nachricht soll wie folgt sein: @<alias> <msg> Wenn wir also Joe ganz privat Hallo sagen wollen, tippen wir im Client ’@Joe Hallo Joe, wie gehts?’. Für Joe sollte bei der Zustellung der Nachricht ersichtlich sein, dass er diese Nachricht privat von uns erhalten hat. Wenn Sie Teilaufgabe 2 nicht gemacht haben, können Sie statt des Alias auch die IP-Adresse zur Identifikation benutzen. Orientieren Sie sich am Code für den Broadcast (Methode server process broadcast) und implementieren Sie die Methode server process private. 2.4 (Optional:) Implementieren von Channels Der Aufwand für diese Aufgabe sprengt den Rahmen dieses Praktikums. Wenn Sie uns Ihre Lösung mit funktionierenden privaten Channels zeigen, schreiben wir Ihnen ein weiteres Fachpraktikum gut! Der Server unterstützt nun zwar private Nachrichten, für längere vertrauliche Gespräche ist die Syntax jedoch etwas umständlich. Auch wenn man mehrere Personen gleichzeitig ins Vertrauen ziehen möchte ist die bestehende Lösung nicht sehr komfortabel. Eine mögliche Lösung für diese Probleme sind Channels, eine Art Chat-Räume, in die man eintreten kann. Man chattet dann nur mit den anwesenden Personen (ähnlich wie im IRC). Solche Channels können auch privat sein, man braucht dann eine Einladung um am Channel teilzunehmen. 1. Öffentliche Channels Implementieren Sie die grundlegenden Kommandos um Channels zu verwalten: 5 JOIN <name> LEAVE LIST Eintreten in einen Channel. Gibt es einen solchen Channel nocht nicht, so soll er erstellt werden. Verlassen des aktuellen Channels. Verlässt die letzte Person einen Channel, so soll er gelöscht werden. Zeigt die verfügbaren Channels mit den darin befindlichen Personen an. 2. Private Channels Private Channels sollen den Zugang nur auf eine Einladung hin ermöglichen. Die Person, die den privaten Channel erstellt, wird automatisch zum Besitzer des Channels. Private Channels sollen auch bei LIST mit dem Hinweis angezeigt werden, dass diese private sind. Erweitern Sie die Channels um folgende Kommandos: JOINP <name> INVITE <alias> EXPEL <alias> Eintreten in einen privaten Channel, falls der Nutzer vom Channelbesitzer vorher freigeschalten wurde. Gibt es einen solchen Channel nocht nicht, so wird er erstellt und der Ersteller wird zum Channelbesitzer. Wird vom Besitzer des privaten Channels aus dem Channel heraus ausgeführt um einen Chatter in den Channel einzuladen. Kann vom Besitzer benutzt werden um einen unliebsamen Gast aus seinem Channel zu entfernen. 6