Developing SignalR Applications Using Persistent Connections - Pro ASP.NET SignalR: Real-Time Communication in .NET with SignalR 2.1 (2014)

Pro ASP.NET SignalR: Real-Time Communication in .NET with SignalR 2.1 (2014)

Chapter 4. Developing SignalR Applications Using Persistent Connections

This chapter shows you how to develop applications using persistent connections. We define a persistent connection is and show you how it can be configured. The next step is to see the communication and event signaling that occurs between the server and client. You will explore this communication with a persistent connection example with the JavaScript client. Finally, we discuss groups and how they can be used with persistent connections.

Here is a brief list of the topics covered in this chapter:

· How to configure persistent connections

· Server communication to clients over persistent connections

· Signaling between server and clients

· Example using the JavaScript client

· Groups

What Is a Persistent Connection?

A persistent connection is a communication channel between a server and a client that is kept open to facilitate secure, robust, low-latency, and full-duplex communication. The channel is identified by a unique connection ID. The channel is provided by one of a variety of transports that have logic to give the illusion that the connection is always persisted.

Properties of a Persistent Connection

There are four key properties that make persistent connections ideal for many implementations. These properties are the following:

· Robust connection

· Full-duplex communication

· Low latency

· Secure communication (optional)

The following sections discuss each property and the benefits it provides.

The first property is the robust connection, which ensures that a connected connection stays connected. If it is disconnected, it raises an event so that corrective action may be taken. The mechanisms that a persistent connection has to keep it robust are keep-alive packets, disconnect timeout and connection timeout monitors, reconnect logic, and connection state event notification. The keep-alive packets keep the channel “warm,” which prevents routers and switches from prematurely closing a connection because of lack of data movement. The keep-alive packets also provide a heartbeat for the connection. This heartbeat updates the last update time, which is used to check for disconnect timeouts, which are used to detect connections that were terminated but have not signaled that they were disconnecting. Once the disconnected timeout has occurred, the connection has logic to reconnect. The reconnect logic attempts to reconnect with the same connection ID that returns the connection to the state before it disconnected. The connection timeout is used to provide new connections for the long polling transport because it does not receive keep-alive packets.

The second property is full-duplex communication, which allows communication to occur bidirectionally and asynchronously. Each connection has the capability to send and receive data. Depending on the transport, there may be one or two channels to provide full-duplex communication. The Web Sockets transport allows one channel to be created for full-duplex operation. The other transports require two channels, one for sending and one that receives.

The third property is low latency, which allows the connection to be real time or near real time. The low-latency property is ideal for applications that need to be responsive without having to deal with connection handshaking. The latency is different for most of the transports, and the transport with the lowest latency in both directions is Web Sockets. This transport keeps one channel open to communicate both ways so it does not need to complete a connection handshake after it has been connected. The ServerSendEvents and ForeverFrame transports have to complete the receive handshake only once and keep the receive channel open. However, the send channel has to be re-created for every message sent, which adds latency to every send for the connection handshake. Long polling, which has the worst latency, has to do a connection handshake everyconnection timeout interval, after receiving data from the server, or any time it needs to send data upstream to the server.

The final property, secure communication, enables safe and trusted communication over the connection. This property is optional, depending on the implementation chosen. Secure communication for a persistent connection may be provided by being encrypted by SSL and/or secured by using encrypted tokens for the connection and group IDs. Any time a connection or group token is transmitted to the user, it is encrypted by the server. The server encrypts the token based on the authenticated user. These secure tokens prevent attackers from forging requests for a connection ID or joining groups that it does not have permission to join.

How Persistent Connection Works

To the user, a persisted connection always seems connected, but SignalR has logic in place for the multiple phases of a connection: connecting, maintaining, and disconnecting. For a persistent connection, the connection phase consists of the following steps:

1. The client sends a negotiation request.

2. The server responds to the negotiation request with a payload of negotiation properties.

3. The client uses the payload to negotiate the best transport option.

4. The client sends a connect request with the negotiated transport.

5. Once the server has accepted the connect request, the persistent connection is made.

After the connection is made, the following steps are taken simultaneously to keep maintaining the connection:

· Retrieve any data that is on the server

· Send any data that is pending to be sent to the server

· Retrieve and acknowledge keep-alive packets or reconnect after polling timeout

Finally, when a connection is no longer needed and goes into the disconnecting phase, there is separate logic on the client and server. For the client, it sends an abort command and then closes the connection. If the server receives the abort command, it cleans up the connection. If the server does not receive the abort command, there is a timeout that fires to clean up any connections in which the abort message was missed. Later in the chapter, you will learn more about the way various aspects of the persistent connection work.

Using a Persistent Connection Instead of a Hub

When determining whether to use a persistent connection or a hub, keep the following few factors in mind:

· Message format

· Communication model

· SignalR customization

Depending on the application, these factors can have varying degrees of impact on the decision. To demonstrate the differences, we show partial examples of persistent connection and hub. The examples can request the time or broadcast a message. Although the implementations are slightly different, they demonstrate the differences well.

The persistent connection example is shown first, with the server shown in Listing 4-1 and the client shown in Listing 4-2.

Listing 4-1. Persistent Connection Server Example

public class TestPersistentConnection : PersistentConnection
{
protected override Task OnReceived(IRequest request, string connectionId, string data)
{
return (data.StartsWith("GetTime")) ? Connection.Send(connectionId, "Time:" + DateTime.Now.ToString()) : Connection.Broadcast(data);
}
}

Listing 4-2. Persistent Connection Client Example

var connection = $.connection('/TestPC');
connection.received(function (data) {
var messageData = '';
if (data.indexOf('Time:') > -1) { messageData = 'The time is: ' + data.substring(5); }
else { messageData = data;}
$('#messages').append('<li>' + messageData + '</li>');
});

connection.start().done(function () {
$("#send").click(function () {
connection.send($('#data').val());
});
$("#getTime").click(function () {
connection.send('GetTime');
});
});

Next is the hub example, with the server shown in Listing 4-3 and the client shown in Listing 4-4.

Listing 4-3. Hub Server Example

public class TestHub : Hub
{
public void BroadcastMessage(string message)
{
Clients.All.SendMessage(message);
}
public void GetTime()
{
Clients.Caller.SendTime(DateTime.Now.ToString());
}
}

Listing 4-4. Hub Client Example

var connection = $.hubConnection();
var hubProxy = connection.createHubProxy('TestHub');
hubProxy.on('SendMessage', function (data) {
$('#messages').append('<li>' + data + '</li>');
});
hubProxy.on('SendTime', function (data) {
$('#messages').append('<li>' + 'The time is: ' + data + '</li>');
});
connection.start().done(function () {
$('#send').click(function () { hubProxy.invoke('BroadcastMessage', $('#data').val()); });
$('#getTime').click(function () { hubProxy.invoke('GetTime'); });
});

The first area to look at (with no focus on importance) is the message format. In persistent connections, you are responsible for parsing and tokenizing the data that goes back and forth; in hubs, this message format is already handled. As shown in Listing 4-5, the data payload for a persistent connection is very simple, but it may be complicated to parse on the server. On the other hand, looking at the data payload in Listings 4-6 and 4-7 for hubs, the message is in a format that the hub logic parses automatically into static types on the server.

Listing 4-5. Request Body of a Persistent Connection with the Data “Hello”

data=Hello

Listing 4-6. Request Body of a Hub BroadcastMessage function with the “Hello” Parameter

data=%7B%22H%22%3A%22testhub%22%2C%22M%22%3A%22BroadcastMessage%22%2C%22A%22%3A%5B%22Hello%22%5D%2C%22I%22%3A1%7D

Listing 4-7. Decoding of Listing 4-6

Data= {"H":"testhub","M":"BroadcastMessage","A":["Hello"],"I":1}

Another aspect of the message format is the size of the message. If the size is very important to the application, the persistent connection has the advantage of having smaller payloads. You can see in Listings 4-5 and 4-8 for persistent connections that the data payloads are considerably smaller than the hubs in Listings 4-7 and 4-9.

Listing 4-8. Persistent Connection Response to the GetTime Function

{"C":"d-4E3C7594-B,4|L,2|M,0","M":["Time:4/26/2014 2:00:00 AM"]}

Listing 4-9. Hub Response to the GetTime function with a call to the SendTime function

{"C":"d-2CF99ADA-E,0|I,1|J,1|K,0","M":[{"H":"TestHub","M":"SendTime","A":["4/26/2014 2:00:00 AM"]}]}

Next is the communication model of each of the APIs. For persistent connections, this model closely resembles the connection model, which usually has one function for sending and one function for receiving on each end of the connection. A hub abstracts this model and presents aremote procedure call (RPC) model, which provides many functions with unique function signatures on either the client or server. Look at the examples provided earlier in the chapter to see how they fit into their respective models.

In the persistent connection server example shown in Listing 4-1, there is only one function that receives requests: OnReceived. And for sending data to the client, there is only one function: Send. Even though the Broadcast function is shown in the example, it calls the Sendfunction internally.

The persistent connection client example shown in Listing 4-2 also provides only one function to send and one function to receive. The received function calls the callback function, which has logic to determine what to do with the payload that is received. The Send function is called with different input data, depending on what type of request is being made to the server.

For a hub example, look at Listing 4-3, in which there are two functions provided by the server: BroadcastMessage and GetTime. These functions take one and zero parameters, respectively, and make calls to unique functions on the clients.

