2016-05-12 3 views
13

J'ai juste besoin de lire jusqu'à N octets à partir d'un SslStream mais si aucun octet n'a été reçu avant un délai, annuler, tout en laissant le flux dans un état valide afin d'essayer à nouveau plus tard. (*)Comment lire (à plusieurs reprises) à partir de .NET SslStream avec un délai d'expiration?

Cela peut être fait facilement pour les flux non-SSL, c'est-à-dire NetworkStream, simplement en utilisant sa propriété ReadTimeout qui fera que le flux lancera une exception au délai d'attente. Malheureusement, cette approche ne fonctionne pas sur SslStream par les documents officiels:

SslStream suppose qu'un délai d'attente ainsi que tout autre IOException quand on est éjecté du flux interne sera traitée comme fatale par son appelant. Réutiliser une instance de SslStream après un délai d'expiration retournera la poubelle. Une application doit fermer le SslStream et lancer une exception dans ces cas.

[Mise à jour 1] J'ai essayé une approche différente comme ceci:

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

Mais cela ne fonctionne pas si Wait() retourné false: lorsque vous appelez ReadAsync() plus tard, il lance une exception:

Exception levée: 'System.NotSupportedException' dans System.dll Tests.exe Avertissement: 0: Lecture impossible à partir de s ocket: System.NotSupportedException: La méthode BeginRead ne peut pas être appelée lorsqu'une autre opération de lecture est en attente.

[Mise à jour 2] J'ai essayé une autre approche pour mettre en œuvre les délais d'attente en appelant Poll(timeout, ...READ) sur la prise TcpClient sous-jacente: si elle retourne true, puis appelez Read() sur le SSlStream, ou si elle retourne false alors nous avons un temps libre. Cela ne fonctionne pas non plus: parce que SslStream utilise vraisemblablement ses propres tampons intermédiaires internes, Poll() peut renvoyer false même s'il reste des données à lire dans le SslStream.

[Mise à jour 3] Une autre possibilité serait d'écrire une sous-classe personnalisée Stream qui siègerait entre NetworkStream et SslStream et capturer l'exception de délai d'attente et retour 0 octets au lieu de SslStream. Je ne suis pas sûr de savoir comment faire cela, et plus important encore, je ne sais pas si retourner 0 octets lus à SslStream ne serait toujours pas le corrompre en quelque sorte.

(*) La raison pour laquelle j'essaye de faire ceci est que la lecture synchrone avec un délai d'une prise non-sûre ou sûre est le modèle que j'utilise déjà sur iOS, OS X, Linux et Android pour code multiplateforme. Il fonctionne pour les sockets non sécurisés dans .NET, le seul cas restant est SslStream.

+0

Une question connexe ici: http://stackoverflow.com/questions/24198290/net-4-5-sslstream-cancel-a-asynchronous-read-write-call – Pol

Répondre

6

Vous pouvez certainement faire fonctionner l'approche n ° 1. Vous devez simplement suivre la tâche et continuer à attendre sans avoir à relire ReadAsync. Donc, très grossièrement:

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

a besoin d'être étoffée un peu afin que l'appelant peut voir que la lecture ne soit pas encore terminée, mais l'extrait est trop petit pour donner des conseils concrets.

+0

Intéressant idée, mais cela signifie que lorsque les applications veulent mettre fin à la connexion, il pourrait y avoir une tâche de lecture asynchrone en cours. Est-ce que la fermeture de 'SslStream' annulera la lecture asynchrone? – Pol

1

J'ai également rencontré ce problème avec un SslStream retournant cinq octets de données parasites sur la lecture après un délai d'attente, et j'ai séparément proposé une solution qui est similaire à la mise à jour OP # 3.

J'ai créé une classe wrapper qui enveloppe l'objet Tcp NetworkStream lorsqu'il est transmis au constructeur SslStream. La classe wrapper passe tous les appels sur le NetworkStream sous-jacent sauf que la méthode Read() inclut un essai supplémentaire ... catch pour supprimer l'exception Timeout et retourner 0 octets à la place.

SslStream fonctionne correctement dans cette instance, y compris en levant l'exception IOException appropriée si le socket est fermé. Notez que notre flux renvoyant 0 à partir d'un Read() est différent d'un TcpClient ou d'un Socket renvoyant 0 à partir d'un Read() (ce qui signifie généralement une déconnexion du 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; } 
    } 
} 

Cela peut ensuite être utilisé en enveloppant l'objet TcpClient NetworkStream avant qu'il ne soit transmis à la SslStream, comme suit:

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

Le problème se résume à SslStream corrompant son état interne à une exception de la flux sous-jacent, même un timeout inoffensif. Curieusement, les cinq (ou plus) octets de données que renvoie le read() suivant sont en fait le début des données de charge utile chiffrées TLS du réseau.

Hope this helps