Logo Oracle Deutschland   Application Express Community
Websockets, Node.js und APEX: Hintergrund-Nachrichten ohne Polling
Erscheinungsmonat APEX-Version Datenbankversion
Februar 2015 alle ab 10.2

Heute stellen wir Ihnen vor, wie Sie die neue HTML5-Technologie Websockets in APEX Anwendungen nutzen können. Websockets (Wikipedia) sind eine Erweiterung des HTTP-Protokolls und erlauben es, dass der Browser und der Webserver die Netzwerkverbindung nach dem Ausliefern der HTML-Seite offen halten. Über diese offen gehaltene Verbindung kann der Server später noch Daten an den Browser senden.

Diese Technologie ist unglaublich nützlich, wenn es darum geht, "Hintergrundbenachrichtigungen" an den Client, also den Browser zu senden. Als Beispiel sei eine Statusnachricht genannt, die im Browser stets aktuell gehalten werden soll - auch wenn der Anwender gerade nicht arbeitet. Ohne Websockets muss der Browser regelmäßig beim Server anfragen, ob es Aktualisierungen gibt (Polling) - wie man das in APEX umsetzen kann, ist im Community-Tipp "Berichte automatisch aktualisieren" beschrieben. Der Nachteil dieses Ansatzes ist, dass das Polling-Interval recht kurz gehalten werden muss, wenn der Endanwender ein "Real Time" Gefühl haben soll. Das bewirkt andererseits sehr viel Netzwerkverkehr und viel Last auf der (APEX)-Datenbank durch das ständige Ausführen ein- und derselben Abfrage. Und solange es keine Aktualisierungen gibt - für "nichts und wieder nichts".

Mit Websockets ist das alles viel einfacher: Die Netzwerkverbindung wird einmal instanziiert und bleibt dann offen. Es findet keinerlei Polling statt. Nur wenn es eine Aktualisierung gibt, werden alle Clients vom Server aus benachrichtigt. Die Last auf den Systemen wird um ein Vielfaches reduziert.

Erforderlich dafür ist, dass sowohl Browser als auch Webserver das Websocket-Protokoll unterstützen. Was die Browser angeht, so ist dies bei allen modernen Browsern, die HTML5 unterstützen, der Fall. Serverseitig kommt es nicht nur auf die Protokollunterstützung an sich an, sondern auch darauf, dass der Server mit sehr vielen gleichzeitig offenen Websocket-Verbindungen umgehen kann. Denn es muss ja davon ausgegangen werden, dass sehr viele Client (= Browser-Fenster) geöffnet sein werden.

Eine Technologie, die sich auf der Serverseite anbietet, ist Node.js (Wikipedia). Node.js bietet sich durch seine eventgetriebene, asynchrone Natur als Technologie für einen Websocket-Server geradezu an. Seit Januar 2015 steht nun auch noch ein Node.js Treiber für die Oracle-Datenbank zur Verfügung - der Nutzung von Websockets in APEX-Anwendungen steht also nichts mehr im Wege.

Vorbereitungen - Tabelle

Das Beispiel soll eine stets aktuelle "Tagesnachricht" in einer APEX-Anwedung darstellen. Diese Nachricht wird aus einer Tabelle gelesen und mit einer APEX-Anwendung gepflegt. In anderen APEX-Anwendungen wird sie in einer Alert Region am Seitenanfang dargestellt. Und mit Hilfe von Websockets werden wir sie stets aktuell halten. Auf Basis dieses einfachen Beispiels können Sie dann auch komplexere Szenarien umsetzen - die Vorgehensweise ist immer die gleiche. Zunächst braucht es aber die Tabelle.

create table messages (
  id number          primary key, 
  msg varchar2(4000) not null, 
  datetime date      not null
);

create sequence seq_messages;

create or replace trigger tr_messages
before insert on messages
for each row
begin
 :new.id := seq_messages.nextval;
 :new.datetime := sysdate;
end;
/

Vorbereitungen - Anwendung zur Nachrichten-Pflege