The hub client example in Listing 4-4 shows the RPC model with different functions that are callable from the server and has logic to invoke different functions on the server. Look at the invoking calls: the BroadcastMessage function takes one parameter, and the GetTime function takes zero parameters. The client also provides two functions (SendMessage and SendTime) that are for receiving message data and the time, respectively.

Finally, depending on the customization that is to be done to the SignalR classes, it is easier to extend and customize persistent connection classes. Hubs are built on top of the persistent connection APIs, so they are more rigid and present more challenges to customize. Many of the components of persistent connections and hubs are swappable using the dependency resolver. Although these components are changeable, the hub classes have shared hub classes that you might want to change (for example, creating a customized encrypted data parser for incoming data for specific endpoints). For persistent connections, you can override the OnReceived method with your custom encryption for that connection. It is much more difficult for hubs, considering that they have logic to bind to static types to which modifying could affect all the hubs.

How to Configure Persistent Connections

Depending on the type of application that you are writing, sometimes you need to configure the persistent connection to be tailored to your application. There are many options available to configure the persistent connection. The first required configuration is the route configuration, so that your persistent connection is registered to the correct endpoint. Another critical piece that can be configured is the supported transports. Other properties can be configured using the OWIN properties that a connection uses. If the configuration does not provide everything you need, the persistent connection can be extended with custom classes. (Extending with custom classes is discussed in Chapter 7).

Persistent Connection Route Configuration

To create a persistent connection class, derive it from the PersistentConnection class, as demonstrated in Listing 4-10.

Listing 4-10. The PersistentConnection Class Deriving from a PersistentConnection

public class TestPersistentConnection : PersistentConnection

Although you have created the persistent connection class to access it, you must configure the binding to a route. Under IIS and self-host applications, you create this mapping in the Startup.cs file.

Mapping Routes in Startup.cs

Mapping routes should occur in the Startup.cs file. If the file doesn’t exist, you can add it by adding a file of type OWIN startup class. Once you have the file, you configure the mapping in the Configuration function using the IAppBuilder interface. You can use theIAppBuilder to register all your OWIN middleware components, including the PersistentConnection and Hub classes.

The easiest way to map a connection is to use an extension method provided by SignalR (see Listing 4-11). The example maps the TestPersistentConnection class shown in Listing 4-10 to the path TestPC. So if your host were http://localhost, you could access theTestPersistentConnection at http://localhost/TestPC.

Listing 4-11. Example of Mapping a Route in Startup.cs

public void Configuration(IAppBuilder app)
{
app.MapSignalR<TestPersistentConnection>("/TestPC");
}

Note that the order in which routes are added is the order used in matching a route. Beyond route configuration, there are other areas of a persistent connection that can be configured, such as the connection timeouts and Web Sockets support discussed in the next couple of sections.

Global Timeout and Keep-Alive Configurations

The GlobalHost class provides a static property that exposes an IConfigurationManager interface that can be used to set the connection timeout, disconnect timeout and keep-alive interval settings.

The ConnectionTimeout property is the amount of time that a connection remains open without receiving data. After this timeout, the connection is closed, and another connection is opened. The default ConnectionTimeout is 110 seconds; this property is used only by the long polling transport.

The DisconnectTimeout property is the amount of time to wait after a connection goes away before raising the disconnect event. The default DisconnectTimeout is 30 seconds whenever the DisconnectTimeout is set; the KeepAlive property is set to 1/3 of the value set.

The KeepAlive property is the amount of time between the sending of keep-alive messages. This property is set to 1/3 the value of the DisconnectTimeout property by default, except for the long polling transport, for which it is set to null. If the value is set to null, theKeepAlive property is disabled; if it is set to a value, the minimum value must be at least 2 seconds, and the maximum value is 1/3 of the DisconnectTimeout.

Image Note To configure the DisconnectTimeout and KeepAlive settings, you must set the DisconnectTimeout first, or else an invalid operation exception will be thrown.

HostContext Configuration

The HostContext is created for every request that comes into a persistent connection. The configuration can be updated using HostContext and overriding the Initialize method in the PersistentConnection derived class.

SupportsWebSockets

This property provides a flag to the clients to tell them whether a connection supports Web Sockets. The property can be set by setting the key HostConstants.SupportsWebSockets in the HostContext Items collection (see Listing 4-12). Set the value of true to flag for the client to attempt to use Web Sockets or to false to skip the attempt to use the Web Sockets transport.

Listing 4-12. Example of Setting SupportsWebSockets

public override void Initialize(IDependencyResolver resolver, HostContext context)
{
context.Items[HostConstants.SupportsWebSockets] = true;
base.Initialize(resolver, context);
}

Image Note The expected object type for this value is a Boolean. If SupportsWebSockets were to be set using true as a value, the code would throw an exception.

WebSocketServerUrl

