如何设置TcpClient的超时时间

本文关键字:TcpClient 超时 时间 设置 何设置 | 更新日期: 2023-09-27 18:18:24

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

这对于处理任务来说足够好了。如果可以,它就发送它,如果不能连接到远程计算机,它就捕获异常。但是,当它无法连接时,需要10到15秒才能抛出异常。我需要它在一秒钟内停止?如何更改超时时间?

如何设置TcpClient的超时时间

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

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

您需要使用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);

使用https://stackoverflow.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)
{
    ...
}

上面的答案没有涵盖如何干净地处理已超时的连接。调用TcpClient。结束连接,关闭一个成功的连接,但在超时后,并处理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();
    }

需要注意的一点是,在超时到期之前,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);

这是一个基于并行解决方案的代码改进。为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)
{
    ...
}

如果使用async &等待并希望在不阻塞的情况下使用超时,那么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.

查看任务。

正如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 .

我正在使用这些泛型方法;他们可以为任何异步任务添加超时和取消令牌。如果你看到任何问题,请告诉我,这样我就可以相应地解决它。

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

Since 。. NET 5, ConnectAsync接受一个取消令牌作为开箱即用的附加参数[1]。这样,就可以简单地设置一个CancellationTokenSource并将其令牌交给connect方法。

超时可能通过捕获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可以通过一个小的扩展方法隐藏(额外的async/await开销很小):

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/en us/dotnet/api/system.net.sockets.tcpclient.connectasync?view=net - 6.0

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