DEV Community

S. Olusegun Ojo
S. Olusegun Ojo

Posted on • Edited on

Creating Real-Time Applications with SignalR

Introduction

In this post, I will use a simple example to show how SignalR can be used to create real-time applications. First, we will look at what SignalR is and why it exists, we will also see possible uses of SignalR. Then we will make a simple collaborative whiteboard application using SignalR. This application will enable a number of users to draw on a canvas and have the same drawings displayed for all connected users.

Remote work has grown in recent years

As the world grows increasingly remote, a greater number of applications need to consider online collaboration, many times, requiring real-time systems to achieve near-instantaneous communication to achieve similar results as would be achieved by in-person communication. Traditional communication over the web involves a client making a request to a server and the server returning a response. This means that the client can only receive a response when it makes a request. Some applications, however, require consistent, real-time updates. These include resource usage dashboards, chat applications, warehouse applications, security systems, theft-detection systems, weather monitoring and alert systems, stock trading applications, gaming software, smart home systems, other IoT applications and other apps that use notifications.

Techniques for Real-time Applications

A solution to this is HTTP polling, where the client makes requests at regular intervals to fetch updates from the server. This requires the creation of a new connection every time the client makes a request. This is unsuitable for some applications, such as security systems or medical monitoring systems, where notifications have to be delivered almost instantaneously.

In polling, the client has to make a request before receiving server updates

An alternative is long polling, where the client maintains a connection for a bit longer than a regular request, leaving a wider time window for the server to respond with updates. This is an improvement on regular polling but still may not be suitable for some applications.

Websockets provides a way for the client and server to maintain a persistent, bidirectional connection. This means that the server can send information to the client instantaneously, without the client having to make a new request. SignalR is a .NET library that implements Websockets to ensure that real-time communication can be implemented conveniently without worrying about the underlying technology. In addition to this, SignalR implements regular HTTP polling for clients that do not support websockets. This provides resilience and wide support.

In polling, the client has to make a request before receiving server updates

SignalR libraries exist for several clients including Javascript (for web clients), Java, .NET, Swift, and C++. The server side works on ASP.NET Core.

Furthermore, SignalR is very resilient, working even where the client does not support websockets by falling back to server-sent requests or long polling. It can also be deployed to Azure using Azure SignalR Service, which provides automatic scaling based on demand.

Demo Project: Simple Collaborative Whiteboard

To demonstrate SignalR, we will create a simple whiteboard application using Javascript canvas. We will create it using ASP.NET Core.

To begin, create a new ASP.NET Core application.

Here are the critical files:

Program.cs

using Microsoft.AspNetCore.SignalR;
using SignalRDemo;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddCors(options => // to add Cross-Origin Resource Sharing, especially if your frontend is in a different location
{
    options.AddPolicy("CorsPolicy", builder =>
    {
        builder.AllowAnyMethod()
        .AllowAnyHeader()
        .WithOrigins("https://localhost")
        .AllowCredentials();
    });
});

builder.Services.AddSignalR(options => // activate SignalR
{
    options.EnableDetailedErrors = true; // Detailed errors on the frontend for debugging. Remove this when deploying.
});

builder.Services.AddControllers();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.UseCors("CorsPolicy");

app.UseEndpoints(endpoints =>
{
    endpoints.MapHub<WhiteboardHub>("/whiteboardHub"); // The SignalR Hub
    endpoints.MapControllers();
});

app.MapRazorPages();

app.Run();

Enter fullscreen mode Exit fullscreen mode

In Program.cs, we configure SignalR. I also left Razor Pages configured as I am hosting the frontend from within the same ASP.NET Core application.

WhiteboardHub.cs

using Microsoft.AspNetCore.SignalR;
using System.Threading.Tasks;

namespace SignalRDemo
{
    public class WhiteboardHub : Hub
    {
        // the SendDraw action is to be invoked by the client when the user draws in the canvas
        // it receives data of type DrawData which must conform to the data being sent by the client
        public async Task SendDraw(DrawData data)
        {
            try
            {
                await Clients.Others.SendAsync("ReceiveDraw", data); // this invokes the ReceiveDraw action on all the connected clients.
                // Console.WriteLine("Drawn: {0}, {1}", data.X, data.Y);
            }
            catch(Exception ex)
            {
                Console.Error.WriteLine($"Error in SendDraw: {ex.Message}");
            }
        }
    }

    // the data from the client is an object containing two doubles referring to the [relative] coordinates being drawn on on the canvas
    public class DrawData
    {
        public double X { get; set; }
        public double Y { get; set; }
    }
}

Enter fullscreen mode Exit fullscreen mode

Here, the SignalR hub is defined. This is the component on the server responsible for communicating and managing the clients.

Index.cshtml

@page
@model IndexModel

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Collaborative Whiteboard</title>
    <style>
        #whiteboard {
            border: 1px solid #000;
        }
    </style>
