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 ?
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));