A requirement of a recent project was to have asynchronous communication between a set of load balanced servers and multiple web clients. SignalR has the ability to work across multiple servers using a SQL server backplane through the NuGet package. I found a good guide on asp.net‘s website to get the server up and running, but all of the examples involved sending messages to all users at the same time.
Clients.All.addNewMessageToPage(name, message);
In my scenario I needed to signal each user individually.
Clients can be sent to different servers behind the load balancer as they browse the site. Consider this scenario:
1. Client A makes a request to be added to the notification list.
2. The load balancer sends Client A to Server 1.
3. Server 1 registers Client A with it’s local instance of SignalR.
4. Client B requests to notify Client A that some event has occurred.
5. The load balancer sends Client B to Server 2.
6. Server 2 is not aware of the connection between Server 1 and Client A, and the request fails.
With the SQL backplane all of the servers share the same connection list, allowing a message to be sent to a Client A from a Server 2, even though Client A registered with Server 1.
In order to send a message to a single client the server needs to know the specific connection ID to send the message to. SignalR doesn’t offer a way to store a connection ID “key value pair”, so I created one on my own.
In my Hub.cs file I added a method that runs during the client registration:
public void registerClient(long SpecificIdentifier)
{
HubMethods.RegisterClient(SpecificIdentifier, Guid.Parse(Context.ConnectionId));
}
In the RegisterClient method I store the SignalR connection ID along with a identifier specific to my application. A table stores the ID, the SignalR connection ID, and a timestamp. A procedure was created to clear out the table nightly.
public static void RegisterClient(long SpecificIdentifier, Guid ConnectionID)
{
using (dbEntities db = new dbEntities())
{
Notification notification = db.Notifications.Where(x => x.ID == SpecificIdentifier).FirstOrDefault();
if (notification == null)
{
notification = new Notification()
{
ID = SpecificIdentifier,
ConnectionID = ConnectionID,
LastModified = DateTime.UtcNow,
};
db.Notifications.Add(notification);
}
else
{
notification.ID = SpecificIdentifier;
notification.ConnectionID = ConnectionID;
notification.LastModified = DateTime.UtcNow;
}
db.SaveChanges();
}
}
With the SQL backplane all of the servers share the same connection list, allowing a message to be sent to a Client A from a Server 2, even though Client A registered with Server 1.
When it comes time to send a message to a client, the notification method simply looks up the SignalR connection ID based on the ID specific to our application. The SQL backplane gathers the required information automatically based on the connection ID and the message is sent.
public static void NotifyClient(long SpecificIdentifier, Hub.Statuses Status)
{
using (dbEntities = new dbEntities ())
{
Notification notification = db.Notifications.Where(x => x.ID == SpecificIdentifier).FirstOrDefault();
if (notification != null)
{
String message = String.Empty;
if (Status== Hub.Statuses.Confirmed)
{
message = "Here's the message for confirmation";
}
else if (Status == Hub.Statuses.Failed)
{
message = "Here's the message for failure";
}
try
{
IHubContext notificationHub = GlobalHost.ConnectionManager.GetHubContext<Hub>();
notificationHub.Clients.Client(notification.ConnectionID.ToString()).addMessage(message);
}
catch (Exception ex)
{
ex.LogToElmah();
}
}
else
{
throw new Exception("Unable to find connection");
}
}
}