DAS WEBSOCKET API Bidirektional

Foto: LOVELUCK / Shutterstock
DAS WEBSOCKET API
Bidirektional
Das WebSocket API ermöglicht die bidirektionale Kommunikation zwischen Client und Server.
Bevor wir uns dem WebSocket API und damit der bidirektionalen Kommunikation zwischen Client und Server widmen, sei einleitend zunächst nochmal ein Überblick über die normale Kommunikation zwischen Client und Server gegeben, sprich über die unidirektio-nale Kommunikation, und darüber, welche Vorgehensweisen sich in den vergangenen Jahren herausgebildet haben, um die Einschränkungen dieser Art der Kommunikation zu umgehen.

Bei der Kommunikation über HTTP geschieht die Kommunikation vom Client aus, unabhängig davon, ob es sich um eine klassische Website handelt oder um eine moderne Webanwendung, die auf Ajax basiert. In allen Fällen ist es der Client, der die (HTTP-)Anfragen an den Server formuliert, auf die dieser dann mit (HTTP-)Ant-worten reagiert. Der Server kann über HTTP keine Daten von sich aus an einen Client schicken. So viel dürfte bekannt sein.
Die Kommunikation von Client und Server geschieht also unidirektional, in eine Richtung vom Client zum Server. Auch die Tatsache, dass der Server mit Antworten reagiert und man
Das Prinzip des Polling 

somit natürlich in zwei Richtungen kommuniziert, ändert nichts daran, dass die aktive Kommunikation nur in eine Richtung geht. Der Server kann also erst einmal nicht aktiv Daten an den Client senden.

In der Vergangenheit haben sich daher verschiedene Techniken herausgebildet, die diese Einschränkungen umgehen. Eine dieser Techniken ist das sogenannte Polling: Dabei fragt der Client im Hintergrund (durch Ajax-Anfragen) beim Server regelmäßig nach neuen Daten, und der Server beantwortet diese Anfragen dann entsprechend (Bild 1).

Eine Variante der Polling-Technik ist dabei das Long Polling, bei dem der Client ebenfalls im Hintergrund Anfragen an den Server sendet (Bild 2), das sich aber folgendermaßen vom normalen Polling unterscheidet: Liegen neue Daten auf dem Server bereit, liefert der Server umgehend eine Antwort; liegen dagegen keine neuen Daten vor, hält der Server die HTTP-Verbindung so lange offen, bis entweder neue Daten auf dem Server vorliegen oder ein zuvor festgelegter Timeout überschritten wurde.

In beiden Fällen gilt: sobald der Client die entsprechende Antwort vom Server erhält, sendet er erneut eine Anfrage, wobei der Server die Verbindung wieder offenhält und so weiter. Der Nachteil hierbei ist jedoch der relativ hohe Overhead, der durch die ständigen Anfragen vom Client an den Server hervorgerufen wird, weshalb sowohl Long Polling als auch das normale Polling heute nur noch der Abwärtskompatibilität wegen verwendet werden sollten.

Bidirektionale Kommunikation

