如何(重复)从 .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] 另一种可能性是编写一个位于 NetworkStream
和 SslStream
之间的自定义 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 加密负载数据的开始。
希望对您有所帮助
我只需要从 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] 另一种可能性是编写一个位于 NetworkStream
和 SslStream
之间的自定义 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 加密负载数据的开始。
希望对您有所帮助