Real-Time Communication

In this chapter, we will learn about MicrosoftSignalR, which is a library fordoingreal-time communication between the client and the server. It allows the server to call the client of its own initiative, not as a result of a request. It builds on well-known technologies such as AJAX, WebSockets, and server-sent events, but in a transparent manner. You do not need to know what it's using—it basically just works, regardless of the browser you have. It also supports quite a lot of browsers, including mobile phones. Let's explore this technology and see what it has to offer—essentially, the following:

  • Setting up SignalR
  • Sending messages from the client to the server
  • Broadcasting messages from the server to all/some clients
  • Sending messages from outside a hub

After reading this chapter, you will learn how to communicate in real time from the server to clients connected to a page, whether they are on a PC or a mobile device.

Technical requirements

To implement the examples introduced in this chapter, you will need the .NET Core 3 SDK and a text editor. Of course, Visual Studio 2019 (any edition) meets all of the requirements, but you can also use Visual Studio Code, for example.

The source code can be retrieved from GitHub here: https://github.com/PacktPublishing/Modern-Web-Development-with-ASP.NET-Core-3-Second-Edition.

Setting up SignalR

Before starting to use SignalR, several things need to be sorted out first, namely, installing libraries locally.

Perform the following steps to begin this setup:

  1. First, install the Microsoft.AspNetCore.SignalR NuGet package.
  2. You also need a JavaScript library that is made available through npm (short for node package manager) as @microsoft/signalr.
  3. Once you install it, you need to copy the JavaScript file, either the minimized or the debug version to some folder under wwwroot, as it needs to be retrieved by the browser.
  4. The file containing the SignalR library is called signalr.js or signalr.min.js (for the minimized version) and it is available under node_modules/@aspnet/signalr/dist/browser.
  5. You will also require the @aspnet/signalr-protocol-msgpack package for using MessagePack serialization (more on this in a moment) if you wish to use it, but it's not strictly needed.

Unlike previous pre-Core versions, SignalR does not need any other library, such as jQuery, but it can happily coexist with it. Just add a reference to the signalr.js file before using the code.

For npm, add a package.json file similar to this one:

{"name":"chapter16","version":"1.0.0","description":"","main":"","scripts":{},"author":"","license":"ISC","dependencies":{"@microsoft/signalr":"^3.1.4","@aspnet/signalr-protocol-msgpack":"^1.1.0","msgpack5":"^4.2.1"}}

npm files are stored inside the node_modules folder but need to be copied to some location inside wwwroot, from where they can be made publicly available, for example, served to the browser. A good way to copy the files from the node_modules folder into your app is to leverage the MSBuild build system. Open your .csproj file and add the following lines:

<ItemGroup>
<SignalRFiles Include="node_modules/@microsoft/signalr
/dist/browser/*.js" />
<SignalRMessagePackFiles
Include="node_modules/@aspnet/signalr-protocol-msgpack
/dist/browser/*.js" />
<MessagePackFiles Include="node_modules/msgpack5/dist/*.js" />
</ItemGroup>

<Target Name="CopyFiles" AfterTargets="Build">
<Copy SourceFiles="@(SignalRFiles)"
DestinationFolder="$(MSBuildProjectDirectory)wwwrootlib
signalr" />
<Copy SourceFiles="@(SignalRMessagePackFiles)"
DestinationFolder="$(MSBuildProjectDirectory)wwwrootlib
signalr" />
<Copy SourceFiles="@(MessagePackFiles)"
DestinationFolder="$(MSBuildProjectDirectory)wwwrootlibmsgpack5" />
</Target>

This will copy the required JavaScript files from their npm-provided directory into a folder inside wwwroot, suitable for inclusion on a web page. Another option is to use Libman, described in Chapter 14, Client-Side Development, in a section of its own. Do have a look! And do not forget that because you are serving static files, you must add the appropriate middleware in the Configure method:

app.UseStaticFiles();

We shall begin with the core concepts of SignalR and move from there.

Learning core concepts

The appeal of SignalR comes from the fact that it hides different techniques for handling (near) real-time communication over the web. These are the following:

Each has its own strengths and weaknesses, but with SignalR, you don't really need to care about that because SignalR automatically picks up the best one for you.

So, what is it about? Essentially, with SignalR, you can do two things:

  • Have a client application (such as a web page) send a message to the server and have it routed to all (or some) parties also connected to the same web app
  • Have the server take the initiative to send a message to all (or some) connected parties

Messages can be plain strings or have some structure. We don't need to care about it; SignalR takes care of the serialization for us. When the server sends a message to the connected clients, it raises an event and gives them a chance to do something with the received message.

SignalR can group connected clients into groups and can require authentication. The core concept is that of a hub: clients gather around a hub and send and receive messages from it. A hub is identified by a URL.

You create an HTTP connection to a URL, create a hub connection from it, add event listeners to the hub connection (system events such as close and hub messages), then start receiving from it, and possibly start sending messages as well.

After setting up SignalR, we will see now how a hub is hosted.

Hosting a hub

A hub is a concept that SignalR uses for clients to come together in a well-known location. From the client side, it is identified as a URL, such as http://<servername>/chat. On the server, it is a class that inherits from Hub and must be registered with the ASP.NET Core pipeline. Here's a simple example of a chat hub:

public class ChatHub : Hub
{
public async Task Send(string message)
{
await this.Clients.All.SendAsync("message", this.Context.User.
Identity.Name, message);
}
}

The Send message is meant to be callable by JavaScript only. This Send method is asynchronous and so we must register this hub in a well-known endpoint, in the Configure method, where we register the endpoints:

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

You can pick any name you want—you don't need to be restrained by the hub class name.

And you can also register its services, in the ConfigureServices method, as follows:

services.AddSignalR();

The Hub class exposes two virtual methods,OnConnectedAsyncandOnDisconnectedAsync, which are fired whenever a client connects or disconnects. OnDisconnectedAsync takes Exception as its parameter, which will only be not null if an error occurred when disconnecting.

To call a hub instance, we must first initialize the SignalR framework from JavaScript, and for that, we need to reference the ~/lib/signalr/signalr.js file (for development) or ~/lib/signalr/signalr.min.js (for production). The actual code goes like this:

var logger = new signalR.ConsoleLogger(signalR.LogLevel.Information);
var httpConnection = new signalR.HttpConnection('/chat', { logger: logger });

Here, we are calling an endpoint named chat on the same host from where the request is being served. Now, in terms of the communication between the client and the server itself, we need to start it:

var connection = new signalR.HubConnection(httpConnection, logger);

connection
.start()
.catch((error) => {
console.log('Error creating the connection to the chat hub');
});

As you can see, the start method returns a promise to which you can also chain a catch method call to catch any exceptions while connecting.

We can detect that a connection was unexpectedly closed by hooking to the onclose event:

connection.onclose((error) => {
console.log('Chat connection closed');
});

After the connection succeeds, you hook to your custom events, ("message"):

connection.on('message', (user, message) => {
console.log(`Message from ${user}: ${message}`);
});

The call to on with the name of an event, ("message"), should match the name of the event that is called on the hub, in the SendAsync method. It should also take the same number (and type) of parameters.

I'm also using arrow functions, a feature of modern JavaScript (you can find out more by reading this article: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions). This is just syntax, and it can be achieved with anonymous functions.

As a remark, you can pass additional query string parameters that may later becaught in the hub. There is another way to do this, using HubConnectionBuilder:

var connection = new signalR.HubConnectionBuilder()
.configureLogging(signalR.LogLevel.Information)
.withUrl('/chat?key=value')
.build();

It is sometimes useful, as there are not many ways to pass data from the client to a hub other than, of course, calling its methods. The way to retrieve these values is shown in the following code:

var value = this.Context.GetHttpContext().Request.Query["key"].SingleOrDefault();

Now, we can start sending messages:

function send(message) {
connection.invoke('Send', message);
}

Essentially, this method asynchronously calls the Send method of the ChatHub class, and any response will be received by the 'message' listener, which we registered previously (see hub.on('message')).

In a nutshell, the flow is as follows:

  1. A connection is created on the client side (signalR.HubConnection) using a hub address (signalR.HttpConnection), which must be mapped to a .NET class inheriting from Hub.
  2. An event handler is added for some event (connection.on()).
  3. The connection is started (start()).
  4. Clients send messages to the hub (connection.invoke()), using the name of a method declared on the Hub class.
  5. The method on the Hub class broadcasts a message to all/some of the connected clients.
  6. When a client receives the message, it raises an event to all subscribers of that event name (the same as declared in connection.on()).
The client can call any method on the Hub class, and this, in turn, can raise any other event on the client.

But first things first, let's see how we can define the protocol between the client and the server so that the two can talk.

Choosing communication protocols

SignalR needs to have clients and the server talking the same language—a protocol. It supports the following communication protocols (or message transports, if you like):

  • WebSockets: In browsers that support it, this is probably the most performant one because it is binary-, not text-, based. Read more about WebSockets here: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API.
  • Server-sent events: This is another HTTP standard; it allows the client to continuously poll the server, giving the impression that the server is communicating directly to it; see https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events.
  • AJAX long polling: Also known as AJAX Comet, it is a technique by which the client keeps an AJAX request alive, possibly for a long time, until the server provides an answer, which is when it returns and issues another long request.

Usually, signalR determines the best protocol to use, but you can force one from the client:

var connection = new signalR.HubConnectionBuilder()
.configureLogging(signalR.LogLevel.Information)
.withUrl('/chat', { skipNegotiation: false, transport: signalR.
TransportType.ServerSentEvents })

This can be retrieved from the server as well, but in general, it is recommended to leave it open to all protocols:

app.UseEndpoints(endpoints =>
{
endpoints.MapHub<ChatHub>("chat", opt =>
{
opt.Transports = HttpTransportType.ServerSentEvents;
});
});

Forcing one protocol may be required in operating systems where, for example, WebSockets is not supported, such as Windows 7. Or it may be because a router or firewall might not allow some protocol, such as WebSockets. The client side and server side configuration must match, that is, if the server does not have a specific protocol enabled, setting it on the client side won't work. If you don't restrict transport, SignalR will try all of them and choose the one that works best automatically. You may need to choose a specific protocol if you have some sort of restriction, such as protocol incompatibility between client and server. If you do, don't forget to also skip negotiation, as it will save some time.

Do not restrict the transport types unless you have a very good reason for doing so, such as browser or operating system incompatibilities.

We've seen how to configure a connection, so let's see now how to reconnect automatically in case of a failure.

Automatic reconnection

You can either catch the close event and respond to it or have SignalR automatically reconnect when the connection is dropped accidentally (these things happen on the internet, you know!). For that, call the withAutomaticReconnect extension method on the client code:

var connection = new signalR.HubConnectionBuilder()
.withAutomaticReconnect()
.withUrl('/chat')
.build();

This method can be called without parameters or with an array of numeric values that represent the time, in milliseconds, to wait before each attempt to reconnect. The default value is [0, 2000, 10000, 30000, null], which means that first, it will try immediately, then it will wait two seconds, then one second, then three seconds, then it will stop trying (null). A third option is with a function that takes a few parameters and returns the next delay, as this example shows:

var connection = new signalR.HubConnectionBuilder()
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext)=> {
//previousRetryCount (Number)
//retryReason (Error)
return 2 * retryContext.elapsedMilliseconds;
})
.build();

In this example, we return an object that takes a lambda function that takes as its parameters the previous return count, the retry reason (an exception), and the elapsed time since the last retry and expects you to return the next delay or null if it's to cancel reconnection.

When automatic reconnection is used, some events are raised:

connection.onreconnecting((error) => {
//a reconnect attempt is being made
});

connection.onreconnected((connectionid) => {
//successful reconnection
});

These events are self-explanatory:

  • onreconnecting is raised when SignalR is trying to reconnect as the consequence of an error, which is passed as its sole parameter to the callback
  • onreconnected is raised after a successful reconnection and the new connection ID is passed to the callback

Messages sent to and from a hub need to be serialized, and that is the subject of the next topic.

Message serialization

Out of the box, SignalR sends messages in plaintext JSON, but there is an alternative, which is MessagePack. It is a compressed format that can provide a better performance, especially for bigger payloads.

As mentioned earlier, we will need to install the @aspnet/signalr-protocol-msgpack npm package and the Microsoft.AspNetCore.SignalR.Protocols.MessagePack NuGet package.

An example, where we can specify the MessagePack protocol, would be as follows:

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

If you chose to use MessagePack, you also need to add support for it when you register SignalR services:

services
.AddSignalR()
.AddMessagePackProtocol();

Now that we've seen how we can start a conversation, let's look at the SignalR context, where we can get information from the current SignalR session.

Exploring the SignalR context

The SignalR context helps us to understand where we are and who is making the request. It is made available through the Context property of the Hub class. In it, you will find the following properties:

  • Connection (HubConnectionContext): This is low-level connection information; you can get a reference to the current HttpContext from it (GetHttpContext()) as well as metadata stuff (Metadata) and it is possible to terminate the connection (Abort()).
  • ConnectionId (string): This is the one and only connection ID that uniquely identifies a client on this hub.
  • User (ClaimsPrincipal): This is the current user (useful if using authentication) and all of its claims.

The Context property is available when any of the hub methods is called, including OnConnectedAsync and OnDisconnectedAsync. Do not forget that for a context, a user is always identified by its ConnectionId; only if you use authentication will it also be associated with a username (User.Identity.Name).

And if we need to pass arbitrary parameters to the hub? Up next!

Using the query string

Any query string parameters passed on the URL (for example, "/chat?key=value") can be accessed on the server side through the Query collection of Request:

var value = Context.GetHttpContext().Request.
Query["key"].SingleOrDefault();

Now, let's find out to what entities a message canbe sent out to.

Knowing the message targets

A SignalR message can be sent to any of the following:

  • All: All connected clients will receive it
  • Group: Only clients in a certain group will receive it; groups are identified by a name
  • Group Except: All clients in a certain group except certain clients, identified by their connection IDs
  • Client: Only a specific client, identified by its connection ID

Clients are identified by a connection ID, that can be obtained from the Context property of the Hubclass:

var connectionId = this.Context.ConnectionId;

Users can be added to any number of groups (and removed as well, of course):

await this.Groups.AddAsync(this.Context.ConnectionId, "MyFriends");
await this.Groups.RemoveAsync(this.Connection.ConnectionId, "MyExFriends");

To send a message to a group, replace the All property by a Group call:

await this.Clients.Group("MyFriends").InvokeAsync("Send", message);

Or, similarly, to a specific client use the following:

await this.Clients.Client(this.Context.ConnectionId).InvokeAsync("Send", message);

Groups are maintained internally by SignalR, but, of course, nothing prevents you from having your own helper structures. This is how:

  • To send messages to all connected clients (to the hub), you do this:
await this.Clients.All.SendAsync("message", message);
  • To send messages to just a specific client, identified by its connection ID, use the following:
await this.Clients.Client("<connectionid>").SendAsync("message", message);
  • To a named group, we can use this:
await this.Clients.Group("MyFriends").SendAsync("message", message);
  • Or simply to groups, the following can be used:
await this.Clients.Groups("MyFriends", "MyExFriends").SendAsync("message", message);
  • To all members of a group except one or two connection IDs, we use the following:
await this.Clients.GroupExcept("MyFriends", "<connid1>", "<connid2>").SendAsync("message", message);

What if we need to communicate to the hub from outside the web app? Well, that is the subject of the next section.

Communicating from the outside

As you can imagine, it is possible to communicate with a hub, meaning to send messages to a hub. There are two possibilities:

  • From the same web application
  • From a different application

Let's study each of these.

Communication from the same web application

It is possible to send messages into a hub from the outside of SignalR. This does not mean accessing an instance of, for example, the ChatHub class, but only its connected clients. You can do this by injecting an instance of IHubContext<ChatHub> using the built-in dependency injection framework, shown as follows:

public class ChatController : Controller
{
private readonly IHubContext<ChatHub> _context;

public ChatController(IHubContext<ChatHub> context)
{
this._context = context;
}

[HttpGet("Send/{message}")]
public async Task<IActionResult> Send(string message)
{
await this._context.Clients.All.SendAsync("message", this
.User.Identity.Name, message);
}
}

As you can see, you are responsible for sending all of the parameters to the clients. It is also, of course, possible to send to a group or directly to a client.

Imagine you want to send a recurring message to all clients; you could write code like this in your Configure method (or from somewhere where you have a reference to the service provider):

public class TimerHub : Hub
{
public async Task Notify()
{
await this.Clients.All.SendAsync("notify");
}
}

//in Configure
TimerCallback callback = (x) =>
{
var hub = app.ApplicationServices.GetService<IHubContext<TimerHub>>();
hub.Clients.All.SendAsync("notify");
};

var timer = new Timer(callback);
timer.Change(
dueTime: TimeSpan.FromSeconds(0),
period: TimeSpan.FromSeconds(1));

The preceding code shows a registration fortimerHub and a notify event. When the event is raised, a message is written to the console. If an error occurs when starting the subscription, the error is also logged.

Timer will fire every second and broadcast the current time to a hypothetical TimerHub class. This TimerHub class needs to be registered as an endpoint:

app.UseEndpoints(endpoints =>
{
endpoints.MapHub<TimerHub>("timer");
});

It also needs to be registered on the client side:

var notificationConnection =newsignalR.HubConnectionBuilder()
    .withUrl('/timer')
    .withAutomaticReconnect()           
    .configureLogging(signalR.LogLevel.Information)
    .build();

notificationConnection.on('notify', () => { console.log('notification received!'); }); notificationConnection .start() .catch((error) => { console.log(`Error starting the timer hub: ${error}`); });

Next, let's see how communication happens from a different application

Communicating from a different application

This is a different approach: we need to instantiate a client proxy that connects to the server hosting the SignalR hub. We need the Microsoft.AspNet.SignalR.Client NuGet package for this. HubConnectionBuilder is used to instantiate HubConnection, as can be seen in the following example:

var desiredProtocols = HttpTransportType.WebSockets | HttpTransportType.LongPolling | 
HttpTransportType.ServerSentEvents;

var connection = new HubConnectionBuilder()
.WithUrl("https://<servername>:5000/chat?key=value", options =>
{
options.Transports = desiredProtocols;
})
.WithAutomaticReconnect()
.ConfigureLogging(logging =>
{
logging.SetMinimumLevel(LogLevel.Information);
logging.AddConsole();
})
.AddMessagePackProtocol()
.Build();

connection.On<string>("message", (msg) =>
{
//do something with the message
});

connection.Closed += (error) =>
{
//do something with the error
};

await connection.StartAsync();
await connection.SendAsync("message", message);

This example does several things:

  • Defines the acceptable communication protocols (WebSockets, long polling, and server-sent events)
  • Registers two event handlers (Closed and On("message"))
  • Creates a connection, with reconnection, logging set to Information and to the console, using the MessagePack protocol, and passing a query string value of "key"="value"
  • Starts the connection asynchronously
  • Invokes the Send method on the hub, passing it a string message

This code can be called from any place that has HTTP access to the server hosting the SignalR hub. Notice the setting of the desired protocols and the WithAutomaticReconnect and AddMessagePackProtocol extension methods. The AddConsole extension method comes from the Microsoft.Extensions.Logging.Console NuGet package.

We've seen how to send messages to a SignalR hub from the outside of the app hosting it. The following topic explains how authentication works with SignalR.

Using user authentication

SignalR uses the same user authentication mechanism as the encapsulating web app, which means if a user is authenticated to the app, it is authenticated to SignalR. It is possible to send a JWT token upon each request too, and it's done like this:

var connection = new signalR.HubConnectionBuilder()
.withUrl('/chat', { accessTokenFactory: () => '<token>' })
.build();

Notice the accessTokenFactory argument; here, we are passing a lambda (it could be a function) that returns a JWT token. On the client code, if you are calling SignalR from an outside app, you need to do this:

var connection = new HubConnectionBuilder()
.WithUrl("http://<servername>/chat", options =>
{
options.AccessTokenProvider = () => Task.FromResult("<token>");
})
.Build();

Where SignalR is concerned, the identity of users is dictated by their connection ID. So, depending on your requirements, you may need to build a mapping between this and the user IDs that your app uses.

So, we've seen how to enable authentication; let's see now how we can log the workings of SignalR.

Logging

Logging can help us to diagnose failures and know what is happening in the system in real time. We can enable detailed logging for SignalR in either configuration or through code. For the first option, add the last two lines to your appsettings.json file (for "Microsoft.AspNetCore.SignalR" and"Microsoft.AspNetCore.Http.Connections"):

{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information",
"Microsoft.AspNetCore.SignalR": "Trace",
"Microsoft.AspNetCore.Http.Connections": "Trace"
}
}
}

To the latter, add the configuration to the bootstrap code:

publicstatic IHostBuilder CreateHostBuilder(string[] args) => 
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(builder =>
{
builder
.ConfigureLogging(logging =>
{
logging.AddFilter("Microsoft.AspNetCore.SignalR",
LogLevel.Trace);
logging.AddFilter("Microsoft.AspNetCore.Http.
Connections"
, LogLevel.Trace);
})
.UseStartup<Startup>();
});

Mind you, this is just for enabling the flags that make the code output the debugging information. Trace is the most verbose level, so it will output pretty much everything, including low-level network calls. To actually log, you need to add loggers, like the console, for server-side code, or your own custom provider (mind you, this is just a sample):

.ConfigureLogging(logging => {
logging.AddFilter("Microsoft.AspNetCore.SignalR", LogLevel.Trace)
logging.AddFilter("Microsoft.AspNetCore.Http.Connections",
LogLevel.Trace);

logging.AddConsole();
logging.AddProvider(new MyCustomLoggingProvider());
})

For the client side, you need to register a custom function that takes two parameters:

function myLog(logLevel, message) {
//do something
}

var connection = new signalR.HubConnectionBuilder()
.configureLogging({ log: myLog })
.withUrl('/chat')
.build();

The first parameter, logLevel, is a number that represents one of the possible log levels:

  • signalR.LogLevel.Critical (5)
  • signalR.LogLevel.Error (4)
  • signalR.LogLevel.Warning (3)
  • signalR.LogLevel.Information (2)
  • signalR.LogLevel.Debug (1)
  • signalR.LogLevel.Trace (0): everything, including network messages

The second parameter, message, is the textual message that describes the event.

In this section, we've seen how to enable logging in both the client and the server side, with different levels of granularity.

Summary

In this chapter, we saw that we can use SignalR to perform the kind of tasks that we used AJAX for—calling server-side code and getting responses asynchronously. The advantage is that you can use it for having the server reach out to connected clients on its own when it needs to broadcast some information.

SignalR is a very powerful technology because it essentially adapts to whatever your server and client support. It makes server-to-client communication a breeze. Although the current version is not release-quality, it is stable enough for you to use in your projects.

Some advanced aspects of SignalR, such as streaming or clustering, haven't been discussed, as these are more for a dedicated book.

We are reaching the end of this book, so, in the next chapter, we will have a look into some of the APIs that weren't covered in previous chapters.

Questions

You should now be able to answer these questions:

  1. What are the two serialization formatters supported by SignalR?
  2. What transports does SignalR support?
  3. What are the benefits of the MessagePack protocol?
  4. To which targets can we send messages?
  5. Why would we restrict the transport to be used by SignalR?
  6. Can we send messages to SignalR from outside the web application where it is hosted?
  7. Can we use authentication with SignalR?
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.106.100