3. Interaktion mit dem Server

3.2 TCP: Datenströme zwischen zwei laufenden Programmen

Wir müssen erst einmal einen Schritt zurücktreten und verstehen, wie überhaupt Daten im Netz herumgeschickt werden. Ich will mich hier nicht zu sehr mit der Vorlesung Computernetzwerke überschneiden, daher werde ich das Thema nur recht oberflächlich behandeln.

Eingabe / Ausgabe.

Um sinnvoll mit dem User interagieren zu können, brauchen Programme eine Eingabe und eine Ausgabe. Haben Sie schon einmal in Java mit Dateien und generell Eingabe gearbeitet? Hier sehen Sie ein kleines Programm, das nichts anderes tut, als Zeichen für Zeichen von der Terminal-Eingabe zu lesen und dann die ASCII-Nummer des gelesenen Zeichens auf dem Terminal auszugeben.

    import java.io.*;
    public class CharToAscii {
        public static void main(String args[]) throws Exception {
            int b;
            while ((b = System.in.read()) != -1) {
                System.out.println(b);
            }
        }
    }
    javac CharToAscii.java
    java CharToAscii
    Hello!    
    72
    101
    108
    108
    111
    33
    10

Ich habe hier den Output des Programms blau gefärbt; in diesem Beispiel liest das Programm vom Terminal und schreibt aufs Terminal, daher wären ohne Farbe Input nicht von Output zu unterscheiden. Wir können statt vom Terminal auch direkt aus einer Datei lesen:

    import java.io.*;
    public class CharToAsciiFromFile {
        public static void main(String args[]) throws Exception {
            int b;
            InputStream inputStream = new FileInputStream(args[0]);
            while ((b = inputStream.read()) != -1) {
                System.out.println(b);
            }
        }
    }           
    
    javac CharToAsciiFromFile.java
    java CharToAsciiFromFile ascii.txt
    65
    66
    10
    10
    97
    98
    32
                            

In beiden Fällen lesen wir von einem Input-Stream und schreiben in einen Output-Stream. So ein Abstrahiert gesehen ist ein Output Stream-etwas, auf das wir Character für Character schreiben können, linear, nur schreiben, nicht löschen, ohne Beschränkung. Ein Input-Stream ist etwas, von dem wir Characters lesen können (falls denn einer da ist); wenn keiner da ist, wartet unser Programmthread geduldig; wir können nur linear lesen, nicht "zurückspulen"; wenn mehr Zeichen ankommen, als wir gerade lesen können, dann stauen sich die Zeichen an in einer (zumindest theoretisch) unbeschränkt großen Warteschlange. Hier ist eine schematische Darstellung:

Egal ob vom Terminal-Input oder aus einer Datei, aus Sicht des Programmes schauen beide Input-Streams im Prinzip so aus wie oben. Und das ist auch das Prinzip, dem das Protokol TCP folgt: es erlaubt uns, zwischen zwei Rechnern (nein: zwischen zwei Prozessen, die womöglich auf verschiedenen Rechnern laufen) eine Input-Output-Stream-Verbindung herzustellen; was Prozess A reinschreibt, kann Prozess B dann lesen, und umgekehrt. Wie aus einer Datei. Hier wieder eine schematische Darstellung:

Dass die gesendeten Bytes auch tatsächlich alle und in der richtigen Reihenfolge ankommen, dass der Sender nicht dauerhaft schneller die Daten schickt, als der Empfänger sie verarbeiten kann etc., dafür sorgt das Protokoll TCP (Transmission Control Protocol). Dass die Datenpakete, in die die Bytes vor dem Verschicken gesammelt werden, ihrem Weg um die Welt von Rechner X zu Rechner Y finden, dafür sorgt das Protokoll IP (Internet Protocol). Wir können die Details getrost ignorieren und einfach Input- und Outputstreams arbeiten.

Client und Server

