Chat Websockets
als Präsentation ▻HTTP fängt immer mit dem Request des Browsers an und endet mit dem Response des Servers. Dieses Protokoll ist nicht geeignet für Chat oder Spiele, wo die Kommunikation auch vom Server ausgehen muss.
Das Websocket Protokoll ermöglicht die Kommunikation in beide Richtungen auf einer dauerhaften Verbindung zwischen Client und Server. Es wird in RFC 6455 definiert.
▻Das Websocket Protokoll
Das Websockets Protokol baut auf HTTP and HTTPS auf:
- es verwendet die ports 80 bzw. 443
- es startet immer mit einem normalen HTTP Request
- es verwendet Cookies
Soweit die Ähnlichkeiten mit HTTP.
▻URLs für Websockt beginnen mit ws
oder wss
für die verschlüsselte Version:
- ws://server.example.com/chat
- wss://server.example.com/chat
Nach dem “Upgrade: websocket” Request wird die TCP Verbindung auf Dauer aufrechterhalten, und Client und Server wechseln in das eigentliche Websocket Protokoll:
GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= Sec-WebSocket-Protocol: chat
Nach diesem ersten Austausch schalten Server und Client auf das Senden von “Frames” um. Nun können beide jederzeit senden und müssen bereit sein, eingehenden Nachrichten zu empfangen.
Websocket Frames sind viel sparsamer als HTTP Requests und Responses: 10 bis 18 Byte für Meta-Information plus der eigentliche Payload bilden einen Frame.
▻Der (4 bit) Opcode gibt an welchen Typ der Payload hat:
0000
- continuation frame, voriger Frame wird fortgesetzt0001
- text frame0010
- binary frame1000
- connection close1001
- ping1010
- pong
Wenn einer der beiden Endpunkte ein ping sendet muss der andere mit einem pong antworten. Dieser Mechanismus wird verwendet um die TCP-Verbindung aufrecht zu erhalten auch wenn gerade keine Daten geschickt werden müssen (“keepalive”).
▻Werkzeuge
▻Developer Tools
Beim Programmieren und Debuggen von Websockets braucht man die Developer Tools: In der “Netzwerkanalyse” sieht man den ersten Request, noch mit HTTP, der dann mit “101 Switching Protocol” in eine Websocket umgewandelt wird:
Im Tab “Antwort” sieht man die Frames die hin oder her geschickt werden:
Hier sieht man auch dass Client und Server sich gegenseitig Ping- und Pong-Frames senden wenn sonst nichts zu senden ist.
▻node.js
Für die Programmierung am Server kann man alle typischen
Backend-Programmiersprachen verwenden (PHP, Ruby etc.). Wir nutzen die Gelegenheit
um node.js
kennen zu lernen. Damit kann man das Backend auch in JavaScript programmieren.
Ryan Dahl hat node.js im Jahr 2009 herausgebracht. Es war nicht die erste Möglichkeit, JavaScript am Server zu verwenden, aber die erste, die echte Erfolge hatte.
Node und der dazugehörige package manager npm
werden heute nicht
nur im Backend Development, sondern auch sehr viel als Werkzeug
für das Frontend Development verwendet.
Für Node zu programmieren ist nicht einfach: wie im Browser, so wird auch in Node viel mit asynchronen Aufrufen gearbeitet.
▻Replit
Node.js kann man am eigenen Rechner installieren, das ist aber für diese Beispiel nicht nötig. Wir verwenden https://replit.com/.
Replit ist eine browserbasierte Entwicklungsumgebung, mit der man - neben vielen anderen Sprachen - node.js Programme direkt im Browser schreiben. Der Code wird am Server von Replit gespeichert, und kann dort auch ausgeführt werden.
Damit braucht man also am eigenen Computer nur den Webbrowser.
Das “Frontend” findet sich in der Datei public/index.html
bzw. public/index.js
, das Backend in index.js
.
Socket.io
socket.io
ist eine JavaScript Library für die Client- und Server-Seite
von Websocket Verbindungen.
Programmierung des Client
Am Client ist bereits eine Eingabefeld für Chat-Messages vorhanden.
Alle Chat-Messages sollen in der Liste mit der id messages
angezeigt werden:
<ul id="messages"></ul> <form action=""> <input id="input" autocomplete="off" /> <input type="submit" value="Senden" /> </form> <script src="socket.io/socket.io.js"></script> <script src="index.js"></script>
An den Server senden:
Wenn das Formular abgeschickt wird (durch den submit-button oder durch drücken von Enter), wird eine Nachricht an den Server geschickt:
var socket = io(); const form = document.getElementById('form'); const input = document.getElementById('input'); form.addEventListener('submit', function(event) { event.preventDefault(); // Form wird nicht "normal" gesendet socket.emit('chat message', input.value ); // sondern nur über Socket input.value=""; // Eingabefeld leeren });
Vom Server empfangen:
Wenn vom Server eine Nachricht kommt,
wird sie als neues li
an die Liste angefügt:
const list = document.getElementById('messages'); socket.on('chat message', function(msg) { const message = document.createElement('li'); message.textContent = msg; list.appendChild(message); });
Programmierung des Servers:
Achtung: mit Node.js programmiert man nicht nur irgendein Programm, das am Webserver läuft. Man schreibt ein JavaScript-Programm, das den gesamten Job des Webserver miterledigt.
Das JavaScript-Programm läuft also die ganze Zeit und behandelt alle Anfragen. Ein Apache oder nginx ist nicht nötig.
In folgendem Code repräsentiert io
den ganzen Websocket:
Die Variable socket
repräsentiert einen verbundenen Client.
io.emit()
ist ein Broadcast an alle verbundenen Clients,
socket.emit()
würde nur an den einen Client senden.
io.on('connection', function(socket){ console.log('a user connected'); socket.on('chat message', function(msg){ console.log(`got message '${msg}', broadcasting to all`); io.emit('chat message', msg); }); socket.on('disconnect', function(){ console.log('a user disconnected'); }); });
Messages
Wir haben vom Client zum Server gesendet mit dem Befehl:
socket.emit('chat message', input.value );
Am Server reagieren wir mit socket.on() auf die Message. Das kann man sich wie einen Eventlistener vorstellen, welcher auf das Event “chat message” hört.
socket.on('chat message', function(msg){ console.log(`got message '${msg}'...`); });
Der String ‘chat message’ ist frei gewählt. Wir haben hier ein Protokoll auf dem Websocket erfunden, das bisher nur dieses eine Event kennt. Es gibt auch andere Arten von Events, die schon belegt sind. “disconnect”, welches du schon in dem Beispielcode finden kannst, ist eines davon. Mehr vordefinierte Events findest du hier: Server Socket Events
▻Weiterentwicklung
Es gibt viele Möglichkeiten das Beispielprogramm weiter zu entwickeln:
▻Neues Event
In diesem Beispiel haben wir nur chat message
Events
gesendet und empfangen. Wir können beliebig neue Arten von
Events, mit oder ohne payload, dazuerfinden.
Andere Ein- und Ausgabe im Client
Nicht jede Eingabe muss aus dem Texteingabefeld kommen: auch clicks auf Buttons, Mausbewegungen, usw. können Websocket-Botschaften auslösen.
Nicht jede Ausgabe ist ein Chatmeldung. Man könnte zum Beispiel einen Audio-Clip abspielen.
▻Andere Logik am Server
Am Server könnte man mitzählen, wie viel User anwesend sind.
Dazu legt man eine globale Variable users
an. Solange das JavaScript-Programm
am Server läuft bleibt diese Variable erhalten. Wenn der Server neu gestartet
werden muss - z.B. weil man die Datei index.js
editiert hat - dann geht der
Inhalt der Variable verloren.
Wir könnten jetzt ein neues Event erfinden (zB “update users”), auf welches der Client hört, und dann einen User-Counter in unserer Chat-App updaten.
let users = []; function remove_from(socket){ users = users.filter(user => user.id !== socketId); } io.on('connection', function(socket){ // Wird jedes mal aufgerufen, wenn sich ein neuer Client verbindet console.log(`Ein neuer User mit id ${socket.id}`); users.push(socket); io.emit('update users', users.length); .... socket.on('disconnect', function(){ remove_from(socket); // Wenn ein User disconnected muss er wieder entfernt werden console.log('User disconnected'); io.emit('update users', users.length); }); });