2016-05-12 13 views
13

Мне просто нужно прочитать до N байт от SslStream, но если байт не был получен до истечения таймаута, отмените его, оставив поток в допустимом состоянии, чтобы повторить попытку позже. (*)Как (неоднократно) читать с .NET 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: Ошибка чтения m socket: System.NotSupportedException: метод BeginRead не может быть вызван при ожидании другой операции чтения.

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

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

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

+0

Родственный вопрос здесь: http://stackoverflow.com/questions/24198290/net-4-5-sslstream-cancel-a-asynchronous-read-write-call – Pol

ответ

6

Конечно, вы можете сделать подход № 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; 
    } 
    } 

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

+0

Интересное но это означает, что, когда приложения хотят завершить соединение, может продолжаться асинхронная работа чтения. Закрытие прерывания 'SslStream', которое будет читать async? – Pol

1

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

Я создал класс оболочки, который обертывает объект Tcp NetworkStream, поскольку он передается в конструктор SslStream. Класс wrapper передает все вызовы на базовый NetworkStream, за исключением того, что метод Read() включает дополнительный try ... 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 с проводкой.

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