Chapter 6. Persistent connections and hubs from other threads

All server implementations seen up to this point in the book have had something in common: they have always been responding to direct messages from a client. Even though they are push systems, a client has always been initiating the procedure:

  • In a chat, when a user sends a message to the server, this message is sent to the rest of the connected users.

  • In the tracking system that we showed in Chapter 4 a user’s mouse movement was the action that originated the notification to the other users.

  • In the shared drawing board example in Chapter 5 when a user drew a point or pressed the button to erase the canvas, the action was sent to the others so that they could update their whiteboards.

Although this is what we will need in most cases, SignalR takes a step further and allows us to send messages to clients connected to a hub or a persistent connection from another thread of the same application—that is, outside the hub and without the need for a SignalR client to initiate the sequence explicitly.

Access from other threads

This approach can be very interesting in scenarios where there are unattended or automatically executed processes in the same application where SignalR services are found, and where these processes need to send information to clients who need to receive it in real time.

For example, there might be a background thread obtaining information from a data source (such as stock quotes, meter readings, and so on) and sending it to clients connected to a hub or a persistent connection. It would also be quite easy to create a system that registered the exceptions thrown in an ASP.NET application and sent an alert at exactly that point to a set of users previously connected to a service in real time. Or we could enter logic in the method treating a Web Forms event or an ASP.NET MVC action to notify clients connected to a hub or persistent connection that something important for them has occurred. Any of these cases would consist in accessing hubs or persistent connections from different threads to those where the hubs or persistent connections are running, but always within the same process or app domain. Certainly, the range of possibilities that unfolds thanks to this capability of SignalR is immense.

Although it is probably unnecessary to remark on this, it would also be possible to access real-time services offered by SignalR from other physically separated systems, but such a scenario would be very different: the external system could simply be another client of the services, and to access them, it would have to use client libraries provided by the framework. An example of an external system could be something as simple as a mobile application or as complex as an ERP system, which would need to exchange information with a SignalR server in real time. The structure would be the one displayed in Figure 6-1.

A diagram showing a SignalR server to which several clients are connected. There is also an external system that connects to the SignalR server just like another client.

Figure 6-1. Accessing SignalR services from physically separate systems.

If we just need the external system to inform the users connected to the services about an event, we could also use more disconnected architectures and create a façade of web services in the application where SignalR resides (using Web API, MVC, WCF, and so on), as shown in Figure 6-2. The external system would notify this façade, and it would transfer the notification to the users, using the techniques that we will look at in this chapter.

Having said this, we are now going to explain how to access SignalR services from other threads of the same application. We will review the other scenarios—connection from external systems both directly and through a façade of services—in Chapter 7 and Chapter 9.

A diagram showing a SignalR server to which several clients are connected. This server has a fa?e based on web services, connected to the real-time SignalR services just like another client. There is also an external system that sends information to the server through said fa?e.

Figure 6-2. Accessing SignalR services from external systems via web services.

External access using persistent connections

To submit information to the clients connected to a persistent connection, we simply have to obtain a reference to said connection and use the methods that we normally use inside the PersistentConnection classes.

In the following example, we see how, from a Web Forms application, we could notify connected clients that an event of interest to them has taken place:

protected void btnDeleteInvoice_Click(object sender, EventArgs e)
{
   var id = invoiceId.Text;
   _invoiceServices.DeleteInvoice(id);
   var context = GlobalHost.ConnectionManager
                              .GetConnectionContext<ErpNotifications>();
   context.Connection.Broadcast(
          "Invoice #" + id + " deleted by " + User.Identity.Name);
}

The GetConnectionContext<T>() method used in the preceding example returns a type T reference to the context of the persistent connection. The call returns an object of the IPersistentConnectionContext type, which gives access to all the functionalities normally available from inside PersistentConnection: sending broadcasts directly, sending data to groups, or even actual management of groups of clients. The members available through this interface are the following:

  • Connection, which provides access to the IConnection type object (the same one that we found in the PersistentConnection base class) and, at the same time, allows using the following methods:

    • Send(), to send messages to specific clients whose connectionId is known.

    • Broadcast(), to send them to all the clients connected to the persistent connection.

  • Groups, of the IConnectionGroupManager type. Just like the PersistentConnection class, it offers services for managing groups and sending messages to them using the following methods:

    • Add(), which we can use to add a client, identified by its clientId, to a group.

    • Remove(), to withdraw a specific client from a group.

    • Send(), which allows sending messages to clients that are members of specific groups.