Erzeugen Sie dann die APEX-Anwendung zur Pflege dieser Tabelle MESSAGES. Machen Sie es sich einfach: Nehmen Sie eine Seite vom Typ Report and Form zu Ihrer Anwendung dazu und nehmen Sie ansonsten die Defaults (Abbildung 1).

Einfache APEX-Standardanwendung zur Pflege der Nachrichten

Abbildung 1: Einfache APEX-Standardanwendung zur Pflege der Nachrichten

Navigieren Sie nach Erstellung der Anwendung zur Formularseite und stellen Sie dort die Elemente für die Spalten ID und DATETIME auf Hidden um und machen Sie aus dem Textfeld für die Nachricht eine Textarea. Das Ergebnis sollte dann so aussehen. Testen Sie das Formular, in dem Sie die erste Nachricht direkt erstellen: Dies ist die erste Nachricht.

Formular zur Pflege der Nachrichten

Abbildung 2: Formular zur Pflege der Nachrichten

Der Bericht zeigt die vorhandenen Nachrichten an.

Bericht mit Übersicht über die vorhandenen Nachrichten

Abbildung 3: Bericht mit Übersicht über die vorhandenen Nachrichten

Vorbereitungen - Nutzung der Nachrichten

So weit, so gut. Nun geht es an die Anwendung, in der die Nachrichten dargestellt werden soll. Nehmen Sie eine vorhandene oder erzeugen Sie sich eine neue mit einer Seite und Regionen (was diese enthalten, ist erst einmal egal).

Fügen Sie dieser Anwendung dann eine Global Page hinzu (Seite 0). Auf diese Seite legen Sie eine Region vom Typ HTML mit folgendem Quelltext.

<div id="MESSAGE">&P0_MESSAGE.</div>

Das verwendete Element P0_MESSAGE brauchen Sie natürlich auch. Legen Sie es als Hidden-Element an und setzen Sie folgende Einstellungen im Bereich Source.

  • Source Used: Always, replacing any existing value in session state
  • Source Type: SQL Query
  • Source value: select msg from (select msg from messages order by datetime desc) where rownum = 1
Einstellungen für das Element P0_MESSAGE

Abbildung 4: Einstellungen für das Element P0_MESSAGE

Damit wird das Element P0_MESSAGE immer die jüngste Nachricht enthalten. Es wird in einer Region auf der (globalen) Seite 0 dargestellt, also auf jeder Anwendungsseite. Eine solche sieht dann in etwa wie in Abbildung 5 aus.

Anwendungsseite mit Nachrichten-Region

Abbildung 5: Anwendungsseite mit Nachrichten-Region

Damit sind alle Vorbereitungen abgeschlossen. In der "klassischen" Welt wären Sie jetzt fertig. Denn beim Aufbau wird stets die jüngste Nachricht dargestellt. Wenn die Seite aktualisiert wird, wiederholt sich der Vorgang und die dann jüngste Nachricht wird dargestellt. Mit Websockets werden wir nun aber die Nachricht austauschen, ohne dass die Seite neu aufgebaut wird - und ohne, dass der Browser ständig per AJAX beim Server anfragt.

Node.js auf dem APEX-Server installieren

Laden Sie Node.js und den Oracle-Datenbanktreiber node-oracledb herunter und installieren Sie diesen auf dem Knoten, auf dem bereits Ihr APEX-Webserver läuft. Eine Anleitung auf Deutsch ist für Linux verfügbar; für Windows und andere Plattformen gibt es einen englischen Installation Guide.

Wenn Sie die Installation abgeschlossen haben, können Sie mit diesem kleinen Programm emp.js testen, ob Ihre Node.js-Umgebung und Ihre Datenbankverbindung funktioniert.

var oracledb = require('oracledb');

function showEmp(conn) {
  conn.execute(
    "SELECT * from EMP where EMPNO = 7839",
    [],
    function(err, result)
    {
      if (err) {
        console.log('%s', err.message);
        return;
      }
      console.log(result.rows);
    });
}

