如何设置 TcpClient 的超时时间?

Posted

技术标签:

【中文标题】如何设置 TcpClient 的超时时间?【英文标题】:How to set the timeout for a TcpClient? 【发布时间】:2013-06-11 16:50:31 【问题描述】:

我有一个 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

这足以处理任务。如果可以,它会发送它,如果它无法连接到远程计算机,它会捕获异常。但是,当它无法连接时,需要十到十五秒才能抛出异常。我需要它在一秒钟左右超时吗?如何更改超时时间?

【问题讨论】:

【参考方案1】:

您需要使用TcpClient 的异步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);

【讨论】:

使用异步连接有什么意义,然后用等待“同步”回来?我的意思是,我目前正在尝试了解如何通过异步读取实现超时,但解决方案不是完全禁用异步设计。应该使用套接字超时或取消令牌或类似的东西。否则,只需使用连接/读取... @RoeeK:问题的关键是:以编程方式为连接尝试选择任意超时。这不是关于如何进行异步 IO 的示例。 @RoeeK:这个问题的重点是TcpClient 不提供具有可配置超时的同步连接功能,这是您提出的解决方案之一。这是启用它的解决方法。如果不重复我自己,我不确定还能说什么。 你说得对。我可能在真正不需要的异步操作中看到了太多“WaitOnes”.. @JeroenMostert 感谢您指出这一点,但请记住,这不是生产级代码。人们,请不要在您的生产系统中复制带有注释“类似这样的东西”的粘贴代码。 =)【参考方案2】:

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

var client = new TcpClient();
if (!client.ConnectAsync("remotehost", remotePort).Wait(1000))

    // connection failure

【讨论】:

ConnectAsync 的另一个好处是 Task.Wait 可以接受 CancellationToken 以在需要时立即停止,甚至在超时之前。 .Wait 将同步阻塞,消除“异步”部分的任何好处。 ***.com/a/43237063/613620 是一个更好的全异步实现。 @TimP。您在哪里看到问题中的“异步”一词? 我认为这是一个很好的答案,但是我会返回 return client.Connected;我的测试用例表明,单独等待是不够的 您刚刚将我对 10 个客户的响应时间从 28 秒缩短到 1.5 秒!!!太棒了!【参考方案3】:

使用https://***.com/a/25684549/3975786的另一种选择:

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)

    ...

【讨论】:

这是正确的完全异步实现。 为什么不能使用Task.Delay 创建一个在一定时间后完成的任务,而不是使用CancellationTokenSource/TaskCompletionSource 提供延迟? (我试过了,它锁了,但我不明白为什么) 任务什么时候取消?确定这会在超时后解除阻塞,但 ConnectAsync() 是否仍在某个线程池上运行? 我也想知道@MondKin 问题的答案 要回答这个问题(虽然有点晚了),如果实际操作在WhenAny 调用延迟之前完成,则使用简单的Task.Run(()=&gt;Task.Delay(5)) 可以防止处理延迟Task,并且不处理任务会泄漏异步句柄。您可以在使用重载处理之前取消延迟,但是无论如何您都需要CancellationToken,这样您就不会保存任何东西(实际上取消延迟会比这个解决方案增加更多的 LoC)【参考方案4】:

上面的答案没有涵盖如何干净地处理已超时的连接。调用 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? 已经应该了。当 Connect 调用完成(成功或以其他方式)或超时结束时,WaitOne 将释放,以先发生者为准。如果连接“快速失败”,对 !client.Connected 的检查将引发异常。【参考方案5】:

需要注意的一点是,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);

【讨论】:

【参考方案6】:

这是基于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)

    ...

【讨论】:

【参考方案7】:

正如Simon Mourier所说,可以将ConnectAsyncTcpClient的方法与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 &lt;&gt; Client

【讨论】:

【参考方案8】:

如果使用 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 article。

【讨论】:

【参考方案9】:

我正在使用这些通用方法;他们可以为任何异步任务添加超时和取消令牌。如果您发现任何问题,请告诉我,以便我进行相应的解决。

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

【讨论】:

以上是关于如何设置 TcpClient 的超时时间?的主要内容,如果未能解决你的问题,请参考以下文章

超时连接后如何关闭TCP连接?

C# TcpClient的Connect超时处理(Timeout)

如何正确使用 TcpClient ReadTimeout

TCPClientTCPListener的用法

如何检查 TcpClient 连接是不是关闭?

TcpClient 是单个连接使用吗?如何发送第二条消息?