为什么 NetworkStream.ReadAsync 没有超时?

Why is NetworkStream.ReadAsync not timing out?

我正在使用 NetworkStreamTcpClient

  1. 首先我设置我的 tcp 客户端:

        tcp = new TcpClient(AddressFamily.InterNetwork)
        { NoDelay = true, ReceiveTimeout = 5000};
    
  2. 我的主要数据接收循环:

        while (true)
        {
            //read available data from the device
            int numBytesRead = await ReadAsync();
            Console.WriteLine($"{numBytesRead} bytes read"); //BP2
        }
    
  3. 而实际的TCP数据读取:

    public Task<int> ReadAsync()
    {
        var stream = tcp.GetStream();
        return stream.ReadAsync(InBuffer, 0, InBuffer.Length); //BP1
    }
    

我将其连接到测试台,让我可以手动发送数据包。通过设置断点和调试,我检查了 stream.ReadTimeouttcp.

中获取值 5000

如果我经常发送数据,一切都会按预期进行。 但是如果我不发送任何数据,5 秒后似乎什么也没有发生,没有超时。我看到断点 BP1 在调试器中被击中,但在我从测试平台发送数据之前,BP2 没有被击中。我可以让它等待一分钟或更长时间,它似乎只是在等待,但在一分钟后收到发送的数据,这似乎是不正确的行为。 5 秒后 肯定会发生某些事情 (我理解的例外情况)?

已经晚了,所以我期待一些非常基本的东西,但是有人能看出我的错误是什么吗?


附录

好的,所以当我对我正在使用的实际 .Net 版本进行 RTFM 时(有多少次我被 MS 默认为 .Net Core 3 发现了,我确实说过它已经晚了)我在评论中看到ReadTimeout 部分:

This property affects only synchronous reads performed by calling the Read method. This property does not affect asynchronous reads performed by calling the BeginRead method.

我现在不清楚我是否可以使用现代可等待调用 完全 来安全地读取套接字数据并特别设置超时。除了超时外它都在工作,但我不确定给定的 ReadAsync 如何在 NetworkStream 中没有覆盖。我必须做一些丑陋的黑客攻击还是有简单的解决方案?

在我的例子中,5000 是我在断定存在问题之前预计不会接收到数据的最长时间 - 该协议没有 ping 机制,因此如果没有出现,我假设连接已断开。因此,认为具有 5000 毫秒超时的异步读取会很好而且整洁。

网络对象的超时值仅适用于同步操作。例如,来自 the documentation:

This option applies to synchronous Receive calls only.

对于 Socket.ReceiveTimeoutTcpClient.ReceiveTimeoutNetworkStream.ReadTimeout,这些实现最终都会导致对 SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReceiveTimeout, ...) 的调用,而后者又有效地调用了本机 setsockopt() 函数。来自 that documentation:

SO_RCVTIMEO DWORD Sets the timeout, in milliseconds, for blocking receive calls.

(强调我的)

底层原生 API 中的这种限制是托管 API 中相同限制的原因。超时值不适用于网络对象上的异步 I/O。

您需要自己实现超时,方法是在超时发生时关闭套接字。例如:

async Task<int> ReadAsync(TcpClient client, byte[] buffer, int index, int length, TimeSpan timeout)
{
    Task<int> result = client.GetStream().ReadAsync(buffer, index, length);

    await Task.WhenAny(result, Task.Delay(timeout));

    if (!result.IsCompleted)
    {
        client.Close();
    }

    return await result;
}

关于这个主题的其他变体可以在其他相关问题中找到:
NetworkStream.ReadAsync with a cancellation token never cancels
Cancel C# 4.5 TcpClient ReadAsync by timeout

关闭套接字真的是您所能做的全部。即使对于同步操作,如果发生超时,套接字将不再可用。没有可靠的方法来中断读取操作并期望套接字保持一致。

当然,您可以选择在关闭套接字之前提示用户。但是,如果您要这样做,您将在应用程序架构的更高级别实现超时,这样 I/O 操作本身就完全没有超时意识。