oracledb.getConnection(
  {
    user          : "scott",
    password      : "tiger",
    connectString : "{db-server}:{db-port}/{service-name}"
  },
  function(err, connection)
  {
    if (err) {
      console.error(err.message);
      return;
    }
    showEmp(connection);
  }
);

Ihre Verzeichnisstruktur sollte wie in Abbildung 6 aussehen. Speichern Sie den Code in eine Datei namens emp.js, passen Sie den Datenbank-Connection-String und ggfs. Usernamen und Passwort an Ihre Umgebung an und führen Sie das Skript mit dem Executable node aus (das Vorhandensein der Tabelle EMP im Datenbankschema wird vorausgesetzt).

Arbeitsverzeichnis in Ihrer Node.js Umgebung

Abbildung 6: Arbeitsverzeichnis in Ihrer Node.js Umgebung

Wenn Sie folgende Ausgabe sehen, funktioniert Ihre Node.js-Umgebung.

$ node emp.js 
[ [ 7839,
    'KING',
    'PRESIDENT',
    null,
    Tue Nov 17 1981 00:00:00 GMT+0100 (CET),
    5000,
    null,
    10 ] ]

Node.js Pakete für Websockets installieren

Nun kann der Websockets-Server auf Basis von Node.js angegangen werden. Node.js kennt Packages und einen Package Manager: npm - der wurde beim Installieren des Oracle-Datenbanktreibers auch schon verwendet. Mit diesem können Funktionsbibliotheken zur Node.js Umgebung hinzugefügt werden. Für den Websocket-Server werden zwei Pakete benötigt: Express und Websocket. Installieren Sie diese wie folgt in Ihre node.js Umgebung (achten Sie darauf, dass Sie sich in Ihrem Arbeitsverzeichnis befinden).

$ pwd
/home/oracle/node/work    # Das ist das "Arbeitsverzeichnis"

$ ls
emp.js  node_modules

$ npm install express
$ npm install websocket

Wenn sich Ihre Node.js-Umgebung hinter einer Firewall befinden, müssen Sie den Proxy-Server setzen, damit npm sich die Pakete aus dem Internet holen kann. Das geht einmalig wie folgt.

$ npm config set proxy=http://{proxy-server}:{port}
$ npm config set https-proxy=http://{proxy-server}:{port}

Anschließend sollte die Verzeichnisstruktur wie in Abbildung 7 aussehen.

Arbeitsverzeichnis in Ihrer Node.js Umgebung mit Express und Websocket

Abbildung 7: Arbeitsverzeichnis in Ihrer Node.js Umgebung mit Express und Websocket

Node.js Skript für den Websocket-Server erzeugen und starten

Nun wird es Zeit für das eigentliche Skript, welches den Websocket-Server implementiert. Passen Sie im folgenden Skript die Zeilen 14 bis 18 an Ihre Datenbank an (verwenden Sie das APEX-Parsing-Schema, welches die eingangs erzeugte Tabelle MESSAGES enthält) und speichern Sie alles in einer Datei namens apex-websocket.js, die (wie schon emp.js) in Ihrem Node.js Arbeitsverzeichnis liegen muss.

/*
 * Websocket Server für das APEX-Websocket Beispiel 
 * der deutschsprachigen APEX Community
 */

// Benötigte Node.js Pakete einbinden
var oracledb = require('oracledb');
var wsserver = require('websocket').server;
var express  = require('express');
var http     = require('http');

// Variable für Datenbank-Verbindungsinformation
var dbConnection = {
  user          : "{username}",
  password      : "{passwort}",
  connectString : "{db-server}:{db-port}/{service-name}",
  poolMin       : 2,
  poolMax       : 4
};
// Variable für Datenbank-Connection Pool
var pool;

// Array hält zur Zeit verbundene Websocket-Clients
var clients = [];

