Real-Time Application using Azure WebPub Sub

In this article we will see how we can leverage on Azure Web PubSub to have real time communications between our frontend applications and our backend services.

Real-Time Application using Azure WebPub Sub
Photo by Joey Kyber / Unsplash

Today, at Watèa By Michelin more and more of our business use cases require our frontend applications to have access to real-time information.

  • To display in real-time our vehicle metrics
  • To provide real-time notifications 
  • To provide real-time customer reporting
  • To allow real-time collaborative features

To achieve this Β« real-time Β» communication, different mechanisms exist regarding your use case and maturity. Here is a short list of possibilities :

  • HTTP simple polling strategy :
    • Unidirectional communication where frontend applications regularly query an HTTP API to fetch data. This involves performing the same query at fixed intervals to mimic real-time communication.
    • πŸ‘ Pros :
      • Simple to implement.
    • πŸ‘Ž Cons :
      • Doesn't check if there's new data to query, potentially leading to resource wastage as queries are made even when unnecessary.
  • Long polling :
    • Unidirectional communication, where the client initiates an HTTP request to the server.
    • The server keeps the request open, waiting until it has data to send back. If no data are available, it waits until there is something to respond with.
    • When new data becomes available or a predefined timeout occurs, the server responds to the client.
    • Upon receiving the response, the client immediately initiates a new request to the server.
    • This cycle continues, simulating real-time communication.
    • πŸ‘ Pros:
      • This approach is easy to implement without requiring new technologies on both the client and server sides.
    • πŸ‘Ž Cons:
      • However, it's not easily scalable as the backend needs to handle a large number of concurrent connections.
  • Server Sent Event
    • Unidirectional communication (server push)
    • The client open a persistant HTTP connection to the server using the EventSource API (https://developer.mozilla.org/en-US/docs/Web/API/EventSource)
    • The server sends events to the client using text/event-stream format.
    • πŸ‘ Pros:
      • Simple to implement on the client side as EventSource API is standardized as part of HTML5
      • Supported by numerous frameworks and libraries across different programming languages.
    • πŸ‘Ž Cons:
      • Unidirectional. Limited to server-to-client communication.
  • gRPC 
    • Bi-Directional communication over HTTP/2
    • Both the client and the server are using a gRPC librairie in order to communicate.
    • The communication is done over HTTP and data is sent using Protocol Buffer format.
    • πŸ‘ Pros:
      • Provides high performance and efficiency due to its binary serialization format and the use of HTTP/2.
      • Supports multiple programming languages, making it versatile for various development environments.
      • Offers features like authentication, load balancing, and bi-directional streaming, enhancing flexibility in application design.
    • πŸ‘Ž Cons:
      • Requires both client and server to use gRPC, which may introduce compatibility issues in existing systems.
      • Protocol Buffers may have a learning curve for developers unfamiliar with the format.
      • Introduces complexity in debugging and troubleshooting compared to simpler communication protocols.
  • WebSocket
    • Bi-directional, full-duplex and stateful protocol over HTTP
    • It enables real-time, bi-directional communication between clients and servers, allowing data to be transmitted asynchronously without the overhead of HTTP request/response headers.
    • πŸ‘ Pros:
      • Facilitates real-time, bi-directional communication, making it suitable for interactive applications like chat applications, online gaming, and live data updates.
      • Offers low latency and overhead compared to traditional HTTP-based communication methods.
      • Supports a wide range of programming languages and frameworks, with WebSocket libraries available for most platforms.
    • πŸ‘Ž Cons:
      • Requires both client and server to implement WebSocket support, which may not be feasible in all scenarios.
      • As WebSockets are stateful, it makes the WebSocket servers hard to operated in large-scale systems. Having multiple WebSocket server imply to share connection state between all the servers instance

At Watèa by Michelin, we initially adopted an HTTP polling solution to quickly enter the market. However, we soon realized it was inefficient and led to unnecessary resource wastage due to redundant API calls.

To address this issue, we decided to transition to a more robust and scalable solution. While our operations mostly involved server-to-client events, we wanted to ensure that the solution was flexible and could handle bi-directional communication for future use cases.

We opted for the WebSocket solution primarily because of its extensive framework support and the manageable learning curve for our developers. To simplify the process and avoid the complexity of maintaining a WebSocket server, we decided to use a cloud-managed service.

On the cloud providers side, only AWS and Microsoft Azure offer this kind of service:

On the SaaS Solutions side we identified Ably Pub/Sub channels.

Since the rest of our infrastructure was already on Microsoft Azure, we chose to go with Microsoft Azure Web PubSub.

Azure Web PubSub to the rescue

Azure Web PubSub is a fully managed service provided by Microsoft Azure that enables real-time bi-directional communication between clients and serverless applications. It allows developers to easily build interactive web applications, chat rooms, live data dashboards, and other real-time scenarios without managing infrastructure.

Azure Web PubSub will act as a layer between our frontend applications and our backend systems, managing for us all the scalability, high availability aspects. 

It also provides other capabilities like authentication, authorization, monitoring (alerting + metrics).

Let's start by creating a new Web PubSub Service in the Azure Portal.

Actually 3 pricing tiers are available : Free, Standard, Premium.

These tiers are differentiated by the number of concurrent connections, messages and maximum units allowed (1 unit = 1000 concurrent connections / 2M message per day)

The Premium tiers will provide a 99,95% SLA, availability zone support, auto-scaling and possibility to define a custom domain name.

Pricing and options are available here : https://azure.microsoft.com/en-us/pricing/details/web-pubsub/


In Azure Web PubSub, the concepts of hub, group, and user are fundamental for organizing and managing real-time communication channels within applications

  • A Hub serves as a central communication endpoint where clients connect to send and receive messages. It acts as a logical container for organizing communication contexts.
  • Within a hub, groups can be created to group clients with similar interests or purposes, enabling targeted message delivery.
  • Users, on the other hand, represent individual clients connected to the hub. They can belong to one or more groups within the hub, facilitating tailored communication based on specific user interests or roles

Together, these concepts provide a flexible approach to target the right audiences.

Let's create a simple frontend and backend application to see how we can interact with Azure Web PubSub.

  • The frontend application will be a Single Page Application using VueJS
  • The backend will be a Java application built on top of the Quarkus framework.

The Frontend application will first authenticate to the backend service to obtain the Web PubSub connection string.

1 - We start by creating the backend application.

mvn io.quarkus.platform:quarkus-maven-plugin:3.11.1:create \
    -DprojectGroupId=com.watea \
    -DprojectArtifactId=websocket \
    -Dextensions='rest'
cd websocket

2 - Add the needed dependencies in the pom.xml

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>

<dependency>
  <groupId>com.azure</groupId>
  <artifactId>azure-messaging-webpubsub</artifactId>
  <version>1.2.8</version>
</dependency>

We will start by creating the authentication mechanism. Our backend needs to expose an API for a client to retrieve the WebSocket url with a dedicated access token crafted regarding their permissions.

We create a dedicated service to handle the authentication and communication with Azure Web PubSub

package com.watea.service;

...

@ApplicationScoped
public class WebPubSubService {

    private static final Logger LOG = Logger.getLogger(WebPubSubService.class);

    @ConfigProperty(name = "webpubsub.connection-string")
    String connectionString;

    public WebPubSubServiceClient webPubSubServiceClient;

    void startup(@Observes StartupEvent event) {
        this.webPubSubServiceClient = new WebPubSubServiceClientBuilder()
                .connectionString(connectionString)
                .hub("portal")
                .buildClient();
    }
    
    public Optional<WebPubSubClientAccessToken> getClientAccessToken(String userId, List<String> groups, List<String> roles) {
        Objects.requireNonNull(userId);

        try {
            GetClientAccessTokenOptions options = new GetClientAccessTokenOptions()
                    .setUserId(userId)
                    .setGroups(groups)
                    .setRoles(roles)
                    .setExpiresAfter(Duration.of(60, ChronoUnit.MINUTES));

            return Optional.of(this.webPubSubServiceClient.getClientAccessToken(options));
        } catch (Exception e) {
            LOG.error("Error while retrieving ClientAccessToken", e);
        }

        return Optional.empty();
    }

We retrieve the Web PubSub connection string from the configuration property webpubsub.connection-string and connect to a Hub named portal when the WebPubSubService is instantiated.

All messages sent/received will be restricted to this specific Hub.

getClientAccessToken method retrieves from Web PubSub a Client Access Token for a given user identifier, list of groups, and roles (permissions on groups)

Roles represent permissions for a user on specific groups. Currently, two permissions exist:

joinLeaveGroup: allow the client to consume messages from a group.
sendToGroup: allow the client to publish messages to a group.

Roles are strings formatted as : webpubsub.<permission>.<group> (ex: webpubsub.joinLeaveGroup.mygroup)

The Client Access Token is composed of :

  • the access token
  • the websocket url the client can connect to with the access token as parameter (ex : wss://watea-poc.webpubsub.azure.com/client/hubs/portal?access_token=xxxx)

The Access Token is encoded using HS256 algorithm and has the following structure (it can be easily debugged using JWT.io)

{
  "aud": "https://watea-poc.webpubsub.azure.com/client/hubs/portal",
  "sub": "8eb86bd8-639b-4a2c-b17d-85dfc03297a4", // user id
  "role": [
    "webpubsub.joinLeaveGroup.customer1" // list of permissions
  ],
  "webpubsub.group": [  // list of groups
    "customer1"
  ],
  "exp": 1717680142  // expiration time
}

We can now expose an API to allow a client to negotiate and retrieve this token:

package com.watea;

import com.azure.messaging.webpubsub.models.WebPubSubClientAccessToken;
import com.azure.messaging.webpubsub.models.WebPubSubPermission;
import com.watea.service.WebPubSubService;
import io.vertx.core.json.JsonObject;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.jboss.resteasy.reactive.RestQuery;

import java.util.List;
import java.util.Optional;

@Path("/auth")
public class AuthResource {
    @Inject
    WebPubSubService webPubSubService;

    @GET
    @RolesAllowed("user")
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    @Path("/negotiate")
    public Response negotiate(@RestQuery String userId) {
        
        // we get the user customer identifier
        var customerId = securityContext.getUserPrincipal().getCustomerId();
        var notificationGroup = customerId + "-notifications";

        // we allow the user to join the notification group for its customer
        List<String> groups = List.of(notificationGroup);

        // we also restrict the right of the user to only join and leave this group.
        // the access url generated will not allow to send any message in this group for this client
        List<String> roles = List.of("webpubsub." + WebPubSubPermission.JOIN_LEAVE_GROUP + notificationGroup);

        final Optional<WebPubSubClientAccessToken> accessToken = webPubSubService.getClientAccessToken(userId, groups, roles);

        if (accessToken.isPresent()) {
            JsonObject response = new JsonObject().put("url", accessToken.get().getUrl());
            return Response.ok().entity(response).build();
        } else {
            return Response.serverError().build();
        }
    }
}

Assuming this resource is protected (@RoleAllowed) and the user authenticated, we define here what groups the client will have access to, with which permissions.

In this example, we will allow the client to consume messages from a notification group dedicated to its customer. The API will give the user the WebSocket URI (wss://) with the access token the client needs to connect to the Web PubSub service.

On the frontend side, we will create a dedicated VueJS Component named `EventViewer` to interact with our backend application.

<script lang="ts">
import axios from 'axios'
import { v4 as uuidv4 } from 'uuid';

export default {
    name: 'EventViewer',
    props: {
      group: String
    },
    data() {
        return {
            userId: uuidv4(),
            wsUrl: null as any,
            connectionError: false,
            wsConnection: null as any,
            events: [],
        }
    },
    mounted() {
        axios
            .get('http://localhost:8080/auth/negotiate?userId='+this.userId) // call backend ws auth
            .then(response => {
                this.wsUrl = response.data.url
                this.connectWebSocket(this.wsUrl)
            })
            .catch(error => {
                this.connectionError = true
            })
    },
    methods: {
        connectWebSocket(url: string) {
            var vm = this;
            this.wsConnection = new WebSocket(url, ' 'json.webpubsub.azure.v1'');
            this.wsConnection.onopen = (event) => {
              console.log("connected")
            };
        }
    }
}
</script>

When the component is mounted we use axios to retrieve the WebSocket URI and we connect to Web PubSub using the native WebSocket API.

We are using here the json.webpubsub.azure.v1 WebSocket subprotocol. This subprotocol provide a JSON contract interface for :

We can check if the WebSocket connection is open by adding an `onopen` event handler.

Server to Client events

Now, if we want to dispatch messages from our backend app to our client we can simply add a few methods to our WebPubSubService class

@ApplicationScoped
public class WebPubSubService {

...

    public void sendToAll(JsonObject payload) {
        this.webPubSubServiceClient.sendToAll(payload.toString(), WebPubSubContentType.APPLICATION_JSON);
    }

    public void sendToGroup(String group, JsonObject payload) {
        this.webPubSubServiceClient.sendToGroup(group, payload.toString(), WebPubSubContentType.APPLICATION_JSON);
    }

    public void sendToUser(String userId, JsonObject payload) {
        this.webPubSubServiceClient.sendToUser(userId, payload.toString(), WebPubSubContentType.APPLICATION_JSON);
    }
    
    ...
}

We are now able to dispatch JSON messages to all clients connected to the hub, a specific group, or a specific client user.

On our `EventViewer` component we just need to add an `onmessage` event handler to start consuming all messages this client is concerned about.

// EventViewer.vue
...
export default {
    name: 'EventViewer',
    data() {
        return {
            userId: uuidv4(),
            wsUrl: null as any,
            connectionError: false,
            wsConnection: null as any,
            events: [],
        }
    },
    mounted() {
        ...
    },
    methods: {
        connectWebSocket(url: string) {
            var vm = this;
            this.wsConnection = new WebSocket(url);

            this.wsConnection.onmessage = function (event: any) {
                console.log(event);
                vm.events.push(event.data)
            }
        }
    }
}

<template>
    <div  v-for="event in events">
        {{ event }}
    </div>
</template>

We store all received events in an events array and simply display them. Pretty simple!

Because we are using the json.webpubsub.azure.v1 subprotocol, we will receive a connected event when the websocket is connected.

{"type":"system","event":"connected","userId":"49620d56-a92e-4178-9c02-f12e360e577c","connectionId":"7rCJJyZG9l_ZZMSRrQKONgyyfj7wd02"}

Client to Server events

As we saw previously, WebSocket is a bidirectional protocol. It is now time to make our frontend application talk with our backend one.

Azure Web PubSub gives us the possibility to trap activity happening client-side sides like connectivity events (a client connect / disconnect to the service) or a client sending custom events.

It can be done in Web PubSub by configuring :

  • One or multiple Event Handler (webhook) that will forward all or specific events
  • One or multiple Event Listener that will send all or specific events to an an Azure EventHub. Your backend system should then consume the Azure Event Hub in order to process the events. (For now only EventHub is supported...)

Note that messages sent to group will not be forwarded to the EventHandler only custom events will be.

We will use the first option and add an event handler endpoint on our backend to collect events from the Web PubSub webhook.

@Path("eventhandler")
public class EventResource {

    private static final Logger LOG = Logger.getLogger(AuthResource.class);
    
    @OPTIONS
    public Response options() {
        return Response.ok().header("WebHook-Allowed-Origin", "*").build();
    }

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_JSON)
    public Response events(JsonObject payload) {
        LOG.info("received -" + payload);
        
        // code here the logic to handle messages regarding the message header.
        
        return Response.ok().build();
    }
}

We define a new POST /eventhandler route. We also need the OPTIONS method on this endpoint to respond with a specific `WebHook-Allowed-Origin` HTTP header and start receiving events.

On Azure Web PubSub we simply configure a new event handler for the portal hub. Settings > Add (Configure Event Handler)

We will receive all connectivity and custom user events.

If you are developing locally, you will need to expose your backend over internet to start receiving event. You can use awps-tunnel to help !

It is now time to send some custom events from our clients.

// EventViewer.vue
...

export default {
    name: 'EventViewer',
    props: {
      group: String
    },
    data() {
        return {
            userId: uuidv4(),
            wsUrl: null as any,
            connectionError: false,
            wsConnection: null as any,
            events: [],
            message: ''
        }
    },
    mounted() {
        ...
    },
    methods: {
        ...
       
        send() {          
          var payload = {
              "type": "event",
              "event": "my-custom-event",
              "dataType" : "json",
              data: {
                  "message": this.message
              }
          }
          this.wsConnection.send(JSON.stringify(payload))
        }
    }
}

<template>
  ...
  <textarea id="message" v-model="message"></textarea>
  <button @click="send">
    <i class="material-icons">send</i>
  </button>
  ...
</template>

We just have to create a send method that will use the WebSocket connection to send an event named my-custom-event.

Acknowledgment mechanism is also available if needed using "ackId": uint64 Number. If not specified, the event will be delivered in a "fire & forget" mode.

On the backend side we should see the following log displayed !

2024-06-11 17:17:17,591 INFO  [com.wat.EventResource] (executor-thread-3) received -{"message":"my custom message"}

CONCLUSION

In conclusion, Azure Web PubSub provides a powerful solution for scaling real-time web applications by removing the need to manage active WebSocket sessions. This is particularly beneficial in large architectures with multiple instances of the same microservice that need to share these sessions.

Additionally, Azure Web PubSub helps decouple application architecture, enabling each component to operate independently and communicate efficiently.

In a microservice architecture, this decoupling allows individual microservices to publish and subscribe to events within their own domains, enhancing modularity and facilitating a more resilient and maintainable system.

However, it’s important to note that choosing the appropriate granularity for hubs and especially groups can be the most challenging aspect of implementing Azure Web PubSub.

We chose to have the following approach :

  • 1 Hub per frontend / mobile application
  • 1 Group for each customer and feature based on a unique customer identifier and the feature name
    • <customer-id>.<feature-name> : 866f3c1e-1b7d-4f72-8941-9527248a8473.notifications / 866f3c1e-1b7d-4f72-8941-9527248a8473.vehicles...
    • This approach reduces coupling. In our microservice architecture, the microservice responsible of customer notifications management will be tasked with dispatching events in the customer-id.notifications group. On the frontend side the WebComponent responsible for displaying notifications to the client will consume the appropriate notification group for the connected user.

The group granularity is heavily context dependent and linked to your frontend architecture. You may not have the same group definition between a monolithic frontend application architecture, where a single WebSocket consumer can handle all event types, and a micro frontend architecture were you want each component to consume its own domain events.