Wie stelle ich das Timeout für einen TcpClient ein?

78

Ich habe einen TcpClient, mit dem ich Daten an einen Listener auf einem Remotecomputer sende. Der Remotecomputer ist manchmal ein- und manchmal ausgeschaltet. Aus diesem Grund kann der TcpClient häufig keine Verbindung herstellen. Ich möchte, dass der TcpClient nach einer Sekunde eine Zeitüberschreitung aufweist. Daher dauert es nicht lange, bis keine Verbindung zum Remotecomputer hergestellt werden kann. Derzeit verwende ich diesen Code für den 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
}

Dies funktioniert gut genug, um die Aufgabe zu erledigen. Es sendet es, wenn es kann, und fängt die Ausnahme ab, wenn es keine Verbindung zum Remotecomputer herstellen kann. Wenn jedoch keine Verbindung hergestellt werden kann, dauert es zehn bis fünfzehn Sekunden, um die Ausnahme auszulösen. Ich brauche es, um in ungefähr einer Sekunde eine Auszeit zu nehmen. Wie würde ich die Auszeit ändern?

msbg
quelle

Antworten:

95

Sie müssten die asynchrone BeginConnectMethode verwenden, TcpClientanstatt zu versuchen, eine synchrone Verbindung herzustellen, was der Konstruktor tut. Etwas wie das:

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);
Jon
quelle
2
Was bringt es, eine asynchrone Verbindung zu verwenden und sie wieder mit Wait zu "synchronisieren"? Ich meine, ich versuche derzeit zu verstehen, wie Timeout mit asynchronem Lesen implementiert wird, aber die Lösung besteht nicht darin, das asynchrone Design vollständig zu deaktivieren. sollte Socket-Timeouts oder Stornierungs-Token oder ähnliches verwenden. Andernfalls verwenden Sie stattdessen einfach Verbinden / Lesen ...
RoeeK
5
@RoeeK: Der Punkt ist, was die Frage sagt: programmgesteuert ein beliebiges Zeitlimit für den Verbindungsversuch auswählen. Dies ist kein Beispiel für asynchrone E / A.
Jon
8
@RoeeK: Der springende Punkt dieser Frage ist, dass TcpClientkeine Synchronisierungsverbindungsfunktion mit einem konfigurierbaren Zeitlimit angeboten wird. Dies ist eine Ihrer vorgeschlagenen Lösungen. Dies ist eine Problemumgehung, um dies zu aktivieren. Ich bin mir nicht sicher, was ich sonst sagen soll, ohne mich zu wiederholen.
Jon
Du hast absolut recht. Ich habe wahrscheinlich zu viele "WaitOnes" bei asynchronen Operationen gesehen, bei denen es wirklich nicht benötigt wird.
RoeeK
2
@JeroenMostert, danke, dass Sie darauf hingewiesen haben, aber denken Sie daran, dass dies kein Code für die Produktion ist. Leute, bitte kopieren Sie keinen Einfügecode, der mit dem Kommentar "so etwas" in Ihrem Produktionssystem kommt. =)
Jon
82

Ab .NET 4.5 verfügt TcpClient über eine coole ConnectAsync- Methode, die wir wie folgt verwenden können. Das ist jetzt ganz einfach:

var client = new TcpClient();
if (!client.ConnectAsync("remotehost", remotePort).Wait(1000))
{
    // connection failure
}
Simon Mourier
quelle
3
Ein weiterer Vorteil von ConnectAsync besteht darin, dass Task.Wait ein CancellationToken akzeptieren kann, das bei Bedarf sofort vor dem Timeout angehalten wird.
Ilia Barahovski
9
.Wait wird synchron blockiert, wodurch alle Vorteile des Teils "Async" beseitigt werden. stackoverflow.com/a/43237063/613620 ist eine bessere vollständig asynchrone Implementierung.
Tim P.
9
@ TimP. Wo haben Sie das Wort "async" in der Frage gesehen?
Simon Mourier
Ich denke, es ist eine gute Antwort, ich würde jedoch return client.Connected zurückgeben; Meine Testfälle haben gezeigt, dass das Warten allein nicht ausreicht, um eine endgültige Antwort zu erhalten
Walter Vehoeven
1
Sie haben gerade meine Antwortzeit für 10 Kunden von 28 Sekunden auf 1,5 Sekunden reduziert !!! Genial!
JeffS
16