// HTTP-Webserver ohne Funktionalität - als Fundament für Websocket-Server
// Starte diesen auf Port  1337
var myhttpserver = http.createServer(function(request, response) { });
myhttpserver.listen(1337, function() { });

// Websocket-Server auf Basis des HTTP-Server erstellen
mywsserver = new wsserver ({ httpServer: myhttpserver });

// Websocket-Funktionen ... 
// Bei Verbindungsaufnahme: Client in Array aufnehmen
// Bei Verbindungsabbau:    Client aus Array entfernen
mywsserver.on('request', function(request) {
  var connection = request.accept(null, request.origin);
  var indx = clients.push(connection) - 1;
  console.log("client " + indx + " connected.");

  connection.on('message', function(message) { });

  connection.on('close', function(connection) {
    clients.splice(indx, 1);
    console.log("client " + indx + " disconnected.");
  });
});

// Diese Funktion sendet eine Nachricht an alle verbundenen Websocket Clients
// Trigger via HTTP-Request auf http://{host}:{port}/update
function sendUpdate(req, res) {
  // Datenbankverbindung aus Connection Pool holen
  pool.getConnection(function(err, connection){
    // SQL ausführen
    connection.execute(
      "select msg from (select msg from messages order by datetime desc) where rownum = 1",
      [],
      function(err, results) {
        // Datenbankverbindung in den Pool zurückgeben
        connection.release(function (err) {});

        // Query Ergebnis an alle Websocket-Clients senden
        for (var i=0; i < clients.length; i++) {
          clients[i].sendUTF(JSON.stringify(
            {"message": results.rows[0][0]}
          ));
        }

        // Bestätigung ausgeben
        res.writeHead(200, {'Content-Type': 'text/plain'});
        res.end("Sent message to " + clients.length + " clients.");
        console.log("Sent message to " + clients.length + " clients.");
      }
    )
  })
}

// HTTP-"Kontrollserver" auf Port 9099 starten; dieser nimmt
// die Kommandos für den Websocket-Server entgegen
function startControlServer () {
  var app =  express();
  // Url-Präfix /update/: Neue Nachricht aus der DB holen und an die Websocket-Clients senden
  app.get ("/update/*", sendUpdate);

  var server = app.listen(9099, function () {
    var host = server.address().address
    var port = server.address().port
    console.log('Websocket Control Server listening at http://%s:%s', host, port);
  });
}

// Datenbank-Connection Pool aufbauen ...
oracledb.createPool(
  dbConnection,
  function(err, ppool){
    if (err) {
      console.log(err);
      return;
    }
    // Connection Pool aufgebaut? 
    // Nachricht ausgeben und den "Kontrollserver" starten
    pool = ppool;
    console.log("Database connection pool established");
    startControlServer();
  }
);

Starten Sie das Skript nun wiederum mit dem Executable node. Wenn Sie alles richtig gemacht haben, sollten Sie folgende Meldungen sehen.

$ node apex-websock.js
Database connection pool established
Websocket Control Server listening at http://0.0.0.0:9099

Das Node.js Skript hat nun zwei Webserver gestartet.

  • Auf Port 1337 "lauscht" der Websocket-Server; im nächsten Schritt werden wir der APEX-Anwendung Javascript-Code hinzufügen, der sich auf diesen Server verbinden soll. Über diesen Port bekommen die Clients dann die Websocket-Nachrichten zugesendet.

  • Auf Port 9099 "lauscht" ein "Kontrollserver" - schließlich muss das Node.js Skript irgendwie das Kommando bekommen, "jetzt" die Inhalte aus der Tabelle MESSAGES zu lesen und an die Clients auszuliefern. Das geschieht mit einem einfachen HTTP-Zugriff auf den "Kontrollserver". Ruft man die URL {hostname}:9099/update/ auf, so wird der Websocket-Server aktiv. Im Moment gibt es noch keine verbundenen Clients, daher passiert nicht viel.

Beide Ports können Sie natürlich ändern: Port 1337 für den Websocket-Server wird in Zeile 29 festgelegt, die 9099 für den "Kontrollserver" findet sich in Zeile 86.

