RealTimeWebChatApps-870x220

We explore SignalR server/client technologies to build chat applications for the web.

In this day and age, software systems have an undeniable need for real-time communications. Thankfully despite the hurdles, there are several techniques to enable real-time communications and polished frameworks that do much of the heavy lifting for developers. However, it is 2020, and software isn't siloed to specific platforms. As developers try to bring on newer platforms/devices to connect in real time, flexibility is key for technology stacks.

Let's pick one of the most popular real-time communication frameworks to dig into — the beloved SignalR on top of ASP.NET Core. And just like every human for the past million years, let's use chat apps as our playground. In a way, chat applications are the "Hello world" examples to prove real-time actually works. But let's use some fun APIs and try to build the same app across variety of platforms to see how SignalR caters to modern cross-platform needs, starting with web clients. And a little sprinkle of some polished UI never hurt good ole chat apps. Let's go.

SignalR

SignalR facilitates adding real-time communication to web applications running on ASP.NET and connected clients across wide variety of platforms. While SignalR started years back with ASP.NET MVC, the latest reboot is called SignalR Core, which runs on ASP.NET Core and brings a ton of maturity. Developers benefit from SignalR providing a uniform API canvas for connection and client management, as well as scaling to handle increased traffic.

SignalR provides APIs for bidirectional remote procedure calls (RPCs) between server and client and abstracts away real-time communication complexities. This is, by far, the biggest benefit of bringing in SignalR to facilitate real time communications — shield developers from having to deal with network layer implementations. Given a specific server/client pair, SignalR expertly chooses the best transport mechanism for real-time exchanges. The most common techniques used are: WebSockets, Server-Sent Events and Long Polling, in order of gracious fall-back plans.

SignalR Back End

SignalR uses the concept of hubs on the server side — a literal hub in a hub-spoke model. The SignalR Hub works as an in-memory object on the server side that all clients connect up to for real time communications. The hub allows SignalR to send and receive messages across machine boundaries, thus allowing clients to call methods on the server and vice versa. In addition to method invocation across programming paradigms, hubs allows transport of named and strongly typed parameters. SignalR automatically provides object serialization/deserialization to aid the process.

Let's build a quick demo chat application with a SignalR backend. Yes, yet another chat app — but demonstrating quick developer tricks and taking the chat app cross-platform. First, we have to begin with our server-side SignalR Hub. We'll start with a new ASP.NET Core Web Application running on .NET Core 3.0 — you can do this in VS on Windows, VS Code, VS for Mac or plain old CLI commands. We'll define our ChatHub on the server-side like below. If you envision having multiple hubs, it may make sense to organize them in a hubs folder:

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

namespace SignalRChat.Hubs
{
    public class ChatHub : Hub
    {
        public async Task SendMessage(string user, string message)
        {
            await Clients.All.SendAsync("ReceiveMessage", user, message);
        }
    }
}

Essentially, we're defining a method called SendMessage() on the server that all connected clients can call and send in parameter values. When invoked, this server-side method turns around and tries to call ReceiveMessage() method on clients — in particular, every connected client through Clients.All.SendAsync(). The client-side methods would need to be defined in each client across platforms. Next up is a little bit of configuration in the ASP.NET Core app's Startup.cs file - essentially adding a middleware to route appropriate requests to the SignalR Hub, like so:

using SignalRChat.Hubs;

namespace SignalRChat
{
    public class Startup
    {
        ...

        public void ConfigureServices(IServiceCollection services)
        {
            ...
            services.AddSignalR();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
              ...

            app.UseEndpoints(endpoints =>
            {
                ...
                endpoints.MapHub<ChatHub>("/chatHub");
            });
        }
    }
}

Web Client

With our SignalR server side backend all squared up, we'll begin with clients across various platforms, starting with web. While the SignalR bits to power hubs server side is included in ASP.NET Core 3.0 shared framework, the client side packages need to be brought in manually. Sure NuGet packages can be brought into projects, but a smarter way is to use Library Manager to get the client side dependencies through Unpkg. This ensures the SignalR JS files are being delivered through the Unpkg Content Delivery Network (CDN) — close to anywhere in the world where your users may be. Two CLI commands do the trick, adding the dependencies and moving them into the static files repository:

dotnet tool install -g Microsoft.Web.LibraryManager.Cli

libman install @microsoft/signalr@latest -p unpkg -d wwwroot/js/signalr --files dist/browser/signalr.js --files dist/browser/signalr.min.js

Once done, you should see the static dependencies brought in, as well as, a libman.json file which provides details of provider, packages and destination, like so:

{
  "version": "1.0",
  "defaultProvider": "unpkg",
  "libraries": [
    {
      "library": "@microsoft/signalr@latest",
      "destination": "wwwroot/js/signalr",
      "files": [
        "dist/browser/signalr.js",
        "dist/browser/signalr.min.js"
      ]
    }
  ]
}

Now, let's write some boilerplate code, like in the SignalR docs, to have some UI for our SignalR-powered chat frontend. We essentially need a couple of input controls for chat, and a list to show to display chat messages. This goes in the ASP.NET site's Index.cshtml file in Pages directory:

@page
    <div class="container">
        <div class="row">&nbsp;</div>
        <div class="row">
            <div class="col-2">User</div>
            <div class="col-4"><input type="text" id="userInput" /></div>
        </div>
        <div class="row">
            <div class="col-2">Message</div>
            <div class="col-4"><input type="text" id="messageInput" /></div>
        </div>
        <div class="row">&nbsp;</div>
        <div class="row">
            <div class="col-6">
                <input type="button" id="sendButton" value="Send Message" />
            </div>
        </div>
    </div>
    <div class="row">
        <div class="col-12">
            <hr />
        </div>
    </div>
    <div class="row">
        <div class="col-6">
            <ul id="messagesList"></ul>
        </div>
    </div>
<script src="~/js/signalr/dist/browser/signalr.js"></script>
<script src="~/js/chat.js"></script>

And now we'll add the JavaScript code to power web clients to talk to our SignalR Hub. This could go in a chat.js file:

"use strict";

var connection = new signalR.HubConnectionBuilder().withUrl("/chatHub").build();

document.getElementById("sendButton").disabled = true;

connection.on("ReceiveMessage", function (user, message) {
    var msg = message.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
    var encodedMsg = user + " says " + msg;
    var li = document.createElement("li");
    li.textContent = encodedMsg;
    document.getElementById("messagesList").appendChild(li);
});

connection.start().then(function () {
    document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
    return console.error(err.toString());
});

document.getElementById("sendButton").addEventListener("click", function (event) {
    var user = document.getElementById("userInput").value;
    var message = document.getElementById("messageInput").value;
    connection.invoke("SendMessage", user, message).catch(function (err) {
        return console.error(err.toString());
    });
    event.preventDefault();
});

The code above essentially does three basic things:

  1. Have the web client open a connection to the SignalR Hub on server and do an invisible handshake to figure our network transport.
  2. Granted we have a connection, on the user's button click, we capture inputs and invoke the SendMessage()method on SignalR Hub with appropriate parameters.
  3. Define the ReceiveMessage() method that the SignalR server would turn around to call — this essentially captures chat messages from server and displays in a list.

That's it for a basic chat application powered by SignalR on both server and client side. Let's fire up two browser instances of our local app — real time chat just works.

ChatWeb

Connection Management

SignalR Hubs on the server side offer a plethora of APIs for connection management. Some really useful ones are:

  • Hubs expose a context property that carries a wealth of information about connection with clients, including unique identifiers for each connection/user
  • A clients property holds the collection of all connected clients and allows for fine tuned management
  • SignalR Hubs can be strongly typed through hub where client methods need to implement interfaces
  • SignalR Hubs raise events when clients connect/disconnect, as well as, having reconnect functionality for select client platforms

Since we started with a chat application, a common business scenario is to build a chatroom. Thankfully, SignalR Hubs offer granular control over how messages are distributed to connected clients. As clients connect up to the hub, they can be put into groups and messages can be streamed to the caller/everyone connected/named groups etc. Here's some sample code in our SignalR Hub:

public Task SendMessageToCaller(string message)
{
    return Clients.Caller.SendAsync("ReceiveMessage", message);
}

public override async Task OnConnectedAsync()
{
    await Groups.AddToGroupAsync(Context.ConnectionId, "ChatRoom");
    await base.OnConnectedAsync();
}

public override async Task OnDisconnectedAsync(Exception exception)
{
    await Groups.RemoveFromGroupAsync(Context.ConnectionId, "ChatRoom");
    await base.OnDisconnectedAsync(exception);
}

public Task SendMessageToGroup(string message)
{
    return Clients.Group("ChatRoom").SendAsync("ReceiveMessage", message);
}

The code above should be self-explanatory; we're simply grouping clients and sending messages selectively. Here's the result, after some UI cleanup:

ChatWithGrouping

Transport Configuration

