如何(重复)读取.NET SslStream超时?

我只需要从SslStream读取N个字节,但如果在超时之前没有收到任何字节,则取消,同时让流保持有效状态以便稍后再试。 (*)

对于非SSL流,即NetworkStream只需使用其ReadTimeout属性即可轻松完成,这将使流在超时时抛出exception。 不幸的是,根据官方文档,这种方法不适用于SslStream

SslStream假定当从内部流抛出一个IOException时,超时以及任何其他IOException将被其调用者视为致命的。 超时后重用SslStream实例将返回垃圾。 应用程序应该关闭SslStream并在这些情况下抛出exception。

[更新1]我尝试了一种不同的方法:

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

但是如果Wait()返回false则这不起作用:稍后再次调用ReadAsync()时会抛出exception:

抛出exception:System.dll Tests.exe中的“System.NotSupportedException”警告:0:从套接字读取失败:System.NotSupportedException:当另一个读取操作挂起时,无法调用BeginRead方法。

[更新2]我尝试了另一种通过在底层TcpClient套接字上调用Poll(timeout, ...READ)来实现超时的TcpClient :如果它返回true ,则在SSlStream上调用Read() ,或者如果它返回false那么我们有一个超时。 这也不起作用:因为SslStream可能使用自己的内部中间缓冲区,即使在SslStream还有数据需要读取, Poll()也可以返回false

[更新3]另一种可能性是编写一个位于NetworkStreamSslStream之间的自定义Stream子类,捕获超时exception并返回0字节而不是SslStream 。 我不知道如何做到这一点,更重要的是,我不知道是否返回一个0字节读取到SslStream仍然不会以某种方式破坏它。

(*)我试图这样做的原因是,与非安全或安全套接字的超时同步读取是我已经在iOS,OS X,Linux和Android上用于某些跨平台代码的模式。 它适用于.NET中的非安全套接字,因此剩下的唯一情况是SslStream

你当然可以让方法#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; } } 

需要充实一点,以便调用者可以看到读取尚未完成,但片段太小而无法提供具体建议。

我也遇到了这个问题,SslStream在超时后返回读取五个字节的垃圾数据,我分别提出了类似于OP的Update#3的解决方案。

我创建了一个包装类,它包装Tcp NetworkStream对象,因为它传递给SslStream构造函数。 包装器类将所有调用传递给底层NetworkStream,除了Read()方法包含一个额外的try … catch来抑制Timeoutexception并返回0字节。

SslStream在此实例中正常工作,包括在套接字关闭时引发适当的IOException。 请注意,我们的Stream从Read()返回0不同于TcpClient或Socket从Read()返回0(这通常意味着套接字断开)。

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

然后可以通过在传递给SslStream之前包装TcpClient NetworkStream对象来使用它,如下所示:

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

问题归结为SslStream在来自底层流的任何exception中破坏其内部状态,甚至是无害的超时。 奇怪的是,下一个read()返回的五(或大)字节数据实际上是来自线路的TLS加密有效载荷数据的开始。

希望这可以帮助