2016-05-12 11 views
13

Ho solo bisogno di leggere fino a N byte da un SslStream ma se nessun byte è stato ricevuto prima di un timeout, annullare, lasciando il flusso in uno stato valido per riprovare più tardi. (*)Come leggere (ripetutamente) da .NET SslStream con un timeout?

Questo può essere fatto facilmente per flussi non SSL, ad esempio NetworkStream, semplicemente utilizzando la proprietà ReadTimeout che farà sì che il flusso generi un'eccezione sul timeout. Purtroppo questo approccio non funziona su SslStream per la documentazione ufficiale:

SslStream presuppone che un timeout insieme a qualsiasi altra IOException quando si è gettato dal flusso interno sarà trattato come fatale per il suo chiamante. Riutilizzare un'istanza SslStream dopo un timeout restituirà garbage. Un'applicazione dovrebbe chiudere lo SslStream e generare un'eccezione in questi casi.

[Aggiornamento 1] ho provato un approccio diverso come questo:

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

Ma questo non funziona se Wait() restituito false: al momento della chiamata ReadAsync() più tardi viene generata un'eccezione:

Eccezione generata: "System.NotSupportedException" in System.dll Tests.exe Avviso: 0: lettura non riuscita da s ocket: System.NotSupportedException: il metodo BeginRead non può essere chiamato quando è in attesa un'altra operazione di lettura.

[Update 2] ho provato ancora un altro approccio per implementare timeout chiamando Poll(timeout, ...READ) sul sottostante TcpClient presa: se restituisce true, quindi chiamare Read() sul SSlStream, o se restituisce false poi abbiamo una tempo scaduto. Anche questo non funziona: poiché SslStream utilizza presumibilmente i propri buffer di intermediazione interni, Poll() può restituire false anche se i dati restano da leggere nello SslStream.

[Update 3] Un'altra possibilità sarebbe quella di scrivere un Stream sottoclasse personalizzata che si sedeva tra il NetworkStream e SslStream e catturare l'eccezione di timeout e restituire 0 byte invece SslStream. Non sono sicuro di come farlo e, cosa più importante, non ho idea se restituire 0 byte letti a SslStream non lo danneggerebbe in qualche modo.

(*) La ragione per cui sto provando a fare questo è che leggere in sincronia con un timeout da un socket non sicuro o sicuro è lo schema che sto già usando su iOS, OS X, Linux e Android per alcuni codice multipiattaforma. Funziona con socket non protetti in .NET, quindi l'unico caso rimanente è SslStream.

+0

Una questione collegata qui: http://stackoverflow.com/questions/24198290/net-4-5-sslstream-cancel-a-asynchronous-read-write-call – Pol

risposta

6

È possibile che l'approccio n. 1 funzioni. Devi semplicemente tenere traccia dell'attività e continuare ad aspettare senza chiamare nuovamente ReadAsync. Quindi, molto approssimativamente:

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; 
    } 
    } 

ha bisogno di essere concretizzati-out un po 'in modo che il chiamante può vedere che la lettura non è ancora completato, ma il frammento è troppo piccolo per dare consigli concreti.

+0

Interessante idea, ma questo significa che quando le app vogliono terminare la connessione, potrebbe esserci un'attività di lettura asincrona ancora in corso. Chiuderà l'interruzione di 'SslStream' che legge asincronizzazione? – Pol

1

Ho anche riscontrato questo problema con uno SslStream che restituiva cinque byte di dati inutili sulla lettura dopo un timeout e ho individuato separatamente una soluzione simile all'Aggiornamento n. 3 dell'OP.

Ho creato una classe wrapper che avvolge l'oggetto Tcp NetworkStream mentre viene passato al costruttore SslStream. La classe wrapper trasferisce tutte le chiamate al NetworkStream sottostante, tranne che il metodo Read() include un ulteriore try ... catch per sopprimere l'eccezione Timeout e restituire invece 0 byte.

SslStream funziona correttamente in questa istanza, compreso il sollevamento della IOException appropriata se il socket è chiuso. Si noti che il nostro Stream che restituisce 0 da un Read() è diverso da un TcpClient o Socket che restituisce 0 da un Read() (che in genere significa disconnettere il socket).

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; } 
    } 
} 

Questo può quindi essere utilizzato avvolgendo l'oggetto TcpClient NetworkStream prima che sia passato alla SslStream, come segue:

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

Il problema si riduce a SslStream corrompere il suo stato interno su una qualsiasi eccezione dal flusso sottostante, anche un timeout innocuo. Stranamente, i cinque (o così) byte di dati che i successivi read() restituiscono sono in realtà l'inizio dei dati del carico utile crittografato TLS dal filo.

Spero che questo aiuti