Node.js – Erstellen einer einfachen Server-Client Anwendung mit Node.js

6. Anwendungsbeispiel für Node.js und Angular

In einem kleinen Beispiel soll eine Client-Webanwendung entwickelt werden, die einen Heartbeat vom Server empfängt und die Anzahl der verbundenen Clients anzeigt. Dazu verbindet sich der Client mit dem Server, sobald die Webanwendung im Browser aufgerufen wird. Der Server registriert dann den Client und soll in einem Zeitintervall einen Ping an alle verbundene Clients senden mit der Information der insgesamt verbundenen Clients.
Als Javascript-Framework für die Webanwendung soll das großartige AngularJS verwendet werden.

Heartbeat vom Server

Beginnen wir mit dem Server. Die Initialisierung des Servers übernehmen wir aus den vorherigen Codebeispielen.


var app = require('connect')();
var server = require('http').Server(app);
var io = require('socket.io')(server);
app.use(require('serve-static')("./client"));
server.listen(8085);

Als nächstes können wir die Funktion zum Senden des Ping und Anzahl der Verbindungen angehen.
Zunächst definieren wir eine Variable zum Zählen der Verbindungen und belegen diese initial mit dem Wert 0. Dann schreiben wir eine rekursive Funktion sendHeartbeat in welcher der Ping an die Clients gesendet wird.


var connectionCounter = 0;

function sendHeartbeat(){
  io.sockets.emit('ping', { beat : 1, connectedClients: connectionCounter });
setTimeout(sendHeartbeat, 5000);
};

Über die Event-Methode emit wird das von uns definierte Event ping an alle verbundenen Clients gesendet. Als Parameter übergeben wir einen Herzschlag und die Anzahl der verbundenen Clients. Mit der letzten Zeile veranlassen wir einen rekursiven Aufruf der Methode in einem Zeitintervall von 5 Sekunden.

Als nächstes wollen wir im Server angeben, was passieren soll, wenn sich ein Client verbindet. Da wäre zum einen das Hochzählen des Counters beim Verbinden bzw. das Herunterfahren beim Disconnect, zum anderen der initiale Aufruf der Heartbeat-Funktion nach 5 Sekunden. Wer will, kann auch noch auf ein mögliches Pong vom Client reagieren.


io.on('connection', function (socket) {
  connectionCounter += 1;
  socket.on("disconnect", function(){
    connectionCounter -= 1;
  });

  // receive pong from client
  socket.on('pong', function(data){
  console.log("server - Pong received from client");
});

sendHeartbeat();

});

Das war auch schon alles in Sachen Server. Weiter geht es clientseitig.

 

Client – AngularJS Ping Controller

Einbinden des Frameworks und unserer Skripte

Hier binden wir in der index.html erst einmal AngularJS ein. Unseren Angular Controller packen wir in die neu angelegte Datei app.js, welche wir gleich einbinden. Ebenso verfahren wir mit der Datei service.js, in welcher wir unser eigenes Service-Modul für Angular implementieren. Und socket.io nicht vergessen.


<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular.min.js"></script>
<script src="/js/app.js"></script>
<script src="/js/service.js"></script>
<script src="/socket.io/socket.io.js"></script>

 

Die Applikation

Jetzt können wir die AngularJS Application in der app.js angehen.
Zuerst definieren wir ein Root-Modul für die Applikation. Diesem übergeben wir den Namen App und fordern die Abhängigkeit von einem Modul mit dem Namen Services an. Dazu später. Den Controller nennen wir  pingController und machen den Scope und das Socket-Modul bekannt.


var app = angular.module('App', ['Services'])
  .controller('pingController', function($scope, Socket) {
    ...
});

 

Über die Direktive ng-App wird das RootModul als DIE Applikation festgelegt. Möglichst nahe dem Root Element der Seite (html, body, etc). Pro HTML Dokument ist nur eine AngularJS Applikation erlaubt. bei mehreren ngApp Vorkommen wird das erste verwendet.


<body ng-app="App">...</body>

Über die Direktive ngController werden die Controller eingebunden.


<div ng-controller="pingController">...</div>

 

Services

Als nächstes implementieren wir am besten unser Service-Modul. In der service.js definieren wir dazu das Modul und nennen es Services. Damit haben wir den Namen des Services registriert.

Die factory-Funktion generiert das Objekt oder die Funktion, welche den Service in der restlichen Anwendung repräsentiert. Der Service liefert das Objekt oder die Funktion dann als Return und ist in allen Komponenten (Controller, Service, Filter, Directives) verwendbar, welche die Abhängigkeit von dem Service angefordert haben. Registrieren wollen wir es unter dem Namen Socket.


angular.module('Services', []).
factory('Socket', function($rootScope) {
  var newServiceInstance;
  return newServiceInstance;
});

