WebSockets

Which is best? WebSockets or SignalR


Intro

In this article we’ll build fully working chat apps with c# and .NET Core, using both WebSockets and SignalR, helping you decide which will work best for you.

What You’ll Learn

By the end of this article you’ll understand:

  • Synchronous Vs Asynchronous principles
  • HTTP, XMLHttpRequest, WebSocket and SignalR concepts
  • WebSocket & SignalR APIs
  • How to build WebSocket & SignalR Server apps
  • How to build JavaScript WebSocket & SignalR web clients

Ingredients

If you want to follow along with the examples in this article, you’ll need:

  • Visual Studio Code (Free)
  • .NET Core SDK (Free)
  • Web Browser (I recommend Firefox or Chrome)

Synchronous Vs Asynchronous

So just to ensure we’re all aligned on language I wanted to cover off what we mean by Synchronous Vs Asynchronous as they are recurring themes as we move through the tutorial:

In short:

  • A synchronous operation does its work before returning to the caller – I.e. you wait until it’s complete before you move on
  • An asynchronous operation does most or all of it’s work after returning to the caller – I.e. you can move on, (if possible), while an asynchronous operation completes work

If you’ve done any programming, (as I’m sure you have!), then the vast majority of operations would be synchronous. This typically fine as the majority of operations execute within milliseconds. Asynchronous operations are useful when you have scenarios where the operation  may take time to complete and you don’t want to “block” the running of your app while you wait.

Examples of “blocking” operations would be:

  • A call to an API over the internet
  • Brute force look up on a DB table

We cover asynchronous programming (as it’s required), later in the tutorial.

HTTP, XMLHttpRequest & WebSockets

This tutorial is obviously about WebSockets, but before we dive into them, it’s useful to contrast them with some other protocols in wide-spread use. This will help position the “value-proposition” of WebSockets – i.e. why you’d use them

HTTP

Everyone has heard of HTTP, (Hypertext Transfer Protocol)! There are reams of documentation on HTTP if you want to read up on it’s history, prevalence etc, but the points I wanted to convey here are:

  • Works on a Request / Response Cycle
  • It’s Synchronous
  • It’s Stateless

HTTP Protocol

Indeed we still actually need to use HTTP when we are “setting-up”, (or negotiating), a WebSocket connection.

XMLHttpRequest (XHR)

We now move into Asynchronous territory here with XMLHttpRequest, in short this protocol kick-started the “Ajax revolution”, (partial, asynchronous UI updates), and also underpins approaches like polling and “long-polling”, to summarise:

  • First debut in Internet Explorer 5!
  • Allows for partial UI updates (think address search / lookup on e-commerce web sites)
  • Asynchronous
  • Request / Response based
AJAX Lookup

Address look up on the Australia Post web site

Long Polling with XHR

Aside from streamlining the apparent responsiveness of interactive web-pages, XHR, is also used as the mechanism to support both polling and long polling, which could be used to build applications like stock tickers, chat apps etc., (in-fact exactly the type of apps we’d use WebSockets for!).

Long polling works like this:

  • XHR Request is made to server
  • We “hold” the connection open until the server completes it’s work
  • We immediately send the Response back to the client
  • Rinse & Repeat

It’s still basically a Request / Response approach, but we hold on responding until we are ready, (sounds synchronous!). Couple that however, with the ability to partially update the UI, and we give the impression of an asynchronous, real-time experience.

WebSockets

WebSockets remove the need to “poll for updates” via a Request / Response cycle and instead make use of a persisted, “full-duplex” connection. This is what I call an “nailed-up” connection between the client and server, where messages can be sent back and forth at any time. It’s the closest thing to a “raw network” socket in the browser and therefore ideal for real-time applications like chat.

WebSocket Connection

WebSockets:

  • Start with HTTP request to “upgrade” to WebSocket Connection
  • Bi-directional
  • Persistent or “nailed-up” connection

WebSocket API

Confusingly, (for me anyway!), WebSockets are composed of multiple standards:

We’ll be concerned with the WebSocket API only here, as a developer that’s all I’m interested in! You can read about the full API spec by reading the W3C documentation, (which personally I think is a difficult, hard to read mess!), but I’ve included the primary Methods and Events we’ll be interested in below:

Events

  • close: Fired when a WebSocket connection is closed
  • open: Fired when a WebSocket connection is opened
  • message: Fired when data is received through a WebSocket
  • error: Fired when a WebSocket has been closed because of an error

Methods

  • close: Closes the connection
  • send: Enqueues data to be transmitted

Now as we’ll be using both JavaScript (for our client) and C# on .NET Core for our server, there are obviously “vendor” implementations of the WebSocket API, however we’ll cover any deviations as we move into the code. For completeness though I’ve linked to both the JavaScript & .NET specs for WebSockets below:

Irrespective, you can see that the WebSocket API is very simple!

Application Architecture (WebSocket Chat)

The high-level “architecture” of our WebSocket Chat app is laid out below:

WebSocket Application Architecture

The custom .NET classes we will build are:

  • WebSocketServerMiddleware
  • WebSocketServerMiddlewareExtensions (not shown)
  • WebSocketServerConnectionManager

We’ll cover what these are in the build sections.

The design principles I used for this build were:

Client App

  • Basic JavaScript & HTML only – no additional frameworks, e.g. jQuery, Bootstrap etc
  • Single page, running as a “file” – I’m not even using a web server to keep it super simple, clean and direct

Server

  • Use basic .NET Core “web” template to provide minimal features out the box
  • No use of additional libraries, plugin’s etc.

Client Application User Interface

The user interface design, (if you can call it that!), for our client app is listed below:

WebSocket User Interface

As per our design principles it’s pretty basic looking, almost like something out of the 1990’s for those of you who were around then! Each of the UI elements are explained below:

  1. Connection State of our WebSocket
  2. Our “Connection ID”: a unique identifier for our WebSocket connection (we use this to send messages to the other clients)
  3. The URL of our WebSocket Server (our .NET Core app)
  4. Connect button
  5. Close Connection / Socket button
  6. The Message we want to send
  7. Send button
  8. We place the ConnID of another client in here so send a message to it
  9. Comms Log – will detail messaging both in and out on the open WebSocket

Overview of our Build

I have split the construction of our application build into 5 phases:

Build Phase 1: Get Connected

We start the build of both client and server components and establish a WebSocket Connection, we also cover:

  • Asynchronous programming in .NET
  • The Request Pipeline & Request Delegates

Build Phase 2: Send Messages

  • No surprises here we leverage the ability to send messages from the client to the server

Build Phase 3: Upgrade Our Middleware

We have to build some custom middleware to handle WebSocket connections, but we need to tidy up our code and split out our Request Delegate into a separate class

Build Phase 4: Add a Manager

Our WebSocket connections are not managed, and we can’t yet send messages between clients, for that we need to add a manager and introduce our unique “ConnID”

Build Phase 5: Adding a Message Router

We complete our WebSocket chat app buy adding a simple router so that we can target messages at individual clients, or broadcast on all active connections.

WebSocket Build Phase 1: Get Connected

In this first phase of our WebSocket Chat App build we will start building both our web client and server components with the following features:

Client App

  • Will have all the HTML UI elements we need
  • Will have the necessary JavaScript UI state rendering functions we need
  • JavaScript function to connect to our WebSocket server

Server

  • Will allow us to set up a WebSocket Connection

Note: If you don’t fancy typing in the code yourself, (although I recommend that you do!), you can find the supporting code repositories on Github.

Our Client

Our client is going to be a simple 1 page web app that uses HTML and JavaScript only. We won’t be using any additional frameworks like JQuery or Bootstrap, I’ve made this a conscious decision as it will allow us to focus purely on WebSockets, and not any of that other “fluff”. As a consequence the UI looks like something out of the 1990’s but I personally quite like that!

If you follow the official Microsoft .NET Core tutorial on WebSockets you’ll see more than a passing resemblance to the client code here… That’s because I’ve basically lifted and re-used most of it – so full credit to the authors.

I have modified it to introduce some additional features to support our chat app though.

We’re going to keep our client and server builds as separate projects, so for our client create a working folder for our single file, and open that folder as a “workspace” in VS Code:

Open Work Space

The HTML and JavaScript for our Phase 1 Client App is as follows:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>WebSocket JavaScript Client</title>
</head>

<body>
    <h1>WebSocket JavaScript Client</h1>
    <p id="stateLabel">Ready to connect</p>
    <p id="connIDLabel">ConnID: N/a</p>
    <div>
        <label for="connectionUrl">WebSocket Server URL:</label>
        <input id="connectionUrl" />
        <button id="connectButton" type="submit">Connect</button>
        <button id="closeButton" disabled>Close Socket</button>
    </div>
    <p></p>
    <div>
        <label for="sendMessage">Message:</label>
        <input id="sendMessage" disabled />
        <button id="sendButton" type="submit" disabled>Send</button>
    </div>
    <p></p>
    <div>
        <label for="recipients">Recipient IDs:</label>
        <input id="recipients" disabled />
    </div>
    <p></p>
    <h2>Communication Log</h2>
    <table style="width: 800px">
        <thead>
            <tr>
                <td style="width: 100px">From</td>
                <td style="width: 100px">To</td>
                <td>Data</td>
            </tr>
        </thead>
        <tbody id="commsLog">
        </tbody>
    </table>
    <p></p>
