89
votes

I'm using SignalR 2.0 with the .NET client in a mobile application which needs to handle various types of disconnects. Sometimes the SignalR client reconnects automatically - and sometimes it has to be reconnected directly by calling HubConnection.Start() again.

Since SignalR magically auto-reconnects some of the time, I'm wondering if I'm missing a feature or config setting?

What's the best way to set up a client that reconnects automatically?


I've seen javascript examples that handle the Closed() event and then Connect after a n-seconds. Is there any recommended approach?

I've read the documentation and several articles about the lifetime of SignalR connections, but I'm still unclear on how to handle the client reconnect.

5

5 Answers

73
votes

I finally figured this out. Here's what I've learned since starting this question:

Background: We're building an iOS app using Xamarin / Monotouch and the .NET SignalR 2.0.3 client. We're using the default SignalR protocols - and it seems to be using SSE instead of web sockets. I'm not sure yet if it's possible to use web sockets with Xamarin / Monotouch. Everything is hosted using Azure websites.

We needed the app to reconnect to our SignalR server quickly, but we kept having problems where the connection didn't reconnect on its own - or the reconnect took exactly 30 seconds (due to an underlying protocol timeout).

There were three scenarios we ended up testing for:

Scenario A - connecting the first time the app was loaded. This worked flawlessly from day one. The connection completes in less than .25 seconds even over 3G mobile connections. (assuming the radio is already on)

Scenario B - reconnecting to the SignalR server after the app was idle/closed for 30 seconds. In this scenario, the SignalR client will eventually reconnect to the server on its own without any special work - but it seems to wait exactly 30 seconds before attempting to reconnect. (way too slow for our app)

During this 30 second waiting period, we tried calling HubConnection.Start() which had no effect. And calling HubConnection.Stop() also takes 30 seconds. I found a related bug on the SignalR site that appears to be resolved, but we're still having the same problem in v2.0.3.

Scenario C - reconnecting to the SignalR server after the app was idle/closed for 120 seconds or longer. In this scenario, the SignalR transport protocol has already timed out so the client never automatically reconnects. This explains why the client was sometimes but not always reconnecting on its own. The good news is, calling HubConnection.Start() works almost instantly like scenario A.

So it took me a while to realize that the reconnect conditions were different based on whether the app was closed for 30 seconds vs 120+ seconds. And although the SignalR tracing logs illuminate what's going on with the underlying protocol, I don't believe there's a way to handle the transport level events in code. (the Closed() event fires after 30 seconds in scenario B, instantly in scenario C; the State property says "Connected" during these reconnect waiting periods; no other relevant events or methods)

Solution: The solution is obvious. We're not waiting for SignalR to do its reconnection magic. Instead, when the app is activated or when the phone's network connection is restored, we're simply cleaning up the events and de-referencing the HubConnection (can't dispose it because it takes 30 seconds, hopefully garbage collection will take care of it) and creating a new instance. Now everything is working great. For some reason, I thought we should be reusing a persisted connection and reconnecting instead of just creating a new instance.

46
votes

Setting a timer on the disconnected event to automatically attempt reconnect is the only method I am aware of.

In javascript it is done like so:

$.connection.hub.disconnected(function() {
   setTimeout(function() {
       $.connection.hub.start();
   }, 5000); // Restart connection after 5 seconds.
});

This is the recommended approach in the documentation:

http://www.asp.net/signalr/overview/signalr-20/hubs-api/handling-connection-lifetime-events#clientdisconnect

17
votes

Since the OP asking for a .NET client (a winform implementation below),

private async Task<bool> ConnectToSignalRServer()
{
    bool connected = false;
    try
    {
        Connection = new HubConnection("server url");
        Hub = Connection.CreateHubProxy("MyHub");
        await Connection.Start();

        //See @Oran Dennison's comment on @KingOfHypocrites's answer
        if (Connection.State == ConnectionState.Connected)
        {
            connected = true;
            Connection.Closed += Connection_Closed;
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error: {ex.Message}");
    }
    return connected;
}

private async void Connection_Closed()
{   // A global variable being set in "Form_closing" event 
    // of Form, check if form not closed explicitly to prevent a possible deadlock.
    if(!IsFormClosed) 
    {
        // specify a retry duration
        TimeSpan retryDuration = TimeSpan.FromSeconds(30);
        DateTime retryTill = DateTime.UtcNow.Add(retryDuration);

        while (DateTime.UtcNow < retryTill)
        {
            bool connected = await ConnectToSignalRServer();
            if (connected)
                return;
        }
        Console.WriteLine("Connection closed")
    }
}
0
votes

I add some update for ibubi answer. May be somebody need it. I found that in some case signalr don't rise "closed" event after reconnecting stopped. I solved it using event "StateChanged". Method which connect to SignalR server:

private async Task<bool> ConnectToSignalRServer()
        {
            bool connected = false;
            try
            {
                var connection = new HubConnection(ConnectionUrl);
                var proxy = connection.CreateHubProxy("CurrentData");
                await connection.Start();

                if (connection.State == ConnectionState.Connected)
                {
                    await proxy.Invoke("ConnectStation");

                    connection.Error += (ex) =>
                    {
                        Console.WriteLine("Connection error: " + ex.ToString());
                    };
                    connection.Closed += () =>
                    {
                        Console.WriteLine("Connection closed");
                    };
                    connection.StateChanged += Connection_StateChanged;
                    Console.WriteLine("Server for Current is started.");
                    connected = true;
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Error: {ex.Message}");
            }
            return connected;
        }

Method for reconnecting:

private async void Connection_StateChanged(StateChange obj)
        {
            if (obj.NewState == ConnectionState.Disconnected)
            {
                await RestartConnection();
            }
        }

Method of endless attempts to connect to the server (Also I use this method for creating the fist connection):

public async Task RestartConnection()
        {
            while (!ApplicationClosed)
            {
                bool connected = await ConnectToSignalRServer();
                if (connected)
                    return;
            }
        }
-2
votes

You might try to invoke server method from your android before reconnect state start to prevent magic reconnect problem.

SignalR Hub C#

 public class MyHub : Hub
    {
        public void Ping()
        {
            //ping for android long polling
        }
 }

In Android

private final int PING_INTERVAL = 10 * 1000;

private boolean isConnected = false;
private HubConnection connection;
private ClientTransport transport;
private HubProxy hubProxy;

private Handler handler = new Handler();
private Runnable ping = new Runnable() {
    @Override
    public void run() {
        if (isConnected) {
            hubProxy.invoke("ping");
            handler.postDelayed(ping, PING_INTERVAL);
        }
    }
};

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    System.setProperty("http.keepAlive", "false");

    .....
    .....

    connection.connected(new Runnable() {
        @Override
        public void run() {
            System.out.println("Connected");
            handler.postDelayed(ping, PING_INTERVAL);
    });
}