Search code examples
c#socketstcpclientsocket-timeout-exception

How to detect before throwing SocketException a closed connection?


I try to write a TcpClientPool to manage a KeepAlive behavior with the host

public class TcpClientPool : IDisposable
{
    private readonly ConcurrentDictionary<string, TcpClient> _clients = new ConcurrentDictionary<string, TcpClient>();

    public TcpClient GetClient(string hostname, int port)
    {
        var clientKey = $"{hostname}_{port}";

        var client = _clients.GetOrAdd(clientKey, (key) => TcpClientFactory(hostname, port));

        if (!client.Connected)
        {
            client.Dispose();
            _clients.Remove(clientKey, out _);
            client = _clients.GetOrAdd(clientKey, (key) => TcpClientFactory(hostname, port));
        }

        return client;
    }

    private TcpClient TcpClientFactory(string hostname, int port)
    {
        var tcpClient = new TcpClient();

        tcpClient.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
        tcpClient.Client.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, 10000);
        tcpClient.Client.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, 10000);

        tcpClient.Connect(hostname, port);

        return tcpClient;
    }
}

My problem is when I check the Connected boolean, the value is not correct when the server close the socket for any reason, so it throw an IOException. I would like to find a way to check efficiently the real active state of my connection to let the TcpClientPool manage the connectivity logic.

If I use Poll, the problem is the same because if there is no data to read, the check is not relevent. I can be in a pending state and receive nothing for a time and keeping my connection active.

// this logic doesn't work
if (tcpClient.Client.Poll(0, SelectMode.SelectError))
{
    // Connection is closed or has an error
    // Re-establish the connection
}
else
{
    // Connection is still active
    // Use the TcpClient instance
}

How can I go ahead of this problem ?


Solution

  • As suggested by Charlieface in comments, I wrote a static method to retry if fail:

    public static async Task<T> RetryIfFailAsync<T>(Func<Task<T>> operation, T defaultValue, Action? beforeRetry)
    {
        if (operation == null)
        {
            return defaultValue;
        }
    
        T? result;
    
        try
        {
            result = await operation();
        }
        catch
        {
            beforeRetry?.Invoke();
            result = await operation();
        }
    
        return result ?? defaultValue;
    }
    

    And I use it like this for my use case:

    response = await RetryIfFailAsync(
        // The main operation to do
        async () => await client.SendMessage(request),
        // The default value
        null,
        // An optional action to perform before retry the main operation
        () => client.Reconnect(hostname, port));