Saturday, January 25, 2014

Simple chat server example using UI5 WebSocket

Today i will show you an example using node.js to provide a simple chat server to play around with the WebSocket API sap.ui.core.ws.WebSocket that is part of UI5.

To offer some useful tricks and features i blowed up the example with the following features:

  • 5-Way-Model-Binding
  • WebSocket based data connectivity
  • adding 3rd party notification library (noty)
  • JSON stringify and parsing (to be able to use objects inside messages)

The result will look like this

Chrome and Firefox parallel chat

Now let us come to the more interesting part, the implementation.

The node.js server will work as a http server serving static files and also listen for web socket requests.

app.js
/**
 * UI5 WebSocket minimalistic chat example by Holger Schäfer
 * using node.js and NPM package ws
 */
var express = require('express'),
    path = require('path'),
    httpPort = process.env.PORT || 80,
    wsPort = 8080,
    app = express();

// all environments
express.static.mime.default_type = "text/xml";
app.use(express.compress());
app.use(express.static(path.join(__dirname, 'public')));

// development only
if ('development' == app.get('env')) {
    app.use(express.errorHandler());
}

// start web socket server
var WebSocketServer = require('ws').Server,
    wss = new WebSocketServer({
        port: wsPort
    });

wss.broadcast = function (data) {
    for (var i in this.clients)
        this.clients[i].send(data);
    console.log('broadcasted: %s', data);
};

wss.on('connection', function (ws) {
    ws.on('message', function (message) {
        console.log('received: %s', message);
        wss.broadcast(message);
    });
    ws.send(JSON.stringify({
        user: 'NODEJS',
        text: 'Hallo from server'
    }));
});

// start http server
app.listen(httpPort, function () {
    console.log('HTTP Server: http://localhost:' + httpPort);
    console.log('WS Server: ws://localhost:' + wsPort);
});

In fact the server simply broadcasts all incomming messages to all connected clients. I used the WS module because it has a very small footprint and is quite simple to use.

The most popular node web socket libary is Socket.IO but this example should use the UI5 based client library while socket.io needs his own client libary. Socket.io is much more powerful than WS. In order to provide realtime connectivity on every browser, Socket.IO selects the most capable transport at runtime, without it affecting the API (WebSocket, Adobe® Flash® Socket, AJAX long polling, AJAX multipart streaming, Forever Iframe, JSONP Polling). The client also has a heartbeat service that automatically reconnects on connection disruption.

Now let us take some attention on the client side.