The WebSocketServerUrl property provides the client with an override server URL to call for Web Sockets connections. This property can be set by setting the key HostConstants.WebSocketServerUrl in the HostContext Items collection to set the value of theWebSocketsServerUrl (see Listing 4-13).

Listing 4-13. Example of Setting WebSocketServerUrl

public override void Initialize(IDependencyResolver resolver, HostContext context)
{
context.Items[HostConstants.WebSocketServerUrl] = "ws://localhost:8219";
base.Initialize(resolver, context);
}

Server Communication to Clients Over Persistent Connections

Persistent connections have a set of communications that occurs between the client and server to initialize and maintain the connection, and to send and receive data. The communications start with a negotiate request to determine which transports are available on the server. There is a set of logic that each client has to determine which transport is the best. Once the transport has been agreed on, the connect communication sets up an upgraded socket for Web Sockets or a receiving channel for the other transports. When data needs to be sent to the server, and the transport is not Web Sockets, the send communication is used to send data.

Because the long polling transport is not as reliable, it has two communication methods. The first is the ping method, which determines whether the server is available; the second is the poll method, which is used to keep an open receive channel. Finally, for any transports that want to close their connection, there is an abort communication that terminates the connection.

Negotiation

The negotiation is the first SignalR-based communication that occurs between the server and client. In this first phase, the server receives a request ending in /negotiate. In the processing of this negotiate request, the server generates the ConnectionId and ConnectionToken for that connection. This process also returns a payload of server properties, which are returned as a JSON payload. If the negotiation is a JSONP request, the payload is returned with a callback.

Negotiation Properties

The negotiation properties are returned in the payload from the negotiate request (see Listing 4-14). Let’s take a look at each property and see what they are used for.

Listing 4-14. Example of Negotiation Properties Payload

{"Url":"/SamplePC","ConnectionToken":"Udy6quBS2y3yQpElIQKg3memfXI56A4tdBqzwTNLB2jQND0z2YYVFGwpFJKxjCrF81t+p0IItZKoOuqcU7ZlWNwLnPJfod7E9fuBK1gEIb6UTfNhFiFSEt4dTEfDi1Z0","ConnectionId":"6a246327-fd16-4a90-8a76-e87ef5d14642","KeepAliveTimeout":20.0,"DisconnectTimeout":30.0,"TryWebSockets":true,"ProtocolVersion":"1.3","TransportConnectTimeout":5.0}

URL

The URL property is the relative URL to the persistent connection endpoint. It is used only by the JavaScript SignalR client library.

ConnectionId

The ConnectionId property is generated by the .NET Framework Guid.NewGuid() function, formatted with dashes. ConnectionId is a critical key that is used to identify the connection.

ConnectionToken

The ConnectionToken property is generated by appending together ConnectionId, a colon, and the user identity. The user identity is the current request’s username provided by the .NET Framework or an empty string. This token is then encrypted before being sent to the user.

KeepAliveTimeout

The KeepAliveTimeout property is the value specified in the IConfigurationManager for KeepAlive. (More information about KeepAlive was discussed previously in the section called “Global Timeout and Keep-Alive Configurations.”)

DisconnectTimeout

The DisconnectTimeout property is the value specified in the IConfigurationManager for DisconnectTimeout. (More information about DisconnectTimeout was discussed previously in the section called “Global Timeout and Keep-Alive Configurations.”)

TryWebSockets

The TryWebSockets property value is returned true if the TransportManager supports Web Sockets, the ServerRequest is of type IWebSocketRequest, and the OWIN SupportsWebSockets environment variable is true. The TransportManager check is true if the transport name webSockets is present in the collection of transport names. If the SignalR library is built with .NET 4.5, the ServerRequest object derives from IWebSocketRequest; otherwise, it derives from the IRequest class. The third check, which is a little more complex, checks HostContext for the supportsWebSockets entry that is determined in the Invoke method of the call handler. It is based on the websocket.Version key being present in the server.Capabilities environment variable passed into the OWIN Invoke function.

WebSocketsServerUrl

By default, this property is null. The property value is determined from the HostConstants.WebSocketServerUrl property in the HostContext Items collection.

ProtocolVersion

ProtocolVersion is the current version of SignalR, which is provided so that the clients can maintain compatibility. As of the time of this writing, the current version is 1.3.

TransportConnectTimeout

TransportConnectTimeout is the amount of time in seconds that a client should allow before trying another transport or failing.

Client Negotiation

Once the negotiation payload has been processed, the client has enough information to determine which transport it can use to connect to the server. The client first looks at its list of supported clients; if the list contains Web Sockets, it evaluates the TryWebSockets parameter of the negotiate payload to see whether the server supports Web Sockets.

If it is not supported, the next two transports that a client has in its list of transports are usually ServerSendEvents and ForeverFrame, respectively. The client checks its compatibility with each transport to see whether it is supported. If not satisfactory, the last transport tried is the long polling transport, which is usually the last supported transport in the clients list. Because there are no other transport options left to check, the client then throws an error, and no connection is made.