Die obige schematische Abbildung einer TCP-Verbindung sieht symmetrisch aus: es gibt keinen konzeptuellen Unterschied zwischen X und Y. Das täuscht etwas. Damit die Verbindung überhaupt hergestellt werden kann, müssen die Prozesse ja zueinander finden. Und hier gibt es eine Asymmetrie. Einer der Prozesse, sagen wir B, ist der Server. Die IP-Adresse des Rechners Y, auf dem Prozess B läuft, muss dem Client A bekannt sein. Darüberhinaus kann es ja sein, dass auf dem Rechner Y mehrere Prozesse (Server) laufen, die bereit sind, TCP-Verbindung aufzubauen. Daher braucht es eine weitere "Durchwahl", mit der der Prozess B auf dem Rechner Y identifiziert werden kann: die Port-Nummer. Nur wenn der Prozess A die IP-Adresse vom Rechner Y und die Port-Nummer, an der Prozess B Verbindungen entgegennimmt, kann er um Aufbau einer Verbindung bitten.

Werkzeuge

Ich will jetzt, dass Sie selbst mit TCP etwas rumspielen können. Dafür brauchen wir drei kleine Werkzeuge.

  1. Einen generischen TCP-Client. Also ein Programm, dass eine TCP-Verbindung aufbaut, diese mit dem Terminal (cmd, shell) verknüpft und alles, was der User ins Terminal tippt, in die TCP-Verbindung weiterschickt und alles
  2. was es aus dem Input-Stream der TCP-Verbindung liest, auf dem Terminal ausgibt. Schematisch soll es also dies hier tun:
    Hierfür gibt es schon ein Programm: telnet auf Windows und nc auf OSX. Falls nichts davon bei Ihnen installiert sein sollte, kompilieren Sie sich mein Java-Programm MyTelnet.java.
  3. Einen Dummy-TCP-Server. Also ein Programm, das TCP-Verbindungsanfragen annimmt, dann in einer Endlosschleife liest und etwas "damit macht". In unserem Fall schickt der Server einfach den Text, den er vom Client geschickt bekommt, leicht modifiziert zurück. Das dienst einzig und allein Demonstrationszwecken: damit Sie sehen, dass hier wirklich zwei Programme (eventuell auf zwei verschiedenen Rechnern) kommunizieren. kompilieren Sie AltCapsServer.java.
  4. Einen generischen TCP-Server. Dieser nimmt eine TCP-Verbindungsanfrage an und verknüpft die dann mit dem Terminal, d.h. verhält sich nach Aufbau der Verbindung einfach wie telnet / nc / MyTelnet.java. Speichern und kompilieren Sie hierfür OnePersonServer.java.

Um jetzt damit "rumspielen" zu können, öffnen Sie bitte zwei Terminals. Auf einem (hier: links) starten Sie den Server; auf dem anderen (hier: rechts) starten Sie den Client:

    java AltCapsServer 1234                            
    AltCapsServer listening at port 1234
                            
    nc localhost 1234                            
    Hello! This is Dominik! I'm right now teaching Web Engineering I
    hElLo! ThIs iS DoMiNiK! i'm rIgHt nOw tEaChInG WeB EnGiNeErInG I
    asdfjkasdjfklajwklerjkjasdiweriopiopicvoxzc
    AsDfJkAsDjFkLaJwKlErJkJaSdIwErIoPiOpIcVoXzC
                            

Blauer Text ist der Output des Programms. Sie sind nun hoffentlich überzeugt, dass hier wirklich zwei verschiedene Prozesse miteinander kommunizieren. Probieren Sie es auf Ihrem Rechner aus!

Funktioniert das auch, wenn die Prozesse auf verschiedenen Rechnern laufen? Das auszuprobieren, ist im Rechnerraum der HSZG leider nicht möglich: die Sicherheitseinstellungen erlauben keine "ungenehmigten" TCP-Verbindungen. Sie können also nicht so einfach Ihren privaten TCP-Server aufmachen; zumindest kann der keine Verbindungen von anderen Rechnern annehmen, nicht einmal innerhalb des Rechnerraumes. Wenn es Sie dennoch ausprobieren wollen, haben Sie verschiedenen Möglichkeiten:

  • Sie machen mit Ihrem Smartphone einen Wifi-Hotspot auf und verbinden zwei Laptops mit dem Wifi (hier müssen Sie sich also eventuell in Gruppen zusammentun). Auf einem Laptop starten Sie den Server: java AltCapsServer 1234. Finden Sie die IP-Adresse des Server-Laptops heraus, sagen wir 192.168.178.20, um konkret zu sein. Auf den anderen Laptops, die im gleichen Hotspot-Wifi sind, öffnen Sie ein Terminal und tippen nc 192.168.178.20 1234. Dann testen Sie es.
  • Sie probieren, meinem virtuellen Server zu kontaktieren: nc 193.174.103.62 2523. Das funktioniert allerdings nur, wenn Sie im HSZG-Netz drin sind (also physisch an der Hochschule oder über VPN), und ich weiß auch nicht, ob ich den Server immer laufen lassen werde.
        nc 193.174.103.62 2523                            
        Ist da jemand? Liest jemand, was ich hier schreibe?
        IsT Da jEmAnD? lIeSt jEmAnD, wAs iCh hIeR ScHrEiBe?
                                

