Ошибка при разрыве соединения TcpClient

Попытался реализовать класс подключения клиентов к серверам, выводить сообщения, информировать о подключении и ошибках, выполнять автоматическое переподключение в случае разрыва соединения.

При удачном подключении вроде бы всё ок и сообщения я получаю, но возникает ошибка при разрыве соединения с сервером или попытке выполнить Disconnect() + бесконечно выводятся пустые сообщения. Скорее всего, я что-то не правильно реализовал, но не могу понять в чем проблема.

Подскажите пожалуйста, в чем я допустил ошибку или может уже существуют готовые решения моей задачи?

Ошибка: System.ObjectDisposedException: "CancellationTokenSource был удален."

Код программы:

public SocketClient(Setting setting)
{
   _setting = setting;

   Thread thread = new Thread((o) =>
   {
      while (true)
      {
         Connect();

         while (_tcpClient != null && _tcpClient.Connected)
         {
            // Проверка соединения каждые 5сек
            if (_tcpClient.Client.Poll(0, SelectMode.SelectRead))
            {
                byte[] buff = new byte[1];
                if (_tcpClient.Client.Receive(buff, SocketFlags.Peek) == 0)
                {
                   NLogger.Info("Соединение закрыто");
                   Disconnect();
                }
             }

             Thread.Sleep(5000);
          }

          // Переподключение каждые 10 сек  
          Thread.Sleep(10_000);
      }
   });
   thread.IsBackground = true;
   thread.Start();
}

Подключение:

public async void Connect()
{
    try
    {
       using (_tcpClient = new TcpClient())
       {
          await _tcpClient.ConnectAsync(IPAddress.Parse(_setting.Address), _setting.Port);

          if (_tcpClient.Connected)
          {
             using (_cancelToken = new CancellationTokenSource())
             {
                await ReceiveMessage(_cancelToken.Token);
             }
          }
       }
    }
    catch (SocketException)
    {
        NLogger.Info("Ошибка подключения");
    }
    catch (Exception ex)
    {
        NLogger.Error(ex, "Ошибка программы: Connect");
        Disconnect();
    }
}

Отключение:

public void Disconnect()
{
   _cancelToken.Cancel();
   _cancelToken.Dispose();

   _tcpClient.Close();
   _tcpClient.Dispose();
}

Чтение сообщения:

private async Task ReceiveMessage(CancellationToken token)
{
    try
    {
        using (NetworkStream stream = _tcpClient.GetStream())
        {
           while (!token.IsCancellationRequested)
           {
              if (stream.CanRead)
              {
                 byte[] responceData = new byte[1024];
                 int bytes = 0;
                 StringBuilder sb = new StringBuilder();

                 do
                 {
                    bytes = await stream.ReadAsync(responceData, 0, responceData.Length);
                    sb.Append(Encoding.ASCII.GetString(responceData, 0, bytes));
                    _message = sb.ToString();
                 }
                 while (stream.DataAvailable);
              }
           }
        }
   }
   catch (ObjectDisposedException ex)
   {
      NLogger.Info(ex, "Отключен");
      Disconnect();
   }
   catch (Exception ex)
   {
      NLogger.Error(ex, "Ошибка программы: ReceiveMessage");
      Disconnect();
   }
}

Ответы (1 шт):

Автор решения: aepot

У вас процессор не кипит?

while (!token.IsCancellationRequested)
{
    if (stream.CanRead)
    {
    }
}

Если stream.CanRead возвращает false, цикл просто выдаст максимальную нагрузку на процессор. К тому же синхронно повесит код.

private async Task ReceiveMessage(CancellationToken token)
{
    try
    {
        using (NetworkStream stream = _tcpClient.GetStream())
        {
            byte[] buffer = new byte[1024];
            StringBuilder sb = new StringBuilder();
            while (true)
            {
                do
                {
                    int bytes = await stream.ReadAsync(buffer, 0, buffer.Length, token);
                    sb.Append(Encoding.ASCII.GetString(buffer, 0, bytes));
                }
                while (stream.DataAvailable);
                _message = sb.ToString();
                sb.Clear();
            }
        }
    }
    catch (ObjectDisposedException ex)
    {
        NLogger.Info(ex, "Отключен");
    }
    catch (Exception ex)
    {
        NLogger.Error(ex, "Ошибка программы: ReceiveMessage");
    }
}

Как-то так оно должно выглядеть.

Вот теперь будет работать, так как этот код не будет вешать родительский метод.

убрал using у tcpClient и сancelToken. В Disconnect() сделал, если сancelToken != null, то .Close(), Dispose(), сancelToken = null. Так же и с _tcpClient. Вроде бы всё стало работать

Возвращайте using назад. А метод Disconnect() уберите совсем везде, он не нужен.

Ещё вот этот код можно немного упростить

public SocketClient(Setting setting)
{
    _setting = setting;

    Task.Run(() =>
    {
        while (true)
        {
            Connect();

            while (_tcpClient?.Connected ?? false)
            {
                // Проверка соединения каждые 5сек
                if (_tcpClient.Client.Poll(0, SelectMode.SelectRead))
                {
                    byte[] buff = new byte[1];
                    if (_tcpClient.Client.Receive(buff, SocketFlags.Peek) == 0)
                    {
                        NLogger.Info("Соединение закрыто");
                        Disconnect();
                    }
                }

                Thread.Sleep(5000);
            }

            // Переподключение каждые 10 сек  
            Thread.Sleep(10_000);
        }
    });
}

И вот здесь ещё небольшая правочка

if (_tcpClient.Connected)
{
    using (_cancelToken = new CancellationTokenSource())
    {
        await ReceiveMessage(_cancelToken.Token);
    }
    _cancelToken = null;
}

Если вручную надо закрыть подключение, просто вызовите _cancelToken?.Cancel();. Больше ничего делать не нужно.

Ну или тот же Disconnect

public void Disconnect()
{
    _cancelToken?.Cancel();
}

Но вызывать его только вручную, не из методов Connect или ReceiveMessage.

По-хорошему, вам нужен протокол передачи данных. То есть вы сейчас не знаете, какого размера будет принимаемое сообщение. Нужно передавать длину сообщения, затем само сообщение.

Вот пример: TCP клиент-сервер как реализовать передачу данных без краша сервера?

→ Ссылка