Real Time Web Apps mit vert.x

vert.x ist ein leichtgewichtiges Framework mit dem real time web applications sehr einfach entwickelt werden können. In diesem Artikel möchte ich mit einem Praxisbeispiel einen Blick auf vert.x werfen.

Real time Applications

logo vertxUnter real time application verstehe ich Anwendungen, die sofort und in Echtzeit auf Ereignisse reagieren, ohne dass der Anwender etwas tun muss. Für Web Anwendungen heißt das, dass diese z.B. das UI aktualisieren, ohne dass der Browser aktiv aktualisiert werden muss. Ein Beispiel ist E:D-Footprints, eine Anwendung, die ich für das Spiel Elite Dangerous entwickelt habe. Bei der Anwendung wird immer, wenn ein Spieler im Spiel in ein anderes Sonnensystem springt, ein jumped-to-Event publiziert. Alle Browser, die auf der E:D-Footprints Web-Seite sind, zeigen dieses Event sofort in einem Event Stream an. Ist man auf der E:D-Footprints-Web-Seite des Spielers der gesprungen ist, wird sofort die Ansicht aktualisiert, in welchem Sonnensystem sich der Spieler aktuell befindet. Das Ganze erfolgt ohne eine Aktualisierung des Browsers.

vert.x Theorie

vert.x benötigt eine JVM (Java Virtual Machine) zur Ausführung. Somit kann vert.x auf allen (Server-) Systemen genutzt werden, für die es eine JVM gibt.
Das heißt aber nicht, dass vert.x Anwendungen in Java implementiert werden müssen. vert.x ist Polyglot und unterstützt Java, JavaScript, Groovy und Ruby. Die einzelnen Komponenten einer vert.x Anwendung können auch in unterschiedlichen Sprachen entwickelt werden und trotzdem miteinander interagieren. Somit kann für jede Aufgabe die am besten geeignete Sprache gewählt werden

vert.x ist sehr leichtgewichtig, der Kern ist gerade mal 650 KB groß. Und es wird kein Application Server benötigt, vert.x selbst ist der Server. Somit hat man eine sehr kleine Server Infrastruktur und muss nicht schon, nur für den Application Server, eine dicke Hardware hin stellen.

Die Kommunikation innerhalb von vert.x ist Event getrieben und ausschließlich asynchron. Es gibt keine synchronen calls zwischen Komponenten.
Zudem ist vert.x non-blocking, d.h Zugriffe auf Ressourcen wie das Filesystem, blockieren nicht den laufenden Thread. Dies wird ebenfalls über eine asynchrone Kommunikation und Callbacks ermöglicht.
Aus Konsequenz daraus benötigt man bei vert.x keinen Thread-Pool wie bei Application Servern üblich. Per default läuft vert.x mit einem einzigen Thread, der maximal ausgelastet ist, da er durch das non-blocking Verhalten, nicht auf Ressourcen warten muss. Man kann vert.x auch mit mehreren Threads laufen lassen, üblicherweise hat man so viele Threads wie man Prozessorkerne hat. Jeder Kern bearbeite somit einen dedizierten Thread.
Damit erreicht man eine optimale Hardware Ausnutzung und kann seine Anwendung, bei minimaler Hardware, maximal skalieren. Vorbei die Zeiten, in denen eine dicke Hardware anscheinend überlastet ist und keine Requests mehr annimmt, obwohl die CPU bei 0% Auslastung liegt. Diese Situation habe ich selbst schon erlebt, Grund war, dass alle Threads im Application Server belegt waren und auf langsame Ressourcen gewartet haben.

Die ausführbaren Komponenten in vert.x werden Verticals genannt. Diese sind vergleichbar mit Servlets oder EJBs. Die Verticals können in den oben genannten Sprachen implementiert werden.
Im Kern von vert.x steht ein Event Bus, über den die Verticals miteinander kommunizieren können.  Die Verticals sind Event getrieben und werden erst ausgeführt, wenn sie eine Nachricht über den Bus empfangen. Verticals können Nachrichten an Adressen im Event Bus senden oder Listener an Adressen registrieren, worüber sie Nachrichten empfangen. Der Nachrichtenversand im Event Bus erfolgt ausschließlich asynchron, Sender und Empfänger wissen nichts voneinander. Dies führt zu einer losen Kopplung zwischen den Komponenten.
vert.x bietet ein Publish-Subscribe und ein Point-to-Point Messaging. Werden Messages publiziert, empfangen alle registrierten Empfänger die Message. Werden Messages gesendet, empfängt nur ein Empfänger die Message. Der Empfänger kann eine Antwort senden, damit ist eine Request-Reply Kommunikation möglich.
Idealerweise werden Nachrichten im JSON-Format auf den Event Bus gestellt. Damit ist sichergestellt, dass jede Programmier-Sprache mit der Nachricht umgehen kann.