</body>
<script>
    var connectionUrl = document.getElementById("connectionUrl");
    var connectButton = document.getElementById("connectButton");
    var stateLabel = document.getElementById("stateLabel");
    var sendMessage = document.getElementById("sendMessage");
    var sendButton = document.getElementById("sendButton");
    var commsLog = document.getElementById("commsLog");
    var closeButton = document.getElementById("closeButton");
    var recipients = document.getElementById("recipients");
    var connID = document.getElementById("connIDLabel");
    connectionUrl.value = "ws://localhost:5000";

    connectButton.onclick = function () {
        stateLabel.innerHTML = "Attempting to connect...";
        socket = new WebSocket(connectionUrl.value);
        socket.onopen = function (event) {
            updateState();
            commsLog.innerHTML += '<tr>' +
                '<td colspan="3" class="commslog-data">Connection opened</td>' +
                '</tr>';
        };
        socket.onclose = function (event) {
            updateState();
            commsLog.innerHTML += '<tr>' +
                '<td colspan="3" class="commslog-data">Connection closed. Code: ' + htmlEscape(event.code) + '. Reason: ' + htmlEscape(event.reason) + '</td>' +
                '</tr>';
        };
        socket.onerror = updateState;
        socket.onmessage = function (event) {
            commsLog.innerHTML += '<tr>' +
                '<td class="commslog-server">Server</td>' +
                '<td class="commslog-client">Client</td>' +
                '<td class="commslog-data">' + htmlEscape(event.data) + '</td></tr>';
        };

    };

    function htmlEscape(str) {
        return str.toString()
            .replace(/&/g, '&amp;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;');
    }

    function updateState() {
        function disable() {
            sendMessage.disabled = true;
            sendButton.disabled = true;
            closeButton.disabled = true;
            recipients.disabled = true;
        }
        function enable() {
            sendMessage.disabled = false;
            sendButton.disabled = false;
            closeButton.disabled = false;
            recipients.disabled = false;
        }
        connectionUrl.disabled = true;
        connectButton.disabled = true;
        if (!socket) {
            disable();
        } else {
            switch (socket.readyState) {
                case WebSocket.CLOSED:
                    stateLabel.innerHTML = "Closed";
                    connID.innerHTML = "ConnID: N/a";
                    disable();
                    connectionUrl.disabled = false;
                    connectButton.disabled = false;
                    break;
                case WebSocket.CLOSING:
                    stateLabel.innerHTML = "Closing...";
                    disable();
                    break;
                case WebSocket.CONNECTING:
                    stateLabel.innerHTML = "Connecting...";
                    disable();
                    break;
                case WebSocket.OPEN:
                    stateLabel.innerHTML = "Open";
                    enable();
                    break;
                default:
                    stateLabel.innerHTML = "Unknown WebSocket State: " + htmlEscape(socket.readyState);
                    disable();
                    break;
            }
        }
    }
</script>

</html>

We can split the above code into 2 sections:

  • Our HTML
  • Our JavaScript

The HTML

The HTML is simple, we make use of a number of form elements, (Input Boxes, Labels, Submit Buttons etc.), but interestingly no actual “form” element – our button clicks are are dealt with by our JavaScript code. We also make use of a table to handle our “Communications Log Display”.

As mentioned above it looks simple and it is, the main thing you should really make note of is the use of the “id” tag for our elements as this is what our JavaScript uses to reference and use those elements.

The JavaScript

The JavaScript is a little more complex, and therefore interesting, so let’s take a closer look, I’ve broken it down into sections that we’ll go through:

Referencing Our UI

Referencing our UI

This first section of JavaScript create a number of UI objects that we can manipulate later, e.g.:

  • Cick Events for buttons
  • Setting the value of labels

We also manually set the value of our Connection URL text box to: ws://localhost:5000 as this is where our WebSocket server will run by default, (if you start your server in a different location you’ll need to update this object).

Connect Button Event Handler

Connect Button Event Handler

In this build section we’re only establishing a connection to our sever so only “wire up” an event handler to our Connect button, here’s what’s going on:

  1. connectButton onclick event handler
  2. This is where we create our WebSocket object, (socket), that we use throughout. Note we pass in the URL of our server
  3. We hook into the “open” event of our socket, (refer to the WebSocket API events above), and we call updateState, (See below), and add an entry to our comms log
  4. We hook into the “close” event of our socket and perform essentially the same actions for open, (except with different values)
  5. We hook into the “error” event, nuff said!
  6. We hook into the “message” event
Support Functions

We’ll build out some more “support functions” as we move through the tutorial, but for now we have 2:

  • htmlEscape: Replaces troublesome html character sets with HTML safe ones (primarily so our displays render correctly)
  • updateState: Simply enables / disables / update UI elements when we have connection state change. E.g. we want to enable the “Close” button if we have an active connection etc…

Our Server

So back in more familiar territory for me here, while I appreciate the power and necessity of JavaScript – I dislike coding with it, (mainly because I’m not that great at it!)…

New Projects in VS Code

