6. Dynamisches Verhalten im Browser mit Server

6.2 Websockets

Im letzten Abschnitt haben wir gelernt, wie wir aus dem Javascript-Code heraus HTTP-Requests stellen und den Response darauf behandeln können. Das hat zum Beispiel den Vorteil, dass nicht immer die ganze HTML-Seite serverseitig neu erstellt werden muss, sondern der Server nur gewisse Daten schickt und der Browser die dann per Javascript in die Seite "einbaut." Potentiell reduziert dies die Datenmenge, die ausgetauscht wird, um ein Vielfaches. Aus der Sicht des Servers sieht jedoch alles aus wie zuvor: er bekommt einen Request und beantwortet ihn. Er merkt nicht unbedingt, ob es ein AJAX-Request war oder ein "normaler", also z.B. durch klicken auf einen Link oder durch ein HTML-<form>-Element ausgelöstes. Mit "er merkt es nicht" meine ich, dass sich am zugrundeliegenden Protokoll HTTP nichts geändert hat. AJAX-Requests stellen also nur eine zusätzliche Browser-Funktionalität dar.

In diesem Abschnitt lernen wir Websockets kennen. Im Gegensatz zu AJAX ist dies wirklich ein neues Konzept, inklusive einem neuen Protokoll, das über HTTP hinausgeht. Rufen Sie sich in Erinnerung: HTTP erlaubt Datenaustausch in dem Muster Browser fragt, Server antwortet. Dinge wie Server meldet sich, wenn es was Neues gibt, sind nicht vorgesehen.

Hier ist eine Beispielanwendung für einen anonymen Chatraum. Falls ich den entsprechenden Server auf 193.174.103.62 gestartet habe, klicken Sie einfach auf http://193.174.103.62:6006 und schreiben etwas (falls der Server nicht läuft, laden Sie sich index.html und server.js herunter und starten Sie den Server lokal). Sinn macht das nur, wenn mehrere User anwesend sind. Falls Sie einsam sind, öffnen Sie also ein zweites Browser-Fenster und spielen Sie mehrere User-Rollen auf einmal. Es sollte in etwa so aussehen:

und, nachdem Sie sich eingeloggt haben:

Der Witz an dieser Beispielanwendung ist, dass Sie den anderen Usern beim Tippen "zuschauen" können.

Übung Überlegen Sie sich, wie Sie mit den Werkzeugen, die Sie bis jetzt gelernt haben, einen solchen Chat-Room programmieren würden.

Die Funktionalität, die mit herkömmlichen Werkzeugen (HTMl, Javascript, Ajax) schwierig wird, ist, dass Änderungen, die andere User getätigt haben, sofort auf Ihrem Bildschirm erscheinen. Eine Möglichkeit, dies zu implementieren, wäre, dass Ihr Browser per Javascript-Funktion setInterval dazu veranlasst wird, z.B. alle 100 Millisekunden einen Request an den Server zu schicken, nach dem Motto Hat sich was geändert? Das Interval sollte so gewählt werden, dass es unterhalb der Wahrnehmungsgrenze liegt. Eine elegantere und effizientere Lösung stellen Websockets dar. Aber eins nach dem Anderen.

Laden Sie sich index.html, index-2.html und server.js herunter; falls ich den Server im Netz gestartet habe, klicken Sie auf http://193.174.103.62:6007. Die Seite http://193.174.103.62:6007/index-2.html erlaubt Ihnen, in einem von Ihnen vorgegebenen Zeitinterval wiederholt HTTP-Requests an den Server zu schicken, der dann mit einer Zufallszahl antwortet.

Übung Laden Sie sich index.html, index-2.html und server.js auf Ihren lokalen Rechner herunter. Führen Sie einen "Last-Test" aus: öffnene Sie mehrere Browser-Fenster, stellen Sie sie nebeneinander, und geben als Interval 4 Millisekunden ein. Klicken Sie auf get stream of numbers. Wieviel "Durchsatz" schaffen Sie, also wieviele Millisekunden pro Request hat Ihre Anwendung gebraucht?

Die Anwendung, so einfach sie auch ist, ist ineffizient. Jedes mal, wenn der Browser eine neue Zufallszahl will, wird ein neuer HTTP-Request erstellt. Hierzu muss erst einmal eine neue TCP-Verbindung aufgebaut werden; bereits dies erzeugt einiger Overhead. Schlimmer noch: wenn die Komunikation verschlüsselt sein sollte, müsste jedes Mal ein neuer Key-Exchange stattfinden. Wie wir gesehen haben, ist der HTTP-Header von der Größe her recht stattlich, gemessen daran, dass wir nur eine Zufallszahl übertragen wollen.

Übung Laden Sie sich ServerSniffer.java herunter und belauschen Sie die Kommunikation zwischen der Seite index-2.html und dem Server server.js. Hier ist eine schematische Darstellung, was ServerSniffer tut:

Websockets

