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

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

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

SslStream предполагает, что тайм-аут вместе с любым другим исключением IOException, когда оно выбрасывается из внутреннего потока, будет рассматриваться вызывающей стороной как фатальное. Повторное использование экземпляра 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.


person Pol    schedule 12.05.2016    source источник
comment
Связанный вопрос здесь: stackoverflow.com/questions/24198290/   -  person Pol    schedule 12.05.2016


Ответы (2)


Вы, безусловно, можете заставить работать подход № 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;
     }
  }

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

person Community    schedule 15.05.2016
comment
Интересная идея, но это означает, что когда приложения хотят разорвать соединение, может выполняться задача асинхронного чтения. Прервет ли закрытие SslStream это асинхронное чтение? - person Pol; 16.05.2016

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

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

SslStream в этом случае работает корректно, в том числе вызывает соответствующее исключение IOException, если сокет закрыт. Обратите внимание, что наш Stream, возвращающий 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 из сети.

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

person Trevor    schedule 12.01.2018
comment
это потрясающий класс-оболочка. В моей программе пришлось изменить всего пару строк кода, и теперь у меня есть чтение с короткой блокировкой в ​​моем SSL без необходимости переделывать все для запуска асинхронно. Отличная работа. - person Shad; 29.12.2020