Skip to content

Instantly share code, notes, and snippets.

@taai
Created February 17, 2022 22:07
Show Gist options
  • Save taai/c7f5520c376ce23601440a423402fdb1 to your computer and use it in GitHub Desktop.
Save taai/c7f5520c376ce23601440a423402fdb1 to your computer and use it in GitHub Desktop.
TcpClient extensions
using System;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace Extensions.Tcp
{
public static class TcpClientExtensions
{
/// <summary>
/// Connects the client to the specified TCP port on the specified host as an asynchronous operation.
/// Helps to set a timeout and to distinguish timeout from cancellation of the cancellation token.
/// </summary>
/// <example>
/// var tcpClient = new TcpClient();
///
/// try
/// {
/// await tcpClient.ConnectAsyncWithTimeout("127.0.0.1", 1234, 1000, cancellationToken).ConfigureAwait(false);
/// }
/// catch (TimeoutException)
/// {
/// // timeout
/// }
/// catch (OperationCanceledException)
/// {
/// // the `cancellationToken` has been cancelled
/// }
/// catch (Exception)
/// {
/// // some other exception was thrown
/// }
///
///
///
/// // NOTE: You can do the same thing without this extension:
///
/// using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
/// {
/// var tcpClient = new TcpClient();
///
/// cts.CancelAfter(1000);
///
/// try
/// {
/// await tcpClient.ConnectAsync("127.0.0.1", 1234, cts.Token).ConfigureAwait(false);
/// }
/// catch (OperationCanceledException)
/// {
/// if (cancellationToken.IsCancellationRequested)
/// {
/// // the `cancellationToken` has been cancelled
/// }
/// else
/// {
/// // timeout
/// }
/// }
/// catch (Exception)
/// {
/// // some other exception was thrown
/// }
/// }
/// </example>
/// <exception cref="System.TimeoutException">Connection attempt timed out.</exception>
/// <exception cref="System.ArgumentNullException">The hostname parameter is empty or is null.</exception>
/// <exception cref="System.ArgumentOutOfRangeException">The port parameter is not between System.Net.IPEndPoint.MinPort and System.Net.IPEndPoint.MaxPort.</exception>
/// <exception cref="System.Net.Sockets.SocketException">An error occurred when accessing the socket.</exception>
/// <exception cref="System.ObjectDisposedException">System.Net.Sockets.TcpClient is closed.</exception>
public static async Task ConnectAsyncWithTimeout(this TcpClient tcpClient, string host, int port, int millisecondsTimeout, CancellationToken cancellationToken = default)
{
if (tcpClient == null)
throw new ArgumentNullException(nameof(tcpClient));
if (string.IsNullOrWhiteSpace(host))
throw new ArgumentNullException(nameof(host));
if (port < IPEndPoint.MinPort || port > IPEndPoint.MaxPort)
throw new ArgumentOutOfRangeException(nameof(port));
cancellationToken.ThrowIfCancellationRequested();
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
cts.CancelAfter(millisecondsTimeout);
try
{
await tcpClient.ConnectAsync(host, port, cts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException e)
{
tcpClient.Close();
// throw the parent CancellationToken, if it caused the exception
cancellationToken.ThrowIfCancellationRequested();
throw new TimeoutException("Connection attempt timed out.", e);
}
catch
{
tcpClient.Close();
throw;
}
}
}
/// <summary>
/// Sets the keep-alive mode for the TCP connection.
/// </summary>
/// <param name="keepAliveSeconds">How many seconds to wait after last real data packet was sent/received, before starting to send keep-alive packets.</param>
/// <param name="keepAliveIntervalSeconds">The interval (in seconds) between sending each keep-alive packet.</param>
/// <param name="keepAliveRetryCount">The number of TCP keep alive probes that will be sent before the connection is terminated</param>
/// <exception cref="System.Net.Sockets.SocketException"></exception>
/// <exception cref="System.ObjectDisposedException"></exception>
public static void SetKeepAliveValues(this TcpClient tcpClient, int keepAliveSeconds = 10, int keepAliveIntervalSeconds = 1, int keepAliveRetryCount = 1)
{
if (tcpClient == null)
throw new ArgumentNullException(nameof(tcpClient));
var socket = tcpClient.Client;
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveTime, keepAliveSeconds);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveInterval, keepAliveIntervalSeconds);
socket.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.TcpKeepAliveRetryCount, keepAliveRetryCount);
}
/// <summary>
/// Checks if the connection is still alive. Uses keep-alive.
/// </summary>
/// <exception cref="System.Net.Sockets.SocketException">An error occurred when attempting to access the socket.</exception>
/// <exception cref="System.ObjectDisposedException">The System.Net.Sockets.Socket has been closed.</exception>
public static bool IsReallyConnected(this TcpClient tcpClient)
{
if (tcpClient == null)
throw new ArgumentNullException(nameof(tcpClient));
var socket = tcpClient.Client;
if (!socket.Connected)
return false;
try
{
// NOTE: `Available` is checked before because it's faster, `Available` is also checked after to prevent a race condition.
if (socket.Available == 0 && socket.Poll(0, SelectMode.SelectRead) && socket.Available == 0)
return false;
}
catch
{
return false;
}
return socket.Connected;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment