Kontinuierlich und wechselseitig mit dem Server in Kontakt

Websockets mit Angular 2: Asynchron? Kein Problem!

Karsten Sitterberg

© Shutterstock.com/Aaron Kohr

In dieser Folge der Kolumne „Webentwicklung mit Angular 2“ zeigt Karsten Sitterberg, wie man mit Angular 2 und Websockets asynchrone Webanwendungen bauen kann. Ganz nach dem Motto: Asynchron mit Angular 2? Kein Problem!

Websockets mit Angular 2

In bestimmten Anwendungsfällen ist es notwendig, nicht nur ab und zu Daten vom Server zu laden. Die Daten müssen kontinuierlich und sogar wechselseitig mit dem Server ausgetauscht werden. HTTP ist dafür nicht optimal geeignet, da das Request-Response-Modell zu hohem Overhead und auch unerwünschter Latenz und Jitter führt. Bei diesen Rahmenbedingungen wird daher eher zu den mittlerweile von allen Browsern unterstützten Websockets als Basistechnologie zurückgegriffen.

Telefonanwendungen, bei denen Sprachdaten von beiden Teilnehmern übertragen werden müssen, sind ein Beispiel für kontinuierlichen und wechselseitigen Datenaustausch mit dem Server. Ähnlich gelagert sind kollaborative Anwendungsfälle, etwa das gemeinsame Erstellen von Textdokumenten oder Quellcode in Web-IDEs. In Multiplayer-Online-Games können Websocket-Verbindungen zum Einsatz kommen, um Umgebungsinformationen an den jeweiligen Client zu senden, während der Client seinerseits Informationen über Aktivitäten des Spielers an den Server sendet.

Situation in Angular 2

Websockets werden von JavaScript nativ unterstützt. Um auf asynchrone Datenströme, die auch bei Websockets auftreten, mit entsprechenden UI-Updates zu reagieren, wurde Angular 2 unter anderem auf Zone.js aufgebaut. Zone.js ist eine Bibliothek, die Angular ermöglicht, alle Funktionsaufrufe abzufangen – auch asynchrone – und auf Änderungen zu reagieren (Change Detection).

Um asynchrone Datenströme effizient zu modellieren, wurde bei Angular 2 auf RxJS zurückgegriffen. RxJS ist eine Bibliothek, welche die Reactive Extensions implementiert und somit reaktive Programmierung mit JavaScript ermöglicht. In Angular 2 wird das explizit für die Kommunikation mittels HTTP verwendet. Im Fall von Websockets lässt sich das native HTML5 JavaScript API leicht per Adapter an RxJS und Angular 2 anbinden.

Websocket-Anbindung: Ein Beispiel

Zur Demonstration der Anbindung von Websockets mit Angular 2 benutzen wir hier den Echo Test von websocket.org. Der Echo Test stellt eine Schnittstelle zur Verfügung, an die man per Websockets eigene Nachrichten verschicken kann. Diese Nachrichten werden dann ebenfalls per Websocket zurück an den Absender verschickt. In der Beispielanwendung wird ein auf reaktiven Streams basierender Zähler implementiert. Der Wert des Zählers wird über ein HTML Template ausgegeben. Der Zähler wird einmal pro Sekunde hochgezählt und sein neuer Wert per Websocket an den Echo Test gesendet. Die Antwort des Echo Tests wird dann auch im HTML Template ausgegeben.

Zum Aufsetzen der Anwendung wird hier der Einfachheit halber das Angular-CLI Tool verwendet. Die Anbindung der Websockets wird durch den WebsocketService implementiert. Websockets sind bidirektionale Verbindungen. Diese lassen sich am besten über RxJS Subjects abbilden. Ein Subject vereinigt zwei reaktive Elemente in sich: Zum Einen das Observable, das Daten an einen oder mehrere Empfänger senden kann, zum Anderen den Observer, der selbst als Empfänger von Daten fungiert. Somit kann ein Subject sowohl dazu genutzt werden, Daten von einem Websocket ans UI weiterzuleiten, als auch Daten aus dem UI an den Websocket zu übergeben.

Der folgende Quellcode zeigt die Implementierung des WebsocketService:

<strong>websocket.service.ts</strong>
import { Injectable } from '@angular/core';
import { Subject, Observer, Observable } from 'rxjs/Rx';
@Injectable()
export class WebsocketService{
  public createWebsocket(): Subject<MessageEvent> {
        let socket = new WebSocket('wss://echo.websocket.org');
        let observable = Observable.create(
                    (observer: Observer<MessageEvent>) => {
                        socket.onmessage = observer.next.bind(observer);
                        socket.onerror = observer.error.bind(observer);
                        socket.onclose = observer.complete.bind(observer);
                        return socket.close.bind(socket);
                    }
        );
        let observer = {
                next: (data: Object) => {
                    if (socket.readyState === WebSocket.OPEN) {
                        socket.send(JSON.stringify(data));
                    }
                }
        };
        return Subject.create(observer, observable);
  }
}

