【问题标题】:NetworkStream.ReadTimeout failing to block on NetworkStream.Read(...) when daylight savings are applied and clock moves backwards当应用夏令时并且时钟向后移动时,NetworkStream.ReadTimeout 无法阻止 NetworkStream.Read(...)
【发布时间】:2018-07-20 11:31:35
【问题描述】:

我希望有人能解释这种行为,或者这可能是 .NET 中的错误。

由于夏令时而向后移动意味着 NetworkStream 不注意其属性 ReadTimeout,并且在此代码的情况下会导致循环旋转。 (这只是一个例子来证明它正在发生)。

要重现我看到的问题,您需要设置为使用夏令时的时区,例如英国。

  1. 将您的时区设置为 UTC London,并确保勾选了夏令时。
  2. 将日期改回 2017 年 10 月 29 日
  3. 将时间设置为上午 01:58:50
  4. 运行下面的代码并观察它在凌晨 2 点应用夏令时时旋转,如果应用正确,时间应该回到凌晨 1 点。
  5. 请务必等待,它最多可能需要 30 秒才能开始旋转。

编辑:经过深入调查,1 小时后,它停止旋转,行为恢复正常并遵守 ReadTimeout。

任何想法将不胜感激!

客户端代码:

class Program
{
    static bool running = false;

    static void Main(string[] args)
    {
        running = true;
        Task.Factory.StartNew(() => Run());

        Console.ReadKey();

        running = false;
    }


    static void Run()
    {
        TcpClient connection = new TcpClient("127.0.0.1", 1234);

        while (running)
        {
            if (connection != null && connection.Connected)
            {
                try
                {
                    NetworkStream stream = connection.GetStream();
                    stream.ReadTimeout = 1000;

                    byte[] buffer = new byte[1024];
                    int readCount = stream.Read(buffer, 0, 1024); // Should block here for the ReadTimeout duration if nothing received
                                                                  // However when daylight savings is applied and time moves backwards an hour, the stream.ReadTimeout = 1000; 
                                                                  // is not honoured and it falls through and spins

                    if (readCount > 0)
                    {
                        Console.WriteLine("Received some data");
                        //process read here
                    }
                    else
                    {
                        Console.WriteLine("ReadTimeout was not honoured");
                    }
                }
                catch (IOException)
                {
                    Console.WriteLine("Read timed out");
                }
            }
        }
    }
}

服务器代码:

class Program
    {
        static bool running = false;

        public static void Main()
        {
            TcpListener server = null;
            try
            {
                // Set the TcpListener on port 13000.
                Int32 port = 5000;
                IPAddress localAddr = IPAddress.Parse("192.168.1.69");

                // TcpListener server = new TcpListener(port);
                server = new TcpListener(localAddr, port);

                // Start listening for client requests.
                server.Start();

                // Enter the listening loop.
                while (true)
                {
                    Console.Write("Waiting for a connection... ");

                    // Perform a blocking call to accept requests.
                    // You could also user server.AcceptSocket() here.
                    TcpClient client = server.AcceptTcpClient();
                    Console.WriteLine("Connected!");


                    // Get a stream object for reading and writing
                    NetworkStream stream = client.GetStream();

                    running = true;

                    Task.Factory.StartNew(() => Run(stream));


                    Console.ReadKey();

                    // Shutdown and end connection
                    client.Close();
                }
            }
            catch (SocketException e)
            {
                Console.WriteLine("SocketException: {0}", e);
            }
            finally
            {
                // Stop listening for new clients.
                server.Stop();
            }


            Console.WriteLine("\nHit enter to continue...");
            Console.Read();
        }

        static async Task Run(NetworkStream stream)
        {
            byte[] stuffToSend = Encoding.ASCII.GetBytes("Stuff to send");
            while (running)
            {
                stream.Write(stuffToSend, 0, stuffToSend.Length);
                await Task.Delay(1000);
            }
        }
    }

【问题讨论】:

  • 只是为了确定,您是说失败是重复返回负值,无一例外?
  • 失败是返回值== 0,因为它根本没有等待,根本没有读取任何数据。
  • 返回值0 通常意味着连接已经干净地关闭。确认实际情况并非如此(可能是由于对时间变化做出反应的结果)。从理论上讲,这可能是一个 .NET 错误,但不太可能,因为设置 ReadTimeout 所做的只是使用 SO_RCVTIMEO 选项调用本机 setsockopt。完全不清楚为什么内核甚至应该关心与网络超时相关的相对系统时间。
  • 我绝对可以保证连接没有断开,我编辑了帖子,所以它也在这里。您仍然可以向 tcpclient 发送数据,并且当它处于此状态时它仍然会读取它。 1小时后,即凌晨2点,它恢复正常。老实说,它开始感觉像是 .NET 中的一个错误
  • 如果您还没有这样做,也可以在另一台机器上复制它,最好是只安装 Windows 的干净 VM,以尽量减少网络堆栈的任何可能的恶作剧。此外,看看您是否可以发布您在端口1234 上监听的任何代码,以便我们进行完整的复制。如果你做不到,试着让它变得无伤大雅和琐碎,比如netcat(或你自己的简单TcpListener循环)。如果您可以仅使用localhost 和琐碎的网络代码重现此问题,那么这应该是Windows 本身的一个错误,而不仅仅是.NET。这就是为什么它如此不可能(但并非不可能)。