Ping

The ping request is one of the simplest client-server communications. This request is initiated only by the long polling transport when using the JavaScript client. The request does a simple get-to-the-base URL with /ping appended. The response from the server is a very basic JSON data payload. The JSON object is a single variable "Response" with a value of "pong" that is verified by the client (see Listing 4-15).

Listing 4-15. Example of a Response from a Ping Request

{"Response":"pong"}

Connect

Once the client has found the most appropriate transport, it sends a connect response. For Web Sockets, the request is a transport of “websockets” and the connection token provided in the negotiation. The Web Sockets connection is returned with an HTTP status of 101, which means that the response has been upgraded. For all other transports, the connection include the transport and the connection token.

The transport signifies which type of transport it is sending for (ServerSendEvents, ForeverFrame, or long polling). The connection token is the encrypted token for the connection. For the non–Web Sockets transports, this connection is the listening channel until the connection is reconnected for a timeout or poll.

Send

The send request is a post for all the transports besides Web Sockets, which the send occurs on the channel so a new request is not created. The send command contains the transport and connection token in the header, and data in the body. The transport signifies which type of transport it is sending for (ServerSendEvents, ForeverFrame, and long polling). The connection token is the encrypted token for the connection. The data section of the body is the value of the object that was sent. In Listing 4-16, the data sent is User A:Hello.

Listing 4-16. Example of Sending Hello from User A

data=User+A%3A+Hello

Poll

Poll is a get request that is used only when the transport is long polling. The parameters of the get request are transport, connection token, message ID, and the optional group token. The transport signifies which type of transport it is sending for, but for this request only long polling is supported. The connection token is the encrypted token for the current connection. The optional group token is present when the connection is a member of one or more groups and contains the keys to those groups. The message ID is simply the message ID of the current poll request.

The data that is returned from the poll is from a class called PersistentResponse. This class contains several properties that affect the connection:

· Messages: An array of messages being sent to the client.

· Disconnect: An indicator that the connection has received a disconnect command.

· TimedOut: An indicator that the connection timed out.

· GroupsToken: An encrypted token of the list of groups the connection is a member of. GroupsToken is null if the connection is not part of a group.

· LongPollDelay: The length of time the client should wait before reconnecting if no data was received. LongPollDelay is null for any transport other than long polling.

· Cursors: A special data set that contains a string of the minified event keys and IDs in a hexadecimal format. The cursors represent the message ID, but are encoded by converting the values to numbers.

The JSON representation of PersistentResponse is written in the format key: value with a comma separating the key value pairs. If the Disconnect, TimedOut, GroupsToken, or LongPollDelay properties do not have a value or are false, they are not included in the JSON response. Table 4-1 is a map from key to property in the JSON response.

Table 4-1. Relation Table of Key to Property PersistentResponse JSON Object

Key

Property

C

Cursors

D

Disconnect

T

TimedOut

G

GroupsToken

L

LongPollDelay

M

Messages

The response of the poll is JSON data returned from the PersistentResponse class. An example of the response is shown in Listing 4-17. This data is evaluated by the client to determine the connection’s current state. If the transport is long polling, it inspects this data for the L key to see whether it contains a numeric value for a new polling delay. If the D key is present, the client interprets it as a disconnect signal from the server and issues a stop locally. If the C key is present, clients set their message ID to the cursor data provided. When the G key is present, the client updates its list of groups. Finally, the client goes through each message provided under the M key. For each message, it triggers the client OnReceived command if appropriate.

Listing 4-17. Example of the Persistent Response Received on a Poll

{"C":"d-C16EF02C-B,1|C,1|D,0","M":["User A: Hello"]}

Abort

The abort request is sent when the connection is being terminated by the client. Whenever the stop method is issued on the connection or (with a JavaScript client) the web page is navigated away from, the abort command is issued. The abort request is posted for all transports besides Web Sockets, in which the abort occurs on the channel so a new request is not created. The abort command contains transport and connection token in the header. The transport signifies which type of transport it is sending for (Web Sockets, ServerSendEvents, ForeverFrame, or long polling). The connection token is the encrypted token for the current connection.

Signaling Between Server and Clients

Throughout the life cycle of a persistent connection, signals occur on the server and client that represent events affecting the connection. Although the signals for the server and client are similar, they are usually used differently. These events signal the state of a connection, removing the need to poll each connection to determine its current connection state.

Server-side Events

Server-side events are events raised on the server that can be generated by any connection or the server if it realizes that a connection is no longer available.

OnReceived

The OnReceived event, which is one of the most important, occurs when data is received from a persistent connection. On this event, the message is decoded, and the choice of data distribution occurs. Depending on the application, you can choose to broadcast, send to a group, or consume the data without redistributing.

