如何为TcpClient设置超时时间?

104

我有一个TcpClient,用来向远程计算机上的监听器发送数据。远程计算机有时会开启,有时会关闭。由于这个原因,TcpClient 经常连接失败。我想让TcpClient在一秒后超时,这样当它无法连接到远程计算机时就不会花费太多时间。目前,我的TcpClient使用以下代码:

try
{
    TcpClient client = new TcpClient("remotehost", this.Port);
    client.SendTimeout = 1000;

    Byte[] data = System.Text.Encoding.Unicode.GetBytes(this.Message);
    NetworkStream stream = client.GetStream();
    stream.Write(data, 0, data.Length);
    data = new Byte[512];
    Int32 bytes = stream.Read(data, 0, data.Length);
    this.Response = System.Text.Encoding.Unicode.GetString(data, 0, bytes);

    stream.Close();
    client.Close();    

    FireSentEvent();  //Notifies of success
}
catch (Exception ex)
{
    FireFailedEvent(ex); //Notifies of failure
}

这对于处理任务来说已经足够好了。如果可以发送,则发送,如果无法连接到远程计算机,则捕获异常。然而,当它无法连接时,要抛出异常需要十到十五秒钟。我需要它在大约一秒钟内超时。我该如何更改超时时间?

11个回答

119

从.NET 4.5开始,TcpClient拥有一个很酷的ConnectAsync方法,我们可以像这样使用它,所以现在非常容易:

var client = new TcpClient();
if (!client.ConnectAsync("remotehost", remotePort).Wait(1000))
{
    // connection failure
}

4
ConnectAsync 的另一个好处是,Task.Wait 可以接受 CancellationToken,在超时之前甚至在必要的情况下立即停止。 - Ilia Barahovsky
11
"Wait"会同步阻塞,这将削弱"Async"部分的效果。 https://dev59.com/jWQn5IYBdhLWcg3wETvj#43237063 是一个更好的完全异步实现。 - Tim P.
12
@TimP。你在问题中看到了“async”这个词吗? - Simon Mourier
我认为这是一个很好的答案,但我会返回return client.Connected; 我的测试用例表明仅等待还不足以得出确定性的答案。 - Walter Verhoeven
2
您仅仅将我的响应时间从28秒降低到了1.5秒,为10个客户节约了时间!!太棒了! - JeffS
现在可以使用"WaitAsync()"来保持连接异步。 - undefined

115

你需要使用异步的 BeginConnect 方法来连接,而不是尝试同步连接(这是构造函数所做的)。像这样:

var client = new TcpClient();
var result = client.BeginConnect("remotehost", this.Port, null, null);

var success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(1));

if (!success)
{
    throw new Exception("Failed to connect.");
}

// we have connected
client.EndConnect(result);

2
使用异步连接并将其与等待“同步”回来有什么意义呢? 我的意思是,我目前正在尝试理解如何使用异步读取实现超时,但解决方案不是完全禁用异步设计。 应该使用套接字超时或取消令牌或类似的东西。 否则,只需使用connect/read即可... - RoeeK
6
重点是问题所说的:以编程方式选择连接尝试的任意超时时间。这不是有关如何执行异步IO的示例。 - Jon
10
这个问题的重点是TcpClient没有提供带有可配置超时的同步连接函数,而这是你所提出的解决方案之一。这是一种解决方法来实现它。如果不重复自己,我就不知道还能说什么了。 - Jon
1
如果连接无法建立,此代码将泄漏资源(具体来说是等待句柄),因为从未调用EndConnect。如果您预计连接问题不太频繁,这可能是您愿意支付的代价,但值得注意。 - Jeroen Mostert
2
@JeroenMostert 谢谢您指出,但请记住,这不是生产级别的代码。各位,请不要在生产系统中复制粘贴带有“类似于此”的评论的代码。=) - Jon
显示剩余2条评论

23

使用https://dev59.com/-IPba4cB1Zd3GeqPoCBD#25684549提供的另一种选择:


var timeOut = TimeSpan.FromSeconds(5);     
var cancellationCompletionSource = new TaskCompletionSource<bool>();
try
{
    using (var cts = new CancellationTokenSource(timeOut))
    {
        using (var client = new TcpClient())
        {
            var task = client.ConnectAsync(hostUri, portNumber);

            using (cts.Token.Register(() => cancellationCompletionSource.TrySetResult(true)))
            {
                if (task != await Task.WhenAny(task, cancellationCompletionSource.Task))
                {
                    throw new OperationCanceledException(cts.Token);
                }
            }

            ...

        }
    }
}
catch(OperationCanceledException)
{
    ...
}

1
这是正确的完全异步实现。 - Tim P.
1
为什么不能使用 Task.Delay 来创建一个在一定时间后完成的任务,而要使用 CancellationTokenSource/TaskCompletionSource 提供延迟?(我尝试过了,但它会锁死,但我不明白为什么) - Daniel
任务何时被取消?当然,这会在超时后解除调用,但是ConnectAsync()仍然在线程池中运行吗? - TheColonel26
我也想知道@MondKin的问题的答案。 - TheColonel26
这是最佳答案。 - Castor Mann
显示剩余2条评论

11

以上的回答没有覆盖如何清理已超时的连接。要处理这种情况,可以调用TcpClient.EndConnect来关闭连接,如果成功但在超时之后,也可以关闭连接并释放TcpClient。

这样做可能有些过分,但对我而言非常有效。

    private class State
    {
        public TcpClient Client { get; set; }
        public bool Success { get; set; }
    }

    public TcpClient Connect(string hostName, int port, int timeout)
    {
        var client = new TcpClient();

        //when the connection completes before the timeout it will cause a race
        //we want EndConnect to always treat the connection as successful if it wins
        var state = new State { Client = client, Success = true };

        IAsyncResult ar = client.BeginConnect(hostName, port, EndConnect, state);
        state.Success = ar.AsyncWaitHandle.WaitOne(timeout, false);

        if (!state.Success || !client.Connected)
            throw new Exception("Failed to connect.");

        return client;
    }

    void EndConnect(IAsyncResult ar)
    {
        var state = (State)ar.AsyncState;
        TcpClient client = state.Client;

        try
        {
            client.EndConnect(ar);
        }
        catch { }

        if (client.Connected && state.Success)
            return;

        client.Close();
    }

感谢您提供详细的代码。如果连接调用在超时之前失败,是否可能抛出SocketException? - Macke
它应该已经这样做了。WaitOne将在Connect调用完成(成功或失败)或超时到期时释放,以先发生的为准。如果连接“快速失败”,!client.Connected的检查将引发异常。 - Adster

10

需要注意的一点是,在超时时间到之前,BeginConnect调用可能会失败。 如果您正在尝试进行本地连接,则可能会发生这种情况。 这是 Jon 代码的修改版本...

        var client = new TcpClient();
        var result = client.BeginConnect("remotehost", Port, null, null);

        result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(1));
        if (!client.Connected)
        {
            throw new Exception("Failed to connect.");
        }

        // we have connected
        client.EndConnect(result);

3

这里是基于mcandal解决方案的代码改进。为client.ConnectAsync任务添加了异常捕获(例如:当服务器不可达时,会生成SocketException异常)。

var timeOut = TimeSpan.FromSeconds(5);     
var cancellationCompletionSource = new TaskCompletionSource<bool>();

try
{
    using (var cts = new CancellationTokenSource(timeOut))
    {
        using (var client = new TcpClient())
        {
            var task = client.ConnectAsync(hostUri, portNumber);

            using (cts.Token.Register(() => cancellationCompletionSource.TrySetResult(true)))
            {
                if (task != await Task.WhenAny(task, cancellationCompletionSource.Task))
                {
                    throw new OperationCanceledException(cts.Token);
                }

                // throw exception inside 'task' (if any)
                if (task.Exception?.InnerException != null)
                {
                    throw task.Exception.InnerException;
                }
            }

            ...

        }
    }
}
catch (OperationCanceledException operationCanceledEx)
{
    // connection timeout
    ...
}
catch (SocketException socketEx)
{
    ...
}
catch (Exception ex)
{
    ...
}

2
正文:

Simon Mourier 所提到的,可以使用 ConnectAsync TcpClient 的方法并结合 Task 使用,以便尽快停止操作。
例如:

// ...
client = new TcpClient(); // Initialization of TcpClient
CancellationToken ct = new CancellationToken(); // Required for "*.Task()" method
if (client.ConnectAsync(this.ip, this.port).Wait(1000, ct)) // Connect with timeout of 1 second
{

    // ... transfer

    if (client != null) {
        client.Close(); // Close the connection and dispose a TcpClient object
        Console.WriteLine("Success");
        ct.ThrowIfCancellationRequested(); // Stop asynchronous operation after successull connection(...and transfer(in needed))
    }
}
else
{
    Console.WriteLine("Connetion timed out");
}
// ...

此外,我建议查看AsyncTcpClient C#库,其中提供了一些示例,例如Server <> Client

2
如果使用async&await并且希望使用超时而不阻塞,则可以从mcandal提供的答案中提供的替代方法是在后台线程上执行连接并等待结果。例如:
Task<bool> t = Task.Run(() => client.ConnectAsync(ipAddr, port).Wait(1000));
await t;
if (!t.Result)
{
   Console.WriteLine("Connect timed out");
   return; // Set/return an error code or throw here.
}
// Successful Connection - if we get to here.

请参阅Task.Wait MSDN 文章以获取更多信息和其他示例。

1
我正在使用这些通用方法;它们可以为任何异步任务添加超时和取消令牌。如果您发现任何问题,请告诉我,以便我可以相应地进行修复。
public static async Task<T> RunTask<T>(Task<T> task, int timeout = 0, CancellationToken cancellationToken = default)
{
    await RunTask((Task)task, timeout, cancellationToken);
    return await task;
}

public static async Task RunTask(Task task, int timeout = 0, CancellationToken cancellationToken = default)
{
    if (timeout == 0) timeout = -1;

    var timeoutTask = Task.Delay(timeout, cancellationToken);
    await Task.WhenAny(task, timeoutTask);

    cancellationToken.ThrowIfCancellationRequested();
    if (timeoutTask.IsCompleted)
        throw new TimeoutException();

    await task;
}

使用方法

await RunTask(tcpClient.ConnectAsync("yourhost.com", 443), timeout: 1000);

1

自从.NET 5开始,ConnectAsync默认接受一个取消标记作为额外的参数[1]。通过这个改进,我们可以简单地设置一个CancellationTokenSource并将其标记传递给连接方法。

超时可能会通过捕获OperationCanceledException来处理,就像处理类似情况(TaskCanceledException)一样。请注意,大部分清理工作已经由using块完成。

        const int TIMEOUT_MS = 1000;

        using (TcpClient tcpClient = new TcpClient())
        {
            try
            {
                // Create token that will change to "cancelled" after delay
                using (var cts = new CancellationTokenSource(
                    TimeSpan.FromMilliseconds(TIMEOUT_MS)
                ))
                {
                    await tcpClient.ConnectAsync(
                        address,
                        port,
                        cts.Token
                    );
                }

                // Do something with the successful connection
                // ...
            }

            // Timeout reached
            catch (OperationCanceledException) {
                // Do something in case of a timeout
            }

            // Network-related error
            catch (SocketException)
            {
                // Do something about other communication issues
            }

            // Some argument-related error, disposed object, ...
            catch (Exception)
            {
                // Do something about other errors
            }
        }

CancellationTokenSource 可能会通过一个小的扩展方法进行隐藏(使用额外的异步/等待有微小的额外成本):
public static class TcpClientExtensions
{
    public static async Task ConnectAsync(
        this TcpClient client,
        string host,
        int port,
        TimeSpan timeout
    )
    {
        // Create token that will change to "cancelled" after delay
        using (var cts = new CancellationTokenSource(timeout))
        {
            await client.ConnectAsync(
                host,
                port,
                cts.Token
            );
        }
    }
}

[1] https://learn.microsoft.com/zh-cn/dotnet/api/system.net.sockets.tcpclient.connectasync?view=net-6.0

ConnectAsync的源代码(.NET 6): https://github.com/dotnet/runtime/blob/v6.0.16/src/libraries/System.Net.Sockets/src/System/Net/Sockets/Socket.Tasks.cs#L85-L126


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接