Complete example: Monitoring connections at the server

To illustrate how to implement persistent connections from processes that are external to them, we will now give a complete example consisting of a system with which the server side “spies” the requests made to the website where this component has been installed.

The project consists of a website, implemented in just one page called default.aspx, which will change its content based on a parameter. Free browsing will be allowed, while the requests made by clients of the website will be able to be queried in real time by accessing a monitoring page called “spy.html”. We will achieve this by capturing the requests in the Application_BeginRequest event of the application global class (Global.asax) and sending a message from here to the clients connected to a persistent connection so that they can display the information. See Figure 6-3.

Note

The purpose of this example is simply to show an implementation of persistent connections used from external processes. Using this system can seriously penalize performance at the server, so it is not recommended at all to use it in production.

A screen shot showing three open browsers browsing the website developed in this example. The requests generated by them are shown in another browser in real time.

Figure 6-3. System for tracing connections in operation.

Project creation and setup

For the purpose of creating the application that we will develop over the following pages, it is necessary to first create a project of the “ASP.NET Web Application” type from Visual Studio 2013 and then select the “Empty” template to create a completely empty project[25]. The version of the .NET Framework used must be at least 4.5.

After we have created it, we must install the following package using NuGet:

PM> install-package Microsoft.AspNet.SignalR

Implementing the website

Page markup (default.aspx)

<%@ Page Language="C#" AutoEventWireup="true"
         CodeBehind="Default.aspx.cs"
         Inherits="ConnectionSpy.Default" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title><%: Title %></title>
    <script src="Scripts/jquery-1.6.4.min.js"></script>
</head>
<body>
    <h1><%: Title %></h1>
    <form id="form1" runat="server">
        <div>
            <p>This is the content of the <%: Title %></p>
            <h3>Navigation links</h3>
            <asp:PlaceHolder runat="server" ID="placeHolder">
            </asp:PlaceHolder>
        </div>
        <a href="Spy.html" target="_blank">
            Spy requests (new window)
         </a>
    </form>
</body>
</html>

Code-behind (default.aspx.cs)

using System;
using System.Text;
using System.Web.UI;
namespace ConnectionSpy
{
    public partial class Default : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            int id;
            this.Title = int.TryParse(Request["id"], out id)
                ? "Page " + id
                : "Home page";
            var html = new StringBuilder();
            html.AppendLine("<ul>");
            for (int i = 1; i < 11; i++)
            {
                var text = "Page " + i;
                var link = string.Format(
                    "<li><a href='Default.aspx?id={0}'>{1}</a></li>",
                    i, text
                );
                html.AppendFormat(link);
            }
            html.AppendLine("</ul>");
            placeHolder.Controls.Add(
                new LiteralControl(html.ToString())
            );
        }
    }
}

System for tracing requests (server side)

Persistent connection (ConnectionSpy.cs)

using Microsoft.AspNet.SignalR
public class ConnectionSpy: PersistentConnection
{
}

Note that the body of the persistent connection is empty. We will not need to take control in it when the SignalR clients connect to it, nor will we need to send messages to clients from here; this will be done from the application global class, as we shall see in the “Application global class (Global.asax.cs)” code.

Startup code (Startup.cs)

using Owin;
public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.MapSignalR<ConnectionSpy>("/spy");
    }
}

Application global class (Global.asax.cs)

using System;
using System.Web;
using Microsoft.AspNet.SignalR;
public class Global : HttpApplication
{
  private static IPersistentConnectionContext connSpy =
          GlobalHost.ConnectionManager.GetConnectionContext<ConnectionSpy>();
  protected void Application_BeginRequest(object sender, EventArgs e)
  {
        var context = ((HttpApplication)sender).Context;
        var message = string.Format(
            "{0}: Requested '{1}' from IP {2} using {3}",
            DateTime.Now.ToShortTimeString(),
            context.Request.Url.ToString(),
            context.Request.UserHostAddress,
            context.Request.Browser.Type
            );
        connSpy.Connection.Broadcast(message);
  }
}

