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
Great Article Dries Van Vaerenbergh ! Keep sharing ...
Hi Yogananda,
Thanks for the feedback! Definitely will do! 🙂
Best regards,
Dries
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
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:
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.
Furthermore I handle the "upgrade" event as part of the default cds server:
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
Hi Benjamin,
Thanks a lot for sharing your implementation! I really appreciate it!
I'm sure this will fix my deployment issue as well.
Best regards,
Dries
Hello Benjamin
Thanks for your post. I find myself struggling implementing the websocket on my CAP project, though. Where is the socket:
... being created?
My coding looks something like this:
Hi Chris
You put this in the server.js of the srv folder:
Here you'll find an example project where websockets are used:
fs-demo-2022/server.js at main · RizInno/fs-demo-2022 (github.com)
Best regards,
Ben
Hello Benjamin
This looks good, thanks for the hint! I will try this tonight.
Kind regards
Chris
***EDIT***
This was in the blog since the beginning but I didn't see it at first. Maybe it's what you're missing just like me
***
I found this blog this morning and succeed to made it work with a CAP backend and a Frontend UI5 app deployed in a standalone launchpad.
One small trick among all the others that are legit is to add 'websockets.enabled = true' as follow in xs-app.json.