Skip to Content
Technical Articles

CAP WebSockets

Hi CAP-Developers and enthusiasts!

In 2020 I’ve been playing around with SAP CAP and Cloud Foundry quite a lot. In the very beginning it was a lot to process, but I really enjoyed the journey and I love the results/demos it brought along.

I believe the coolest thing about CAP and the Cloud Foundry Environment, is the freedom it offers the developer. Every extra feature or integration is just one “plugin” away. If you can think it, you can build it. Obviously always make sure it makes sense.

On top of this CAP and Cloud Foundry freedom, we can even choose our preferred IDE. Usually this will be VS Code or the SAP Business Application Studio. Both IDE’s have their own advantages and disadvantages of course.

Now in this blog I want to make use of this freedom by using WebSocket’s in an SAP CAP application. As some of you may already know, I’m a big fan of real-time applications. Which is why I wanted to investigate this possibility and share it in a very basic example to get started. In the end you will notice there are more ways and maybe better and more performant solutions to implement WebSocket’s. But like I said, this is more about how to get started with the basics.

I chose the SAP Business Application Studio as development IDE for this example app. This because it offers me all the tools and features I need in a preconfigured workspace. Which makes it easier to follow the example as well.

What are we building?

We will be building an application where an employee of a company has the possibility to add an idea for a team event (mocked via service / no UI5 coding). The user will be notified (via a “MessageToast” in UI5) of all the newly created ideas, added by his or her colleagues in real-time. This without the need to refresh the application.

Long story short => a real-time application using WebSocket’s.

Building the app

The development of such an application might sound a little hard. But in the end you will see it only requires a few commands, CDS and JavaScript files.

The power of CAP to the fullest!

I will not go over every single step to setup a CAP application or UI-module since this blog is about “how to integrate WebSocket’s in a CAP app”. The setup of a CAP app will not be a secret to most of you anymore. 😊  But I will list alle the commands to prepare the project in the SAP Business Application Studio so you can follow along.

Let’s get started!

Execute the following commands to initialize the CAP-project and add a Fiori Module and Approuter:

cds init RealTimeCAP
cd RealTimeCAP
yo fiori-module

Pass the following arguments to the “Yeoman fiori-module” generator:

Execute the following command to create a database schema:

touch db/schema.cds

Add following code to it, to create an “Idea” entity:

using {managed} from '@sap/cds/common';

namespace RealTimeCAP.db;

entity Ideas : managed {
    key ID          : UUID;
        name        : String;
        description : String;
}

Execute the following command to create an OData Service for your “Idea” entity:

touch srv/catalog-idea-service.cds

Add following code to it, to expose the “Idea” entity:

using {RealTimeCAP.db as db} from '../db/schema';

service CatalogIdeaService {
    entity Ideas as projection on db.Ideas;
}

Execute the following command to add custom logic to your OData Service:

touch srv/catalog-idea-service.js

Add following code to it:

const WebSocketServer = require('ws').Server;
const ws = new WebSocketServer({
    port: process.env.PORT || 8080
});

module.exports = async (srv) => {
    srv.after('CREATE', '*', async (ideas, req) => {
        for (const client of ws.clients) {
            client.send(JSON.stringify(ideas));
        }
    });
}

This custom logic in the “catalog-idea-service.js” file starts a WebSocketServer and will send the created entity response to every connected client. This because of the “after event handler” attached to the service. It will only be triggered for a “CREATE” operation, but for every single entity because of the wildcard “*”.

Execute the following command to install the WebSocket NPM module:

npm i ws

Execute the following command to install all the other dependencies inside the “package.json” file:

npm install

Execute the following command to deploy your database module to a local SQLite database:

cds deploy --to sqlite:myDatabase.db

Replace the configuration in the “xs-app.json” file inside your “realtimecap-approuter” with the following configuration:

{
  "welcomeFile": "HTML5Module/index.html",
  "authenticationMethod": "none",
  "websockets": {
    "enabled": true
  },
  "logout": {
    "logoutEndpoint": "/do/logout"
  },
  "routes": [
    {
      "source": "^/NodeWS(.*)$",
      "target": "$1",
      "authenticationType": "none",
      "destination": "NodeWs_api",
      "csrfProtection": false
    },
    {
      "source": "^/odataSrv/(.*)$",
      "target": "/catalog/$1",
      "authenticationType": "none",
      "destination": "srv_api",
      "csrfProtection": false
    },
    {
      "source": "^/HTML5Module/(.*)$",
      "target": "$1",
      "authenticationType": "none",
      "localDir": "../HTML5Module/webapp"
    }
  ]
}

Notice that we enabled the “WebSockets” and a route for the “HTML5Module”, “OdataSrv” and “NodeWS” was added.

Execute the following command to create a “default-env.json” file to “mock” your destinations:

touch app/realtimecap-approuter/default-env.json

Add the following configuration to it:

{
    "destinations": [
        {
            "name": "srv_api",
            "url": "http://localhost:4004",
            "forwardAuthToken": true
        },
        {
            "name": "NodeWs_api",
            "url": "ws://localhost:8080",
            "forwardAuthToken": true
        }
    ]
}

Like configured in the “routes” inside the “xs-app.json” file we define the URLs to these services with the appropriate ports.

Execute the following command to install the dependencies inside your “Approuter” directory:

npm install --prefix app/realtimecap-approuter/

Replace the code of your “IdeaOverview.controller.js” file inside your “HTML5Module” with the following code:

sap.ui.define([
    "sap/ui/core/mvc/Controller",
    "sap/ui/core/ws/WebSocket",
    "sap/m/MessageToast"
],
    /**
     * @param {typeof sap.ui.core.mvc.Controller} Controller
     */
    function (Controller, WebSocket, MessageToast) {
        "use strict";

        return Controller.extend("ns.HTML5Module.controller.IdeaOverview", {
            onInit: function () {
                var connection = new WebSocket("/NodeWS");
                // connection opened
                connection.attachOpen(function (oControlEvent) {
                    console.log(oControlEvent.getParameter("data"));
                });

                // server messages
                connection.attachMessage(function (oControlEvent) {
                    var oIdea = JSON.parse(oControlEvent.getParameter("data"));
                    MessageToast.show(JSON.stringify(oIdea));
                });

                // error handling
                connection.attachError(function (oControlEvent) {
                    console.log(oControlEvent.getParameter("data"));
                });

                // onConnectionClose
                connection.attachClose(function (oControlEvent) {
                    console.log(oControlEvent.getParameter("data"));
                });
            }
        });
    });

This code creates a new WebSocket and opens the connection to the WebSocketServer. This via the “/NodeWS” path, earlier configured in the “Approuter” its “xs-app.json” file. It includes an “onmessage”, “onError” and “onClose” listener as well.

Start your “Approuter” (npm run start in Approuter directory) and “Service” (cds watch in root directory). Next you create and execute the following “createIdea.http” file from your projects root-directory to create an Idea:

touch createIdea.http

Add the following request to it:

POST http://localhost:4004/catalog-idea/Ideas HTTP/1.1
Content-Type: application/json

{
    "name": "BBQ",
    "description": "With a lot of good food!"
}

You will see the “Idea” has been created successfully:

As you can see the “MessageToast” shows the created “Idea” data.

Duplicate the current tab so you have the app opened twice. You can create/add another idea and you will see that both apps receive the newly created “Idea”.

Once received you could put them in a JSON-Model and display them in the app. On initial load you could also consume your OData-service to display all the current/earlier created ideas. Or you could add an “Idea” via the app and all the clients will be notified via the WebSockets.

Wrap up

In this blog we generated a CAP-project from scratch by executing some commands. We added a WebSocketServer and WebSocketClients to our project. Once an entity inside the CAP OData Service has been created, the result will be passed to every single connected WebSocketClient (the UI5 app). This UI5 app connects to the socket and OData Service via the Approuter, which has the “WebSocket” feature enabled in its configuration file.