OnConnected

The OnConnected event occurs when a new connection is made. On this event, the logic to add a user to a group can be added. Logic can also be added to maintain a user presence of being logged in. Depending on the type of transport, the connection can be logged in very frequently. So instead of directly displaying whether the user is currently logged in, displaying a last-logged-in time might provide a better experience.

OnDisconnected

The OnDisconnected event occurs when a connection disconnects, either through sending an abort command or the server realizes that the connection is no longer available. On this event, the logic to remove users from groups is added. Logic can also be added here to complement the logic added to the OnConnected event to maintain the user state by knowing when the user is disconnected. Depending on the transport used, updating the user state based on the connect and disconnect state might provide a bad experience, so maybe OnConnected should be used only to determine the last-logged-in time.

OnReconnected

When a connection is reconnected after a timeout using the same connection ID, the OnReconnected event is raised. On this event, logic can be added to see whether the client is in the correct state because of missing data during the timeout.

OnRejoiningGroups

The OnRejoiningGroups event occurs when a connection reconnects after a timeout to determine which groups should be rejoined automatically. On this event, you might have additional logic check to see whether the connection should be added back to a group instead of automatically adding to the groups that it had before the connection timed out.

AuthorizeRequest

The AuthorizeRequest event occurs before every request to authorize the user. On this event, you can add customized logic that returns a Boolean value whether the client is authorized to use the persistent connection and/or requested resource that is specified in the request object.

Client-side Events

Client-side events are events that are raised on the client for the persistent connection. These events signal how the connection has changed or data has arrived from the server. These events signal that a connection is starting, reconnecting, or closed. They also signal when there is an error, when new data is available from the server to the connection, when connection has slowed, or when it is changing connection state.

Received

The Received event, which is one of the most important, is raised when the connection has received data from the server. The event is called with one parameter that contains the data that the server has sent.

Error

The Error event occurs when the connection has encountered an error; generally this returns for errors in creating the connection. The event is called with one parameter that might not have data on the reason why the error was generated.

Closed/Disconnected

The Closed/Disconnected event occurs when the connection is stopped. The event name is dependent on the client that is being used.

Reconnecting

The Reconnecting event occurs when the connection starts reconnecting after a connection interruption. While the connection is reconnecting, the connection is unavailable for use.

Reconnected

Once a connection has been reestablished after a timeout, the Reconnected event signals that the connection is available for use again.

StateChanged

This event occurs when the connection state changes. There are four connection states: connecting, connected, reconnecting, and disconnected. There are seven possible state transitions (see Figure 4-1).

image

Figure 4-1. ConnectionState state diagram

ConnectionSlow

This event occurs when the connection has crossed more than two-thirds of the disconnect timeout without a keep-alive message being received. Once the event has been fired, it fires again only if a keep-alive message is received before the connection times out.

OnStart

Used by all the transports, but are exposed as events only in the SignalR JavaScript library. This event occurs once the Start function has been called by the client.

OnStarting

Used by all the transports, but are exposed as events only in the SignalR JavaScript library. This event occurs after a successful negotiate request is made.

Communication and Signaling Example Using a JavaScript Client

To demonstrate the communication and signaling that occurs between a server and client, we show a JavaScript example. This example focuses heavily on the signaling that occurs on the client when events are raised. Some of these events, such as the connection state, are exposed on the client UI to get a visual feel of what is going on in the application.

Server Code for Client Example

We reuse the persistent connection server example from Chapter 2. It is a brief overview; if more detail is needed, please revisit Chapter 2.

1. Create a new ASP.NET web application using the model-view-controller (MVC) template.

2. Run the following command in the Package Manager Console to install the necessary SignalR files: Install-Package Microsoft.AspNet.SignalR.

3. Create a PersistentConnections folder.

4. Add a new class to the PersistentConnections folder called SamplePersistentConnection.

5. Update the new class to look like Listing 4-18. Add any missing using statements.

Listing 4-18. PersistentConnection Sample Code

public class SamplePersistentConnection : PersistentConnection
{
protected override Task OnReceived(IRequest request, string connectionId, string data)
{
return Connection.Broadcast(data);
}
}

6. Add the code in Listing 4-19 to Startup.cs after the ConfigureAuth statement to register the PersistentConnection. Add any missing using statements.

Listing 4-19. Registering PersistentConnection Route

app.MapSignalR<SamplePersistentConnection>("/SamplePC");

Now that the server is created, the next step is to create the client, which is discussed in the next section.

JavaScript Client Example

Chapter 2 showed an example of JavaScript-based persistent connections; here, we expand the sample to show the client-side events in action. As was done before, an HTML page should be added to the project.

1. Add the scripts shown in Listing 4-20 to the head section of the HTML page.

Listing 4-20. Javascript Sample Client Script Code