vert.x kann auf mehrere Server verteil werden. Die Kommunikation zwischen den Servern erfolgt ebenfalls über den Event Bus. Damit können die einzelnen Verticals auf die Server verteilt und das Gesamtsystem besser skaliert werden. Besonders ressourcenintensive Verticals können auf eine eigene Hardware ausgelagert werden. Der Entwickler merkt davon nichts. Er empfängt und sendet Nachrichten immer gleich, über die Adressen im Event Bus, egal ob alles auf dem gleichen Server läuft, oder ob sich die Anwendung über mehrere Server verteilt. An den Adressen ändert sich nichts, da diese logische Namen sind.
Das Besondere ist, dass der Event Bus bis in den Browser geht. JavaScript Clients im Browser können somit über den Event Bus mit dem Server kommunizieren. Damit können Nachrichten, aktiv vom Server zum Client gepuscht werden, ohne dass man sich mit WebSockets auseinander setzen muss.

Bei vert.x ist es wichtig zu beachten, dass man den Thread nicht mit eigenem Code blockieren oder lange belegen darf. Da es aber immer Code gibt, der lange braucht, wird in vert.x zwischen zwei Arten von Threads unterschieden. Der Event-Loop darf nie blockiert werden, in ihm läuft der Event Bus und per default die Verticals. Wie schon erwähnt kann man mehrere Threads, also Event-Loops haben. Für Langläufer gibt es Worker-Threads. Diese laufen parallel zum Event-Loop. Hat man Laufzeitprobleme lagert man als erstes die Verticals, die mit Worker-Threads laufen, auf einen eigenen Server aus.

Schaut man sich die Eigenschaften von vert.x an, ist es ideal geeignet um reaktive Anwendungen, gemäß dem Reaktiven Manifest zu entwickeln.
vert.x eignet sich auch für Microservices, da in einer vert.x Instanz eine Anwendung / ein Service läuft und sich nicht mehrere einen Application Server teilen. Damit ist der Service autark und teilt sich keine Laufzeitumgebung mit anderen Services.

Praxisbeispiel

Ich möchte vert.x in der Praxis an einem real time Hello World zeigen. Mit der Demo kann eine Nachricht gesendet werden, die in allen Browsern, die auf der Seite sind, sofort angezeigt werden. Zudem werden die letzten 20 Nachrichten auf dem Server gespeichert und im Browser angezeigt, wenn die Seite aufgerufen wird.

Einfach mal das Beispiel ausprobieren und die Anwendung in zwei Browsern parallel aufrufen. Oder noch besser auf dem Rechner und dem Smartphone parallel. Wenn im einen Browser eine Nachricht gesendet wird, wird diese im anderen sofort angezeigt.
Implementiert habe ich das Beispiel mit vert.x 2.1.5.

Ich möchte hier kein komplettes Tutorial bereit stellen, sondern nur, mit Code-Beispielen, die Hightlights und Einfachheit von vert.x zeigen. Online steht ein Tutorial von Jakob Jenkov bereit.

Die folgende Grafik zeigt Struktur und Kommunikation des Beispiels in der Übersicht.

Übersicht
Übersicht

Im Beispiel werden 3 Adressen für den Event Bus festgelegt:

  1. hello.list
    Liefert eine Liste der letzten 20 Hello-World-Nachrichten
  2. hello.say
    Sendet eine Hello-World-Nachricht an den Server
  3. event.hello
    Publiziert ein Event mit einer neuen Hello-World-Nachricht

