Як встановити час очікування для TcpClient?


79

У мене є 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
}

Це працює досить добре для вирішення завдання. Він надсилає його, якщо може, і ловить виняток, якщо не вдається підключитися до віддаленого комп’ютера. Однак, коли він не може підключитися, потрібно десять-п’ятнадцять секунд, щоб видалити виняток. Мені потрібен час очікування приблизно за одну секунду? Як змінити час очікування?

Відповіді:


97

Вам потрібно буде використовувати BeginConnectметод async TcpClientзамість спроби синхронного підключення, що і робить конструктор. Щось на зразок цього:

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
який сенс використовувати асинхронне підключення та "синхронізувати" його назад із Wait? Я маю на увазі, що я зараз намагаюся зрозуміти, як реалізувати час очікування з асинхронним читанням, але рішення полягає не в тому, щоб повністю вимкнути асинхронний дизайн. слід використовувати таймаути сокета або маркер скасування або щось подібне. в іншому випадку просто використовуйте підключення / читання замість цього ...
RoeeK

5
@RoeeK: Справа в тому, про що йдеться в запитанні: програмно вибрати довільний тайм-аут для спроби підключення. Це не приклад того, як виконувати асинхронний введення-виведення.
Джон,

9
@RoeeK: Вся суть цього питання полягає в тому, TcpClientщо не пропонується функція синхронізації підключення з настроюваним тайм-аутом, що є одним із запропонованих вами рішень. Це обхідний шлях, щоб увімкнути його. Я не знаю, що ще сказати, не повторюючись.
Джон

Ви абсолютно праві. Я, мабуть, бачив занадто багато "WaitOnes" на асинхронних операціях, де це насправді не потрібно ..
RoeeK

2
@JeroenMostert дякуємо, що вказали на це, але давайте мати на увазі, що це не код виробничого рівня. Люди, будь ласка, не копіюйте вставний код, який додається з коментарем "щось подібне" у вашій виробничій системі. =)
Джон

83

Починаючи з .NET 4.5, TcpClient має класний метод ConnectAsync, який ми можемо використовувати так, тому зараз це досить просто:

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

3
Додатковою перевагою ConnectAsync є те, що Task.Wait може прийняти CancellationToken для негайної зупинки у разі потреби навіть до часу очікування.
Ilia Barahovski

9
.Wait буде синхронно блокувати, видаляючи будь-які переваги частини "Асинхронізація". stackoverflow.com/a/43237063/613620 - це краща повністю асинхронна реалізація.
Тім П.

9
@TimP. де ви бачили слово "асинхронізація" у питанні?
Саймон Мур'є

Я думаю, що це хороша відповідь, однак я б повернув клієнта, що повертається. Connected; Мої тестові випадки показали, що одного лише очікування недостатньо для остаточної відповіді
Вальтер Веховен

1
Ви просто скоротили час моєї відповіді для 10 клієнтів з 28 секунд до 1,5 секунди !!! Чудово!
JeffS

17

Ще одна альтернатива використання 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)
{
    ...
}

Це правильна повністю асинхронна реалізація.
Тім П.

1
Чому не можна використовувати a Task.Delayдля створення завдання, яке виконується через певний час, замість того, щоб використовувати CancellationTokenSource/TaskCompletionSourceдля забезпечення затримки? (Я намагався, і це блокується, але я не розумію, чому)
Даніель

Коли завдання скасовується? Звичайно, це розблокує виклик після закінчення тайм-ауту, але ConnectAsync () все ще не працює в пулі потоків?
TheColonel26,

Я також хотів би знати відповідь на запитання
@MondKin

9

Наведені вище відповіді не стосуються того, як правильно поводитися зі зв’язком, який закінчився. Виклик 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, якщо виклик з'єднання не вдається до часу очікування?
Макке

Це вже повинно. WaitOne звільниться, коли або виклик Connect завершиться (успішно чи іншим чином) або закінчиться час очікування, залежно від того, що трапиться раніше. Перевірка! Client.Connected викликає виняток, якщо підключення "не вдалося швидко".
Adster

8

Одне, на що слід звернути увагу, - це те, що виклик BeginConnect може провалитися до закінчення часу очікування. Це може трапитися, якщо ви намагаєтесь встановити локальне з'єднання. Ось модифікована версія коду Джона ...

        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

Встановіть властивість ReadTimeout або WriteTimeout у NetworkStream для синхронного читання / запису. Оновлення коду OP:

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
}

1

Ось вдосконалення коду на основі рішення 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)
{
    ...
}

1

Якщо ви використовуєте 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

Як згадував Саймон Мур'є , можна використовувати 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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.