Innerhalb der factory-Funktion werden wir jetzt io.socket initialisieren. Zurückgegeben wird ein Objekt mit den Funktionen on und emit.


angular.module('Services', []).
factory('Socket', function($rootScope) {
  var socket = io.connect();
  return {
    on: function (eventName, callback) {
      ...
    },
    emit: function (eventName, data, callback) {
      ...
    }
  };
});

Diese beiden Funktionen stehen nun zur Verfügung, wenn der Service eingebunden wird.

Der on-Funktion übergeben wir nun ein Event und eine Callback-Funktion. Der emit-Funktion zusätzlich noch ein data-Objekt. Die Implementierung sieht dann so aus:


angular.module('Services', []).
  factory('Socket', function($rootScope) {
    var socket = io.connect();
    return {
        on: function (eventName, callback) {
            socket.on(eventName, function () {
                var args = arguments;
                $rootScope.$apply(function () {
                    callback.apply(socket, args);
                 });
             });
        },
        emit: function (eventName, data, callback) {
            socket.emit(eventName, data, function () {
                var args = arguments;
                $rootScope.$apply(function () {
                    if (callback) {
                        callback.apply(socket, args);
                    }
                });
            });
        }
    };
});

Und was macht das? Nun, in den 2 Funktionen sind die Socket.IO Funktionen on und emit implementiert. Über socket.on lassen sich, wie schon auf der Serverseite, Eventhandler registrieren. Wenn wir im Server per emit-Funktion das Event ‚ping‘ und ein Datenobjekt losschicken, kommt das hier im Client an. Die Argumente (z.B. das Datenobjekt) werden dann einfach an die Callback-Funktion übergeben. Genau so verhält es sich mit socket.emit. Lösen wir im Client ein Event aus, können wir diesem ein Datenobjekt und eine Callback-Funktion übergeben und dann geht es ab damit zum Server.

Als nächstes schauen wir uns die Callback-Funktion für das ‚ping‘-Event  im Controller an.

Controller

Da wir nun den Service implementiert haben, den wir der Applikation bereits als Abhängigkeit verpasst hatten, können wir im Controller das Socket-Objekt aus dem Service benutzen. Im Controller definieren wir zunächst zwei Scope-Variablen, die wir im Browser ausgeben wollen, nach dem der Ping vom Server empfangen wurde. Als nächstes folgt dann die Implementierung der Callback-Funktion auf das ‚ping‘ Event, d.h. dort beschreiben wir, wie der Client auf das ‚ping‘-Event vom Server reagieren soll.


var app = angular.module('App', ['Services'])
.controller('pingController', function($scope, Socket) {

  $scope.pingMessage = "";
  $scope.connectedClientMessage = "";

  Socket.on('ping', function(data){
    var now = new Date();
    var datetime = now.getFullYear()+'/'+(now.getMonth()+1)+'/'+now.getDate();
    datetime += ' '+now.getHours()+':'+now.getMinutes()+':'+now.getSeconds();
    $scope.pingMessage = datetime+": Heartbeat vom Server empfangen.";
    $scope.connectedClientMessage = "Aktuell verbundene Clients: " + data.connectedClients;
    Socket.emit('pong', {beat: 1});
 });

});

Viel passiert hier gar nicht. In der Scope-Variable pingMessage wird das Datum und die Zeit beim Empfang des Events und ein kurzer Hinweistext festgehalten. In der Variable connectedClientMessage halten wir die Anzahl der verbundenen Clients, die wir vom Server als Datenobjekt erhielten. Wir erinnern uns: Im Service übergaben wir alle Argumente an die Callback-Funktion. Zum Abschluss schicken wir ein ‚pong‘-Event über unsere emit-Funktion an den Server zurück.

Das war’s schon im Controller. Jetzt können wir die Scope-Variablen ausgeben lassen.

Ausgabe

Die Scope-Variablen aus dem Controller lassen sich nun ganz einfach in der index.html ausgeben.


<div ng-controller="pingController">
<h2>Ping</h2>
<p>{{pingMessage}}</p>
<p>{{connectedClientMessage}}</p>

</div>

Das großartige an AngularJS, die Scope-Variablen werden live aktualisiert. Immer wenn der Client einen neuen Ping vom Server erhält, wird das Datum und die Anzahl der verbundenen Clients aktualisiert.
Probiert es aus. Server starten, Browser starten, http://localhost:8085 aufrufen.

Wenn mann jetzt in einem zweiten Browser die URL in ein paar Tabs öffnet und schließt, kann man schön die Aktualisierung im ersten Browser beobachten.

 

nodejs_socket_ping

In der Konsole des Servers kann man das Senden des Ping und Empfangen des Pong auch verfolgen.