Auf dem Server wird ein Vertical (hello.js) implementiert, dass sich beim Event Bus auf „hello.list“ und „hello.say“ registriert. Dieses Vertical hat die Aufgabe die letzten 20 Hello-World-Nachrichten zu persistieren und sie als Liste zu liefern.
Der Client (hello-client.js) registriert sich beim Event Bus auf „event.hello“. Der Client hat die Aufgabe das UI bereit zu stellen, eine vom Anwender eingegebene Hello-World-Nachricht an die Adresse „hello.say“ zu senden und auf der Adresse „event.hello“ publizierte Hello-World-Nachrichten von allen Anwendern anzuzeigen.
Sowohl für den Server, als auch für den Client Teil wird JavaScript verwendet. Auf dem Client kommt zusätzlich AngularJS zum Einsatz.

Der Ablauf sieht wie folgt aus:

  1. Beim ersten Aufruf der Seite sendet der Client eine Message an die Destination „hello.list“ um die Liste der letzten 20 Nachrichten zu bekommen und zeigt diese an.
  2. Gibt der User eine Hello World Nachricht ein, sendet der Client die Nachricht an die Adresse „hello.say“.
  3. Der Server nimmt die Nachricht entgegen, speichert sie und publiziert die Nachricht an die Adresse „hello.event“
  4. Alle Clients haben sich auf die Adresse „event.hello“ registriert. Dadurch bekommen sie die Nachricht und zeigen sie in der Liste der letzten Nachrichten an

Folgendes Code-Beispiel zeigt, wie in JavaScript eine Message an eine Adresse im Event Bus gesendet wird.

eventBus.send('meine-bus-adresse', meine-message, function(reply-message) {
   // mache etwas mit der Antwort
});

Es wir die Methode send verwendet. Diese bekommt als ersten Parameter die Adresse an die die Message gesendet werden soll. Der zweite Parameter ist die Message, am besten ein JSON-Objekt. Der dritte Parameter ist eine Callback-Funktion, die ausgeführt wird, wenn die Reply-Message vom Empfänger eintrifft. Dieser Callback-Funktion wird die Reply-Message als Parameter übergeben.

Publiziert wird eine Nachricht über die publish Methode. Die Parameter sind identisch zu send, nur entfällt die Callback-Methode, da es bei publish keine Reply-Message gibt.

eventBus.publish('meine-bus-adresse', meine-message);

Um eine Nachricht vom Event Bus zu empfangen muss man einen Handler registrieren.

eventBus.registerHandler('meine-bus-adresse', function(message, responder){
   // mache etwas mit der Message 
   // ...
   // und sende eine Reply an den Sender
   responder(reply-message);
});

Die Registrierung erfolgt über die Methode registerHandler. Diese bekommt als ersten Parameter die Adresse im Event Bus auf die man sich registrieren möchte. Als zweiten Parameter wird die Funktion angegeben, die ausgeführt werden soll, wenn eine Message an der registrierten Adresse eintrifft. Diese Funktion bekommt als ersten Parameter die Message, die eingegangen ist und als zweiten Parameter die Callback-Funktion des Senders.

So nun zum Code aus dem Praxis-Beispiel.

Folgender Code-Ausschnitt zeigt das Vertical auf dem Server mit dem Teil um eine Nachricht über „hello.say“ entgegen zu nehmen, zu speichern und dann die Nachricht an „event.hello“ zu publizieren.

var hellolist = "data/hellolist.json";

eventBus.registerHandler('hello.say', function(message, responder){
   var hello = {msg: message.msg, date: new Date()};
   vertx.fileSystem.readFile(hellolist, function(err, file) {
      var data = JSON.parse(file.getString(0, file.length()));
      // Liste ergänzen mit Objekt hello....
      vertx.fileSystem.writeFile(hellolist, JSON.stringify(data), function(err) {
         responder({status: 'ok'});
         eventBus.publish('event.hello', hello);
      });
   });
});

In Zeile 3 registriert sich das Vertical beim Event Bus auf „hello.say“ mit einer Funktion, die ausgeführt werden soll, sobald eine Nachricht an die Destination „hello.say“ gesendet wird. Dieser Funktion wird als Parameter die gesendete Message übergeben, sowie eine Callback-Funktion des Senders, an die eine Antwort gesendet werden kann.
In Zeile 4 wird das Objekt erstellt, das gespeichert wird. Es enthält neben der Hello-World-Nachricht auch das aktuelle Datum.
In Zeile 5 werden die letzen Nachrichten aus einem File gelesen. Da das Lesen asynchron erfolgt, muss auch hier wieder eine Callback-Methode definiert werden.
In Zeile 6 wird aus dem Inhalt des Files ein JSON-Objekt erzeugt.
Zeile 8 schreibt das ergänzte JSON-Objekt wieder in das File als JSON-String. Auch hier ist wieder ein Callback nötig.
In Zeile 9 wird an den Callback des Senders eine OK-Message gesendet.
In Zeile 10 wird das hello Objekt an die Destination „event.hello“ publiziert. Beim Publizieren wird kein Callback angegeben.