Mittlerweile gibt es nämlich auch Standardtechnologien, bei denen der Server wirklich Daten an den Client senden kann und die damit eine echte bidirektionale Kommunikation ermöglichen. Eine von diesen Technologien ist das sogenannte WebSocket API (www.w3.org/TR/websockets beziehungs-
weise https://html.spec.whatwg.org/multipage/comms.html #network), um die es im Folgenden gehen soll. Über dieses API können - wie der Name schon sagt - Socket-Verbindun-gen zwischen Client und Server hergestellt werden, dauerhafte Verbindungen, über die sowohl Client als auch Server an den jeweils anderen Daten schicken können (Bild 3).

Die Spezifikation umfasst dabei lediglich einen Typ (beziehungsweise ein Interface), und zwar den Typ WebSocket. Um eine WebSocket-Verbindung herzustellen, verwendet ►
Das Prinzip des Long Polling (Bild 2)
Das Prinzip von Web Sockets (Bild 3)
http://caniuse.com
Global

unprefixed:
iOS Safari
Opera
Web Sockets i-ls

Bidirectional communication technology for web apps

Usage relative Date relative Show all •k

Edge Firefox Chrome Safari
„ ... . * Android * Chrome for °Pera Mlni Browser Android
92.02% + 0.33% = 92.! 92.02% + 0.28% = 92.!
mm 53 mm MH 4.4
54 4.4.4
11 14 50 55 10 41 10.1 all 53 54

51 56
52 57
53 58

Browsersupport

für das WebSocket API
man die entsprechende Konstruktorfunktion in der folgenden Weise, wobei man den URL des WebSocket-Servers als Parameter übergibt (Bild 4):

const URL = 'ws://localhost:4080/tests'; let connection = new WebSocket(URL);

Bezüglich des Verbindungsaufbaus stellt WebSocket drei Event-Handler zur Verfügung: Der Event-Handler onopen wird aufgerufen, wenn die WebSocket-Verbindung geöffnet wurde, der Event-Handler onerror, wenn beim Verbindungsaufbau ein Fehler auftritt, und der Event-Handler onclose, wenn die Verbindung wieder geschlossen wurde (Listing 1).

Um Daten, die vom Server gesendet werden, verarbeiten zu können, verwendet man den Event-Handler onmessage. Das Event ist dabei vom Typ MessageEvent, das unter anderem die Eigenschaft data bereitstellt, um an die übermittelten Daten zu gelangen:

connection.onmessage = event => { console.log(event.data);

};

Umgekehrt lassen sich über die Methode send() Daten an den Server schicken, wobei sowohl Strings, Binary Large Objects (kurz Blobs) und Array-Buffer als auch typisierte Arrays übergeben werden können. Um Daten im JSON-Format zu versenden, müssen die entsprechenden JavaScript-Objekte also zunächst per JSON.stringify() umgewandelt werden:

let message = { subject: 'Hallo Welt', body: 'Wie geht's?'

};

connection.send(JSON.stringify(message));

Empfängt man JSON-Daten vom Server, müssen diese analog über JSON.parse() umgewandelt werden:

Listing 2: Implementierung des WebSocket-Servers

'use strict';

let express = require('express'); let app = express();

let server = require('http').Server(app);

let url = require('url');

let WebSocketServer = require('ws').Server;

let wss = new WebSocketServer({ server: server });

let port = 4080;

app.use(express.static(_dirname + '/public'));

app.get('/index', (request, response) => {

response.sendFile(_dirname + '/public/index.html');

});
Listing 1: Event-Handler für WebSocket-Verbindung

'use strict';

let connection = new WebSocket('ws://localhost:4080/

tests');

connection.onopen = event => {

console.log('Verbindung geöffnet');

};

connection.onerror = error => { console.log('Fehler: ${error} ');

};

connection.onclose = event {

console.log('Verbindung geschlossen');

};

connection.onmessage = event => { console.log(event.data);

};
connection.onmessage = event => { let data = JSON.parse(event.data);

};

Des Weiteren können, wie auch schon beim Versenden von Nachrichten an den Server, vom Client auch Binary Large Objects und Array-Buffer empfangen werden. Dazu setzt man entsprechend die Eigenschaft binaryType der WebSo-cket-Verbindung auf den Wert blob oder arraybuffer.

Über die Methode close() lässt sich eine WebSocket-Ver-bindung wieder schließen.

Beispielanwendung: Chat

Am einfachsten lässt sich die Verwendung des WebSocket API an einem konkreten Beispiel demonstrieren. Und was liegt hier näher als die Implementierung einer Chat-Anwendung? Dies ist sicherlich das Paradebeispiel für die Verwendung von Web Sockets.
wss.on('connection', ws => { ws.on('message', message => {

console.log('Empfangene Nachricht: ${message}');

wss.clients.forEach(client => { client.send(message);

});

});

});

server.listen(port, () => {

console.log('Server läuft unter Port ${server.address().port}')

});
Den Code für dieses Beispiel zeigen Listing 2 (JavaScript-Code für den WebSocket-Server in Node.js), Listing 3 (HTML-Code für die Chat-Oberfläche) und Listing 4 (JavaScript-Code für den WebSocket-Client). Bild 5 zeigt die Oberfläche des Clients.

Doch der Reihe nach: Werfen wir zunächst einen Blick auf den Code für den WebSocket-Server. Da die Implementierung in JavaScript unter Node. js geschieht, muss zunächst ein Modul her, das die Implementierung von WebSocket-Servern vereinfacht. Hier bietet sich das Modul ws an (https:// github.com/websockets/ws), das laut Entwicklerseite die schnellste Implementierung des WebSocket-Protokolls (https://tools.ietf.org/ html/rfc6455) darstellt und entweder alleine verwendet werden kann, um einen reinen WebSocket-Server zu implementieren, aber auch in Kombination mit Express.js, wenn man
einen HTTP-Server benötigt (in unserem Beispiel etwa dazu, um die ChatOberfläche bereitzustellen).

Über den folgenden Befehl wird zunächst die Klasse Server importiert, die den WebSocket-Server repräsentiert.:

const WebSocketServer = require('ws').Server

Anschließend wird über new WebSo-cketServer() eine neue Instanz erzeugt, wobei als Parameter ein Konfigurationsobjekt mit einer Referenz auf den HTTP-Server zu übergeben ist.

Über die Methode on() lassen sich im nächsten Schritt Event-Listener registrieren, unter anderem auch für das connection-Event, das immer dann ausgelöst wird, wenn sich ein neuer WebSocket-Client mit dem Server verbindet.
Chats sind das klassische Beispiel für die Verwendung von Web Sockets (Bild 5)
^ Listing 3: HTML-Code für den Chat
<!DOCTYPE html>

<html>

<head>

<title>Node.js Chat</title>

<meta charset="utf-8">

<link rel="stylesheet" href="styles.css" />

<link rel="stylesheet" href=

"https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/ css/bootstrap.min.css" integrity="..." crossorigin="anonymous">

</head>

<body>

<div class="container">

<div class="row">

<div class="col-md-5">

<img id="avatar" class="img-circle" width="50px"/>

<div id="name"></div>

</div>

</div>

<div class="row">

<div class="col-md-5">

<div class="panel panel-primary">

<div class="panel-heading">

<span class="glyphicon glyphicon-comment"> </span> Chat </div>

<div class="panel-body">

<ul id="chat" class="chat"></ul>

</div>
<div class="panel-footer"> <div class="input-group">
<input id="message" type="text" class="form-control input-sm" placeholder="Nachricht eingeben ..." /> <span class="input-group-btn">

<button class="btn btn-warning btn-sm" id="send">

Abschicken</button>

</span>

</div>

</div>

</div>

</div>

</div>

</div>

<script

src="https://code.jquery.com/jquery-3.1.1.min.js"

integrity="sha256-

hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8="

crossorigin="anonymous"></script>

<script src="https://maxcdn.bootstrapcdn.com/ bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHj0MaLkfuWVxZx UPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

<script src="faker.min.js"></script>

<script src="moment.min.js"></script>

<script src="main.js"></script>

</body>

</html>
Die als Event-Listener übergebene Funktion erhält dabei als Parameter ein Objekt, das die WebSocket-Verbindung repräsentiert. Auf diesem Objekt lassen sich wiederum - ebenfalls über die Methode on() - Event-Listener registrieren, die im Zusammenhang mit der WebSocket-Verbindung aufgerufen werden. Im Beispiel wird dies dazu genutzt, eingehende Chat-Nachrichten abzufangen und dann an alle Chat-Teilnehmer weiterzuleiten.

Auf der Clientseite kommt dann das WebSocket API zum Einsatz. Über let connection = new WebSocket(‘ws://local-host:4080‘) wird zunächst eine Verbindung zu dem eben gezeigten WebSocket-Server hergestellt.

Jedes Mal, wenn eine Nachricht eingegeben und auf die Schaltfläche zum Abschicken der Nachricht geklickt wurde, wird über send() ein entsprechendes Objekt im JSON-Format als Zeichenkette an den Server gesendet. Das Objekt enthält
dabei den Nutzernamen (name), der URL des Profilbilds (ava-tarURL) und den eingegebenen Nachrichtentext (body). Nutzername und Profilbild werden zu Demonstrationszwecken bei j edem Neuladen der Anwendung mit Hilfe der Bibliothek Faker.js (https://github.com/marak/Faker.js) zufällig generiert (übrigens eine sehr hilfreiche Bibliothek, wenn es um das Erstellen von Dummy-Daten geht). Nachdem der Server die Nachricht geparst und per Broadcast an alle Chat-Teilnehmer versendet hat, wird die empfangene Nachricht über die Methode appendChatMessage() in das HTML des Chat eingebaut.

Sicherlich ist das Beispiel etwas vereinfacht und verzichtet beispielsweise auf Chat-Räume oder private Nachrichten zwischen einzelnen Nutzern, aber es zeigt schön den prinzipiellen Ablauf bei der Socket-Kommunikation zwischen Client(s) und Server.
4: Client für den Chat
Listing
'use strict'; function init() {

faker.locale = 'de'; let user = { name: faker.name.firstName(), avatarURL: faker.image.avatar()

}

let connection = new WebSocket ('ws://localhost:4080'); connection.onmessage = event => {

let message = JSON.parse(event.data); appendChatMessage(message.name, message.avatarURL, message.body);

}

connection.onopen = event => {

console.log('Verbindung geöffnet');

};

connection.onerror = error => {

console.log('WebSocket Error ' + error);

};

connection.onclose = event => {

console.log('Verbindung geschlossen');

};

let elementUserName =

document.getElementById('name');

elementUserName.innerText = user.name;

let elementAvatar =

document.getElementById('avatar');

elementAvatar.src = user.avatarURL;

let textFieldMessage =

document.getElementById('message');

let buttonSend = document.getElementById('send');

let chatContent = document.getElementById('chat');
buttonSend.addEventListener('click', event => { let message = { name: user.name, avatarURL: user.avatarURL, body: textFieldMessage.value

}

connection.send(JSON.stringify(message));

});

function appendChatMessage(name, avatarURL, message) {

let date = moment().format('h:mm:ss'); let html = '<li class="left clearfix">

<span class="chat-img pull-left">

<img src="${avatarURL}" class="img-circle" width="50px"/>

</span>

<div class="chat-body clearfix">

<div class="header">

<strong class="primary-font">${name} </strong>

<small class="pull-right text-muted"> <span class="glyphicon glyphicon-time"> </span>${date}

</small>

</div>

<p>${message}</p>

</div>

</li>';

chatContent.innerHTML += html; console.log(html)

}
}
document.addEventListener('DOMContentLoaded', init);
Listing 5: Implementierung des WebSocket-Servers

'use strict';

let express = require('express'); let app = express();

let server = require('http').Server(app); let wss = require('socket.io')(server); let port = 4080;

app.use(express.static(_dirname + '/public'));

app.get('/index', (request, response) => {

response.sendFile(_dirname +

'/public/index.html');

});

wss.on('connection', ws => { console.log("connected") ws.on('chat-message', message => {

console.log('Empfangene Nachricht: ${message}'); wss.sockets.emit('chat-message', message);

});

});

server.listen(port, () => {

console.log('Server läuft unter Port ${server.address().port}')

});
Listing 6: Client für den Chat

'use strict';

function init() {

faker.locale = 'de';

let user = { name: faker.name.firstName(), avatarURL: faker.image.avatar()

}

let socket = io();

socket.on('connection', socket => { console.log('Verbindung geöffnet');

});

socket.on('disconnect', event => {

console.log('Verbindung geschlossen');

});

socket.on('error', error => {

console.log('WebSocket Error ' + error);

});

socket.on('chat-message', event => { let message = JSON.parse(event); appendChatMessage(message.name, message.avatarURL, message.body);

});

// Restlicher Code unverändert

}

document.addEventListener('DOMContentLoaded', init);
Socket.io

Eine im Zusammenhang mit Web Sockets interessante Bibliothek ist Socket.io (http://socket.io) .Dabei handelt es sich um eine Bibliothek, die allgemein die bidirektionale Kommunikation zwischen Client und Server abstrahiert: Standardmäßig verwendet Socket.io das WebSocket API, falls dieses jedoch vom jeweiligen Browser nicht unterstützt wird, werden als Fallback-Lösung entweder Polling oder Long Polling verwendet.

Wie das Chat-Beispiel mit Hilfe von Socket.io realisiert werden könnte, zeigen Listing 5 und Listing 6. Vom Ablauf her bleibt alles beim Alten, lediglich das API unterscheidet sich

Links zum Thema

WebSocket API https://html.spec.whatwg.org/multipage/comms. html#network

Socket.io http://socket.io

ws

https://github.com/websockets/ws • WebSocket-Protokoll https://tools.ietf.org/html/rfc6455
in einigen Punkten. Der HTML-Code dagegen bleibt fast unverändert, nur die Socket.io-Bibliothek muss zusätzlich eingebunden werden.

Fazit

Über das WebSocket API lässt sich eine bidirektionale Kommunikation zwischen Client und Server realisieren. Techniken wie das Polling oder das Long Polling sollten nur in Ausnahmefällen wie zum Beispiel aus Gründen der Abwärtskompatibilität verwendet werden. Hier kann auch die Bibliothek Socket.io helfen, die bei älteren Browserversionen auf Fall-back-Lösungen zurückgreift, in modernen Browsern aber Web Sockets verwendet.
Philip Ackermann

arbeitet beim Fraunhofer-Institut für Angewandte Informationstechnik FIT und ist Autor mehrerer Fachbücher und Fachartikel über Java und JavaScript. http://philipackermann.de





You may also like

Keine Kommentare:

Blog-Archiv

Powered by Blogger.