APEX-Anwendung zum Websocket-Client machen

Navigieren Sie nun wieder zur APEX-Anwendung, welche die Nachrichten anzeigen soll (nicht die zur "Pflege"). Dort wurde die Nachrichten-Region ja auf der globalen Seite 0 erzeugt. Navigieren Sie wiederum zur dieser Seite und erzeugen Sie eine neue Dynamic Action.

  • Nennen Sie die Dynamic Action Page Load: Init Websocket Client
  • Die Dynamic Action soll On Page Load ausgeführt werden
  • Als TRUE Action soll folgender Javascript-Code ausgeführt werden:
    // Nur ausführen, wenn der Browser Websockets unterstützt
    if ("WebSocket" in window || 'MozWebSocket' in window) {
    
      window.WebSocket = window.WebSocket || window.MozWebSocket;
    
      // Verbindung zum Websocket-Serve öffnen
      var connection = new WebSocket('ws://' + document.location.hostname + ':1337');
    
      // Debug-Meldung "Connection offen"
      connection.onopen = function () {
        console.log("Websocket connection open.");
      };
    
      // Debug-Meldung "Connection-Fehler"
      connection.onerror = function (error) {
        console.log("Websocket error: " + error);
      };
    
      // Wenn eine Nachricht vom Websocket-Server kommt ...
      connection.onmessage = function (message) {
        // JSON parsen
        var l = JSON.parse(message.data);
      
        // Alert-Region aktualisieren
        $("#MESSAGE").html(JSON.parse(message.data).message);
        // Ein wenig Animation ...
        $("#MESSAGE")
             .animate({"color": "red", "font-size": "12pt"}, 200)
             .animate({"color": "black"},200)
             .animate({"font-size": "10pt"}, 3000)
        ;
      }
    }
    

Die Dynamic Action sollte dann wie in Abbildung 8 aussehen.

Dynamic Action on "Page Load": Websocket Client initialisieren

Abbildung 8: Dynamic Action on "Page Load": Websocket Client initialisieren

Damit sind Sie schon fast fertig. Der Websocket-Server läuft - wenn Sie die APEX-Seite starten, können Sie in der Javascript-Konsole die Nachricht sehen, dass der Browser eine Verbindung zum Websocket-Server geöffnet hat.

Der Client hat eine Websocket-Verbindung geöffnet

Abbildung 9: Der Client hat eine Websocket-Verbindung geöffnet

Wenn der Websocket-Server nun eine Nachricht sendet, wird die Nachricht innerhalb der Alert Region aktualisiert. Probieren Sie das einmal aus - rufen Sie in einem zweiten Browserfenster die URL des "Kontrollservers" auf: http://{hostname}:9099/update/. Sie sollten sehen, dass die Nachricht in der Alert Region nun kurz "aufleuchtet". Die Nachricht selbst ändert sich aber nicht - kein Wunder: Sie haben ja auch keine neue Nachricht gespeichert.

Der Client hat eine Websocket-Nachricht erhalten

Abbildung 10: Der Client hat eine Websocket-Nachricht erhalten"

Websocket-Nachricht nach Erstellen einer Nachricht triggern

Nun muss nur noch sichergestellt werden, dass die URL des "Kontrollservers" aufgerufen wird, wenn eine neue Nachricht eingestellt wurde. Hierfür gibt es verschiedene Ansätze - für den Anfang nehmen wir hier den einfachsten: Navigieren Sie dazu zu der APEX-Seite mit dem Formular zum Erstellen einer neuen Nachricht (das ist die, die Sie eingangs so verändert haben, dass nur noch eine Textarea zu sehen ist).

APEX-Seite zum Erstellen einer neuen Nachricht

Abbildung 11: APEX-Seite zum Erstellen einer neuen Nachricht

