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

时间:2016-05-12 04:39:40

标签: c# .net networkstream sslstream

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

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

  

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

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

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

但是如果Wait()返回false,则无法执行此操作:稍后再次调用ReadAsync()时会抛出异常:

  

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

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

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

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

2 个答案:

答案 0 :(得分:7)

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

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

答案 1 :(得分:4)

我也遇到了这个问题,SslStream在超时后读取了五个字节的垃圾数据,并且我单独提出了一个类似于OP的更新#3的解决方案。

我创建了一个包装类,它在传递给SslStream构造函数时包装Tcp NetworkStream对象。包装类将所有调用传递给底层NetworkStream,除了Read()方法包含一个额外的try ... catch来抑制Timeout异常并返回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; }
    }
}

然后可以通过在将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加密有效载荷数据的开始。

希望这会有所帮助