Azure Web PubSub Example

Azure Web PubSub Example
Azure Web PubSub provides a continuous channel that lets data flow to the requestor; much like a water tap

Azure Web PubSub is a way to open up a websocket between the user client and a resource server to continuously pass data through it. The client (or even multiple clients) does not need to repeatedly send HTTP requests to the server to receive data or repeatedly poll the server for status updates.

The following is a short example that shows a client web page that opens up a websocket to a known Azure Web PubSub service instance, whereby images are served back to the client continuously in response to a Timer triggered Function.

💡
The following is a heavy modification of the common 'Azure Web PubSub Notification' sample, and is run locally for clarity.

Some prerequisites:

  • A free tier Azure Web PubSub Service in Azure
  • Visual Studio plus a set of test jpg Images under 1MB each, added to the root of the project and all configured to Copy to the Output Directory upon Build: In Visual Studio, this can be done from setting the image's Advanced Setting for Copy to Output Directory to Copy if newer;
In VS, click each image and set the Build Action to copy to the output directory to make them available to read when required

To be able to interact with our websocket, we can create an HTML page called index.html that will render the data we are looking for, and this HTML file too should have a Build Action of 'Copy if newer' in Visual studio:

<html>
<body>
    <h1>Azure Web PubSub Image DataGrid</h1>
    <style>
        * {
            box-sizing: border-box;
        }

        .column {
            float: left;
            width: 33.33%;
            padding: 10px;
        }

    </style>

    <div class="row">
        <div class="column" id="col0"></div>
        <div class="column" id="col1"></div>
        <div class="column" id="col2"></div>
    </div>

    <script>

        (async function () {
            //call to the function 'negotiate' to connect first
            let res = await fetch(`${window.location.origin}/api/negotiate`);
            let url = await res.json();
            
            //open socket and wait for any messages to arrive.
            //messages will come from the FetchImage Azure Function discussed later
            var uniSocket = new WebSocket(url.url);

            uniSocket.onmessage = event => {

                try {
                    //convert incoming Stream message to base64 data
                    //clean up base64 string and use it as the img src
                    var reader = new FileReader();
                    reader.readAsDataURL(event.data);
                    reader.onloadend = function () {
                        var base64data =
                        reader.result.replace("data:application/octet-stream;base64,", "");

                        var newImg = document.createElement("img");
                        newImg.setAttribute("src", 'data:image/jpeg;base64,' + base64data);
                        newImg.setAttribute("height", "245");
                        newImg.setAttribute("width", "300");
                        newImg.setAttribute("style",
                                            "border-radius:25px");
                        
                        //choose a random column to display new image on
                        var section = Math.floor(Math.random() * 3);
                        document.getElementById("col" + section).appendChild(newImg);
                    }

                } catch (e) {
                    alert("Nooo, something went wrong!! " + e)
                }

            };
        })();

    </script>

</body>
</html>
index.html page for the client to render

To be able to see this index.html file, we can create an HTTP triggered Function that will read the file from the localhost server like so:

public static class index
{
    [FunctionName("index")]
    public static IActionResult Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req,
    ILogger log)
    {
        string indexFile = "index.html";
        if (Environment.GetEnvironmentVariable("HOME") != null)
        {  //read index.html from current server environment
        	indexFile = Path.Join(Environment.GetEnvironmentVariable("HOME"),
            "site", "wwwroot", indexFile);
        }
        log.LogInformation($"index.html path: {indexFile}.");
        
        //give the client the html to view as the response when
        //this function is called
        return new ContentResult
        {
        	Content = File.ReadAllText(indexFile),
        	ContentType = "text/html",
        };
    }
}
index Http Function for loading the index.html file

Next, add the following HTTP triggered Function (called negotiate) as below. This function gets called from the index.html page when that html file renders and this attempts to connect to the Web PubSub instance:

using Microsoft.Azure.WebJobs.Extensions.WebPubSub;
public static class negotiate
{
    [FunctionName("negotiate")]
    public static WebPubSubConnection Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)]
    HttpRequest req,
    [WebPubSubConnection(Hub = "gridDataHub")] WebPubSubConnection connection,
    ILogger log)
    {
    	log.LogInformation("Connecting...");

    	return connection;
    }
}
Negotiate Http Function for connecting to the Azure Web PubSub service instance in Azure, called from the index.html file

And for connecting to the Web PubSub instance in Azure, go to the local.setting.json file, add the key "WebPubSubConnectionString" and the value will be the connection string for the Azure Web PubSub instance (connection string is found in the Web PubSub instance's 'Keys' detail menu in Azure).

💡
Wait but how??.. The WebPubSubConnection Input binding will look for a "WebPubSubConnectionString" configuration key and its value for a connection string 

To providing images continuously over the websocket back to the client when the index.html loads, we can use a Timer Trigger Function (called FetchImage) that will execute every 10 or 15 seconds for example. Here, an image is picked out from the image set (that live on the localhost server) and written to the websocket as a Stream:

public static class FetchImage
{
    [FunctionName("FetchImage")]
    public static async Task Run([TimerTrigger("*/15 * * * * *")] TimerInfo myTimer, 
    ILogger log, [WebPubSub(Hub = "gridDataHub")] IAsyncCollector<WebPubSubAction> actions)
    {   
        //randomly pick a test image from a set of 52 images
        Random rand = new Random();
        int number = rand.Next(1, 52);
        string jpgFile = $"test{number}.jpg";

        if (Environment.GetEnvironmentVariable("HOME") != null)
        {
            jpgFile = Path.Join(Environment.GetEnvironmentVariable("HOME"),
            "site", "wwwroot", jpgFile);
        }
        
        //write data stream to the websocket (data must be less than 1MB)
        await actions.AddAsync(new SendToAllAction
        {
            Data = BinaryData.FromStream(File.OpenRead(jpgFile)),
            DataType = WebPubSubDataType.Binary
        });
    }
}
FetchImage Function for writing streams on the websocket back to the client

Back on the index.html page, these Streams are received and are converted to base64 strings which are used as the image sources, which the client browser interprets as images that we can see!

Example Run!!

To run the output we need to go to the HTTP function that renders the index.html. After starting a debug session in a local environment, this will be at http://locahost:{YOUR_PORT_NUMBER}/api/index

Here is a video of what would happen once running  (All test source image Credits: Jimmy Chan on Pexels):

0:00
/
Video of images served up to the client via the Azure Web PubSub websocket!!

Conclusions and final thoughts

I think the Azure Web PubService is extremely useful where there might be a need to reduce complexity and ease burden on the client-side by reducing the need to use AJAX calls for example. In my example I use it for a novel use case of generating randomly ordered photo grid where the image source url is virtually hidden from the client with the use of base64 strings (Privacy bonus for the serverless service perhaps!! 🤔).

While the above may be true, I should point out that transmitting heavier data such as images like this is not really the intended usage scenario as the message payload size is rather small on the Free Tier (appears to be 1MB from what I've been able to test), and the data quota per day on the Free Tier is 40 000Kb. Azure Web PubSub is more suited for passing much smaller messages and status updates in real time to multiple clients, automatically. Always fun testing though 😎  

And finally, for production, we should not use Anonymously available Azure Functions as discussed in this exploratory example, use Function level access or place Functions behind APIM instances!!

Cover Image by Luis Tosta on Pexels