Die Methode createWebsocket() bindet den Websocket per RxJS Subject in 4 Schritten an. Zunächst wird per new WebSocket() eine neue Websocket-Verbindung aufgebaut. Der übergebene String entspricht dabei dem URL, unter dem der Websocket zu erreichen ist. Im zweiten Schritt wird dann ein Observable erzeugt, das die Daten vom Websocket an seine Subscriber weiterleitet (z. B. die UI-Logik). Dafür müssen lediglich die onmessage-, onerror- und onclose-Methoden des Websockets auf die reaktiven next-, error- und completed-Callbacks gemappt werden. Der Rückgabewert bestimmt schließlich was passiert, wenn der Subscriber sich vom Observable abmeldet – und damit vom zu erzeugenden Subject. In unserem Beispiel wird der Websocket dann geschlossen.

Der dritte Schritt erzeugt ein Observer-Objekt. Ein Observer-Objekt kann drei Methoden beinhalten: Zum Einen die next-Methode, die dem Observer mitteilt, dass neue Daten angekommen sind. Außerdem können noch die optionalen error- und complete-Methoden definiert werden, mit denen dem Observer mitgeteilt werden kann, dass ein Fehler aufgetreten oder die Datenübertragung beendet worden ist. In unserem Beispiel wird lediglich die next-Methode definiert, welche die übergebenen Daten über die Websocket-Verbindung versendet, solange sie offen ist.

Im vierten Schritt  wird schließlich das Subject aus Observer und Observable erzeugt und zurückgegeben. In unserem Beispiel wird der Service zum Erzeugen des Websocket Subject in der AppComponent verwendet, wie im folgenden Quelltext gezeigt:

<strong>app.component.ts</strong>
import { Component, OnInit } from '@angular/core';
import { WebsocketService } from './websocket.service';
import { Subject, Observable, Subscription } from 'rxjs/Rx';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styles: [
'p,button,h1{font-family:"sans-serif"; color: #333}'
]
})
export class AppComponent implements OnInit {
private socket: Subject<any>;
private counterSubscription: Subscription;
private message: string;
private sentMessage: string;
constructor(websocketService: WebsocketService){
this.socket = websocketService.createWebsocket();
}
ngOnInit(){
this.socket.subscribe(
message => this.message = message.data
);
}
private launchCounter(){ 
//Counter already initialized
if(this.counterSubscription){
this.counterSubscription.unsubscribe();
}
let counter = Observable.interval(1000);
this.counterSubscription = counter.subscribe(
num => {
this.sentMessage = 'Websocket Message '+ num;
this.socket.next(this.sentMessage);
}
);
}
}

Im Konstruktor wird zunächst das Socket Subject von Service geliefert. Im OnInit Callback wird dann auf neue Daten vom Websocket gehorcht. In diesem Fall wird die zurückgegebene Nachricht an die Instanzvariable message zugewiesen, um sie dann im Template auszugeben.
Die Methode launchCounter() wird aus dem Template heraus aufgerufen – durch Klick auf einen Button. In ihr wird zunächst überprüft, ob man sich schon auf dem Zähler registriert hat. Wenn ja, wird diese Registrierung aufgehoben. Anschließend wird ein neues Observable angelegt, welches im Einsekundenintervall von Null ab ansteigende Zahlen emittiert und somit als Zähler dient. Auf diesen Zähler erfolgt dann eine Subscription, in der jede Zahl in eine String-Nachricht eingebettet wird. Diese Nachricht wird dann über die Variable sentMessage ebenfalls im Template ausgegeben und mit dem Aufruf von socket.next() an das Socket-Subject weitergeleitet. Dieses gibt die Nachricht dann an den Websocket weiter.

Die Beispielanwendung kann bei GitHub heruntergeladen werden. Wird sie gestartet, so wird zunächst der Knopf angezeigt, mit dem der Zähler gestartet werden kann. Nachdem der Zähler gestartet wurde, wird dieser hochgezählt und sein Wert per Websocket verschickt. Der Echo-Test-Server sendet dann diese Nachricht über den Websocket zurück an den Angular 2 Client, wo die Nachricht wieder ausgegeben wird. Die durch den Round Trip über den Websocket auftretende Latenz kann ebenfalls wahrgenommen werden, je nach Internetanbindung stärker oder schwächer ausgeprägt. Die empfangene Nachricht aktualisiert sich erst nach einer kurzen Verzögerung.

Fazit

In diesem Artikel haben wir gesehen, dass die Anbindung von WebSockets mit einem Angular 2 Client gut zu handhaben ist. Zone.js stellt dabei das Rüstzeug für die effektive Change Detection zur Verfügung, sodass sich die Arbeit mit asynchronen Events in Angular 2 intuitiv anfühlt. RxJS wiederum ermöglicht die effiziente Modellierung und Anbindung von asynchronen Datenströmen, wie sie in Verbindung mit Websockets auftreten.

Verwandte Themen:

Geschrieben von
Karsten Sitterberg
Karsten Sitterberg
Karsten Sitterberg ist als freiberuflicher Entwickler, Trainer und Berater für Java und Webtechnologien tätig. Karsten ist Physiker (MSc) und Oracle-zertifizierter Java Developer. Seit 2012 arbeitet er mit trion zusammen.
Kommentare

Hinterlasse einen Kommentar

2 Kommentare auf "Websockets mit Angular 2: Asynchron? Kein Problem!"

avatar
400
  Subscribe  
Benachrichtige mich zu:
sharma
Gast

While testing the code, it shows „observer = undefined“. Because of this, the observer block never executes.

slichti
Gast