OK so open VS Code and start an integrated terminal session (Ctrl + Shift + ` ), and type:

dotnet new web -n WebSocketServer

This will create a new .NET Core project called WebSocketServer using the simplest “web” template.

In VS Code open the folder, (WebSocketServer), that was created in the above step, you should have some thing similar to the following:

Note: Click “Yes” if you’re asked to add some assets in order to debug and build the project.

Open the Startup.cs file and delete the contents of the Configure method as well as the default comments for both the ConfigureServices and Configure methods. I want to do this to remove noise from the code we need to write. Your “empty” Startup class should look like this:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace WebSocketServer
{
    public class Startup
    {
        
        public void ConfigureServices(IServiceCollection services)
        {
        }

        
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            
        }
    }
}

Now we’re going to add some WebSocket goodness… Start off by adding the following using directive to the Startup class:

using System.Net.WebSockets;

Then update the Configure method as so:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
  app.UseWebSockets();

  app.Use(async (context, next) =>
  {
    if (context.WebSockets.IsWebSocketRequest)
    {
      WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
      Console.WriteLine("WebSocket Connected");
    }
    else
    {
        await next();
    }
  });
}

That may not look like a lot of code, but there is a HUGE amount to take in here…

huge

Before we tackle the code though let’s run it and test it with our client app. To run the server, at a command line type:

dotnet run

Ensuring that you’re in the project folder (WebSocketServer), this should start the app as follows:

Starting app...

Then move over to our client app, (our HTML web page), and open it in the browser of your choice. Remember to keep it simple we’re just opening it as “a file”, (as opposed to running it on a web server):

File path instead of web

Also make sure that WebSocket Server URL matches the host that our server has been started on above i.e. localhost:5000 in our example above – which is correct, (don’t worry about http:// Vs ws:// for now…)

Click the “Connect” button on the client and you should see the following output on our client and server respectively:

Broken Connection

And our server:

Server Connect

You can see we successfully connected our client to our host but almost immediately the connection was dropped… So what’s going on? Let’s return to our Server code, as shown below and we’ll go through what’s happening:

Configure Code

1: Our Configure Method & the Request Pipeline

Our Configure method is where we set our our HTTP Request Pipeline, which dictates how our web app responds to HTTP requests. We configure our Request Pipeline by adding Middleware components to an IApplicationBuilder instance which is passed in to the Configure method.

To be honest, WebSockets themselves are really quite basic, (remember the API?) – indeed if you look at how you’d implement a WebSocket server in something like NodeJS, (server-side JavaScript), it’s really straightforward. The complexity of a .NET Core implementation really revolves around setting up the Middleware.

So in short if you want to learn WebSockets on .NET Core – you need to learn about Middleware.

What is Middleware?

The official Microsoft documentation on Middleware is decent, so I’d suggest you branch off and come back here when done. However a brief summary is provided below:

Middleware is comprised of different components that you can add, (or not), to your Request Pipeline depending on how you want requests to be handled. Each component:

  • Chooses whether to pass the request to the next component in the pipeline
  • Can perform work before and after the next component in the pipeline.

We use Request Delegates to build this pipeline, which can be configured using one of the following (extension) methods:

  • Run: used at the end of the pipeline
  • Map: used to “branch” the pipeline
  • Use: Can be used to “short circuit the pipeline (it does not need to call the next Request Delegate)

The following diagram, (lifted directly from the Microsoft docs), details this:

request-delegate-pipeline

(c) Microsoft

I’ve update this to represent what we have in our server code so far:

Updated Pipeline

Referring to this diagram and our Configure method code, (above), let’s continue to step through:

2: app.UseWebSockets

This is the first Request Delegate we add to our pipeline which gives us the ability to, yes you guessed it, use WebSockets. The Microsoft Docs on this can be found here. We can further configure how WebSockets work using the WebSocketOptions class, for our app however we’ll stick wit the default options.

As this Request Delegate starts with “Use” it will await on the next Request Delegate in the chain, which is our…

3: “Custom” Request Delegate

Our 2nd Request Delegate is of our own making, and is where we process our WebSocket connections, (or not see #6). Again as with the 1st delegate this uses a “Use” extension method which means that it can:

  • Call / await the next Request Delegate in the chain or
  • “Short Circuit” the Pipeline

Note that this extension method requires a:

  • HttpContext (context)
  • Task (next)

We use the context in our next step, (see #4 below), we’ll come onto the Task in step #6.

4: Check WebSocket

Using the context object we check its “WebSockets” property to establish if this is an upgrade request to establish a new WebSocket connection. If so we move to step #5, otherwise we move to step #6

5: Accept Connection & Create WebSocket

Here we create a new WebSocket by “awaiting” on the AcceptWebSocketAsync() method. This is an example, (as is step #6), of asynchronous programming in .NET. We cover how this works in more detail later on.

As we do nothing with this WebSocket once the connection is opened, it get’s immediately closed, (as it goes out of scope), hence why we get the disconnection event on the client – we remedy this later on…

Also note that here we are “short circuiting” the request pipeline as we do no call the next delegate. This means the pipeline, “returns” back through the prior Request Delegates- this is depicted by the “A” Line on our Pipeline diagram above.

6: Await Next Request Delegate in Chain

If the HTTP request is not a WebSocket request, the we simply call the next Request Delegate in the chain, (we do not short circuit the pipeline). In this case it just so happens we don’t have further delegates…

Further Example

To further embed the concepts mentioned above, we’re going to make some temporary code additions to our configure method, that will:

  • Add a 3rd Request Delegate
  • Display some HTTP Request headers on our Server console

So add the following simple Request Delegate to the end of our chain / pipeline, it adds some text to the HTTP Response, (and writes out to our Server console):

....
  else
  {
    //Add a Console.WriteLine to see when we move to next Delegate
    Console.WriteLine("Hello from 2nd Request Delegate - No WebSocket");
    await next();
  }
});

//New Request Delegate
app.Run(async context => 
{
  Console.WriteLine("Hello from 3rd (terminal) Request Delegate");
  await context.Response.WriteAsync("Hello from 3rd (terminal) Request Delegate");
});

Take note of 2 things:

  • I’ve added a Console.WriteLine statement in the Else block of our 2nd delegate – we use this to test / demonstrate the flow through the Request Pipeline.
  • Our new Request Delegate uses the “Run” extension method, which means it’s at the end of the pipeline. This is also borne out by the fact we only pass the the Context and not a “next Task”.

I’ve also updated the Pipeline Diagram to reflect the addition of this new Request Delegate:

New Pipeline

So ensure you save the server code, and run the app again:

dotnet run

Use our WebSocket client app again to make a WebSocket request as before, the terminal output window of our server app should display something similar to the following:

We get the same result as before, and you can see that our 3rd Request Delegate was not called, (it was “short circuited”)…

Next open a new web browser page and make a standard HTTP request by entering the following in to the main browser URL box: http://localhost:5000, e.g.

HTTP Request

You’ll notice:

  1. We’ve just made a standard HTTP Request to our app
  2. We get the textual output added to the Response by our 3rd Request Delegate

Looking at the terminal output of our server app, you should see:

http request server output

Here you can see that the request passed all the way through the pipeline, hitting out terminating delegate that:

  • Writes a message to the Terminal
  • Adds some text to the Response, (see above).

Before we move on to discussing Asynchronous Programming in .NET, let’s add the following method to our Startup class to display the HTTP Request Headers, (I’ve redacted the ConfigurerServices and Configure methods):

public class Startup
{        
  public void ConfigureServices(IServiceCollection services)...

  public void Configure(IApplicationBuilder app, IHostingEnvironment env)...

  public void WriteRequestParam(HttpContext context, IHostingEnvironment env)
  {
    if (env.IsDevelopment())
    {
      Console.WriteLine("Request Method: " + context.Request.Method);
      Console.WriteLine("Request Protocol: " + context.Request.Protocol);

      if (context.Request.Headers != null)
      {
        Console.WriteLine("Request Headers: ");
        foreach (var h in context.Request.Headers)
        {
          Console.WriteLine("--> " + h.Key + ": " + h.Value);
        }
      }
    }
  }
}

Make sure you place a call to this method in our 2nd Request Delegate:

app.Use(async (context, next) =>
{
  //Add the call to print the Request parameters here
  WriteRequestParam(context, env);

  if (context.WebSockets.IsWebSocketRequest)
  {
    WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
    Console.WriteLine("WebSocket Connected");
  }
  else
  {
    Console.WriteLine("Hello from 2nd Request Delegate - No WebSocket");
    await next();
  }
});

The WriteRequestParam method just takes the current environment and HttpContext and uses (the HttpContext in particular), to display a number of request properties, as well as iterating round the Headers collection.

Once again, save your code changes and start the server app again, and once again make a WebSocket call, you should see output similar to that below:

WebSocket Headers

You’ll see:

  1. The request method is a HTTP GET
  2. The initial request protocol is HTTP/1.1
  3. We have the Upgrade: websocket request header (that proposed we upgrade from the HTTP protocol to the WS – WebSocket protocol)

Our connection is however still immediately terminated, so we need to fix that with an “Asynchronous” method.

Asynchronous Programming in .NET

You will already have seen the use of the async / await keywords in the code we have written so far, and we are going to use it again to deal with messaging across our WebSocket – but what do they do? And when would be use them?

Referring back to our discussion on Synchronous Vs Asynchronous above, async / await allow us, (not surprisingly!), to program asynchronously in .NET: i.e. kicking something off and not necessarily waiting for it to complete while we do other things.

This makes sense for long-running or “blocking” type operations, such as network communications, so we are going to use it further here.

Task Asynchronous Programming Model

I strongly recommend that you read about the Task Asynchronous Programming Model here, as it covers the main concepts well, in particular the control flow of async code. I also recommend that you follow the flow control example here, which is a useful practical hands on with async / await. For completeness though I provide the key concepts of asynchronous programming below, along with a worked example of the flow of code in our app in the next build section.

  • Asynchronous methods use the async modifier.
  • Asynchronous methods typically return a Task (with optional type)
  • Tasks represent ongoing work, and will eventually return with the required result, (or exception)
  • Asynchronous method names will usually have “Async” as the last segment of the name
  • Await statements can be used in Async methods to designate “suspension points”, i.e. we can’t continue past this point until the awaited process is complete.
  • The “awaited” method it’s self must also be asynchronous
  • While “awaiting” control returns to the caller of the the Async method

I understand this is still probably a bit confusing if you’ve never used async / await before, (again I recommend that you read the article mentioned above and follow along with the suggested example too), so in the next build section we’ll introduce our own asynchronous method that we’ll used to receive messages on our WebSocket, I’ll then take you through the execution path.

WebSocket Build Phase 2: Send Messages

In this build section we are going to make the following addition to our Client and Server solution components:

Client App

  • Add the onclick event for our “send” button
  • Send a message down our WebSocket (Client to Server)
  • Update our “Communication Log” to display our messages
  • Add the onclick event for our “close” button

Server

  • Add an Asynchronous method, (ReceiveMessage) to receive events on our WebSocket
  • Call our new Asynchronous method from within our “WebSocket Request Delegate”

Client App

Add the following 2 onclick events to our single page app, (they should appear in the <script> section of the page after the existing connectButton.onclick event):

connectButton.onclick = function () { ... 
};

closeButton.onclick = function () {
  if (!socket || socket.readyState !== WebSocket.OPEN) {
    alert("socket not connected");
  }
  socket.close(1000, "Closing from client");
};
sendButton.onclick = function () {
  if (!socket || socket.readyState !== WebSocket.OPEN) {
    alert("socket not connected");
  }
  var data = sendMessage.value;
  socket.send(data);
  commsLog.innerHTML += '<tr>' +
    '<td class="commslog-client">Client</td>' +
    '<td class="commslog-server">Server</td>' +
    '<td class="commslog-data">' + htmlEscape(data) + '</td></tr>';
};

The code here is straightforward so don’t think I need to detail much more except to say that we utilise the send and close methods of our WebSocket object.

Server

Add the following code as a new class method within out Startup class:

private async Task ReceiveMessage(WebSocket socket, Action<WebSocketReceiveResult, byte[]> handleMessage)
{
  var buffer = new byte[1024 * 4];

  while (socket.State == WebSocketState.Open)
  {
    var result = await socket.ReceiveAsync(buffer: new ArraySegment<byte>(buffer),
                                                       cancellationToken: CancellationToken.None);
    handleMessage(result, buffer);
  }
 }

Additionally you’ll probably need to add the following using statement at the top of the class:

using System.Threading;

Finally, we call this method from our first Request Delegate, as our Receive method is asynchronous we call it using await:

....
if (context.WebSockets.IsWebSocketRequest)
{
  WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();
  Console.WriteLine("WebSocket Connected");

  await ReceiveMessage(webSocket, async (result, buffer) =>
  {
    if (result.MessageType == WebSocketMessageType.Text)
    {
      Console.WriteLine($"Receive->Text");

      return;
    }
    else if (result.MessageType == WebSocketMessageType.Close)
    {
      Console.WriteLine($"Receive->Close");

      return;
    }
  });
}
else ....

Note that while this code will compile you will get a warning in the Request Delegate code complaining that an async method lacks any “await” operators and as such it will run synchronously. Don’t worry we’ll fix that later.

not async

Moving back to our RecieveMessage method, let’s take a look at what that does:

RecieveMessage Method

  1. This is an asynchronous method so we define it with the async modifier
  2. It returns a Task (this is used to provide a reference to the ongoing work)
  3. Our 2nd parameter is in fact an Action Delegate which we use to pass back our result and message
  4. We start a while loop that continues while our WebSocket connection is open
  5. We then await the result of…
  6. … the ReceiveAsync method on our WebSocket, (which as the name suggests is an asynchronous method)
  7. Once Step 5 returns a result we use our Action Delegate to pass back the result and message (stored in our buffer)

Let’s test our code. Make sure you’ve saved everything and:

  • Run the server
  • Refresh our client app

Now:

  1. Click connect
  2. Enter a message and click send

You should see items added to the Communications log on the client:

Build 2: Client Test

You should see the following output on the server:

text received

Don’t worry we’ll add some code to display the actual message in the sections that follow.

To round off this build section, let’s trace the flow of the code:

Async WebSocket Code

  1. We await on the AcceptWebSocketAsync method (we return control to the caller while we “await)
  2. AcceptWebSocketAsync completes, so execution continues to our Receive method…
  3. We call our Receive method, (and await it)
  4. In our Receive method we enter a while loop that remains true while we have an open connection on our WebSocket, we await the ReceiveAsync method…
  5. When we eventually receive a message, (think send method on our API), ReceiveAsync fires, calling our Action Delegate: handleMessage
  6. handleMessge fires
  7. We enter into the main part of our Action Delegate and work through some simple logic to determine of the received message is: Text (it’s a message), or Close.

We have a “deviation” here from the standard WebSocket API in that Microsoft have implemented the following 2 methods, (as opposed to just “send”):

  • ReceiveAsync
  • SendAsync

They are self explanatory, and we’ll use both in our example, you can however read more about them in the .NET WebSocket class documentation.

WebSocket Build Phase 3: Upgrade Our Middleware

In this build section we’re only going to be working on our Server component where we’ll build out the following features:

  • Moving our Request Delegate to a separate “Middleware” class
  • Introduce a static IApplicationBuilder method that will return our middleware class (this just keeps things clean and simple in our Startup class)
  • Displaying the Message Text Sent by our Client(s)

Create a Separate Middleware Class

Our code is working well, but our Startup class is looking a little “overblown”, in particular our Configure method. Wouldn’t it be nice to tidy this up and produce some more reusable, readable, maintainable code? Yes of course! So the first step is to create a new Middleware class called: WebSocketServerMiddleware

  • Create a new folder in out project called “Middleware” (you can do this from within the file explorer of VS Code)
  • Inside that folder create a new class file called: WebSocketServerMiddleware.cs (don’t forget to include the .cs extension to denote it’s a C# class file)

Middleware Folder and Class

  • Inside our newly created WebSocketServerMiddleware.cs file class file , the code should look like this:

using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;

namespace WebSocketServer.Middleware
{
    public class WebSocketServerMiddleware
    {
        private readonly RequestDelegate _next;

        public WebSocketServerMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            if (context.WebSockets.IsWebSocketRequest)
            {
                WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();

                Console.WriteLine("WebSocket Connected");

                await Receive(webSocket, async (result, buffer) =>
                {
                    if (result.MessageType == WebSocketMessageType.Text)
                    {
                        Console.WriteLine($"Receive->Text");
                        Console.WriteLine($"Message: {Encoding.UTF8.GetString(buffer, 0, result.Count)}");
                        return;
                    }
                    else if (result.MessageType == WebSocketMessageType.Close)
                    {
                        Console.WriteLine($"Receive->Close");

                        return;
                    }
                });
            }
            else
            {
                Console.WriteLine("Hello from 2nd Request Delegate - No WebSocket");
                await _next(context);
            }   
        }

        private async Task Receive(WebSocket socket, Action<WebSocketReceiveResult, byte[]> handleMessage)
        {
            var buffer = new byte[1024 * 4];

            while (socket.State == WebSocketState.Open)
            {
                var result = await socket.ReceiveAsync(buffer: new ArraySegment<byte>(buffer),
                                                       cancellationToken: CancellationToken.None);

                handleMessage(result, buffer);
            }
        }
    }
}

You’ll notice this code is very similar to the existing code in our Configure method, indeed I want you to remove that “duplicate” code from the Configure method, so that it now looks like this, (this includes removing the WriteRequestParam method altogether):


namespace WebSocketServer
{
    public class Startup
    {

        public void ConfigureServices(IServiceCollection services)
        {
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseWebSockets();

            app.Run(async context =>
            {
                Console.WriteLine("Hello from 3rd (terminal) Request Delegate");
                await context.Response.WriteAsync("Hello from 3rd (terminal) Request Delegate");
            });
        } 
    }
}

Coming back to our WebSocketServerMiddleware class let’s go through what’s happening:

WebSocketServerMiddleware code breakdown

We have basically built a custom Middleware class, for full details on how to do this refer to the Microsoft documentation.

  1. We create a private RequestDelegate instance we instantiate via constructor dependency injection (see next step)
  2. Our public class constructor with a parameter type of RequestDelegate – mandatory
  3. A public method named InvokeAsync – this method must return a Task, and accept a first parameter of type HttpContext
  4. We add the code to “decode” the text contained the buffer received from our Receive method
  5. If our request is not a WebSocket request, then we just call the next Request Delegate in our pipeline as before.

This “scaffolding” code, (all except point 4 which is only directly relevant to our WebSocket Server), is required in a custom RequestDelegate class, as it provides the necessary fabric to allow the Request Pipeline to continue to function. Again, refer to the full Microsoft documentation if you want a more detailed discussion on this.

Expose Our Middleware through IApplication Builder

We have our custom middleware class, but we’ve stripped all the code from the Configure method in our Startup class – so how can we use it? Wouldn’t it be great if we could make use of our custom class in the same we we have for our other middleware components? E.g. using the pattern: app.UseSomething

Iapplication Builder

Well yes we can and that’s what we’re going to do now!

Create another class file a called: WebSocketServerMiddlewareExtensions.cs in our existing Middleware folder, and add the following code:


using Microsoft.AspNetCore.Builder;

namespace WebSocketServer.Middleware
{
    public static class WebSocketServerMiddlewareExtensions
    {
        public static IApplicationBuilder UseWebSocketServer(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<WebSocketServerMiddleware>();
        }
    }
}

Main points to note:

  • Our class is static
  • Our UseWebSocketSever method, (also static), exposes the middleware class through IApplicationBuilder

Finally in order to use this extension method, (ensure all your newly added code is saved), simply add it to our request pipeline via the Configure method in our Startup class as follows:

Middleware Extension method used

Perform a dotnet run, and using our WebSocket Client app, connect to our server and send a message. It should work as before, but should now display the message text in our server console!

Build 3 WebSocket Client

Working Custom Middleware

WebSocket Build Phase 4: Add a Manager

We could leave the example here, but so far we’ve only sent messages from our client to the server, we’ve not:

  • Sent messages from our server back to a client
  • Sent & Received messages between clients (using the server as a “router” or “hub”)

To make out example useful we really need to be able to do this, we have a problem though and this can be demonstrated in the following way:

  • Start your server app, (if it’s not running)
  • Open 2 separate instances of our Client app
  • Connect from from both clients
  • Send Messages from both clients

This set up is depicted below:

2 unmanaged clients

  1. Message from “Client 1”
  2. Message from “Client 2”
  3. Communication Logs only show activity for each respective client
  4. Our Server is receiving messages from both Clients

The “problem” we have is that while our server can receive messages from many connected clients, the WebSockets themselves are completely unmanaged – meaning that we essentially just have a series of independent WebSocket connections that are not aware of the other connections. This in-turn means we cannot send messages between clients…. yet.

The Simplicity of WebSockets

I’ve described this as a problem but in fact that’s quite unfair, WebSockets are just a method of communication, they are really rather simple and do not come with any kind of management / routing layer – we have to build that ourselves if we want to manage and route our connections.

So the purpose of this build section is to build a WebSocket “Manager” that will allow us to manage our WebSocket connections and provide a foundation to building out a simple message router, (build section 5 below). So the features well add in this section are:

Server

  • Create a WebSocketServerConnectionManager class – this will generate unique Id’s for all our WebSocket connections and allow us to track them (with a view to routing)
  • Send back the Unique WebSocket “Connection ID” to the client (upon successful WebSocket connection request)

Client App

  • Process and display the unique WebSocket connection ID generated by our server

Server

Back in our server project add a new C# class file called: WebSocketServerConnectionManager.cs (I just placed this in the “root” project folder), and add the following code to it:


using System;
using System.Collections.Concurrent;
using System.Net.WebSockets;

namespace WebSocketServer
{
    public class WebSocketServerConnectionManager
    {
        private ConcurrentDictionary<string, WebSocket> _sockets = new ConcurrentDictionary<string, WebSocket>();

        public string AddSocket(WebSocket socket)
        {
            string ConnID = Guid.NewGuid().ToString();
            _sockets.TryAdd(ConnID, socket);
            Console.WriteLine("WebSocketServerConnectionManager-> AddSocket: WebSocket added with ID: " + ConnID);
            return ConnID;
        }

        public ConcurrentDictionary<string, WebSocket> GetAllSockets()
        {
            return _sockets;
        }
    }
}

The code is pretty simple:

  • We have private “ConcurrentDictionary” object. This just allows us to manage our WebSocket connections as a series of Key / Value pairs:
    • Key: a unique ID (in this case a GUID we generate)
    • Value: the WebSocket
  • AddSocket method: Takes a WebSocket, generates a GUID, and adds it to our ConcurrentDictionary
  • GetAllSockets method: Returns all our “managed” WebSocket connections

Now back over in our WebSocketServerMiddleware class we want to introduce and use an instance of our ConnectionManager:


...
public class WebSocketServerMiddleware
    {
        private readonly RequestDelegate _next;

        private readonly WebSocketServerConnectionManager _manager = new WebSocketServerConnectionManager();

        public WebSocketServerMiddleware(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            if (context.WebSockets.IsWebSocketRequest)
            {
                WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();

                string ConnID = _manager.AddSocket(webSocket);

                await Receive(webSocket, async (result, buffer) =>
                {
                  ...

The new code is in “bold”, (we only added 2 lines), and they:

  • Create a private read only instance of our WebSocketServerConnectionManagerClass called _manager
  • Call the AddSocket method to add any new WebSockets created

Save and run your code, then fire up our client and make a connection, you should see something like this:

WebSocket Connected with ID

We have a unique id for this connection, you can do this multiple times, and we’ll get new Id’s for subsequent connections…

Dependency Injection

Before we go on, I’m going to take another slight diversion… We could leave our WebSocketServerMiddleware code as it is and continue to build on it, but I want to clean it up slightly and introduce the concept of Dependency Injection.

I want to follow the same pattern we use for our RequestDelegate in our WebSocketServerMiddleware class where:

  • We declare our RequestDelegate object: _next but don’t instantiate it with the “new” keyword
  • Using Dependency Injection we then “inject” an instance of RequestDelegate into our class constructor and assign it to our _next object

We do this by moving back over to our WebSocketServerMiddlewareExtensions class and adding a new static method that adds our WebSocketServerMiddleware class as a Service to our IServicesCollection:


using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;

namespace WebSocketServer.Middleware
{
    public static class WebSocketServerMiddlewareExtensions
    {
        public static IApplicationBuilder UseWebSocketServer(this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<WebSocketServerMiddleware>();
        }

        public static IServiceCollection AddWebSocketServerConnectionManager(this IServiceCollection services)
        {   
            services.AddSingleton<WebSocketServerConnectionManager>();
            return services;
        }
    }
}

The new code is in bold, to recap:

  • We add a new using directive to include: Microsoft.Extensions.DependencyInjection
  • We add the AddWebSocketServerConnectionManager method that adds our WebSocketServerConnectionManager class to our services collection

By adding our class to our list of available services we can make use of an instance of it without instantiating it via the “new” keyword”. Taking the concept further we can replace “concrete” instances and use Interfaces instead, this allows us to loosely couple our code further. We’ll leave that concept for now though…

For a detailed overview of Dependency Injection, start with the .NET Core documentation here.

Save this code and move back to our Startup class, adding the following line to our ConfigureServices method:

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

This actually calls the AddWebSocketServerConnectionManager method and makes our WebSocketServerConnectionManager class available for use.

Save the code and finally return to the WebSocketServerMiddleware class and make the following changes that mirror the pattern we use for our RequestDelegate, again the changed code is in bold:


...
public class WebSocketServerMiddleware
{
  private readonly RequestDelegate _next;
  private readonly WebSocketServerConnectionManager _manager;

  public WebSocketServerMiddleware(RequestDelegate next, WebSocketServerConnectionManager manager)
  {
    _next = next;
    _manager = manager;
  }
  ...

Save the code and run our server. Repeat the connection test with our client and we should get the same result as before. Our code looks much nicer though!

Pass Our ID to the Client

The final part of this build section is to pass our ID back to the Client so that we can display it on the client, and then use it to route to other clients.

For our example I’ve chosen to generate our unique connection ID’s on the server, (makes sense), but also use that ID as an identifier for the clients too. We’ll then use these Connection IDs to route chat traffic to other client end points.

In the “real world” you’d probably want to identify your clients by some kind of human friendly login id, and associate them to your WebSocket connections. I just felt that this would be a bit out of the scope of our discussion, so have chosen not to implement that.

So the first thing we want to do to achieve this is to write a new method in our WebSocketServerMiddleware class called: SendConnID. Add the following code to that class:

private async Task SendConnIDAsync(WebSocket socket, string connID)
{
  var buffer = Encoding.UTF8.GetBytes("ConnID: " + connID);
  await socket.SendAsync(buffer, WebSocketMessageType.Text, true, CancellationToken.None);
}

The method:

  • Is marked with the async modifier, (the name also ends with Async), this is because it is asynchronous in nature
  • Accepts a WebSocket object along with it’s ConnID
  • Creates a array of bytes, (buffer), from a combination of:
    • The hard-coded prefix of: ConnID:
    • Our manager-generated ConnID
  • We then call, (and await), the SendAsync method on our socket to send our buffer back to the client

To test our method, we’ll call it in our InvokeAsync method, directly after the call to the  AddSocket method, our code looks like this, (changes in bold):


...
public async Task InvokeAsync(HttpContext context)
{
  if (context.WebSockets.IsWebSocketRequest)
  {
    WebSocket webSocket = await context.WebSockets.AcceptWebSocketAsync();

    string ConnID = _manager.AddSocket(webSocket);

    await SendConnIDAsync(webSocket, ConnID); //Call to new method here

    await Receive(webSocket, async (result, buffer) =>
    ...

Save the code, run your server, and make a connection call from our client again, you should see a message received in our client comms log with our message payload:

ConnID passed back to client

While this is actually good enough for what we need going forward, I thought I’d just add a little bit of “zing” to our Client App, and update the “ConnID” attribute at the top:

Add Our ConnID to the attribute at the top of our client

Client App

Back over in our client app we’re going to add a simple function, (method), in our script section that takes a string and decides if it’s a “ConnID” message – basically does the sting start with: “ConnID:”. You can appreciate that this is VERY basic and subject to abuse, bur for now it will suffice, (until we “upgrade” our messaging format in the next build section).

Our JavaScript function looks like this:


function isConnID(str) {
  if (str.substring(0, 7) == "ConnID:")
    connID.innerHTML = "ConnID: " + str.substring(8, 45);
}

Because we expect a GUID and GUID’s are of a fixed length, we can use the substring function to pull it apart and dynamically update our connID label.

Finally we just need to call our function every time we receive a message on our websocket, so just place a call to it in the onmessage event of our WebSocket, (new code in bold):

...
socket.onerror = updateState;
socket.onmessage = function (event) {
  commsLog.innerHTML += '<tr>' +
    '<td class="commslog-server">Server</td>' +
    '<td class="commslog-client">Client</td>' +
    '<td class="commslog-data">' + htmlEscape(event.data) + '</td></tr>';
  isConnID(event.data); //New call here
};
...

Save your code, close any open WebSocket connections from your client app then reload the page to load the new code. Attempt to connect to our server again, (make sure it’s running), and you should see that our ConnID attribute is updated:

ConniD Attribute updated

While it may not seem like we did anything terribly useful in this section we in fact covered the following:

  • Added a management layer to our app, providing unique ids for our WebSocket connections
  • Made use of Dependency Injection via the ConfigureServices method in our Startup class
  • Sent and displayed our ConnID back to our client for use in “routing”

WebSocket Build Phase 5: Adding Message Router

We’ve done a lot of great work, but while our clients can send and receive messages to and from the server, the clients cannot talk to each other – which makes for a pretty rubbish chat application.

No talking

No Talking

In this build section we fix that, and introduce the following features:

Client App

  • Build out a JSON-based messaging format

Server

  • Introduce a routing capability that can:
    • Route messages to a specified client
    • Broadcast messages to all clients
  • Cleanly close our WebSocket connections if we receive a close event from the client

Client App

So in our client we want to write a function that takes:

  • Our Message Text
  • Our ConnID (the “ID of the sending or “From” client)
  • The recipient ConnID (the “To” ConnID)

And constructs a serialised JSON obejct.

The code for this function is shown below, add it to the other support functions in our script section of our client app:


function constructJSONPayload() {
  return JSON.stringify({
    "From": connID.innerHTML.substring(8, connID.innerHTML.length),
    "To": recipients.value,
    "Message": sendMessage.value
  });
}

What this does is build a string representation of a JSON object, an example of such an object would look like this:


{
  "From": "fb2a1715-793f-4852-a28d-489953cf4091",
  "To": "93545030-07b4-4c13-8910-c3c46ac8d3df",
  "Message": "Hello? Is there anybody in there?"
}

JSON or JavaScript Object Notation, is a structured messaging format used widely in web communications. It’s primarily concerned with the representation of object data that can be serialised for transmission and then deserialised at the receiving end-point for further processing.

I’ve already covered this topic extensively in my Deserialising JSON with C# article as well as this YouTube video, so I won’t cover it here. If in doubt, I’d read the article to give you a decent working knowledge of JSON and how we deserialise it using C#.

This simple messaging format contains just enough detail to allow for basic message routing. The routing rules we’ll implement are outlined below:

Routing Flow

Some points to note at each stage:

  1. This equates to False if we don’t have any data, (blank), or if we do have data, but that the data is not a GUID.
  2. Step 1 is False, then we just perform a “Broadcast” this means we send the message to all managed WebSocket connections.
  3. If Step 1 equates to True then we check that the GUID is managed by us. If not it equates to False.
  4. We just write a message to our log if we don’t have a reference to the provided GUID. (We should probably send a message back to the sending client, I’d decided not to implement that for reasons of brevity).
  5. We have a valid GUID that we are managing, so we send the message to the targeted client

Note: This is a very simple set of routing rules, you may wish to adapt these to something more suitable at a later date.

Finally to finish off our client code we need to call this function from within the sendButton.onclick event, (new code in bold):

sendButton.onclick = function () {
  if (!socket || socket.readyState !== WebSocket.OPEN) {
    alert("socket not connected");
  }
  var data = constructJSONPayload();
  socket.send(data);
  commsLog.innerHTML += '<tr>' +
    '<td class="commslog-client">Client</td>' +
    '<td class="commslog-server">Server</td>' +
    '<td class="commslog-data">' + htmlEscape(data) + '</td></tr>';
};

Save your code, close any open WebSocket connections and reload the client app page.

Ensure your server is also running, connect to it then compose a message, you may also choose to put some text in the “Recipient ID” text box. Click send and you should see the constructed JSON Message payload in the client app Communication Log:

JSON Payload

You can see here that I’ve just put some junk text in the Recipient ID text box, (this is where you’d place the GUID of another client that you wanted to send a message to – for now I just want to test the function).

As usual you should see the messaging payload on the received on the WebSocket server as a string:

JSON Message on Server

Client App Complete

And with that final function – our client app is complete. The only feature I’d still like to implement is providing a drop-down list of available connections, instead of a free-form text box where we’ll need to copy-paste in “To” GUID’s. However as nice as that feature would be, it would bloat the tutorial too much and not really add to learning about WebSockets – so I decided to cut it.

We now move on to the final component of our WebSocket server – the router!

Server

Closing Our Connection

Before moving onto our routing function, let’s add some code to gracefully close our WebSocket connection when we receive a close event request from our client.

Back over in our WebSocketServerMiddleware class, we want to add the following code, (in bold), to the “else if’ clause within our call to Receive:

await Receive(webSocket, async (result, buffer) =>
{
  if (result.MessageType == WebSocketMessageType.Text)
  {
    Console.WriteLine($"Receive->Text");
    Console.WriteLine($"Message: {Encoding.UTF8.GetString(buffer, 0, result.Count)}");
    return;
  }
  else if (result.MessageType == WebSocketMessageType.Close)
  {
    string id = _manager.GetAllSockets().FirstOrDefault(s => s.Value == webSocket).Key;
    Console.WriteLine($"Receive->Close");

    _manager.GetAllSockets().TryRemove(id, out WebSocket sock);
    Console.WriteLine("Managed Connections: " + _manager.GetAllSockets().Count.ToString());

    await sock.CloseAsync(result.CloseStatus.Value, result.CloseStatusDescription, CancellationToken.None);

    return;
  }
});

Note: As we make use of “Linq” here, (FirstOrDefault), so we need to include the following using directive at the top of our class:

using System.Linq;

The code does the following:

  • Upon determining we have a Close request…
  • We retrieve the ConnID for the WebSocket we receive the message on
  • Using the TryRemove method of our ConcurrentDictionary of managed WebSockets we remove our WebSocket from our managed collection
    • Here we pass “out” the WebSocket we want
  • We write (purely for debug purposes) the remaining number of managed connections
  • We then call the CloseAsync method on our selected WebSocket
    • A side-effect of adding this asynchronous method call (i.e. using “await” we finally remove the compiler warning that some of our asynchronous code is going to run synchronously…)

We can quickly test the impact of this change by opening and the requesting closure of a WebSocket connection:

Graceful Closure

Prior to this when we requested a closure from the client, we’d get a 1006 message, (“abnormal” client closure), back:

1006 Abnormal Closure

This means we now have a nice clean WebSocket closure going on!

Routing Our Messages

We now move onto the final component of our Server and the final component of the entire WebSocket build – introducing a message router.

We are going to introduce a new method into our WebScoketServerMiddleware class: RouteJSONMessageAsync, before adding that method though we need to add another using directive:

using Newtonsoft.Json;

This allows us to use the excellent JSON.NET library from Newtonsoft, which again I covered in my previous article: Deserialising JSON with C#. Note that at the time of writing Microsoft now includes this library by default so you don’t even have to include it in the csproj file!

We then need to add the following method code to our WebSocketServerMiddleware class:

 
private async Task RouteJSONMessageAsync(string message)
{

  var routeOb = JsonConvert.DeserializeObject<dynamic>(message);

  if (Guid.TryParse(routeOb.To.ToString(), out Guid guidOutput))
  {
    Console.WriteLine("Targeted");
    var sock = _manager.GetAllSockets().FirstOrDefault(s => s.Key == routeOb.To.ToString());
    if (sock.Value != null)
    {
      if (sock.Value.State == WebSocketState.Open)
        await sock.Value.SendAsync(Encoding.UTF8.GetBytes(routeOb.Message.ToString()), WebSocketMessageType.Text, true, CancellationToken.None);
    }
    else
    {
      Console.WriteLine("Invalid Recipient");
    }
  }
  else
  {
    Console.WriteLine("Broadcast");
    foreach (var sock in _manager.GetAllSockets())
    {
      if (sock.Value.State == WebSocketState.Open)
        await sock.Value.SendAsync(Encoding.UTF8.GetBytes(routeOb.Message.ToString()), WebSocketMessageType.Text, true, CancellationToken.None);
    }
  }
}

Looking back to our routing rules:

Routing Flow

We can cross reference with what’s happening in our code:

routing Method

The majority of the code is straightforward so I won’t explain further, with the exception of what is happening at step “A”: our JSON “Deserialisation:

var routeOb = JsonConvert.DeserializeObject<dynamic>(message);

Here we make use of the DeserializeObject method on the JsonConvert class, where we pass in our string, (message), and create an object form it, (routeOb).

The interesting thing to note here is that we make use of a “dynamic” object type to deserialise our string, this means that the method infers what the object is and builds it for us. The only caveat is that the string data must be “JSON”.

Following on from this when we come to accessing the object attributes, e.g.

routeOb.To.ToString()

The “To” attribute is “late-bound” as we simply don’t know what string data we are going to receive.

An alternative to this is to define a concrete C# class to define what object we expect and deserialise our string against that. Again I use both methods in my Deserialising JSON with C# article.

Finally we need to call this route method, to do so we simply add a call to it in our existing Receive method:


...
await Receive(webSocket, async (result, buffer) =>
{
  if (result.MessageType == WebSocketMessageType.Text)
  {
    Console.WriteLine($"Receive->Text");
    Console.WriteLine($"Message: {Encoding.UTF8.GetString(buffer, 0, result.Count)}");
    await RouteJSONMessageAsync(Encoding.UTF8.GetString(buffer, 0, result.Count));
    return;
  }
  else if (result.MessageType == WebSocketMessageType.Close)
  ...

So save the code and run up your server.

To test, fire up 3 new instances of our client and and connect in – so we should have 3 connected clients with 3 GUIDs.

3 Connected Clients

Testing Targeted Routing
  • Copy the ConnID from one client into the Recepient ID of another
  • Write a message
  • Click send

Result: The sent message should only appear on the target client (the sending client will have an entry for the sent message in it’s comms log however).

Targeted Messaging

Testing Broadcast Routing
  • Make sure 3 clients are connected
  • Choose a client that has nothing in it’s Recipient ID text box
  • Compose a message
  • Click Send

Result: All clients, (including the sending client), will receive a copy of the inbound message

Broadcast Message

Conclusion on WebSocket Build

We covered a lot of stuff here! But for the most most part it was elements required to support a more fully functioning WebSocket chat app that took up most of our time. Our use of the core WebSocket class was constrained only to a few method calls.

The point I guess is that WebSockets themselves are pretty simple, (as detailed by the API), but to do anything useful with them requires quite a bit of additional work… (hint, hint). For now though, we can turn our attention to SignalR and what it provides in contrast…

What is SignalR?

Phew! That was a lot of work to get a relatively simple WebSocket chat app up and running – and now we’re going to build essentially the same app using the SignalR framework. Before we do that though let’s talk about what SignalR is and what it offers over and above WebSockets.

SignalR uses WebSockets

In short, SignalR is:

  • A open source “wrapper” framework around WebSockets (and other protocols / approaches)
  • It provides the “missing” management layer absent in pure WebSockets
  • It will use the the “best-fit” protocol/approach automatically, (WebSockets, Long Polling), based on the capability of the available environment. E.g. Some browsers may not support WebSockets, so SignalR will fallback to Long Polling with XHR.

So theoretically, we could build the same Chat app with SignalR with much less code, with little downside.

Architecture & Design

The architecture of our application is very similar to that of the WebSocket app, see if you can spot the differences…

SignalR Architecture

SignalR Build Phase 1: Server Build

In this build phase we’re going to build the entire server component in 1 go, this will provide the same functionality we have in our WebSocket Server app, so it will:

  • Manage Connections
  • Receive Messages
  • Send, (route), messages

As SignalR does a lot of the heavy lifting for us it’ll be relatively quick!

First, we start with a new .NET Core web project, to create this type the following at a command line:

dotnet new web -n SignalRServer

This will create an empty web project for us.

Open this folder in VS Code, and we’re ready to start coding!

Hubs

The central concept in SignalR is that of a “Hub”, comparing it with what we’ve build previously is basically combines the following functionality into one place:

  • Management of connections
  • Message handling (routing)

So in our project create a new folder in the root of out project called “Hubs”, inside this this folder add a file called: ChatHub.cs, so you should have something like the following:

Hub Folder & File

You can read more about Hubs here, but for now we’e going to press on with the coding in our own hub. We are going to add:

  • OnConnectedAsync Event: This fires when, (surprise, surprise), we get a new connection to our hub.
  • SendMessageAsync Method: This method gets called directly from our SignalR client and contains the routing functionality we need, (it will use the same JSON messaging format we designed for our WebSocket solution).

The entire code for out hub looks like this:


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

namespace SignalRServer.Hubs
{
    public class ChatHub : Hub
    {
        public override Task OnConnectedAsync()
        {
            Console.WriteLine("--> Connection Opened: " + Context.ConnectionId);
            Clients.Client(Context.ConnectionId).SendAsync("ReceiveConnID", Context.ConnectionId);
            return base.OnConnectedAsync();
        }
        
        public async Task SendMessage(string message)
        {
            var routeOb = JsonConvert.DeserializeObject<dynamic>(message);
            Console.WriteLine("To: " + routeOb.To.ToString());
            Console.WriteLine("Message Recieved on: " + Context.ConnectionId );
            if(routeOb.To.ToString() == string.Empty)
            {
                Console.WriteLine("Broadcast");
                await Clients.All.SendAsync("ReceiveMessage", message);
            }
            else
            {
                string toClient = routeOb.To;
                Console.WriteLine("Targeted on: " + toClient);
                
                await Clients.Client(toClient).SendAsync("ReceiveMessage", message);
            }
        }
    }
}

Let’s step through the code and I’ll take you through what’s happening.

Hub Code

  1. Our class inherits from the base Hub class, you can read more about these here.
  2. We override the OnConnectedAsync event, this fires when we get a new connection on our hub
  3. Similar to our WebSocket application we send back a unique ConnectionID to our client, (by calling the client function ReceiveConnID),  unlike the WebSocket application, SignalR provides us access to a unique connection ID via the “Context” object out the box.
  4. The SendMessageAsync method is called by our client app, passing in a JSON payload identical to our WebSocket App
  5. We deserialise our JSON object (as before)
  6. If we don’t have a “To” recipient, we broadcast the message to all clients, this is done by calling the SendAsync method using the Clients.All construct.
  7. I explicitly create a string to hold our “To” address, (step 8 errors out if we access it via the the routeOb.To construct…)
  8. We can send a targeted message direct to a given client by calling the SendAsync method once again, this time however we call it using the Clients.Client(toClient) construct.

Method Calls

As demonstrated in our hub code, we can:

  • Send messages from our Hub (and receive them on our client)
  • Send messages from our Client (and receive them on our hub)

Sending Messages From the Hub

In order to send a message from a hub we:

  • Use the SendAsync method on our server and pass in the name of the Method on the client
  • We need a JavaScript Method, (function), in our client with the same name specified in the SendAsync method.

So for example we call 2 different client methods from our hub:

  • ReceiveConnID: Sends our Contect.ConnID back to the client for further use.
  • ReceiveMessage: Sends our received messaged to our clients

We’ll cover off what these functions look like in our client in the next build section

Startup class

We spoke a lot about the Request Pipeline and Request Delegates in the WebSocket tutorial, this has not changed here with SignalR, except that it is much simpler to use and set up, the remaining code required to finish our server is listed below:

using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using SignalRServer.Hubs;

namespace SignalRServer
{
    public class Startup
    {

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


        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseCors(builder => builder
                .WithOrigins("null")
                .AllowAnyHeader()
                .AllowAnyMethod()
                .AllowCredentials());

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

The code is explained in the steps below:

Startup Class: SignalR Project

  1. We add CORS (Cross Origin Resource Sharing), Services for use via Dependency Injection (CORS explained below)
  2. We add SignalR Services for use via Dependency Injection
  3. We add CORS to our Request Pipeline – this allows us to receive connections on our server from a client in a different domain, (our client is running from a file location, not “http://localhost” like our server, so is considered running in a different domain). There is a further discussion on CORS below.
  4. Using the out the box SignalR Request Delegate we add this to our Request Pipeline too, and map our hub as an “endpoint”, we specify “/chatHub” as the URL location for requests to this hub.

Cross Origin Resource Sharing (CORS)

A full explanation regarding CORS with respect to SignalR is provided by Microsoft, but in short CORS allows end points from different domains to interact with each other. It is primarily a security concept.

Looking at how we set up CORS on our SignalR server:

app.UseCors(builder => builder
  .WithOrigins("null")
  .AllowAnyHeader()
  .AllowAnyMethod()
  .AllowCredentials());

We specify which “origins” we want to allow our application to accept, this is done via “WithOrigins”… I had to play around with this for a bit to get the correct configuration to work with our client set up, (as it was running from a file location).

In the end I just examined the value of “origin” in the HTTP request headers issued by our client app in the initial negotiation stage of setting up our connection.

Connection Negotiation

I used Chrome in this example: (click the “ellipsis” -> More Tools -> Developer Tools):

Chrome Dev Tools

There are similar developer tools in Firefox, Safari and Edge etc. – just Google how to find them.

Once in the Developer tools, select “Network” and you can begin to explore what’s happening. In the above example you can see the HTTP negotiation to establish the WebSocket connection to the Hub.

IMPORTANT: The origin header we used above can be faked, so you should not use this as an authentication mechanism…

But wait…

So how did our WebSocket app work without adding “CORS” to the mix? The client and server were both running in different domains and could communicate perfectly well?

Great Question!

By default CORS does not apply to “raw” websockets out the box, if you want to use CORS you can but have to explicitly use it, if you choose not to though connections for other origins will be accepted… You have been warned!

Again this is another example where SignalR provides an addition requirement / framework over and above basic WebSockets.

This article from Microsoft explains this in some more detail.

Server Build Done

That’s all the code we require to get our SignalR server app up and running, it’s immediately obvious that it’s simpler with much less code…

SignalR Build Phase 2: Retrofit Our Client App

We now move onto our client app. Here we take our WebSocket client and “retro-fit” it for SignalR.

First off, create a new folder, (call it something like SignalRClientApp), open that folder in VS Code and create a new html page: client.html

The code for our client is listed below:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>SignalR JavaScript Client</title>
</head>

<body>
    <h1>SignalR JavaScript Client</h1>
    <p id="stateLabel">Ready to connect</p>
    <p id="connIDLabel">ConnID: N/a</p>
    <div>
        <label for="connectionUrl">SignalR Server URL:</label>
        <input id="connectionUrl" size="30" />
        <button id="connectButton" type="submit">Connect</button>
        <button id="closeButton" disabled>Close Socket</button>
    </div>
    <p></p>
    <div>
        <label for="sendMessage">Message:</label>
        <input id="sendMessage" disabled />
        <button id="sendButton" type="submit" disabled>Send</button>
    </div>
    <p></p>
    <div>
        <label for="recipients">Recipient IDs:</label>
        <input id="recipients" disabled />
    </div>
    <p></p>
    <h2>Communication Log</h2>
    <table style="width: 800px">
        <thead>
            <tr>
                <td style="width: 100px">From</td>
                <td style="width: 100px">To</td>
                <td>Data</td>
            </tr>
        </thead>
        <tbody id="commsLog">
        </tbody>
    </table>
    <p></p>
</body>
<script src="lib/signalr/dist/browser/signalr.js"></script>
<script>

    

    "use strict";
    var connectionUrl = document.getElementById("connectionUrl");
    var connectButton = document.getElementById("connectButton");
    var stateLabel = document.getElementById("stateLabel");
    var sendMessage = document.getElementById("sendMessage");
    var sendButton = document.getElementById("sendButton");
    var commsLog = document.getElementById("commsLog");
    var closeButton = document.getElementById("closeButton");
    var recipients = document.getElementById("recipients");
    var connID = document.getElementById("connIDLabel");


    connectionUrl.value = "http://localhost:5000/chatHub";

    var hubConnection = new signalR.HubConnectionBuilder().withUrl(connectionUrl.value).build();
    
    //CONNECT BUTTON
    connectButton.onclick = function () {
        stateLabel.innerHTML = "Attempting to connect...";

        hubConnection.start().then(function () {
            updateState();
            
            commsLog.innerHTML += '<tr>' +
                '<td colspan="3" class="commslog-data">Connection opened</td>' +
                '</tr>';
        });
    };

    closeButton.onclick = function () {
        if(!hubConnection || hubConnection.state !== "Connected") {
            alert("Hub Not Connected");
        }
        hubConnection.stop().then(function () {

        });
    };

    //CLOSE EVENT
    hubConnection.onclose(function (event) {
        updateState();
        commsLog.innerHTML += '<tr>' +
            '<td colspan="3" class="commslog-data">Connection disconnected </td>' +
            '</tr>';
    });


    hubConnection.on("ReceiveMessage", function (message) {
        commsLog.innerHTML += '<tr>' +
            '<td class="commslog-server">Server</td>' +
            '<td class="commslog-client">Client</td>' +
            '<td class="commslog-data">' + htmlEscape(message) + '</td></tr>';
    });

    hubConnection.on("ReceiveConnID", function (connid) {
        connID.innerHTML = "ConnID: " + connid;
        commsLog.innerHTML += '<tr>' +
            '<td colspan="3" class="commslog-data">Connection ID Received from Hub</td>' +
            '</tr>';
    });

    sendButton.onclick = function () {
        var message = constructJSONPayload();
        hubConnection.invoke("SendMessageAsync", message);
        console.debug("SendMessage Invoked, on ID: " + hubConnection.id);
        commsLog.innerHTML += '<tr>' +
            '<td class="commslog-client">Client</td>' +
            '<td class="commslog-server">Server</td>' +
            '<td class="commslog-data">' + htmlEscape(message) + '</td></tr>';
        event.preventDefault();
    };

    function htmlEscape(str) {
        return str.toString()
            .replace(/&/g, '&amp;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;');
    }

    function constructJSONPayload() {
        return JSON.stringify({
            "From": connID.innerHTML.substring(8, connID.innerHTML.length),
            "To": recipients.value,
            "Message": sendMessage.value
        });
    }


    function updateState() {
        function disable() {
            sendMessage.disabled = true;
            sendButton.disabled = true;
            closeButton.disabled = true;
            recipients.disabled = true;

        }
        function enable() {
            sendMessage.disabled = false;
            sendButton.disabled = false;
            closeButton.disabled = false;
            recipients.disabled = false;

        }
        connectionUrl.disabled = true;
        connectButton.disabled = true;
        if (!hubConnection) {
            disable();
        } else {
            switch (hubConnection.state) {
                case "Disconnected":
                    stateLabel.innerHTML = "Disconnected";
                    connID.innerHTML = "ConnID: N/a"
                    disable();
                    connectionUrl.disabled = false;
                    connectButton.disabled = false;
                    break;
                case "Connecting":
                    stateLabel.innerHTML = "Connecting...";
                    disable();
                    break;
                case "Connected":
                    stateLabel.innerHTML = "Connected";
                    enable();
                    break;
                default:
                    stateLabel.innerHTML = "Unknown WebSocket State: " + htmlEscape(hubConnection.state);
                    disable();
                    break;
            }
        }
    }
</script>

</html>

Before we go through the new elements of our code, we need to ensure we have the SignalR JavaScript libraries available for use, as suggested by this line in the code above:

<script src="lib/signalr/dist/browser/signalr.js"></script>

To obtain signalr.js, first check to see that you have “libman” installed: type the following at a command line to check:

libman --version

You should get a response along these lines:

libman version

If not, you’ll need to install it, to do so, run the following at a command prompt:

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

This will install LibMan, (LibMan is just a “Library Manager” tool used to acquire client-side libraries, we’ll use it to obtain the signalr.js client-side library).

So once installed, run the following command “inside” your project folder, (in this case SignalRClientApp):

libman install @aspnet/signalr@next -p unpkg -d lib/signalr --files dist/browser/signalr.js

This gets signalr.js and places it in the following location:

lib/signalr/dist/browser/signalr.js

Review Client Code

The HTML and the “support” JavaScript functions have not changed, or in the case of updateState, have changed very little. So I’ll focus on our new functions:

Connect / Disconnect

SignalR Connect Code

  1. We set up our hubConnection object, this is analogous to our connection object in our WebSocket app
  2. The onclick event of our Connect button, we simply call the “start” method on our connection. If successful we display a message. Note this triggers the OnConnectedAsync event on our server hub – this is where our hub sends us the ConnID…
  3. The onclick event of our close button, we simply call the “stop” event on our hubConnection
  4. The onclose event on our hub. This fires when the connection is actually closed, (pressing the button is merely requesting closure…)

As you can see the connection management stuff is pretty simple.

Messaging Functions

SignalR Messaging Functions

  1. These are the “client” functions that are triggered by the Hub via calls to SendAsyc. we have one that receives and displays the ConnID, and one for message generation. You could combine these into 1 function, but to be honest but I quite like this separation.
  2. This is the onclick event of our send button. This uses the “invoke” method of the hubConnection object, passing in the name of the hub method we want to invoke.

Build Complete!

You should now be able to run and test the SignalR application in the same way as we did for the WebSocket app, for brevity I won’t replicate that here.

Conclusion

So which should you use? WebSockets or SignalR? The choice is up to you, but I’d ask myself some questions about what you’re trying to achieve:

  • Do you require rapid development?
  • Does the app need to scale to 1000’s, 10000’s, millions of users?
  • Do you have “control” of the client landscape?
  • Will you have to outsource the build?
  • What are the time frames for the build?
  • What skill sets does you development team have?
  • What are your security requirements?
  • etc…

Overall though, the choice of technology should never drive the solution, the solution, (or business/customer requirements), should always come first. As a user of our chat app I could not care less if we’re using WebSockets, SignalR, .NET Core, Java, Long Polling etc, all I care about is does work, is it easy to use, is it reliable etc….

Putting my developer hat back on though: I’m divided.

I enjoyed using WebSockets as I like to understand things at a lower level, also a WebSocket is a WebSocket so arguably learning this is a more transferable skill.

With SignalR the learning curve is greatly reduced, so solutions should be quicker to develop. There’s also less code, so it’s more readable, I also like the idea that it can “fall-back” from WebSockets to something else if environment is not capable.

We are however adding another framework / layer, but as you’ve seen we have to add this anyway… So the question boils down to: which additional layer do you want to use if you want to use WebSockets?

  • Custom layer build by you and your team?
  • SignalR layer built by a community of Open Source developers and supported by Microsoft?

The choice is yours…

REST API
Authenticating to a REST API from c#
JSON
Deserializing JSON with c#
Devops
Build, Test and Deploy a REST API with Azure DevOps