Terminal-Client und Terminal-Server

Probieren Sie jetzt aus, unseren Terminal-Server OnePersonServer.java zu starten und mit einem Terminal-Client, also nc oder telnet oder MyTelnet.java kommunizieren zu lassen:

    java OnePersonServer 1234
    
    
    Hello! I'm the nc client!
    Hello! I'm OnePersonServer, written in Java.
    

    nc localhost 1234                            
    Hello! I'm the nc client!
    
    
    Hello! I'm OnePersonServer, written in Java.
                        

Wie immer verwenden wir schwarz für das, was der User am Terminal eingibt und blau für das, was das Programm ausgibt. Hier ist eine schematische Darstellung.

Bis jetzt ist das natürlich reine Spielerei. Im nächsten Teilkapitel aber wird es uns helfen, die Kommunikation zwischen Browser und Webserver zu "belauschen".

Ein Kurznachrichten-Server

Wir haben nun also zumindest oberflächlich verstanden, wie man TCP-Verbindungen entegennimmt, aufbaut und sie dann benutzt. Wir wissen nicht, wie die Rechner (unsere; und die da draußen im Internet) sie realisieren; dies ist Gegenstadt der Vorlesung Computernetzwerke. Dennoch können wir nun versuchen, mit unserem Wissen etwas einigermaßen Brauchbares zu programmieren. Als Beispiel will ich hier einen Kurznachrichtenserver schreiben, auf dem man anonym Nachrichten verschicken kann, wie auf einem Nummernkonto. Ich will also folgendes:

  • Ein User soll anonym einem anderen User eine Nachricht schicken können. Dafür braucht der Empfänger einen Benutzernamen.
  • Ein User soll die Nachrichten abrufen können, die ihn/sie erreicht haben. Dafür brauchen wir also nicht nur einen Benutzernamen sondern auch ein Passwort.
  • Man muss Benutzerkonten mit Name und Passwort anlegen können.

Unser Server muss also drei Funktionen zur Verfügung stellen: CREATE username password erzeugt einen neuen User; WRITE username message schickt die Nachricht message an username; schlussendlich brauchen wir GET username password, womit der User seine neuen Nachrichten abrufen kann.

Wir müssen die Kommunikation zwischen Client und Server formalisieren: müssen CREATE, username und password auf einer gemeinsamen Zeile stehen oder in drei getrennten? Erlauben wir Zeilenumbrüche in einer Nachricht? Wenn ja, woran erkennen wir, dass sie zu Ende ist? Muss der User immer mit GET die Nachrichten abrufen oder schickt der Server automatisch die Nachricht an username, wenn eine neue an ihn/sie geschrieben worden ist? Erlauben wir also Push-Nachrichten?

Egal wie wir uns entscheiden, Sie sehen, wir müssen ein bestimmtes Format festlegen, in welchem der Client seine Wünsche dem Server gegenüber klarmachen kann. Wir müssen ein Protokoll entwerfen.

Hier sehen Sie, wie ich das realisiert habe. Ich habe mir dabei keine besondere Mühe gegeben, Passwörter werden nicht verborgen und noch dazu im Klartext übertrage etc. Egal. Es geht um das Prinzip, dass ich beleuchten will. Ich habe auf drei Terminals mit dem Server kommuniziert. Hier sehen Sie das Ergebnis. Die Idee hinter meiner Darstellung ist, dass parallele Zeilen in meiner Abbildung ungefähr zeitgleich erfolgt sind.

    Dieser User verschickt zwei Nachrichten                            
    nc 193.174.103.62 2049
    Your command [CREATE/GET/WRITE]: WRITE
    Enter recipient's username: dominik
    Your message:
    Dies ist eine Testnachricht.
    Your command [CREATE/GET/WRITE]: WRITE
    Enter recipient's username: rabbit55
    Your message:
    I know who you are.
    
    Dieser User ruft seine Nachrichten ab (hat zuvor bereits ein Konto eröffnet)                                                        
    nc 193.174.103.62 2049  
    
    
    
    
    Your command [CREATE/GET/WRITE]: GET
    Enter username: dominik
    Enter password: dpw
    You have 1 new messages.
    1. Dies ist eine Testnachricht.
                            
    Dieser User legt ein Konto an und ruft seine Nachrichten ab                                  
    nc 193.174.103.62 2049                            
    Your command [CREATE/GET/WRITE]: CREATE
    Enter username:  rabbit55
    Enter password:  xyz
    
    
    
    Your command [CREATE/GET/WRITE]: GET
    Enter username: rabbit55
    Enter password: xyz
    You have 1 new messages.
    1. I know who you are. 
                            

