Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

.Net Core SignalR - connection timeout - heartbeat timer - connection state change handling

just to be clear up-front, this questions is about .Net Core SignalR, not the previous version.

The new SignalR has an issue with WebSockets behind IIS (I can't get them to work on Chrome/Win7/IIS express). So instead I'm using Server Sent Events (SSE). However, the problem is that those time out after about 2 minutes, the connection state goes from 2 to 3. Automatic reconnect has been removed (apparently it wasn't working really well anyway in previous versions).

I'd like to implement a heartbeat timer now to stop clients from timing out, a tick every 30 seconds may well do the job.

Update 10 November

I have now managed to implement the server side Heartbeat, essentially taken from Ricardo Peres' https://weblogs.asp.net/ricardoperes/signalr-in-asp-net-core

  • in startup.cs, add to public void Configure(IApplicationBuilder app, IHostingEnvironment env, IServiceProvider serviceProvider)
    app.UseSignalR(routes =>  
    {  
        routes.MapHub<TheHubClass>("signalr");  
    });

    TimerCallback SignalRHeartBeat = async (x) => {   
    await serviceProvider.GetService<IHubContext<TheHubClass>>().Clients.All.InvokeAsync("Heartbeat", DateTime.Now); };
    var timer = new Timer(SignalRHeartBeat).Change(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(30));            
  • HubClass

For the HubClass, I have added public async Task HeartBeat(DateTime now) => await Clients.All.InvokeAsync("Heartbeat", now);

Obviously, both the timer, the data being sent (I'm just sending a DateTime) and the client method name can be different.

Update .Net Core 2.1+

See the comment below; the timer callback should no longer be used. I've now implemented an IHostedService (or rather the abstract BackgroundService) to do that:

public class HeartBeat : BackgroundService
{
    private readonly IHubContext<SignalRHub> _hubContext;
    public HeartBeat(IHubContext<SignalRHub> hubContext)
    {
        _hubContext = hubContext;
    }
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await _hubContext.Clients.All.SendAsync("Heartbeat", DateTime.Now, stoppingToken);
            await Task.Delay(30000, stoppingToken);
        }            
    }
}

In your startup class, wire it in after services.AddSignalR();:

services.AddHostedService<HeartBeat>();
  • Client
    var connection = new signalR.HubConnection("/signalr", { transport: signalR.TransportType.ServerSentEvents });
    connection.on("Heartbeat", serverTime => { console.log(serverTime); });

Remaining pieces of the initial question

What is left is how to properly reconnect the client, e.g. after IO was suspended (the browser's computer went to sleep, lost connection, changed Wifis or whatever)

I have implemented a client side Heartbeat that is working properly, at least until the connection breaks:

  • Hub Class: public async Task HeartBeatTock() => await Task.CompletedTask;
  • Client:

    var heartBeatTockTimer; function sendHeartBeatTock() { connection.invoke("HeartBeatTock"); } connection.start().then(args => { heartBeatTockTimer = setInterval(sendHeartBeatTock, 10000); });

After the browser suspends IO for example, the invoke method would throw an exception - which cannot be caught by a simple try/catch because it is async. What I tried to do for my HeartBeatTock was something like (pseudo-code):

function sendHeartBeatTock
    try connection.invoke("HeartbeatTock)
    catch exception
         try connection.stop()
         catch exception (and ignore it)
         finally
              connection = new HubConnection().start()
    repeat try connection.invoke("HeartbeatTock")
    catch exception
        log("restart did not work")
        clearInterval(heartBeatTockTimer)
        informUserToRefreshBrowser()

Now, this does not work for a few reasons. invoke throws the exception after the code block executes due to being run asynchronous. It looks as though it exposes a .catch() method, but I'm not sure how to implement my thoughts there properly. The other reason is that starting a new connection would require me to re-implement all server calls like "connection.on("send"...) - which appears silly.

Any hints as to how to properly implement a reconnecting client would be much appreciated.

like image 979
ExternalUse Avatar asked Nov 09 '17 18:11

ExternalUse


People also ask

How do I set SignalR connection timeout?

ConnectionTimeout = TimeSpan. FromSeconds(40); // Wait a maximum of 30 seconds after a transport connection is lost // before raising the Disconnected event to terminate the SignalR connection.

How long does a SignalR connection last?

The default keepalive timeout period is currently 20 seconds. If your client code tries to call a Hub method while SignalR is in reconnecting mode, SignalR will try to send the command.

How many connections can SignalR handle?

In the default mode, the app server creates five server connections with Azure SignalR Service. The app server uses the Azure SignalR Service SDK by default. In the following performance test results, server connections are increased to 15 (or more for broadcasting and sending a message to a big group).

Does SignalR require sticky session?

SignalR requires that all HTTP requests for a specific connection be handled by the same server process. When SignalR is running on a server farm (multiple servers), "sticky sessions" must be used. "Sticky sessions" are also called session affinity by some load balancers.


1 Answers

This is an issue when running SignalR Core behind IIS. IIS will close idle connections after 2 minutes. The long term plan is to add keep alive messages which, as a side effect, will prevent IIS from closing the connection. To work around the problem for now you can:

  • send periodically a message to the clients
  • change the idle-timeout setting in IIS as described here
  • restart the connection on the client side if it gets closed
  • use a different transport (e.g. long polling since you cannot use webSockets on Win7/Win2008 R2 behind IIS)
like image 171
Pawel Avatar answered Sep 30 '22 00:09

Pawel