Eine weitere Alternative mit 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)
{
    ...
}
mcandal
quelle
Dies ist die korrekte vollständig asynchrone Implementierung.
Tim P.
1
Warum ist es nicht möglich, mit a Task.Delayeine Aufgabe zu erstellen, die nach einer bestimmten Zeit abgeschlossen ist, anstatt die CancellationTokenSource/TaskCompletionSourcefür die Bereitstellung der Verzögerung zu verwenden? (Ich habe es versucht und es sperrt, aber ich verstehe nicht warum)
Daniel
Wann wird die Aufgabe abgebrochen? Sicher, dies entsperrt den Aufruf nach dem Timeout, aber läuft ConnectAsync () nicht noch irgendwo im Thread-Pool?
TheColonel26
Ich würde auch gerne die Antwort auf @MondKins Frage wissen
TheColonel26
8

Beachten Sie, dass der BeginConnect-Aufruf möglicherweise vor Ablauf des Zeitlimits fehlschlägt. Dies kann passieren, wenn Sie versuchen, eine lokale Verbindung herzustellen. Hier ist eine modifizierte Version von Jons 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);
NeilMacMullen
quelle
8

In den obigen Antworten wird nicht beschrieben, wie mit einer Verbindung, deren Zeitüberschreitung abgelaufen ist, sauber umgegangen werden kann. Aufrufen von TcpClient.EndConnect, Schließen einer Verbindung, die erfolgreich ist, jedoch nach Ablauf des Zeitlimits, und Entsorgen des TcpClient.

Es mag übertrieben sein, aber das funktioniert bei mir.

    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();
    }
Adster
quelle
Danke für den ausgearbeiteten Code. Ist es möglich, die SocketException auszulösen, wenn der Verbindungsaufruf vor dem Timeout fehlschlägt?
Macke
Das sollte es schon. WaitOne wird freigegeben, wenn entweder der Verbindungsaufruf (erfolgreich oder anderweitig) abgeschlossen ist oder das Zeitlimit abgelaufen ist, je nachdem, was zuerst eintritt. Die Prüfung auf! Client.Connected löst die Ausnahme aus, wenn die Verbindung "schnell fehlgeschlagen" ist.
Adster
3

Legen Sie die ReadTimeout- oder WriteTimeout-Eigenschaft im NetworkStream für synchrones Lesen / Schreiben fest. Aktualisieren des OP-Codes:

try
{
    TcpClient client = new TcpClient("remotehost", this.Port);
    Byte[] data = System.Text.Encoding.Unicode.GetBytes(this.Message);
    NetworkStream stream = client.GetStream();
    stream.WriteTimeout = 1000; //  <------- 1 second timeout
    stream.ReadTimeout = 1000; //  <------- 1 second timeout
    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)
{
    // Throws IOException on stream read/write timeout
    FireFailedEvent(ex); //Notifies of failure
}
mcmcmc
quelle
1

Hier ist eine Codeverbesserung basierend auf der mcandal- Lösung. Ausnahmefang für jede aus der client.ConnectAsyncAufgabe generierte Ausnahme hinzugefügt (z. B. SocketException, wenn der Server nicht erreichbar ist)

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

Wenn Sie async & await verwenden und eine Zeitüberschreitung ohne Blockierung verwenden möchten, besteht ein alternativer und einfacherer Ansatz aus der Antwort von mcandal darin, die Verbindung in einem Hintergrundthread auszuführen und auf das Ergebnis zu warten. Zum Beispiel:

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.

Weitere Informationen und andere Beispiele finden Sie im Task.Wait MSDN-Artikel .

Bob Bryan
quelle
1

Wie Simon Mourier erwähnte, ist es möglich, die ConnectAsyncTcpClient-Methode Taskzusätzlich zu verwenden und den Betrieb so schnell wie möglich zu stoppen.
Zum Beispiel:

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

Außerdem würde ich empfehlen, die AsyncTcpClient C # -Bibliothek anhand einiger Beispiele zu überprüfen, zServer <> Client .

F8ER
quelle