SignalR lets us control several aspects of the connection lifetime on both the server and the client side. SignalR has the goal of delivering a simple experience based on the idea of a persistent connection, but we cannot really rely on the assumption that it will always be available, because too many factors may interfere with it. While it's true that SignalR does its best to isolate us from these kind of problems, thanks to smart connection strategies and recovery procedures, on the other hand we might need to have finer control over what's happening behind the scenes, and we might want to handle the anomalies and the reconnection procedures ourselves.
In this first recipe of the chapter, we'll use a Hub to illustrate how we can monitor when the connection state changes over time. Later, we will also write a simple JavaScript client in order to show how we can perform the same kind of checks on the client side.
Before writing the code of this recipe, we need to create a new empty web application project, which we'll call Recipe30
.
Let's build our project, concentrating first on the server-side code, using the following steps:
EchoHub
class. We just want to monitor the connection status and therefore will not need any specific method on our Hub. Let's make it look like the following code:using System.Diagnostics; using System.Threading.Tasks; using Microsoft.AspNet.SignalR; using Microsoft.AspNet.SignalR.Hubs; namespace Recipe30 { [HubName("echo")] public class EchoHub : Hub { } }
Startup
class, which we will create in the same way we did so far for most of the recipes. However, this time, we will specify a set of useful parameters inside the Configuration()
method, as shown in the following code:public class Startup { public void Configuration(IAppBuilder app) { GlobalHost.Configuration.DisconnectTimeout =TimeSpan.FromSeconds(30); GlobalHost.Configuration.ConnectionTimeout =TimeSpan.FromSeconds(110); GlobalHost.Configuration.KeepAlive =TimeSpan.FromSeconds(10); app.MapSignalR(); } }
The GlobalHost
object exposes a Configuration
member, through which we can change some default settings that influence how the connection handling is performed (the values in the sample code are the current defaults defined by SignalR). The values are described as follows:
DisconnectTimeout
: This asks SignalR to wait a maximum number of seconds after a transport connection is lost before raising a Disconnected
event, and terminating the failed logical connectionConnectionTimeout
: In case long polling is used, transport connections will be kept around for a maximum number of seconds waiting for a response; when that timeout expires with no activity, the client will be forced to open a new connectionKeepAlive
: For transports other than long polling, this regulates how often a keepalive packet is sent to avoid the underlying connection being dropped because of inactivity; this value must be no more than one-third of the DisconnectTimeout
value, and SignalR automatically adjusts this value accordingly to this rule if DisconnectTimeout
is specified without setting KeepAlive
explicitly afterwardsEchoHub
class to override a set of methods exposed by any Hub-derived type. Those methods will notify us about specific logical events happening on the connection. When we say logical event, we are talking in the context of the level of abstraction defined by SignalR on top of the actual events occurring at the transport or physical level on the network. Let's look at the following code:public override Task OnConnected() { Trace.WriteLine(string.Format("Connected: {0}",Context.ConnectionId)); return base.OnConnected(); } public override Task OnDisconnected() { Trace.WriteLine(string.Format("Disconnected: {0}", Context.ConnectionId)); return base.OnDisconnected(); } public override Task OnReconnected() { Trace.WriteLine(string.Format("Reconnected: {0}",Context.ConnectionId)); return base.OnReconnected(); }
As we can see, there are three methods that can be overriddenRedundant:
OnConnected()
: This is triggered only the first time a connection is established, and a new connection ID is assigned to the calling clientOnReconnected()
: This can be triggered several times, once each time a connection is reestablished by SignalR after a lower-level network disconnection, and this happens every time SignalR is able to rebuild the same logical connection on top of a potentially new underlying one; in this case, the connection ID does not changeOnDisconnected()
: This is triggered when a low-level disconnection occurs and SignalR is not able to establish it again before the DisconnectTimeout
interval has expired; the logical connection is dropped, and in order to contact the server again, the client will have to start a new connection, thus receiving a new connection IDInside our method implementations, we just trace the name of the event and the connection identifier involved, and then call the base implementation.
Let's now build a client we will use to test our code. Both the JavaScript (the one we are going to use) and the .NET client libraries have similar connection control features. In our code, we will use them to perform the same kind of checks we've been performing on the server side.
index.html
, which we'll use to build our client, and where we'll reference the usual JavaScript files that have already been added to the project when the Microsoft ASP.NET SignalR package has been referenced by adding EchoHub
. Somewhere in the page body, we'll also add a button that we'll use to toggle a connection to the server and test our control code, as shown in the following code:<script src="Scripts/jquery-2.1.0.js"></script> <script src="Scripts/jquery.signalR-2.0.2.js"></script> <script src="/signalr/hubs"></script> ... <button id="toggle" disabled="true">Toggle connection</button>
<script> $(function() { var hub = $.connection.hub, echo = $.connection.echo; ... }); </script>
hub
variable, we can supply a set of callback functions to be called when specific connection events happen:hub.starting(function () { console.log('starting'), //no hub.id yet! }); hub.reconnecting(function () { console.log('reconnecting: ' + hub.id); }); hub.reconnected(function () { console.log('reconnected: ' + hub.id); }); hub.disconnected(function () { console.log('disconnected: ' + hub.id); }); ...
The names and their corresponding meanings are straightforward. Because a connection is always initiated by the client, we have a starting
event and a reconnecting
event available, which are client-only events with no matching counterpart on the server side.
hub.stateChanged(function (state) {
console.log('stateChanged: ' + hub.id);
console.log(state);
$("#toggle").attr('disabled',state.newState !=
$.signalR.connectionState.connected &&state.newState !=
$.signalR.connectionState.disconnected);
});
...
In the callback supplied to the stateChanged()
method, we can output the current state whenever it changes, and we enable or disable the toggle
button accordingly. The possible connection states are defined as properties exposed by the $.signalR.connectionState
member declared inside the referenced jQuery.signalR-(version).js
file.
echo.client.dummy = function() { }; ...
Why this? In this example, we are using the Hubs API with dynamic proxies because it's the most common, simple, and straightforward way to use SignalR. However, as of today, the previously mentioned server-side events are not generated if there are no client-side callbacks available. We need to have at least one of them in place, even if we will never call it; that's why we added the dummy one. It's worth making clear that, although it's something that we do on the client side, this is not necessary to make the client-side events work. It's there to make the server-side ones work, and there's no need to call that method from the server at all. Of course, if you already have proper client-side callbacks to be invoked from the server side, those will be just fine with no need for a dummy one, and the client-side events will work just fine. It's a curious and quite strange thing to do, but that's how it currently works and it might be improved in future versions.
var starter = function() { return hub .start() .done(function () { console.log('connected: ' +hub.id); }); }; $("#toggle").click(function () { $("#toggle").attr('disabled', true); if ($.signalR.connectionState.disconnected == echo.connection.state) starter(); else if ($.signalR.connectionState.connected == echo.connection.state) hub.stop(); }); starter();
It's interesting to observe the usage of the stop()
member exposed by hub
to explicitly and gracefully end a connection. We never explicitly mentioned it before because it's quite a rare thing to do, but it's good to know that it's possible in case you need it.
We are ready to test the code that we just wrote by launching the application and navigating to the index.html
page. Each time we'll click on the toggle connection button, we'll alternatively turn the connection on and off, and these actions will trigger the client- and server-side events that we hooked into with our code. It will be interesting for you to try breaking the network connection between the browser and the web server in any way you may want too, and observe the reconnection events happening automatically according to the timeout values we have set earlier.
What happens under the hood is pretty smart and complex, and the details are out of the scope of this book. That said, it's important to understand that each time a persistent connection is created, SignalR tries its best to keep it alive despite any possible lower-level network disruption, hiding these kind of problems from us. Whenever these attempts fail and SignalR decides that the connection is lost, it will not try to fix it anymore; the only way for the client to connect again will be to perform a full connection restart, hence generating a new connection ID. This is one of the reasons why the action of associating any state information to a connection identifier, which is one of the most common needs, it's definitely possible but should be done in full awareness of these implications. You will have to write defensive code that is aware of the fact that the connection ID might go away anytime.