<script src="/scripts/jquery-1.10.2.js" type="text/javascript"></script>
<script src="/scripts/jquery.signalR-2.0.3.js" type="text/javascript"></script>

<script>
$(function () {
var connection = $.connection('http://localhost:####/samplepc');

connection.received(function (data) {
$('#messages').append('<li>' + data + '</li>');
});

connection.connectionSlow(function () {
$('#connectionStatus').html('Connection slowed');
});
connection.disconnected(function () {
disableChat();
});
connection.error(function (errorData) {
console.log(errorData);
});
connection.reconnected(function () {
enableChat();
});
connection.reconnecting(function () {
disableChat();
});

connection.stateChanged(function (states) {
var oldState = states.oldState;
var newState = states.newState;
var connectionStatus = '';
switch (newState) {
case $.connection.connectionState.connected:
connectionStatus = 'Connected';
enableChat();
break;
case $.connection.connectionState.connecting:
connectionStatus = 'Connecting';
break;
case $.connection.connectionState.reconnecting:
connectionStatus = 'Reconnecting';
break;
case $.connection.connectionState.disconnected:
connectionStatus = 'Disconnected';
break;
}
$('#connectionStatus').html(connectionStatus);
});

connection.starting(function () {
console.log('Successful negotiation request');
});

$("#btnSend").click(function () {
connection.send($('#name').val() + ': ' + $('#message').val());
});

$("#btnConnect").click(function () {
connection.start();
});

$("#btnDisconnect").click(function () {
connection.stop();
});

function enableChat() {
$("#btnSend").removeAttr("disabled");
$("#btnDisconnect").removeAttr("disabled");
$("#btnConnect").attr("disabled", "disabled");
}

function disableChat() {
$("#btnSend").attr("disabled", "disabled");
$("#btnDisconnect").attr("disabled", "disabled");
$("#btnConnect").removeAttr("disabled");
}
});
</script>

Image Note The JQuery and SignalR library references might need to be updated in the script to the current version supplied by the NuGet package installation. The port that the server is running on needs to be replaced in the script where the #### are.

2. Add the code in Listing 4-21 to the body section of the HTML page.

Listing 4-21. JavaScript Sample Client HTML

<button id="btnConnect" >Connect</button>
<button id="btnDisconnect" disabled="disabled">Disconnect</button>
<label id="connectionStatus">Disconnected</label>
<ul id="messages" style="border: 1px solid black; height: 250px; width: 450px; overflow:scroll; list-style:none;"></ul>
<label>Name: </label>
<input id="name" value="User A" />
<label>Message: </label>
<input id="message" />
<button id="btnSend" disabled="disabled">Send</button>

Once the example is complete, you should see something similar to Figure 4-2.

image

Figure 4-2. JavaScript SignalR client interface

This example showed you the various connection states of a persistent connection. The next section discusses groups, which provide the grouping of connections based on a common factor.

Connection Grouping

When your application needs more than one concurrent persistent connection per user or communication, it has to go out to a group of people. You can use groups to accomplish this. The group management and membership can be controlled by simple interfaces provided in thePersistentConnection class. Depending on where the group data is persisted, the group information may be only on the server for the lifetime of the application or it may be stored in an out-of-process store that will live beyond the lifetime of the application.

GroupManager

GroupManager provides group management for the persistent connection. The GroupManager provides three functions: Send, Add, and Remove. These functions provide the base functionality to communicate and manage the group.

Send Function

The group Send function sends the data locally to any connection IDs that are connected to the local server and then publishes a message bus to send the message to groups that may exist on other servers that are connected via a message bus.

Add Function

The group Add function is responsible for adding a user to a group. It also creates the group if it does not exist and publishes an add command to the other servers that are connected via a message bus.

Remove Function

The group Remove function is responsible for removing a user from the group. It also removes the group if there are no more connections remaining and publishes a remove command to the other servers that are connected via a message bus.

Group Membership

Group membership follows a subscriber/publisher pattern. The membership for groups can be any combination of connections. The lifetime of a group is handled internally by SignalR, including the creation and removal of the group. The membership of the group can be single-user or multiple-user.

Group Subscription

When users join a group, they do so in a subscriber/publisher pattern. SignalR does not expose any methods to return information about subscribers to a group. So if the members of a group need to be known, customized logic needs to be added to capture the group’s subscribers and to expose this list.

Group Life Cycle

A group life cycle begins the first time a connection is added to a group that does not exist. SignalR creates the new group and enrolls that connection in the group. The group membership is updated as connections are added or removed from a group via GroupManager. When all the connections have been removed from a group, SignalR cleans up the group and removes it from memory.

Single-user Group

A single-user group contains the connection ID of only a single user. The group is used to message the user who might have persistent connections open over multiple tabs or moving around a site, so the user’s connection ID regenerates every time a new page is visited.