Jetzt bauen wir die gleiche Seite, mit gleicher Funktionalität, aber mit Websockets. Die Idee ist, dass die TCP-Verbindung nur einmal aufgebaut werden muss. Der Browser lauscht dann an den eingeheneden Verbindung, ganz ähnlich, wie wir mit Java Socket verwendet haben. Der Server muss den entsprechenden Websocket aufmachen und Verbindungen akzeptieren, ähnlich dem ServerSocket in Java. Wenn die Verbindung etabliert ist, können wir einfach Daten hin- und herschicken.

Übung Wiederholen Sie den Lasttest mit der Anwendung, die nun per Websockets Zufallszahlen anfordert / schickt. Laden Sie sich dafür index.html und server.js herunter. Messen Sie den durchschnittlichen Zeitverbrauch pro Request an "Ihren" Server auf Ihrem Rechner; gerne auch pro Request an "meinen" Server auf http://193.174.103.62:6008. Bitten Sie mich zuvor, den zu starten.

Sie können (und sollten) sich den Quelltext von server.ja und index.html in aller Ruhe anschauen. Besonderen Dank an dieser Stelle an Eduard Haas, dessen Websocket-Anwendung ich als Vorlage verwendet habe. Ich will hier nur die wichtigsten Punkte durchgehen.

<!-- Die Datei index.html -->                        
    ... 
socket = new WebSocket(`ws://${location.hostname}:${websocket_portnumber}`);
           also zum Beispiel new WebSocket("localhost:6007");
    ... 
    socket.onopen = function (e) {
            Hier definieren Sie den Code, der ausgeführt werden soll, wenn die Verbindung etabliert worden ist.
            Das kann zum Beispiel eine Anmeldenachricht sein, wo sie z.B. etwas wie {username: "dominik"} als 
            JSON codiert verschicken. In diesem Beispiel initialisieren wir mit setInterval einen Thread,
            der in einem gegebenen Zeitinterval Daten durch den Websocket schickt.
      socket.send("xxx"):
            der Text xxx steht für nichts besonderes; wir wollen einfach irgendwas an den Server schicken
    };
    ... 
      socket.onmessage = function (event) {    
              Hier schreiben Sie, was passieren soll, wenn Sie eine Nachricht erhalten.
              Den Inhalt der Nachricht finden Sie als String in event.data
              Meistens werden Sie also mit JSON.parse(event.data) den in ein Javascript-Objekt parsen.
    };

      socket.onclose = function (event) {    
            ... hier sagen Sie, was geschehen soll, wenn die Verbindung geschlossen wird.
    };
Und nun zur Datei server.js. Serverseitig haben Sie auch socket.onmessage und socket.onclose. Für die Verbindungsherstellung ist der Code jedoch etwas anders, da es hier ja die Asymmetrie zwischen Server (einer) und Clients (potentiell viele) gibt:
/* Die Datei server.js */                        
const serverSocket = new websocket_package.Server({ port: websocket_portnumber });
/* das ist der Server-Socket, über den Sie mehrere "normale" Verbindungssockets bekommen können */
serverSocket.on('connection', function (socket) {
/* der Code, was Sie mit diesem Socket machen wollen */                    

Sehen Sie, dass wir bei serverSocket.on('connection', function (socket) { der inneren, anonymen, Funktion ein Argument socket übergeben. Dies ist der Socket zum neuen Client. Die innere, anonyme Funktion, wird also jedes Mal aufgerufen, wenn ein neuer Client sich verbindet. Diese Code-Zeile entspricht dem Java-Code

    while (true) {
        Socket socket = serverSocket.accept();
        ...
    }

Eine Herausforderung ist, dass der Server sich oft "merken" muss, welcher Socket zu welchem Client gehört, um zum Beispiel hereinkommende Nachrichten zuordnen zu können.

Übung Schreiben Sie eine Anwendung, in der der Server über einen Websocket beliebig viele Verbindungen annimmt und jeder Verbindung eine "client number", beginnend bei 0, zuordnet. Wenn ein Client Daten (z.B. "hello") schickt, soll der Server auf der Konsole in etwa ausgeben client 3: hello.
Übung Schreiben Sie eine Anwendung, in der der Benutzer verschiedene "Felder" wie Name, Alter, Adressen an den Server schicken kann. Die HTML-Seite sollte zwei Texteingabefenster beinhalten: Attribut-Name und Attribute-Wert. Wenn ich bei Attribut-Name firstname und bei Attribut-Wert dominik eingebe, soll der Browser den String {"firstname" : "dominik"} an den Server schicken. Der Server soll für jeden Client ein Javascript-Objekt anlegen und es pro Nachricht mit Werten füllen. Wenn ich jetzt also nochmal auf der HTML-Seite bei Attribut-Name age und bei Attribut-Wert 42 eingebe, sollte server-seitig mein Client-Objekt jetzt {age: "42", firstname: "dominik"} sein. Der Server sollte bei jeder Veränderung den entsprechenden Client auf der Konsole ausgeben (per JSON.stringify) zum Beispiel.