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

How to (repeatedly) read from .NET SslStream with a timeout?

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

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

SslStream assumes that a timeout along with any other IOException when one is thrown from the inner stream will be treated as fatal by its caller. Reusing a SslStream instance after a timeout will return garbage. An application should Close the SslStream and throw an exception in these cases.

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

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

但是如果 Wait() returned false 这不起作用:稍后再次调用 ReadAsync() 时它会抛出异常:

Exception thrown: 'System.NotSupportedException' in System.dll Tests.exe Warning: 0 : Failed reading from socket: System.NotSupportedException: The BeginRead method cannot be called when another read operation is pending.

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

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

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

您当然可以使方法 1 奏效。您只需要跟踪 Task 并继续等待而无需再次调用 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 return在超时后读取了五个字节的垃圾数据,我单独提出了一个类似于 OP 的更新 #3 的解决方案。

我创建了一个包装器 class,它在将 Tcp NetworkStream 对象传递到 SslStream 构造函数时对其进行包装。包装器 class 将所有调用传递到底层 NetworkStream,除了 Read() 方法包含一个额外的 try...catch 来抑制超时异常和 return 0 字节。

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

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() returns 的五个(或左右)字节数据实际上是来自网络的 TLS 加密负载数据的开始。

希望对您有所帮助