如何设置 TcpClient 的超时?

我有一个 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秒才能抛出异常。我需要一秒钟的时间吗?我该如何改变暂停时间?

127816 次浏览

You would need to use the async BeginConnect method of TcpClient instead of attempting to connect synchronously, which is what the constructor does. Something like this:

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

One thing to take note of is that it is possible for the BeginConnect call to fail before the timeout expires. This may happen if you are attempting a local connection. Here's a modified version of Jon's code...

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

The answers above don't cover how to cleanly deal with a connection that has timed out. Calling TcpClient.EndConnect, closing a connection that succeeds but after the timeout, and disposing of the TcpClient.

It may be overkill but this works for me.

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

Starting with .NET 4.5, TcpClient has a cool ConnectAsync method that we can use like this, so it's now pretty easy:

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

Another alternative using 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)
{
...
}

Here is a code improvement based on mcandal solution. Added exception catching for any exception generated from the client.ConnectAsync task (e.g: SocketException when server is unreachable)

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)
{
...
}

If using async & await and desire to use a time out without blocking, then an alternative and simpler approach from the answer provide by mcandal is to execute the connect on a background thread and await the result. For example:

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.

See the Task.Wait MSDN article for more info and other examples.

As Simon Mourier mentioned, it's possible to use ConnectAsync TcpClient's method with Task in addition and stop operation as soon as possible.
For example:

// ...
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");
}
// ...

Also, I would recommended checking out AsyncTcpClient C# library with some examples provided like Server <> Client.

I am using these generic methods; they can add timeout and cancellation tokens for any async task. Let me know if you see any problem so I can fix it accordingly.

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

Usage

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