如何在.NET SslStream中设置超时并重复读取数据?

19

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

这对于非SSL流(例如 NetworkStream)很容易实现,只需使用其 ReadTimeout 属性即可使流在超时时引发异常。不幸的是,根据官方文档,这种方法在 SslStream 上不起作用:

SslStream 假定其调用者将超时和任何其他 IOException(当从内部流抛出时)视为致命错误。在超时后重用 SslStream 实例将返回垃圾数据。此类情况下,应用程序应 Close SslStream 并抛出异常。

[更新1] 我尝试了不同的方法,如下所示:

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

但是,如果Wait()返回了false,那么这种方法就行不通:当稍后再次调用ReadAsync()时,会抛出异常:

引发异常:“System.NotSupportedException”的来源是 System.dll Tests.exe 警告:0:从套接字读取失败:System.NotSupportedException: 另一个读取操作正在进行中,不能调用 BeginRead 方法。

[更新2]我试图通过在基础的TcpClient套接字上调用Poll(timeout, ...READ)来实现超时,如果它返回true,则调用SslStream上的Read(),或者如果它返回false,则说明已经超时。这种方法也不起作用:因为SslStream可能使用其自己的内部中介缓冲区,所以即使在SslStream中仍有数据可以读取,Poll()也可能返回false

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

(*) 我之所以要尝试这样做,是因为从非安全或安全套接字同步读取超时是我已经在iOS、OS X、Linux和Android上使用的一种跨平台代码模式。它对于.NET中的非安全套接字已经可行,所以唯一剩下的情况就是SslStream


这里有一个相关的问题:http://stackoverflow.com/questions/24198290/net-4-5-sslstream-cancel-a-asynchronous-read-write-call - Pol
2个回答

9
我也遇到了一个问题,SslStream在超时后返回五个垃圾数据字节,我单独想出了一个解决方案,与OP的第三次更新类似。
我创建了一个包装类,将Tcp NetworkStream对象包装起来传递给SslStream构造函数。这个包装类将所有调用都传递到底层的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加密负载数据的开头。
希望这能有所帮助。

这是一个很棒的包装类。我程序中只需要更改几行代码,现在就可以进行短阻塞读取我的SSL,而无需重新设计所有内容以运行异步操作。干得好。 - Shad

8

您可以采用方式#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 会中止该异步读取吗? - Pol

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接