</head>

    <canvas id="whiteboard" width="800" height="600"></canvas>
    <script type="text/javascript" src="~/js/jquery-3.7.1.min.js"></script>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/5.0.5/signalr.min.js"></script>
    <script type="text/javascript" src="~/js/app.js"></script>
Enter fullscreen mode Exit fullscreen mode

On this page, we linked to the canvas and the SignalR library. Finally, I included app.js, the script where our application's behaviour and SignalR code will be.

app.js


document.addEventListener("DOMContentLoaded", function () {
    const canvas = document.getElementById("whiteboard"); // fetch the canvas
    const context = canvas.getContext("2d"); // the context for drawing on the whiteboard

    // connect to SignalR hub
    const connection = new signalR.HubConnectionBuilder()
        .withUrl("/whiteboardHub") // this is what was configured in Program.cs
        .configureLogging(signalR.LogLevel.Information) // logging settings can be modified to help with debugging
        .build();

    connection.start() // connection to SignalR Hub
        .then(() => console.log("Connected to Hub"))
        .catch(err => console.error(err.toString()));

  // drawing actions and behaviour
    let isDrawing = false;
    canvas.addEventListener("mousedown", startDrawing);
    canvas.addEventListener("mousemove", mouseDraw);
    canvas.addEventListener("mouseup", stopDrawing);
    canvas.addEventListener("mouseout", stopDrawing);

  // the ReceiveDraw action invoked by the SignalR Hub in WhiteboardHub.cs
    connection.on("ReceiveDraw", data => {
        // const event = new MouseEvent("mousedown", { clientX: data.x, clientY: data.y });
        remoteDraw(data); // the function to be called to draw the received canvas coordinates on the client canvas
    });

  // function to draw when the mouse is clicked
    function startDrawing(event) {
        isDrawing = true;
        mouseDraw(event);
    }

  // function to draw received canvas coordinates sent from SignalR
    function remoteDraw(data) {
        draw(data.x, data.y);
    }

  // function to render the client's drawings on the canvas and send to the SignalR hub
    function mouseDraw(event) {
        if (!isDrawing) return; // only draw when mouse is clicked

        // get relative x and y coordinates for canvas
        const x = event.clientX - canvas.getBoundingClientRect().left;
        const y = event.clientY - canvas.getBoundingClientRect().top;

        draw(x, y);

        // send coordinates to SignalR Hub
        connection.invoke("SendDraw", { x, y })
            .catch(err => console.error(err.toString()));
    }

  // function to draw on the canvas
    function draw(x, y) {
        // drawwwwwwwwwww
        context.beginPath();
        context.arc(x, y, 2, 0, 2 * Math.PI);
        context.fillStyle = "#000";
        context.fill();
        context.stroke();
        context.closePath();
    }

  // called on mouseup to stop drawing on the canvas
    function stopDrawing() {
        isDrawing = false;
    }
})
Enter fullscreen mode Exit fullscreen mode

This file contains the main functionality on the frontend. It instantiates a HubConnection using the SignalR library which must be included. The app, in the mouseDraw function, reads the coordinates of the points on the canvas on which the user draws, rendering these on the canvas and sending them to the SignalR Hub by invoking the Hub's SendDraw method.

Notice that the data sent { x, y } is identical to the DrawData class defined in WhiteboardHub.cs.

In addition, the app awaits the ReceiveDraw invocation from the SignalR Hub. When this is invoked, the received coordinates are also rendered on the canvas.

Demo

Here is the application in action:

demonstrating collaborative whiteboard made using SignalR

In the console, we see the Websockets connection made to the Hub:

[2023-12-13T11:35:31.341Z] Information: Normalizing '/whiteboardHub' to 'https://localhost:7157/whiteboardHub'.
signalr.min.js:1 [2023-12-13T11:35:31.402Z] Information: WebSocket connected to wss://localhost:7157/whiteboardHub?id=Z9PXu0pKwlGorkavnlhpfa.
signalr.min.js:1 [2023-12-13T11:35:31.402Z] Information: Using HubProtocol 'json'.
app.js:13 Connected to Hub
Enter fullscreen mode Exit fullscreen mode

To test over the internet, I made the demo application accessible via the web using ngrok and here are the results.

Testing collaborative whiteboard, viewing in mobile browser

Conclusion

In this post, we examined, using a simple demo whiteboard application, SignalR's functionality, giving a basic overview of SignalR's use and outlining possible applications of the library. SignalR provides more functionality that can help you group clients, send results to specific clients, etc. For more, visit the official documentation at https://learn.microsoft.com/en-gb/aspnet/core/signalr/

The code for this demo project is available on GitHub at https://github.com/shegzee/SignalRDemo-Whiteboard

(This post was originally posted on LinkedIn)

Top comments (0)