We could also have implemented this same process as OWIN middleware, instead of doing it in the application global class. In that case, the module would be the following:

using System;
using System.Threading.Tasks;
using Microsoft.AspNet.SignalR;
using Microsoft.Owin;
public class SpyMiddleware : OwinMiddleware
{
    private static IPersistentConnectionContext  connSpy =
            GlobalHost.ConnectionManager.GetConnectionContext<ConnectionSpy>();
    public SpyMiddleware(OwinMiddleware next): base(next) { }
    public override Task Invoke(IOwinContext context)
    {
        var message = string.Format(
            "{0}: Requested '{1}' from IP {2} using {3}",
            DateTime.Now.ToShortTimeString(),
            context.Request.Uri.ToString(),
            context.Request.Host,
            context.Request.Headers["USER-AGENT"]
            );
            return Next.Invoke(context)
                       .ContinueWith(c =>
                           connSpy.Connection.Broadcast(message));
    }
}

And to enter this module in the request processing pipeline, we would have to modify the configuration code:

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.MapSignalR<ConnectionSpy>("/spy");
        app.Use<SpyMiddleware>();
    }
}

Optional changes in configuration (web.config)

For the tracing system to capture all the requests made to the website, whether or not they are processed by ASP.NET, it is a good idea to add the following configuration to the web.config file:

<configuration>
    ...
    <system.webServer>
      <modules runAllManagedModulesForAllRequests="true"></modules>
    </system.webServer>
</configuration>

Note that this configuration will make the performance at the server even worse.

System for tracing requests (client side)

Spying page (spy.html)

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Spying requests</title>
    <script src="Scripts/jquery-1.6.4.min.js"></script>
    <script src="Scripts/jquery.signalR-2.0.0.min.js"></script>
</head>
<body>
    <h1>Requests log</h1>
    <ul id="requests"></ul>
    <script>
        $(function () {
            var connection = $.connection("/spy");
            connection.received(function (data) {
                $("#requests").append("<li>" + data + "</li>");
            });
            connection.start();
        });
    </script>
</body>
</html>

External access using hubs

If we use hubs, the procedure is similar to what we saw earlier: in the process via which we want to be in contact with clients connected to a hub, we will use the global configuration object GlobalHost. Through its ConnectionManager property, we will obtain a reference to the hub.

The only difference compared to the procedure that we would carry out with persistent connections is that here we will obtain the reference to the hub using the GetHubContext() or GetHubContext<T>() methods.

protected void BtnShutdown(object sender, EventArgs e)
{
   var hubcontext = GlobalHost.ConnectionManager
                                 .GetHubContext<Chat>();
   hubcontext.Clients.All
   .SendMessage("The system is shutting down!");
   ... // Code
}

In the preceding example, we have used the generic method GetHubContext<T>() to obtain the reference using strong typing, although we could have also done it referencing the hub through a character string:

var hubcontext = GlobalHost.ConnectionManager
                              .GetHubContext("Chat");

In either case, the methods return an instance of IHubContext, through which we can access the functionalities for sending information and managing groups available in hubs (see Figure 6-4).

A screen shot showing the help provided by IntelliSense, where we see the members of the IHubContext interface: Clients, Equals(), GetHashCode(), GetType(), Groups, and ToString().

Figure 6-4. Members of the IHubContext interface.

As you can easily guess, the Clients property, of the IHubConnectionContext type, is the gateway to recipient selection methods, discussed in Chapter 5, although it is more limited. For example, because we will use this interface from other processes, we will not have properties such as Caller or Others, which make sense only when the code is inside a hub and it is being executed as a consequence of the reception of a message from a client. However, we will find other useful selectors such as All, AllExcept(), Client(), or Group() to specify the recipients of any code invocation on the client side:

hubContext.Clients.Group("jedis").Alert(
    "I felt a great disturbance in the force"
);

On its part, the Groups property allows managing the members of groups of SignalR clients, through its Add() and Remove() methods.

hubcontext.Groups.Add(lukeConnectionId, "jedis");

Complete example: Progress bar

To better illustrate the operation and possibilities of external access to hubs or persistent connections, we will present the complete development of a system that notifies the progress of expensive processes in real time, as shown in Figure 6-5.

A screen shot showing three browsers running the application independently. In each of them, we see the progress bar showing a different percentage of completion of the process.