Bei Klick auf die Schaltfläche CREATE wird derzeit nur eine Zeile in der Tabelle MESSAGES erzeugt. Um das Versenden der Websocket-Nachricht zu triggern, erzeugen Sie einen weiteren PL/SQL-Prozess, der nach dem schon vorhandenen Prozess Process Row of MESSAGES ausgeführt werden soll. Versehen Sie ihn mit dem folgenden PL/SQL Code.

declare
  v_result clob;
begin
  commit;
  v_result := httpuritype('http://{hostname}:9099/update/').getclob();
end;

Damit der HTTP-Request aus APEX heraus funktioniert, muss das Parsing Schema der APEX-Anwendung in die PL/SQL Netzwerk-ACL eingetragen sein. Hier brauchen Sie ggfs. die Hilfe des DBA. Mehr Informationen dazu finden Sie im Community Tipp Netzwerkzugriffe mit Application Express und Oracle11g.

Stellen Sie dann noch sicher, dass dieser Prozess nur bei Klick auf die Schaltfläche CREATE ausgeführt werden soll (beim Löschen einer alten Nachricht ist keine Websocket-Benachrichtigung nötig). Im Seitenkontext sieht der Prozess dann wie in Abbildung 12 aus.

Ein PL/SQL Prozess zum Triggern des Websocket-Servers wurde erstellt

Abbildung 12: Ein PL/SQL Prozess zum Triggern des Websocket-Servers wurde erstellt

Wer es etwas fortgeschrittener haben möchte, kann auch dafür sorgen, dass die Datenbank diesen HTTP-Request vollautomatisch nach einer Änderung an der Tabelle durchführt. Dazu lässt sich Continuous Query Notification nutzen - die Details würden den Rahmen dieses Community-Tipps sprengen; folgen Sie einfach dem Blog-Posting zum Thema und der Dokumentation. Achtung: Normale Trigger sind der falsche Weg - denn diese feuern unmittelbar nach dem Kommando und noch vor einem Commit oder Rollback. Würde also ein Rollback erfolgen, so würde eine Phantom-Nachricht gesendet. Continuous Query Notification wird dagegen erst nach dem Commit ausgelöst.

Ergebnis

Und nun sind Sie tatsächlich fertig. Zum testen öffnen Sie einige Browserfenster parallel und ordnen Sie diese auf Ihrem Bildschirm an. In einem weiteren Browserfenster öffnen Sie das Formular zum Erstellen einer neuen Nachricht. Tippen Sie schon mal Das ist die zweite Nachricht ein.

Testumgebung: Viele Browser mit der Anwendung - einer zum Erstellen einer Nachricht

Abbildung 13: Testumgebung: Viele Browser mit der Anwendung - einer zum Erstellen einer Nachricht

Nach Klick auf die Schlatfläche CREATE wird die neue Nachricht zunächst in die Tabelle geschrieben, dann wird die URL des "Kontrollservers" aufgerufen. Der Node.js Websocket-Server wird aktiv, liest die zuletzt gespeicherte Nachricht aus der Tabelle aus und sendet sie an alle verbundenen Clients.

Testergebnis: Die neue Nachricht erreicht alle Browser

Abbildung 14: Testergebnis - Die neue Nachricht erreicht alle Browser"

Sie haben nun also - asynchron - die Seiten Ihrer Anwendungsnutzer aktualisiert, ohne dass diese ständig nach neuen Nachrichten gefragt haben - es findet keinerlei Polling statt. Das ist eine sehr ressourcenschonende und moderne Art, eine asynchrone Hintergrundbenachrichtigung zu implementieren. Mit Hilfe von Node.js, dem Oracle-Treiber node-oracledb und APEX steht Ihren Ideen nun nichts mehr im Wege ...

Dieser Community Tipp fokussiert sich allein auf die Funktionalität des Websocket-Beispiels; Fehlerbehandlung und Instrumentierung des Codes fehlen weitgehend. Diese dennoch nötigen Schritte bleiben Ihnen bei der konkreten Umsetzung überlassen. Viel Spaß mit APEX und Websockets.

Zurück zur Community-Seite