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:
Der Witz an dieser Beispielanwendung ist, dass Sie den anderen Usern beim Tippen "zuschauen" können.
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.
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.
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.
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 --> ...Und nun zur Datei server.js. Serverseitig haben Sie auchsocket = 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.};
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.
client 3: hello
.
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.