【问题标题】:How to (repeatedly) read from .NET SslStream with a timeout?如何(重复)从 .NET SslStream 中读取超时?
【发布时间】:2016-05-12 04:39:39
【问题描述】:

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

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

SslStream 假定从内部流中抛出的超时以及任何其他 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] 我尝试了另一种实现超时的方法,方法是在底层 TcpClient 套接字上调用 Poll(timeout, ...READ):如果它返回 true,则在 @ 上调用 Read() 987654335@,或者如果它返回false,那么我们有一个超时。这也不起作用:因为SslStream 可能使用了自己的内部中间缓冲区,所以Poll() 可以返回false,即使SslStream 中还有数据要读取。

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

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

【问题讨论】:

标签: c# .net networkstream sslstream


【解决方案1】:

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

需要充实一点,以便调用者可以看到读取尚未完成,但 sn-p 太小,无法给出具体建议。

【讨论】:

  • 有趣的想法,但这意味着当应用程序想要终止连接时,可能会有异步读取任务仍在进行中。关闭 SslStream 会中止异步读取吗?
【解决方案2】:

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

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

SslStream 在这种情况下可以正常工作,包括在套接字关闭时引发相应的 IOException。请注意,我们从 Read() 返回 0 的 Stream 与从 Read() 返回 0 的 TcpClient 或 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; }
    }
}

然后可以通过在将 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 上有短阻塞读取,而无需重新设计所有内容以运行异步。干得好。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-03
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多