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