I found this an easy way to setup and get started with WebSockets in CAP, and I would be happy to learn more on how you guys would implement it !

Hope this helps to get started easily and quickly with WebSockets in CAP!

Best regards,

Dries Van Vaerenbergh

 

6 Comments
You must be Logged on to comment or reply to a post.
  • Hi Dries Van Vaerenbergh

    Great blog! I was also using Websockets in a CAP project and wanted to ask if you also observed following glitch.

    When testing (cds watch / run) in BAS everything worked fine but after deploying to SCP-CF the upgrade call from clients always got called twice which lead to error

    handleUpgrade() was called more than once with the same socket, possibly due to a misconfiguration

    I fixed it by a workaround storing the client's sec-websocket-key to avoid upgrading twice.. would be interesting if you also observed this?

    Best regards,

    Ben

    • Hi Benjamin Krencker,

      Thanks a lot for your feedback and interesting question!
      Also, my apologies for the late reply. 😊
      I wanted to check out if the "handleUpgrade()" gets called twice, but to be honest I ran into a problem deploying this blog's cap project to Cloud Foundry.
      When I check the logs, I see the srv-app contains an error "address 0.0.0.0:8080 already in use".
      Probably it has something to do with the place I created the WebSocket server? I thought the srv-app would run on port 4004. But maybe that is only in development and it is using port 8080 on Cloud Foundry. Could that be you think?
      I'm curious and really interested on how you deployed the CAP project with the WebSocket’s. Or did you created an extra Node module to take care of the WebSocket?

      That being said and asked, I also had a look at your question. Which I started with initially, but since the deployment issue ... 😊
      So to do a little test at least, I created a basic multi target application project, added a standalone approuter, ui5 app, and an extra Node module for the WebSocket containing the following code:

      const WebSocketServer = require('ws').Server;
      //We will create the websocket server on the port given by Cloud Foundry --> Port 8080
      const ws = new WebSocketServer({
          port: process.env.PORT || 8080
      });
      
      ws.on('connection', function (socket) {
          socket.send(JSON.stringify({
              "message": "Hi, this is the Echo-Server"
          }));
      });
      

      The most basic implementation for a quick test. Also, I did not experience any deployment issues.
      First of all, I saw the message "Hi, this is the Echo-Server" in the app and thus I checked out the network tab its "WS" tab and the call was only performed once. When I extended the app (to send messages to all clients) there was no double request to be found. (see image, all messages send via the socket in the same connection)

      Maybe it could be a specific problem indeed with CAP and WebSocket’s like you mentioned earlier.
      If you could give me a heads up on how you deployed the CAP WebSocket’s app to Cloud Foundry, I would be more than happy to have a look at it again. To see if in that case the request would be send twice as well.
      Thanks a lot for your question and insights Benjamin! Hope to hear from you soon! 😊
      Best regards,
      Dries

      /
      • Hi Dries Van Vaerenbergh

        Yes indeed in my project I followed a slightly different approach to create and upgrade Websocket server connection.

        I placed the creation of the WS-Server into server.js file of the node srv as part of the default CDS server. I did not create a separate module too in order to notify the clients after an INSERT..

        The magic is - I guess - that you need to tell the WebSocket Server to not start a new server. Since it is created as part of the CDS-server, it will use the CDS connection.

        const wss = new WebSocket.Server({ noServer: true });

        Furthermore I handle the "upgrade" event as part of the default cds server:

        cds.on('listening', (cdsserver) => {
        
          cdsserver.server.on('upgrade', function upgrade(request, socket, head) {
        
              wss.handleUpgrade(request, socket, head, function done(ws) {
                console.log("WSS HandleUpgrade ");
                wss.emit('connection', ws, request);
              });
        
            }
          });
        
          global.wss = wss;
        
        });

        Then of course you'll also need a route in xs-app.json and a Destination to consume the WebSocket from UI5 (Client).. here you can use same approach like you did in your project.

        Best regards,

        Ben