Der Client Code ist ähnlich einfach und wird in Ausschnitten im folgenden gezeigt.

eb = new vertx.EventBus(window.location.protocol + '//' + window.location.hostname + ':' + window.location.port + '/eventbus');

eb.onopen = function() {
   eb.registerHandler('event.hello', function(message) {
      // verarbeite die empfangene Message
   });

   eb.send('hello.list', {}, function(message) {
      // Zeige Liste der letzten Nachrichten an
   });
}

Auf dem Client muss man sich zunächst mit dem Event Bus verbinden, was in Zeile 1 erfolgt.
Über einen Callback wird man informiert, sobald die Verbindung steht (Zeile 3). Ab diesem Zeitpunkt kann man mit dem Bus interagieren.
In Zeile 4 erfolgt die Registrierung auf die Destination „event.hello“. Diese Funktion wird aufgerufen, wenn Nachrichten an „event.hello“ publiziert werden.
In Zeile 8 wird eine Nachricht an „hello.list“ gesendet um die Liste der letzten Hello-World-Nachrichten zu lesen. Die Liste wird dem Callback im Parameter message übergeben.

Das Senden einer neuen Hello-World-Nachricht an die Destination „hello.say“ erfolgt analog dem Senden an „hello.list“.

Man könnte die Hello-World-Nachricht auch direkt aus dem Client mit „publish“ an „event.hello“ publizieren anstatt sie gerichtet per „send“ an die Adresse „hello.say“ zu senden. Das im Beispiel gewählte Verfahren hat jedoch 2 Vorteile:

  1. Laufen auf dem Server mehrere Instanzen des Verticals „hello.js“, würden alle Instanzen durch die Publizierung die Message erhalten und aktiv werden. Die Nachricht würde somit mehrfach gespeichert werden. Durch das senden, anstelle des publizierens, sorgt vert.x dafür, dass nur eine Instanz die Message erhält.
  2. Die Nachricht wird erst an die Clients publiziert, wenn die Speicherung auf dem Server erfolgreich war.

An den Code Ausschnitten ist deutlich zu sehen, wie einfach die Kommunikation zwischen Web-Client und Server über den Event Bus ist. Auch ist zu sehen, dass es keinen Unterschied macht, ob man Client- oder Server-Code schreibt. Die Interaktion mit dem Event Bus ist immer gleich.
Auffallend sind aber auch die vielen und verschachtelten Callbacks, die sich aus der konsequenten Asynchronität ergeben. Dies kann schnell zur Callback Hölle führen. Hier können die Bibliotheken von ReactiveX abhilfe schaffen, die von vert.x 3 direkt unterstützt werden.
Ein Beispiel, wie mit vert.x 2 bereits ReactiveX genutzt werden kann, wird im Artikel „Create a simpe RESTful service with vert.x 2.0, RxJava and mongoDB“ gezeigt.

Der kompletten Quellcode des Praxis-Beispiels steht auf GitHub zur Verfügung.

Buchtipp

Die Code-Ausschnitte sollen lediglich einen ersten Einblick in die Programmierung und die Kommunikation über den Event Bus mit vert.x geben. Wer tiefer in vert.x einsteigen möchte dem empfehle ich das Buch „Real-time Web Application Development using Vert.x 2.0„. Das Buch bietet einen kompakten und leicht verständlichen Einstieg in die Entwicklung mit vert.x. Im Buch wird, ganz praxisnah, eine Web Anwendung entwickelt, mit der in Echtzeit und collaborativ Mindsets erstellt werden können. Dabei wird auch auf die Persistierung in einer MongoDB eingegangen.

One Reply to “Real Time Web Apps mit vert.x”

Schreibe einen Kommentar

Anmelden um einen Kommentar abzugeben.

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

*