Figure 6-5. Progress bars operating.

In this case, we will have a client page from which we will use jQuery to launch an AJAX request to a costly process written inside an ASPX page. There will be notifications of the progress in real time from inside this process, and this will be displayed as a progress bar over the page.

Project creation and setup

For the purpose of creating the application that we will develop over the following pages, it is necessary to first create a project of the “ASP.NET Web Application” type from Visual Studio 2013 and then select the “Empty” template to create a completely empty project[26]. The version of the .NET Framework used must be at least 4.5.

After we have created it, we must install the following package using NuGet:

PM> install-package Microsoft.AspNet.SignalR

Implementation on the client side

HTML markup (progress.html)

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Progress bar</title>
    <script src="Scripts/jquery-1.6.4.min.js"></script>
    <script src="Scripts/jquery.signalR-2.0.0.min.js"></script>
    <script src="/signalr/js"></script>
    <script src="Scripts/progressbar.js"></script>
    <link rel="stylesheet" href="styles/progressbar.css"/>
</head>
<body>
    <div id="progressBarContainer">
        <div id="progressBar"></div>
    </div>
    <input type="button"
              id="start" value="Start" disabled="disabled" />
    <div id="result" style="display: none;"></div>
</body>
</html>

Styles (Styles/ProgressBar.css)

#progressBarContainer {
    width: 400px;
    height: 18px;
    border: 1px solid black;
    padding: 2px;
    margin: 20px 0 20px 0;
}
#progressBar {
    width: 0px;
    height: 18px;
    background-color: blue;
    margin: 0;
    overflow: hidden;
    text-align: center;
    color: white;
    font-family: arial;
    vertical-align: middle;
    font-size: 14px;
}
#result {
    border: 1px solid black;
    background-color: yellow;
    padding: 10px 10px 0 10px;
    margin-top: 10px;
}
#result p {
    margin: 0 0 10px 0;
}

Script (Scripts/ProgressBar.js)

$(function () {
    var hub = $.connection.progressBarHub;
    hub.client.update = function (value) {
        $("#progressBar").css("width", value + "%")
                         .text(value + " %");
    };
    $("#start").click(function () {
        $(this).attr("disabled", true);
        $("#result")
            .hide("slow")
            .load("hardprocess.aspx?connId=" + $.connection.hub.id,
                     function () {
                      $(this).slideDown("slow");
                      $("#start").attr("disabled", false);
                  });
    });
    $.connection.hub.start()
        .done(function () {
            $("#start").attr("disabled", false);
        });
});

Implementation on the server side

Hub

using Microsoft.AspNet.SignalR;
namespace ProgressBar
{
    public class ProgressBarHub : Hub { }
}

Note that we do not need any method in the hub, because the information is sent to the clients through the external process, as shown in the following code example.

Expensive process (HardProcess.Aspx)

<%@ Page Language="C#"
         Inherits="System.Web.UI.Page" EnableSessionState="false" %>
<%@ Import Namespace="System.Diagnostics" %>
<%@ Import Namespace="System.Threading" %>
<%@ Import Namespace="Microsoft.AspNet.SignalR" %>
<%@ Import Namespace="ProgressBar" %>
<%
    Response.Expires = -1;
    var connectionId = Request["connId"];
    var hub = GlobalHost.ConnectionManager
                        .GetHubContext<ProgressBarHub>();
    Stopwatch stopWatch = Stopwatch.StartNew();
    // Simulate a very very hard process...
    for (int i = 1; i <= 100; i++)
    {
        hub.Clients.Client(connectionId).update(i);
        Thread.Sleep(150);
    }
%>
<p>The answer to life, the universe and everything is: 42.</p>
<p>
    And it only took <%:stopWatch.ElapsedMilliseconds / 1000 %>
    seconds to find it out.
</p>

This page receives the connection identifier as a parameter, which allows it to send the progress data only to the specific client that initiated the process.

Startup code (startup.cs)

using Owin;
public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.MapSignalR();
    }
}


[25] In Visual Studio 2012, we can achieve the same goal by creating a project from the template “ASP.NET Empty Web Application.”

[26] In Visual Studio 2012, we can achieve the same goal by creating a project from the template “ASP.NET Empty Web Application.”

..................Content has been hidden....................

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