One other interesting aspect of modern SignalR is configurability of network stack. As mentioned before, SignalR chooses the best possible fit for network transport given the server-client pair, gracefully switching between WebSockets, Server-Sent Events and Long Polling. For most modern web clients using evergreen browsers, the obvious transport choice is WebSockets. You get dependable bi-directional communication between client/server. Developers should never be in doubt as to what SignalR is doing under the covers with network transport. While most browser developer tools will show traffic patterns and content, sophisticated network proxies like Fiddler provide developers with even more ammunition to manipulate/test/record SignalR communications.

SignalR also has serialization/deserialization built-in, so that developers need not be constrained to passing strings back and forth. Full objects can be transported and flattened/hydrated across platform barriers. The most common way to pass around seriliazed data is JSON and SignalR is pre-configured to use JSON - the default is JSON serialization/deserialization built into .NET, but one can also switch to using NewtonSoft.Json. Even with WebSockets network, one can look under the covers to see human-readable JSON being sent across the wire, as seen in Chromium DevTools here:

WebSocketsTraffic

Have performance and bandwidth concerns for passing too much data around for your SignalR app? MessagePack is a popular binary serialization format that is much more compact than JSON, and SignalR supports MessagePack with a tiny bit of configuration on server and client. Binary serialization, however, means that SignalR message content is no longer readable, unless the bytes are passed through a MessagePack parser. So developers lose a bit of transparency, but the app gains efficacy and performance.

To configure MessagePack usage in SignalR communications, developers need to pull in the Microsoft.AspNetCore.SignalR.Protocols.MessagePack NuGet package server-side and explicitly mention the MessagePack protocol when adding the SignalR Middleware, like so:

services.AddSignalR().AddMessagePackProtocol();

One can also customize how MessagePack formats data and simple attributes can define how objects are serialized. On the web client side, MessagePack support with SignalR is provided by the @microsoft/signalr-protocol-msgpack npm package, which can be brought in like so:

npm install @microsoft/signalr-protocol-msgpack

We're essentially referencing some Node modules, which can be linked or the corresponding JS files brought in directly into the web project, like below:

MessagePackDependencies

Once our dependencies are in place, we can initialize the use of MessagePack as our web client goes up to connect to the SignalR Hub on server, like so:

var connection = new signalR.HubConnectionBuilder()
                .withUrl("/chatHub")
                .withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
                .build();

With MessagePack protocol in use, we should see network traffic serialized in binary format - better performance through smaller byte packages for the win!

MessagePackTraffic

Conversational UI

Hello world chat applications are fun first steps for real time apps. More realistic real-time apps can help automate complex enterprise workflows and the chat backend could like be an intelligent chatbot. There are a plethora of fabulous bot frameworks to hook up your chat apps to — all with rich tooling and SDKs for most platforms. And SignalR is happy to be the abstraction developers love over network complexities.

One other thing developers may need when building real-world chat apps — yes, polished and performant UI. When your goal is to automate workflows intelligently and deliver value, building chat UI by hand is too laborious and time consuming. Enter Conversational UI - modern UI components for chatbots across Telerik/Kendo UI technologies. With wide framework compatibility, polished chat UI with detailed documentation and flexible APIs, developers can be on their way to implementing natural chat conversational flow quickly.

For SignalR powered web clients, Conversational UI can be brought in for a wide variety of platforms: through Telerik UI for ASP.NET Ajax/MVC/Core or Kendo UI for jQuery/Angular/React/Vue. And these being web apps, they do not need to constrained to desktop browsers. Developers can absolutely build mobile-first PWA apps with polished chat UI. Hallelujah. Check out some of the quick demos of what's possible with real time chat applications that can automate workflows:

Travel

Healthcare

Wrap-Up

Life happens in real time and information exchange should be the same way. Turns out, there are countless modern applications that don't just benefit, but actively depend on real-time communications to be functional. SignalR has tremendous promise as a framework that aids in building real-time apps. With a ton of configurability, SignalR Hubs are easy to set up server-side. And building web clients connected to SignalR Hubs is just as easy, with flexible APIs and wonderful abstraction of network complexities. So SignalR powered web apps are a done deal — easy peasy.

But could we stretch real-time apps to other platforms, like desktop or mobile? Yes, Absolutely. While we take on other platforms to support SignalR in subsequent articles, the web continues to enjoy ubiquity and real time apps grow to be more valuable each day. Developers and consumers rejoice.


Sam Basu
About the Author

Sam Basu

Sam Basu is a technologist, author, speaker, Microsoft MVP, gadget-lover and Progress Developer Advocate for Telerik products. With a long developer background, he now spends much of his time advocating modern web/mobile/cloud development platforms on Microsoft/Telerik technology stacks. His spare times call for travel, fast cars, cricket and culinary adventures with the family. You can find him on the internet.

Related Posts

Comments

Comments are disabled in preview mode.