Created
February 17, 2022 22:07
-
-
Save taai/c7f5520c376ce23601440a423402fdb1 to your computer and use it in GitHub Desktop.
TcpClient extensions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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