Listing 4-22 is an example of the logic for a single-user group. The logic requires that the user be authenticated so that the Identity object is populated with the user’s name.

Listing 4-22. Example of Logic to Add/Remove Connections from a Single-user Group

protected override Task OnConnected(IRequest request, string connectionId)
{
string groupName = request.User.Identity.Name;
if (!string.IsNullOrWhiteSpace(groupName))
this.Groups.Add(connectionId, groupName);
return base.OnConnected(request, connectionId);
}

protected override Task OnDisconnected(IRequest request, string connectionId, bool stopCalled)
{
string groupName = request.User.Identity.Name;
if (!string.IsNullOrWhiteSpace(groupName))
this.Groups.Remove(connectionId, groupName);
return base.OnDisconnected(request, connectionId, stopCalled);
}

Image Note This sample relies on the User.Identity.Name having a valid value by using an authentication method other than anonymous authentication.

Multiple-user Group

A multiple-user group contains multiple users with one or more connection IDs. These groups can be used to target connection subgroups.

Suppose that you run a forum and want to have a chat room associated with the major forum topics. In this example, for every page that the user is under a different major forum topic, we provide a chat window centered on the major forum topic. To accomplish this, we can group the connection from that page to that major topic. With SignalR, this process is very easy: adding group name logic to the client and adding the grouping methods to the server.

Modifying the client is very easy; we use a connection constructor that allows us to pass the query string values, as shown in Listing 4-23.

Listing 4-23. Example of Client Update to Provide Query String Values During Request

var roomName = getRoomName();
var connection = $.connection('http://localhost:8219/chat', 'roomName=' + roomName, false);

Image Note The getRoomName() function is a custom function to return the group or, in this example, the chat room to be part of.

In the example, the first line determines what group to be in from custom logic in the getRoomName function. In the second line, the constructor that allows query string values is used. The first parameter is the SignalR endpoint URL, the second is the query string parameters that should be appended as-is, and the third determines whether logging should be on.

For the server, to determine which chat room the user is viewing, we need to add the logic to add or remove from the group based on the query string value that we are using. Logic also needs to be added so that messages can be sent to the group specified by the query string, similar toListing 4-24.

Listing 4-24. Example of Server Update to Use Query String Values to Determine Group Name

protected override Task OnReceived(IRequest request, string connectionId, string data)
{
string groupName = request.QueryString["roomName"];
return this.Groups.Send(groupName, data, connectionId);
}

protected override Task OnConnected(IRequest request, string connectionId)
{
string groupName = request.QueryString["roomName"];
if (!string.IsNullOrWhiteSpace(groupName))
this.Groups.Add(connectionId, groupName);
return base.OnConnected(request, connectionId);
}

protected override Task OnDisconnected(IRequest request, string connectionId, bool stopCalled)
{
string groupName = request.QueryString["roomName"];
if (!string.IsNullOrWhiteSpace(groupName))
this.Groups.Remove(connectionId, groupName);
return base.OnDisconnected(request, connectionId, stopCalled);
}

The first method we modify is OnReceived. In this method, we look at the query string value to determine the group name. We then use this name and send the data sent from the client to the group, excluding our own connection ID. The second and third methods we modify are theOnConnected and OnDisconnect functions. In these functions, the first thing to do is to determine the group name from the query string. Once we have the group name and we determine that the group name is valid, we call GroupManager to add or remove the function, respectively.

Group Persistence

Persisting a group can be done either in-memory or to a long-term storage medium such as a database or a caching tier. The persistence mediums have trade-offs such as speed, durability, and scalability.

Let’s first take a look at the in-memory group solution, which is very fast because a request does not have to go out of process to obtain group information. The downside of this solution is that it cannot scale beyond the server on which it is running, and the group information is lost if the application is restarted. This solution works well for applications that run on only one server, do not need to have group communication across servers, and can tolerate losing the group data with a restart.

Another solution is to use a database or a caching tier to store group information. The group information can then be shared with many servers and persisted across server restarts. The problem is that to access this group information, every request has to go out of process to get the group data, which is generally multiple times slower than in-memory access. So even if the application can scale, there is a performance penalty for using an external storage medium, plus the added complexity of guaranteeing that the external storage medium is accessible and is synchronized with all the other servers.

To determine which solution is best to use depends on whether you are running on multiple servers and whether the group data needs to be synchronized and/or persisted. If you need to persist group data between restarts, or if you have multiple servers and need the group data synchronized, you should store the group information in an external storage medium, as described in the second solution. If that is not the case, an in-memory solution provides the best benefit. (More information about message buses and scaling using the message buses is provided inChapters 9 and 10).

Summary

In this chapter, we described a persistent connection. We explained the communication and signaling that occurs between the server and client. A JavaScript sample of persistent connections was shown to explain the communication and signaling that occurs. Finally, we discussed how to use groups in the context of a persistent connection.