c# - ¿Cómo(repetidamente) leer de.NET SslStream con un tiempo de espera?
networkstream (2)
Solo necesito leer hasta N
bytes de un SslStream
pero si no se ha recibido ningún byte antes de un tiempo de espera, cancele, dejando el flujo en un estado válido para intentarlo más tarde. (*)
Esto se puede hacer fácilmente para transmisiones no SSL, es decir, NetworkStream
simplemente utilizando su propiedad ReadTimeout
, que hará que la transmisión produzca una excepción en el tiempo de espera. Desafortunadamente, este enfoque no funciona en SslStream
según los documentos oficiales:
SslStream asume que un tiempo de espera junto con cualquier otra excepción IOException cuando se arroja desde el flujo interno será tratado como fatal por su llamador. La reutilización de una instancia de SslStream después de un tiempo de espera devolverá basura. Una aplicación debe cerrar el SslStream y lanzar una excepción en estos casos.
[Actualizado 1] Probé un enfoque diferente como este:
task = stream->ReadAsync(buffer, 0, buffer->Length);
if (task->Wait(timeout_ms)) {
count = task->Result;
...
}
Pero esto no funciona si Wait()
devolvió false
: cuando ReadAsync()
llamar a ReadAsync()
más tarde, lanza una excepción:
Excepción lanzada: ''System.NotSupportedException'' en System.dll Tests.exe Advertencia: 0: Error al leer del socket: System.NotSupportedException: no se puede llamar al método BeginRead cuando hay otra operación de lectura pendiente.
[Actualización 2] Intenté otro enfoque para implementar los tiempos de espera llamando a Poll(timeout, ...READ)
en el zócalo TcpClient
subyacente: si devuelve true
, entonces llame a Read()
en el SSlStream
, o si devuelve false
entonces tener un tiempo de espera. Esto tampoco funciona: como SslStream
probablemente utiliza sus propios búferes intermedios internos, Poll()
puede devolver false
incluso si hay datos SslStream
leer en SslStream
.
[Actualización 3] Otra posibilidad sería escribir una subclase de Stream
personalizada que se ubicaría entre NetworkStream
y SslStream
y capturar la excepción de tiempo de espera y devolver 0 bytes a SslStream
. No estoy seguro de cómo hacer esto, y lo que es más importante, no tengo idea de si devolver un 0 bytes leído a SslStream
aún no lo corrompería de alguna manera.
(*) La razón por la que estoy tratando de hacer esto es que la lectura sincrónica con un tiempo de espera de un socket no seguro o seguro es el patrón que ya estoy usando en iOS, OS X, Linux y Android para algunos códigos multiplataforma . Funciona para sockets no seguros en .NET, por lo que el único caso restante es SslStream
.
Ciertamente puedes hacer que el enfoque # 1 funcione. Simplemente necesita realizar un seguimiento de la Tarea y continuar esperando sin volver a llamar a ReadAsync. Entonces, muy rudamente:
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;
}
}
Necesita ser un poco más amplio para que la persona que llama pueda ver que la lectura aún no se ha completado, pero el fragmento es demasiado pequeño para dar consejos concretos.
También encontré este problema con un SslStream que devolvía cinco bytes de datos de basura en la lectura después de un tiempo de espera, y encontré por separado una solución similar a la Actualización # 3 de OP.
Creé una clase envoltura que envuelve el objeto Tcp NetworkStream cuando se pasa al constructor SslStream. La clase envoltura pasa todas las llamadas al NetworkStream subyacente, excepto que el método Read () incluye un retén adicional de try ... para suprimir la excepción de tiempo de espera y devolver en su lugar 0 bytes.
SslStream funciona correctamente en esta instancia, incluida la generación de la excepción IOException adecuada si el socket está cerrado. Tenga en cuenta que nuestro Stream que devuelve 0 desde un Read () es diferente de un TcpClient o un socket que devuelve 0 desde un Read () (lo que generalmente significa una desconexión de 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; }
}
}
Esto se puede usar envolviendo el objeto TcpClient NetworkStream antes de pasarlo a SslStream, de la siguiente manera:
NetworkStream lTcpStream = lTcpClient.GetStream();
SocketTimeoutSuppressedStream lSuppressedStream = new SocketTimeoutSuppressedStream(lTcpStream);
using (lSslStream = new SslStream(lSuppressedStream, true, ServerCertificateValidation, SelectLocalCertificate, EncryptionPolicy.RequireEncryption))
El problema se debe a que SslStream corrompe su estado interno en cualquier excepción del flujo subyacente, incluso un tiempo de espera inofensivo. Curiosamente, los cinco (o así) bytes de datos que devuelve la próxima lectura () son en realidad el inicio de los datos de carga útil encriptados TLS desde el cable.
Espero que esto ayude