index.html
<!DOCTYPE html>  
<html>
  <head>
    <meta http-equiv='X-UA-Compatible' content='IE=edge' />
    <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
    <title>WebSocket Chat</title>
    <script id='sap-ui-bootstrap' 
      src='https://openui5.hana.ondemand.com/resources/sap-ui-core.js'
      data-sap-ui-theme='sap_bluecrystal'  
      data-sap-ui-libs='sap.ui.commons'></script>   
    <script src="libs/jquery.noty.packaged.min.js" type="text/javascript"></script>
    <script>
      // Chat Model
      var oModel = new sap.ui.model.json.JSONModel(),
       names = ['Holger','Volker','Jörg','Klaudia','Dirk','Thomas'];
      
      oModel.setData({
          user: names[Math.floor(names.length * Math.random())],
          chat: ""
      });
      sap.ui.getCore().setModel(oModel);
      
      // WS handling
      jQuery.sap.require("sap.ui.core.ws.WebSocket");  
      var connection = new sap.ui.core.ws.WebSocket('ws://localhost:8080');
      
      // connection opened 
      connection.attachOpen(function (oControlEvent) {
       notify('onOpen', 'connection opened...', 'success');
      }); 
        
      // server messages
      connection.attachMessage(function (oControlEvent) {
       var data = jQuery.parseJSON(oControlEvent.getParameter('data')),
        msg = data.user + ': ' + data.text,
        lastInfo = oModel.oData.chat;
        
       if (lastInfo.length > 0) lastInfo += "\r\n";  
       oModel.setData({chat: lastInfo + msg}, true); 
         
       // scroll to textarea bottom to show new messages
       $('#chatInfo').scrollTop($('#chatInfo')[0].scrollHeight);
       
       notify('onMessage', msg, 'information');      
      });
      
      // error handling
      connection.attachError(function (oControlEvent) {
       notify('onError', 'Websocket connection error', 'error');
      }); 
       
      // onConnectionClose
      connection.attachClose(function (oControlEvent) {
       notify('onClose', 'Websocket connection closed', 'warning');
      });
      
      // send message
      var sendMsg = function() {
       var msg = oMsg.getValue();
       if (msg.length > 0) {
        connection.send(JSON.stringify(
         {user: oModel.oData.user, text: msg}
        ));
        notify('sendMessage', msg, 'alert');
        oMsg.setValue();  // reset textfield
        oMsg.focus();  // focus field       
       }     
      }   
      
      // notifier 
      function notify(title, text, type) {  
       type = type || 'information'
       // [alert|success|error|warning|information|confirm]  
       noty({
        text: text,
        template: '<div class="noty_message"><b>' + title + ':</b>&nbsp;<span class="noty_text"></span><div class="noty_close"></div></div>',
        layout: "topRight",
        type: type,
                 timeout: 4000
       });  
      } 
       
      // UI5 User Interface
      
      // attach key return handler for textinput
      sap.ui.commons.TextField.prototype.onkeyup = function(oBrowserEvent) {
       if (oBrowserEvent.keyCode === 13) sendMsg();
      }; 
       
      var oUserField = new sap.ui.commons.TextField("userName", {
             value: "{/user}",
             tooltip: "Edit me"
      });
      
      var oIPE1 = new sap.ui.commons.InPlaceEdit("IPE1",{
       content: oUserField
      }).placeAt("username"); 
      
      var oChatInfo = new sap.ui.commons.TextArea("chatInfo", {  
       cols: 60,
       rows: 8,
       editable: false,
       valueState : sap.ui.core.ValueState.Success,
       value: "{/chat}"
      }).placeAt("info");
      
      var oMsg = new sap.ui.commons.TextField("chatMsg", {
             width: '20em'
      }).placeAt("text");
      
      var oSendBtn = new sap.ui.commons.Button("sendBtn", {
             text: "Send",
             press: function(oEvent) {
              sendMsg();
             } 
      }).placeAt("btn");  
    </script>
  </head>
  <body class='sapUiBody'>
    <div style="padding: 10px">
      <h2>WebSocket Chat</h2>
      <div id="username"></div>
      <div id='info' style="background-color: #cce8d8; border: 1px solid #bfbfbf; display: inline-block"></div>
      <div><span id='text'></span><span id='btn'></span></div>
    </div>
  </body>
</html>

Why do i call it 5-Way-Model-Binding?

The reason for that is that UI5 supports Two-Way-Model-Bind between the JavaScript model class and the browser DOM. Changes will be automatically synchronized between input elements and the javascript model object. But in the upper szenario model changes will also be forwarded to the server and from the server to all connected clients. Inside the target clients the model changes will be automatically reflected in the binded user interface widgets (without further coding).

Client DOM <-> Client Model <-> Server <-> Client Model <-> Client DOM

Using this approach you can share your model across all connected clients (and also on the server).

I also added some usability features like In-Place-Editing of user name, jQuery scroll textarea automatically to new entries at the end and browser events like ENTER key on text input field to demonstrate some goodies from UI5.

The full code example is available for download on GitHub

WebSocket Example

First we need to install the needed node packages (not included in repository)
npm install express ws
Now we can start the server from the cmd line
node app.js
and open the app with a browser that supports native web sockets
http://localhost

No comments:

Post a Comment

Thank you for your comment!

The comment area is not intended to be used for promoting sites!
Please do not use such links or your comment will be deleted.

Holger

Note: Only a member of this blog may post a comment.