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.
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.
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.
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();
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; }
}
}
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>
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;
}
})
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:
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
To test over the internet, I made the demo application accessible via the web using ngrok and here are the results.
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)