Den Quelltext dafür finden Sie in der Datei MessageServer.java.

Internet und Web

Unsere Server AltCapsServer.java und MessageServer.java nehmen TCP-Verbindungen an und kommunizieren dann über diese mit ihren Clients. Die physische und virtuelle Infrastruktur, die diese Kommunikation ermöglicht, nennen wir das Internet. Ich habe also zwei Internet-Anwendung in Java geschrieben. Keine Web-Anwendungen. Was ist das Web? Das Web ist wiederum eine Infrastruktur, die uns ermöglicht, Inhalte (oft: Dateien) auszutauschen, und benutzt dafür ein standardisiertes Protokoll (analog zu unserem CREATE/WRITE/GET-Protokoll vom MessageServer.java aber viel allgemeiner, mächtiger und komplexer), das Hypertext Transfer Protocol.

Wenn wir also in der Praxis so etwas wie MessageServer.java schreiben wollten, dann würden wir mit aller Wahrscheinlichkeit nicht den obigen Weg gehen und unser Protokoll selbst entwerfen, sondern würden auf der existierend Infrastruktur des Web mit HTTP aufbauen. Warum? Nun, erstens können die meisten Dinge, die wir tun wollen, bereits mit HTTP erledigt werden, wir müssen also keinen Code schreiben, der den Input-Stream der TCP-Verbindung liest, zerlegt, versteht; zweitens gibt es bereits Bibliotheken, die uns viel Code für das Programmieren von HTTP-Servern bereitstellen; drittens gibt es bereits ausgefeilte HTTP-Clients, die wir verwenden können: die Browser. Wenn wir also ein Front End für unseren Kurznachrichten-Dienst bauen wollen, dann können wir auf dem Ökosystem aus HTML, CSS und HTTP aufbauen; unsere "Kunden" müssen sich nicht erst einen Message-Client installieren, sondern können einfach eine Webseite aufrufen.

Übung

Schreiben Sie einen TCP-Server Ihrer Wahl. Funktionalität steht hier nicht im Vordergrund. Sie sollen das Grundprinzip des Server-Sockets und die Notwendigkeit des Multi-Threadings verstehen. Ihr Server sollte an die Clients eindeutige Nummern vergeben, zum Beispiel in der Reihenfolge, in der sie beigetreten sind. Gerne aber auch zufällige Zahlen. Die Funktionalität Ihres Servers kann sein, was Sie wollen, hier sind aber ein paar Vorschläge.

  • Wenn ein Client sich verbunden hat, muss der Client sich als Elektron oder Proton identifizieren. Der Server verbindet dann beliebig Elektron-Clients mit Proton-Clients zu einem 1-1-Chat. Überlegen Sie sich, was mit einem Client passieren soll, wenn sein Chat-Partner den Chat verlässt.
  • Der Server implementiert einen Gruppenchatraum. Was ein Client schickt, wird an alle Clients weitergeleitet. Optional: jeder Client muss am Anfang einen Spitznamen wählen.
  • Der Server stellt einfache UPLOAD/DOWNLOAD-Funktionalität zur Verfügung. Ein Client kann den Befehl UPLAOD filename schicken, gefolgt von beliebigem Inhalt; dann trennt der Client die Verbindung. Der Server speichert den Inhalt unter dem Namen filename. Wenn ein Client DOWNLOAD filename schickt, antwortet der Server mit dem Inhalt, falls der existiert und mit einer Fehlermeldung, falls nicht. (Sie müssen keinen Speicherung der Dateien auf Ihrer Festplatte implementieren, das würde zu weit führen; meinetwegen können Sie alles im Arbeitsspeicher des Servers lassen.)