标签: c# sockets datetime tcpclient networkstream


【解决方案1】:

the OS documentation 中有一些非常重要的注释在 .NET ReadTimeout 属性中没有提及:

SO_RCVTIMEOSO_SNDTIMEO

使用recv函数时,如果在SO_RCVTIMEO指定的时间段内没有数据到达,则recv函数完成。在 Windows 2000 之前的 Windows 版本中,随后接收到的任何数据都会失败并显示 WSAETIMEDOUT。在 Windows 2000 及更高版本中,如果在SO_RCVTIMEO 指定的时间段内没有数据到达,recv 函数返回WSAETIMEDOUT,如果接收到数据,recv 返回成功。

如果套接字上的发送或接收操作超时,则套接字状态不确定,不应使用;处于这种状态的 TCP 套接字可能会丢失数据,因为操作可能会在操作完成的同时被取消。

目前,每次超时后,您都在循环并尝试在同一个套接字上进行另一个接收操作。但是这个注释很清楚你需要创建一个新的连接。

【讨论】:

  • 嗨@Ben Voigt,为了清楚起见,您仍然可以在此套接字上接收数据,即使它正在“旋转”。 1 小时后,当时间“大于”应用夏令时之前的时间时,它将停止“旋转”并返回等待指定 ReadTimeout 的“读取”。这不是每个人是否应该是一个新的连接的问题,而是一个问题是否有人以前见过这种行为,以及是否有人知道它为什么会发生。谢谢
  • @MikeDev:但是文档说的是,在第一次超时之后,所有操作(接收和进一步超时)都将不可靠:(
  • 除了一年一小时之外,它们都非常可靠。 :(
  • @MikeDev:您是否检查过从 TCP 流中读取具有阻塞时间限制的其他方法是否适合您?在底层 API 中,两种有效的方法是 (1) 设置非阻塞模式并在超时时调用 select()poll(),然后仅在已知数据可用时调用 recv()(基于选择结果) 或 (2) 使用事件设置 OVERLAPPED WSARecv 调用,并在等待事件时传递超时。这两种方法都可以让您在应用程序中超时,而不会实际产生套接字错误。在 C# 中,使用 BeginRead 是等效的。
  • 然后你会得到WaitHandle 并等待它超时。整体效果与底层 API 中的 OVERLAPPED 相同——当超时发生时,接收会在后台继续,但您的应用程序不会被阻止超过您的时间限制。当您想再次等待数据时,您只需在等待操作上再等待一段时间,并且只有在前一个操作完成后才调用BeginRead 开始一个新的操作等待处理。
【解决方案2】:

这里是源代码:http://referencesource.microsoft.com/#mscorlib/system/io/stream.cs,f956b0c07e86df64

    [ComVisible(false)]
        public virtual int ReadTimeout {
            get {
                Contract.Ensures(Contract.Result<int>() >= 0);
                throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_TimeoutsNotSupported"));
            }
            set {
                throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_TimeoutsNotSupported"));
            }
        }

        [ComVisible(false)]
        public virtual int WriteTimeout {
            get {
                Contract.Ensures(Contract.Result<int>() >= 0);
                throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_TimeoutsNotSupported"));
            }
            set {
                throw new InvalidOperationException(Environment.GetResourceString("InvalidOperation_TimeoutsNotSupported"));
            }
        }

【讨论】:

【解决方案3】:

请阅读此内容。 https://msdn.microsoft.com/en-us/library/ms973825.aspx

一般来说,如果您处理的是绝对经过时间,例如 测量超时、执行算术或比较 不同的 DateTime 值,您应该尝试使用通用时间 如果可能的话,这样您就可以获得最佳的准确性,而无需 时区和/或夏令时的影响。

只需调整线程以使用世界时。

编辑 1。

我没听懂你的问题。您不想要解决方法。 它是一个与.net 如何管理相关的已知错误,相关讨论已在此处完成。

Cache.Add absolute expiration - UTC based or not?

夏令时结束后的一小时内,您的当地时间 是模棱两可的,所以你可以得到意想不到的结果,即绝对 到期时间可能比预期长一小时。

编辑 2。 在 OP 问题的上下文中添加了另一个讨论线程。

https://social.msdn.microsoft.com/Forums/vstudio/en-US/cb0369f4-8c96-484a-a33e-5e4c850b995e/wcf-channels-and-datetime-change?forum=wcf

【讨论】:

  • OP 本身显然没有使用相对标记。任何来自框架的东西(因此可能是 .NET 中的错误)
  • 我无法理解 OP 问题。已编辑,导致其为已知错误,之前已在此处发布。
  • 你能告诉我以前在哪里发过吗?我在这里找不到任何参考。
  • 完成@MikeDev。很抱歉误解了你的问题。无论哪种方式,这可能是他们鼓励您使用通用日期时间的原因。由于机器不知道如何处理夏令时带来的变化。
  • @SalvadorRuizGuevara:.NET 的某些部分很可能存在系统时钟更改错误,但您的答案中没有任何内容与NetworkStream 有任何关系,这就是问题所要问的。
猜你喜欢
  • 1970-01-01
  • 2021-11-30
  • 1970-01-01
  • 2011-03-04
  • 1970-01-01
  • 1970-01-01
  • 2020-01-13
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多