nodejs_socket_ping_console

 

Quellcode

Hier noch mal der Code der gesamten Applikation.


<!DOCTYPE html>
<html ng-app="App">
 <head>
   <meta charset="utf-8">
   <title>Test App</title>
   <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
   <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
   <script src="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
   <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.0/angular.min.js"></script>
   <script src="/js/app.js"></script>
   <script src="/js/service.js"></script>
   <script src="/socket.io/socket.io.js"></script>
 </head>
 <body>
   <div class="container">
     <div class="jumbotron">
       <h1>Ich bin die Client-Anwendung</h1>
       <p>...</p>
       <p><a class="btn btn-primary btn-lg" role="button">Learn more</a></p>
     </div>
     <div class="row">
       <div ng-controller="pingController">
         <h2>Ping</h2>
         <p>{{pingMessage}}</p>
         <p>{{connectedClientMessage}}</p>
       </div>
     </div>
   </div>
 </body>
</html>


var app = angular.module('App', ['Services'])
.controller('pingController', function($scope, Socket) {

  $scope.pingMessage = "";
  $scope.connectedClientMessage = "";

  Socket.on('ping', function(data){
    var now = new Date();
    var datetime = now.getFullYear()+'/'+(now.getMonth()+1)+'/'+now.getDate();
    datetime += ' '+now.getHours()+':'+now.getMinutes()+':'+now.getSeconds();
    $scope.pingMessage = datetime+": Heartbeat vom Server empfangen.";
    $scope.connectedClientMessage = "Aktuell verbundene Clients: " + data.connectedClients;
    Socket.emit('pong', {beat: 1});
  });

});


angular.module('Services', []).
  factory('Socket', function($rootScope) {
    var socket = io.connect();
    return {
        on: function (eventName, callback) {
            socket.on(eventName, function () {
                var args = arguments;
                $rootScope.$apply(function () {
                    callback.apply(socket, args);
                 });
             });
        },
        emit: function (eventName, data, callback) {
            socket.emit(eventName, data, function () {
                var args = arguments;
                $rootScope.$apply(function () {
                    if (callback) {
                        callback.apply(socket, args);
                    }
                });
            });
        }
    };
});


var app = require('connect')();
var server = require('http').Server(app);
var io = require('socket.io')(server);
app.use(require('serve-static')("./client"));
server.listen(8085);

var connectionCounter = 0;

function sendHeartbeat(){
  console.log('server - sendHeartbeat, conectedClients: ' + connectionCounter );
  io.sockets.emit('ping', { beat : 1, connectedClients: connectionCounter });
  setTimeout(sendHeartbeat, 5000);
};

io.on('connection', function (socket) {
  connectionCounter += 1;
  socket.on("disconnect", function(){
    connectionCounter -= 1;
  });

  // receive pong from client
  socket.on('pong', function(data){
    console.log("server - Pong received from client");
  });

  sendHeartbeat();

});

 

Fazit

Natürlich ist es nicht der heilige Gral. Natürlich muss man nicht alle bisher verwendeten Techniken wegwerfen. Natürlich tut es für die Auslieferung einfacher statischer Inhalte nach wie vor ein gewöhnlicher Webserver. Ebenso für rechenintensive Prozesse.
Aber! Die Stärken finden wir in anderen Bereichen, da wo die eventbasierte Art zum Tragen kommt. Das wäre z.B. der Abruf und die Verarbeitung von Massendaten, die als JSON aus einer NoSQL Datenbank kommen und viel Traffic verursachen. Das wäre der Nutzen beim Streaming, um Videofilme schon beim Hochladen auf einen Server in ein anderes Format transkodiert zu können. Das wären Vorteile bei der Versorgung von Clients mit aktuellen Daten per WebSockets. Und da wären die Entwickler, die z.B. ihre Buildtools und Helferleins über Node.js laufen lassen können oder bereits bestehende Systeme verwenden wollen. So können z.B. Gulp, Less, Grunt, etc. alle per NPM installiert und mit Node.js verwendet werden.
Von mir aus Entwicklersicht gibt es ein „Daumen hoch“.

Weiterführende Links

Hier noch ein paar Quellen zum Vertiefen:

http://nodecode.de/socket-io
http://socket.io/docs/
http://nodecode.de/chat-nodejs-websocket

Ähnliche Beiträge

2 Kommentare

  1. Gute Anleitung, nur fehlt irgendwie der für die socket.io.js oder wird diese nicht benötigt?
    Muss angular nicht vorher erstmal installieren und zum Projekt hinzufügen bevor man es benutzt? Ich bekomme nämlich keine Ping Zahlen angezeigt, nur {{pingMessage}}.

Schreibe einen Kommentar

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