SignalR, how to validate if a connection id is still active? - c#

As the title says, how to validate if a connection id is still active using SignalR?
I have something similar as below where I map the connection ids to a user id. The problem is that in rare cases OnDisconnectedAsync does not triggeres.
Then I can't make the feature where the user is joining or leaving because it thinks that the user still have a connection.
I do have a "pinger" which run each 5 minutes that is updating a expire date but it is not reliable.
What I want is something like loop through all connection ids and verify if they are still active.
How can this be done? I thought maybe I can send a message to all connection ids for user X and see if I get something back and then do some kind of cleanup?
public class Chat : Hub
{
private IConnectionManager _manager;
public Chat(IConnectionManager manager)
{
_manager = manager;
}
public override Task OnConnectedAsync()
{
// Add connectionId and any other info you want to your connectionManager
_manager.Add(Context.ConnectionId, Context.User, Context.GetHttpContext());
}
public override Task OnDisconnectedAsync(Exception exception)
{
_manager.Remove(Context.ConnectionId);
}
}

SignalR has its own "pinger".
//
// Summary:
// Gets or sets the interval used by the server to send keep alive pings to connected
// clients. The default interval is 15 seconds.
public TimeSpan? KeepAliveInterval { get; set; }
And you can configure it on Startup like:
services.AddSignalR(hubOptions =>
{
hubOptions.KeepAliveInterval = TimeSpan.FromSeconds(hostConfiguration.SignalR.KeepAliveInterval);
}
So basically if the client will not respond in the defined timespan, it will trigger OnDisconnectedAsync.

Related

SignalR core - invalidate dead connections

The problem
I'm using .NET Core 2.2 with ASP.NET Core SignalR. Currently I'm saving all connection states in a SQL database (see this document; even though it's a manual for the "old" SignalR library, the logic is the same). I'm also using a Redis backplane, since my application can scale horizontally.
However, when restarting my application, current connections do not get closed and will get orphaned. The previously linked article states:
If your web servers stop working or the application restarts, the
OnDisconnected method is not called. Therefore, it is possible that
your data repository will have records for connection ids that are no
longer valid. To clean up these orphaned records, you may wish to
invalidate any connection that was created outside of a timeframe that
is relevant to your application.
The question
In the "old" SignalR there is an ITransportHeartbeat (which this script perfectly implements) but there's no such interface for the .NET Core version (atleast, I couldn't find it).
How do I know whether an connection is no longer alive? I want (or actually need) to clean up old connection id's.
The solution I came up with is as follows. It's not as elegant, but for now I see no other option.
I updated the model in the database to not only contain a ConnectionId but also a LastPing (which is a DateTime type). The client sends a KeepAlive message (custom message, not using the SignalR keepalive settings). Upon receiving the message (server side), I update the database with the current time:
var connection = _context.Connection.FirstOrDefault(x => x.Id == Context.ConnectionId);
connection.LastPing = DateTime.UtcNow;
To clean up the orphaned connections (which are not removed by SignalR's OnDisconnected method), I have a task running periodically (currently in Hangfire) which removes the connections where the LastPing field has not been updated recently.
Updated after #davidfowl's comments in the other answer.
.NET Core 2.1 with SignalR has IConnectionHeartbeatFeature which you can use to achieve something similar to what you could with ITransportHeartbeat in old SignalR.
The main crux of the code below is that we maintain an in-memory list that tracks connections that need to be updated in the database. This allows us to do expensive database operations at a controlled interval and in batch. IConnectionHeartbeatFeature.OnHeartbeat() is fired every second for each connection, so hitting the database at that frequency could take your server down at scale.
Firstly create an entity to maintain a list of connections in memory that the server has yet to update:
public interface IConnectionCounter
{
internal ConcurrentDictionary<string, DateTime> Connections { get; }
public void RecordConnectionLastSeen(string connectionId);
public void RemoveConnection(string connectionId);
}
/// <summary>
/// Maintains a dictionary of connections that need to be refreshed in the
/// database
/// </summary>
public class ConnectionCounter : IConnectionCounter
{
private readonly ConcurrentDictionary<string, DateTime> _connections;
ConcurrentDictionary<string, DateTime> IConnectionCounter.Connections
=> _connections;
public ConnectionCounter()
{
_connections = new ConcurrentDictionary<string, DateTime>();
}
public void RecordConnectionLastSeen(string connectionId)
{
var now = DateTime.UtcNow;
_connections.AddOrUpdate(
connectionId,
now, (existingConnectionId, oldTime) => now);
}
public void RemoveConnection(string connectionId)
{
_connections.Remove(connectionId, out _);
}
}
Note, this is NOT a definitive list of all online connections that need to be updated, as connections may be distributed across multiple servers. If you've got many servers, you could reduce the load further by storing these connections in a distributed in-memory store like Redis.
Next, set up the IConnectionCounter in the Hub so that connections are counted.
public class ChatHub : Hub
{
private readonly IConnectionCounter _connectionCounter;
public ChatHub(
IConnectionCounter connectionCounter)
{
_connectionCounter = connectionCounter;
}
[AllowAnonymous]
public override Task OnConnectedAsync()
{
var connectionHeartbeat =
Context.Features.Get<IConnectionHeartbeatFeature>();
connectionHeartbeat.OnHeartbeat(connectionId => {
_connectionCounter.RecordConnectionLastSeen((string)connectionId);
}, Context.ConnectionId);
return base.OnConnectedAsync();
}
}
Now create a service that takes the connections in IConnectionCounter and updates the database with the state of said connection:
public interface IPresenceDatabaseSyncer
{
public Task UpdateConnectionsOnlineStatus();
}
/// <summary>
/// Handles updating the online status of connections whose connections
/// that need to be updated in the database
/// </summary>
public class PresenceDatabaseSyncer : IPresenceDatabaseSyncer
{
private readonly MyDbContext _context;
private readonly IConnectionCounter _connectionCounter;
public PresenceDatabaseSyncer(
MyDbContext context,
IConnectionCounter connectionCounter)
{
_context = context;
_connectionCounter = connectionCounter;
}
public async Task UpdateConnectionsOnlineStatus()
{
if (_connectionCounter.Connections.IsEmpty)
return;
foreach (var connection in _connectionCounter.Connections)
{
var connectionId = connection.Key;
var lastPing = connection.Value;
var dbConnection = _context.Connection
.FirstOrDefault(x => x.ConnectionId == connectionId);
if (dbConnection != null)
dbConnection.LastPing = lastPing;
_connectionCounter.RemoveConnection(connectionId);
}
}
}
I then use a HostedService to continuously run the db sync above:
/// <summary>
/// Runs a periodic sync operation to ensure that connections are
/// recorded as being online correctly in the database
/// </summary>
public class PresenceDatabaseSyncerHostedService : IHostedService, IDisposable
{
private const int SyncIntervalSeconds = 10;
private readonly IServiceScopeFactory _serviceScopeFactory;
private Timer _timer;
public PresenceDatabaseSyncerHostedService(
IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
public Task StartAsync(CancellationToken stoppingToken)
{
_timer = new Timer(
DoWork,
null,
TimeSpan.Zero,
TimeSpan.FromSeconds(SyncIntervalSeconds));
return Task.CompletedTask;
}
private async void DoWork(object state)
{
using var scope = _serviceScopeFactory.CreateScope();
var scopedProcessingService =
scope.ServiceProvider.GetRequiredService<IPresenceDatabaseSyncer>();
await scopedProcessingService.UpdateConnectionsOnlineStatus();
}
public Task StopAsync(CancellationToken stoppingToken)
{
_timer?.Change(Timeout.Infinite, 0);
return Task.CompletedTask;
}
public void Dispose()
{
_timer?.Dispose();
}
}
Finally register these dependencies and services:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IConnectionCounter, ConnectionCounter>();
services.AddScoped<IPresenceDatabaseSyncer, PresenceDatabaseSyncer>();
services.AddHostedService<PresenceDatabaseSyncerHostedService>();
// ...
}
// ...
}
Of course there is still the matter of actually cleaning up the stale connections from the database. I handle this using another HostedService and will leave as an exercise to the reader.
If you're using the Azure SignalR Service, there's an additional benefit over manually sending a KeepAlive message as per #Devator's answer in that you don't need to pay for the message (since OnHeartbeat occurs internally).
Keep in mind that this feature is not really documented that well. I've been using this in production for a few months now, but I haven't seen other solutions using this technique.

How to get Connection id from user id inside signalr hub

I have a signalr hub with a function like this
public void SendSurveyNote(int surveyId,List<string> users){}
Here I want to add all users of the list into a group "Survey_" + surveyId Then sending the group a message. But I only have the user id but joining a group requires a connection id. So how could I manage that.
Also I wonder would it be a performance issue to send each user a message without a group?
I call the function above when I add a new Survey like this
private static HubConnection hubConnection = new HubConnection(ConfigurationManager.AppSettings["BaseUrl"]);
private static IHubProxy hubProxy = hubConnection.CreateHubProxy("myHub");
await hubConnection.Start();
hubProxy.Invoke("SendSurveyNote", model.Id, users);
thanks
You have access to the connection ID within Context. You'll want to establish groups within OnConnected. Observe the following implementation on your hub, where we will call it MyHub. We'll also group by Context.User.Identity.Name to establish a unique group per user, but this could be any value you wish to group by.
public class MyHub: Hub
{
public override Task OnConnected()
{
Groups.Add(Context.ConnectionId, Context.User.Identity.Name)
return base.OnConnected();
}
}
See Working with Groups in SignalR for more information

Anonymous Private Chat in SignalR

I'm somewhat new to SignalR. I understand hubs to a limited degree, but I don't understand how two users can share a connection while excluding others.
My scenario is that I want an unauthenticated public website user to be able to initiate a private (not necessarily secure) chat session with a customer service user.
Is there an example or resource that my point me in the right direction?
I've looked at a few resources, including http://www.asp.net/signalr/overview/signalr-20/hubs-api/mapping-users-to-connections but haven't found the right scenario.
You can create groups, so add some methods to your hub (a subscribe method should return a Task as they are asynchronous...)
public Task SubscribeToGroup(string groupName)
{
return Groups.Add(Context.ConnectionId, groupName);
}
Then you publish notifications to users of that group as normal but via the groups collection...
public void BroadcastMessageToGroup(string groupName, string message)
{
Clients.Group(groupName).onCaptionReceived(message);
}
Now only subscribers of that particular group will get the message!
Hope this helps.
You can find a tutorial here for SignalR Groups.
http://www.asp.net/signalr/overview/signalr-20/hubs-api/working-with-groups
You can create a group in Hub's API, in this method each user is a member of that group. And they send a message to that group ( via the server), and because they are only 2 members they are the only one's who see the messages ( privatly)
You can also message a group member directly by connection ID. This requires your app to keep track of connection IDs of users as they connect and disconnect, but this isn't too difficult:
//Users: stores connection ID and user name
public static ConcurrentDictionary Users = new ConcurrentDictionary();
public override System.Threading.Tasks.Task OnConnected()
{
//Add user to Users; user will supply their name later. Also give them the list of users already connected
Users.TryAdd(Context.ConnectionId, "New User");
SendUserList();
return base.OnConnected();
}
//Send everyone the list of users (don't send the userids to the clients)
public void SendUserList()
{
Clients.All.UpdateUserList(Users.Values);
}
//Clients will call this when their user name is known. The server will then update all the other clients
public void GiveUserName(string name)
{
Users.AddOrUpdate(Context.ConnectionId, name, (key, oldvalue) => name);
SendUserList();
}
//Let people know when you leave (not necessarily immediate if they just close the browser)
public override System.Threading.Tasks.Task OnDisconnected()
{
string user;
Users.TryRemove(Context.ConnectionId, out user);
SendUserList();
return base.OnDisconnected();
}
//Ok, now we can finally send to one user by username
public void SendToUser(string from, string to, string message)
{
//Send to every match in the dictionary, so users with multiple connections and the same name receive the message in all browsers
foreach(KeyValuePair user in Users)
{
if (user.Value.Equals(to))
{
Clients.Client(user.Key).sendMessage(from, message);
}
}
}

Logging server events in signalR

I'm writing a C#-based web application using SignalR. So far I have a 'lobby' area (where open communication is allowed), and an 'session' area (where groups of 5 people can engage in private conversation, and any server interactions are only shown to the group).
What I'd like to do is create a 'logging' object in memory - one for each session (so if there are three groups of five people, I'd have three logging objects).
The 'session' area inherits from Hubs (and IDisconnect), and has several methods (Join, Send, Disconnect, etc.). The methods pass data back to the JavaScript client, which calls client-side JS functions. I've tried using a constructor method:
public class Session : Hub, IDisconnect
{
public class Logger
{
public List<Tuple<string, string, DateTime>> Log;
public List<Tuple<string, string, DateTime>> AddEvent(string evt, string msg, DateTime time)
{
if (Log == null)
{
Log = new List<Tuple<string, string, DateTime>>();
}
Log.Add(new Tuple<string, string, DateTime>(evt, msg, time));
return Log;
}
}
public Logger eventLog = new Logger();
public Session()
{
eventLog = new Logger();
eventLog.AddEvent("LOGGER INITIALIZED", "Logging started", DateTime.Now);
}
public Task Join(string group)
{
eventLog.AddEvent("CONNECT", "User connect", DateTime.Now);
return Groups.Add(Context.ConnectionId, group);
}
public Task Send(string group, string message)
{
eventLog.AddEvent("CHAT", "Message Sent", DateTime.Now);
return Clients[group].addMessage(message);
}
public Task Interact(string group, string payload)
{
// deserialise the data
// pass the data to the worker
// broadcast the interaction to everyone in the group
eventLog.AddEvent("INTERACTION", "User interacted", DateTime.Now);
return Clients[group].interactionMade(payload);
}
public Task Disconnect()
{
// grab someone from the lobby?
eventLog.AddEvent("DISCONNECT","User disconnect",DateTime.Now);
return Clients.leave(Context.ConnectionId);
}
}
But this results in the Logger being recreated every time a user interacts with the server.
Does anyone know how I'd be able to create one Logger per new session, and add elements to it? Or is there a simpler way to do this and I'm just overthinking the problem?
Hubs are created and disposed of all the time! Never ever put data in them that you expect to last (unless it's static).
I'd recommend creating your logger object as it's own class (not extending Hub/IDisconnect).
Once you have that create a static ConcurrentDictionary on the hub which maps SignalR groups (use these to represent your sessions) to loggers.
When you have a "Join" method triggered on your hub it's easy as looking up the group that the connection was in => Sending the logging data to the groups logger.
Checkout https://github.com/davidfowl/JabbR when it comes to making "rooms" and other sorts of groupings via SignalR
Hope this helps!

SignalR: client disconnection

How does the SignalR handle client disconnection? Am I right if I state the following?
SignalR will detect browser page close/refresh via Javascript event handling and will send appropriate packet to server (through the persisting connection);
SignalR will NOT detect browser close/network failure (probably only by timeout).
I aim the long-polling transport.
I'm aware of this question but would like to make it a bit clear for me.
If a user refreshes the page, that is treated as a new connection. You are correct that the disconnect is based on a timeout.
You can handle the Connect/Reconnect and Disconnect events in a Hub by implementing SignalR.Hubs.IConnected and SignalR.Hubs.IDisconnect.
The above referred to SignalR 0.5.x.
From the official documentation (currently for v1.1.3):
public class ContosoChatHub : Hub
{
public override Task OnConnected()
{
// Add your own code here.
// For example: in a chat application, record the association between
// the current connection ID and user name, and mark the user as online.
// After the code in this method completes, the client is informed that
// the connection is established; for example, in a JavaScript client,
// the start().done callback is executed.
return base.OnConnected();
}
public override Task OnDisconnected()
{
// Add your own code here.
// For example: in a chat application, mark the user as offline,
// delete the association between the current connection id and user name.
return base.OnDisconnected();
}
public override Task OnReconnected()
{
// Add your own code here.
// For example: in a chat application, you might have marked the
// user as offline after a period of inactivity; in that case
// mark the user as online again.
return base.OnReconnected();
}
}
In SignalR 1.0, the SignalR.Hubs.IConnected and SignalR.Hubs.IDisconnect are no longer implemented, and now it's just an override on the hub itself:
public class Chat : Hub
{
public override Task OnConnected()
{
return base.OnConnected();
}
public override Task OnDisconnected()
{
return base.OnDisconnected();
}
public override Task OnReconnected()
{
return base.OnReconnected();
}
}

Categories

Resources