Подтвердить что ты не робот

Как (повторно) читать из .NET SslStream с таймаутом?

Мне просто нужно прочитать до N байтов от SslStream, но если байт не был получен до истечения таймаута, отмените его, оставив поток в допустимом состоянии, чтобы повторить попытку позже. (*)

Это можно легко сделать для потоков без SSL, т.е. NetworkStream, просто используя его свойство ReadTimeout, которое сделает поток генерирует исключение в таймаут. К сожалению, этот подход не работает на SslStream в официальных документах:

SslStream предполагает, что тайм-аут вместе с любым другим исключением IO, когда он выкинут из внутреннего потока, будет считаться фатальным его вызывающим. Повторное использование экземпляра SslStream после таймаута возвращает мусор. Приложение должно закрыть SslStream и выбросить исключение в этих случаях.

[Обновлено 1] Я пробовал использовать другой подход:

task = stream->ReadAsync(buffer, 0, buffer->Length);
if (task->Wait(timeout_ms)) {
   count = task->Result;
   ...
}

Но это не работает, если Wait() возвращен false: при вызове ReadAsync() снова он выдает исключение:

Исключение выбрано: System.NotSupportedException в System.dll Tests.exe Предупреждение: 0: Ошибка чтения из сокета: System.NotSupportedException: метод BeginRead не может быть вызван при ожидании другой операции чтения.

[Обновить 2]. Я попытался использовать еще один подход для реализации тайм-аутов, вызвав Poll(timeout, ...READ) в базовом сокете TcpClient: если он возвращает true, затем вызовите Read() в SslStream, или если он возвращает false, тогда у нас есть тайм-аут. Это также не работает: поскольку SslStream предположительно использует собственные внутренние промежуточные буферы, Poll() может возвращать false, даже если в SslStream есть данные, которые нужно прочитать.

[Обновить 3]. Еще одна возможность - создать пользовательский подкласс Stream, который будет находиться между NetworkStream и SslStream и захватить исключение тайм-аута и вернуть 0 байт вместо SslStream. Я не уверен, как это сделать, и, что более важно, я понятия не имею, что возвращение 0 байтов, прочитанных в SslStream, все равно не испортило бы его.

(*) Причина, по которой я пытаюсь это сделать, заключается в том, что чтение синхронно с тайм-аутом из небезопасного или защищенного сокета - это шаблон, который я уже использую в iOS, OS X, Linux и Android для некоторого креста -платформенный код. Он работает для небезопасных сокетов в .NET, поэтому единственным оставшимся случаем является SslStream.

4b9b3361

Ответ 1

Вы можете сделать работу по подходу №1. Вам просто нужно отслеживать Задачу и продолжать ждать, не вызвав снова ReadAsync. Итак, очень грубо:

private Task readTask;     // class level variable
...
  if (readTask == null) readTask = stream->ReadAsync(buffer, 0, buffer->Length);
  if (task->Wait(timeout_ms)) {
     try {
         count = task->Result;
         ...
     }
     finally {
         task = null;
     }
  }

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

Ответ 2

Я также столкнулся с этой проблемой, когда SslStream вернул пять байтов данных мусора при чтении после таймаута, и я отдельно подобрал решение, похожее на OP Update # 3.

Я создал класс-оболочку, который обертывает объект Tcp NetworkStream, поскольку он передается в конструктор SslStream. Класс wrapper передает все вызовы на базовый NetworkStream, за исключением того, что метод Read() включает дополнительную попытку... catch для исключения исключения Timeout и вместо этого возвращает 0 байт.

SslStream работает правильно в этом случае, включая повышение соответствующего IOException, если сокет закрыт. Обратите внимание, что наш поток, возвращающий 0 из Read(), отличается от TcpClient или Socket, возвращающего 0 из Read() (что обычно означает разъединение сокета).

class SocketTimeoutSuppressedStream : Stream
{
    NetworkStream mStream;

    public SocketTimeoutSuppressedStream(NetworkStream pStream)
    {
        mStream = pStream;
    }

    public override int Read(byte[] buffer, int offset, int count)
    {
        try
        {
            return mStream.Read(buffer, offset, count);
        }
        catch (IOException lException)
        {
            SocketException lInnerException = lException.InnerException as SocketException;
            if (lInnerException != null && lInnerException.SocketErrorCode == SocketError.TimedOut)
            {
                // Normally, a simple TimeOut on the read will cause SslStream to flip its lid
                // However, if we suppress the IOException and just return 0 bytes read, this is ok.
                // Note that this is not a "Socket.Read() returning 0 means the socket closed",
                // this is a "Stream.Read() returning 0 means that no data is available"
                return 0;
            }
            throw;
        }
    }


    public override bool CanRead => mStream.CanRead;
    public override bool CanSeek => mStream.CanSeek;
    public override bool CanTimeout => mStream.CanTimeout;
    public override bool CanWrite => mStream.CanWrite;
    public virtual bool DataAvailable => mStream.DataAvailable;
    public override long Length => mStream.Length;
    public override IAsyncResult BeginRead(byte[] buffer, int offset, int size, AsyncCallback callback, object state) => mStream.BeginRead(buffer, offset, size, callback, state);
    public override IAsyncResult BeginWrite(byte[] buffer, int offset, int size, AsyncCallback callback, object state) => mStream.BeginWrite(buffer, offset, size, callback, state);
    public void Close(int timeout) => mStream.Close(timeout);
    public override int EndRead(IAsyncResult asyncResult) => mStream.EndRead(asyncResult);
    public override void EndWrite(IAsyncResult asyncResult) => mStream.EndWrite(asyncResult);
    public override void Flush() => mStream.Flush();
    public override Task FlushAsync(CancellationToken cancellationToken) => mStream.FlushAsync(cancellationToken);
    public override long Seek(long offset, SeekOrigin origin) => mStream.Seek(offset, origin);
    public override void SetLength(long value) => mStream.SetLength(value);
    public override void Write(byte[] buffer, int offset, int count) => mStream.Write(buffer, offset, count);

    public override long Position
    {
        get { return mStream.Position; }
        set { mStream.Position = value; }
    }

    public override int ReadTimeout
    {
        get { return mStream.ReadTimeout; }
        set { mStream.ReadTimeout = value; }
    }

    public override int WriteTimeout
    {
        get { return mStream.WriteTimeout; }
        set { mStream.WriteTimeout = value; }
    }
}

Затем это можно использовать, обернув объект TcpClient NetworkStream до его передачи в SslStream следующим образом:

NetworkStream lTcpStream = lTcpClient.GetStream();
SocketTimeoutSuppressedStream lSuppressedStream = new SocketTimeoutSuppressedStream(lTcpStream);
using (lSslStream = new SslStream(lSuppressedStream, true, ServerCertificateValidation, SelectLocalCertificate, EncryptionPolicy.RequireEncryption))

Проблема сводится к тому, что SslStream разлагает внутреннее состояние любого исключения из основного потока, даже безвредного таймаута. Как ни странно, пять (или так) байтов данных, которые возвращают следующий read(), фактически являются началом зашифрованных данных TLS зашифрованных данных от провода.

Надеюсь, что это поможет