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

Pol*_*Pol 16 .net c# networkstream sslstream

我只需N要从a 读取字节,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;
   ...
}
Run Code Online (Sandbox Code Playgroud)

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

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

[更新2]我尝试另一种方法,通过调用实现超时Poll(timeout, ...READ)基础上的TcpClient插座:如果返回true,然后调用Read()上SSlStream,或者如果它返回false,然后我们有一个超时.这也不起作用:因为SslStream可能使用它自己的内部中间缓冲区,即使还有数据需要读取,Poll()也可以返回.falseSslStream

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

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

Han*_*ant 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;
     }
  }
Run Code Online (Sandbox Code Playgroud)

需要充实一点,以便调用者可以看到读取还没有完成但是片段太小而无法给出具体的建议.


小智 6

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

我创建了一个包装类,它包装了 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; }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

希望这可以帮助