26

基本问题

我的 C# 应用程序中的时间戳有问题。我从远程 TCP 连接异步接收数据。每次收到数据时,我都会将时间戳变量更新为DateTime.Now. 在一个单独的线程上,每秒一次,我检查自上次接收以来它是否超过了预定义的超时时间,如果是,则断开连接。这种方法已经用了很多年了,但是现在我遇到了一个应用程序安装在时间源不稳定的机器上的情况。每隔几天,机器时间就会“自动更正”,我会过早地断开连接。代码基本上如下:

接收过程

void OnReceiveComplete(IAsyncResult ar) {
    ...
    mySocket.EndReceive(ar);
    lastRxTime = DateTime.Now;
    ...
}

检查过程

void CheckConnection() {
    TimeSpan ts = DateTime.Now.Subtract(lastRxTime);
    if(ts.TotalMilliseconds > timeout) {
        Disconnect(string.Format("No packet received from the server for over {0} seconds.", timeout / 1000));
    }
}

在问题期间和断开连接之前,我有有效的 Wireshark 捕获,我看到 NTP 流量在看起来像至少 1 分钟的更正时达到高潮。这显然会导致检查过程失败。

技术细节/预期问题的答案

  1. 我可以控制连接的两端,但不能控制中间的物理层(通常是低质量的卫星链路),这就是设置超时检查的原因。
  2. 由于数据是异步的,如果在等于超时一半的时间段内没有从服务器发送数据,则发送一个小的心跳。
  3. 这个过程可以有多个实例(即在连接到多个不同服务器的机器上。
  4. 所有通信都使用异步方法(据说使用完成端口)。
  5. 检查过程在一个单独的线程上运行,该线程由一台机器上的所有客户端共享。

已完成的研究(即可能的解决方案)

到目前为止,我的 Google 搜索结果如下:

  1. 我意识到我应该使用DateTime.UtcNow而不是DateTime.Now出于性能原因。这不会影响问题本身。
  2. 依赖于 Ticks 的实现将是一个更好的解决方案。
  3. 获取刻度有两种选择 -Environment.TickCountStopwatch.GetTimestamp()
  4. 根据我的研究,Environment.TickCount可能容易受到时间调整的影响,但我不确定在什么情况下。此外,由于我在其他更高性能的情况下使用相同的方法,因此 10-16 毫秒的分辨率可能是一个问题(尽管不是在我在这里介绍的特定情况下)。
  5. Stopwatch.GetTimestamp()可以回退到DateTime.Now.Ticks高性能时钟不可用时。我不确定这种情况会多久发生一次(任何机器都不再配备高性能时钟了),但我确信如果它求助于 Ticks,同样的问题也会发生。
  6. 我还读过Stopwatch.GetTimestamp()将使用QueryPerformanceCounter()API 调用,并且从多个线程调用时可能会不稳定。

终极问题

我很好奇生成lastRxTime时间戳的最佳方法是什么?我是否过于担心Environment.TickCountandStopwatch.GetTimestamp()函数中的低可能性问题?我对替代实现持开放态度,只要它们考虑到应用程序的多线程性质以及链接质量问题。

2013 年 7 月 17 日更新(已部署解决方案!)

我已经部署了一个解决方案,并希望让每个人都了解详细信息。一般来说,可能没有一个公认的答案,但在经历了这次经历之后,我可以说最初的解决方案肯定是一个问题。我会尽量提供尽可能详细的信息:

首先,NTP 问题实际上是另一个问题的症状。出现此问题的网络是 AD 域,其中运行我的代码的两台服务器设置为域控制器。事实证明,DC 是域的时间源。事实证明,在大约 11 天内,系统时间与这些系统上的实时时钟相差最多 1 分钟,此时 Windows 正在纠正错误。一旦它纠正了第一个 DC 上的滑差,第二个 DC 就会同步他的时间,并且都会遇到上述问题。

根据反馈和我最初的研究,我创建了一个测试程序,用于在断开连接期间运行以记录 DateTime.Now、Environment.TickCount 和 Stopwatch.GetTimestamp() 的值。我发现在更正过程中,Environment.TickCount 和 StopWatch.GetTimeStamp() 都没有下滑,这意味着它们可以很好地替代 DateTime.Now()。我选择了 TickCount,因为它保证在我部署的所有服务器上(而秒表可能会退回到我尚未找到的某些机器上的 DateTime 对象)。到目前为止,它的工作没有问题。我对翻转问题进行了尽职调查,以防止该表格成为问题,但需要等待我的系统启动那么长时间才能确定。

我想指出,如果其他人遇到类似的问题,他们不应该忽视使用下面列表中的任何其他提供的解决方案。每个都有自己的优点,事实上,简单的计数器可能是大多数情况下的最佳解决方案。我没有去这个解决方案的原因是我在单独的区域中有类似的代码,这在很大程度上取决于紧迫的时间安排。我可以在那里处理 16 毫秒左右的滴答计数分辨率,但无法处理计数器解决方案所产生的时间漂移​​(我在一个单独的产品中使用了类似的代码,该产品每小时漂移超过一秒并带给我不符合项目规范)。

再次感谢所有人,如果有更多问题出现,我一定会更新问题。

4

3 回答 3

3

我认为使用Environment.TickCount. 该文档指出它返回:

一个 32 位有符号整数,包含自上次启动计算机以来经过的时间量(以毫秒为单位)。

它接着说:

此属性的值源自系统计时器,并存储为 32 位有符号整数。

我没有看到 NTP 时间校正如何影响这一点。您是否有其他建议的链接?棘手的部分是处理环绕。因此,当将当前滴答计数与前一个比较时,如果您得到一个负数,您就会知道发生了回绕。

就个人而言,我认为这是使用的优越机制,因为返回值以毫秒为单位,所以你的超时转换因子将很简单。否则,Stopwatch.GetTimestamp您将不得不做一些额外的工作来考虑滴答频率。

此外,我假设这是通过调用 Win32GetTickCount函数在内部实现的。(dotPeek 显示它标有MethodImplOptions.InternalCall,所以我不能确定)但是该文档指出:

评论

GetTickCount函数的分辨率受限于系统定时器的分辨率,通常在 10 毫秒到 16 毫秒的范围内。GetTickCount 函数的分辨率不受函数调整的影响 GetSystemTimeAdjustment


再想一想,由于您仅每秒定期检查一次,因此计时器的分辨率只需 < 1 秒。尝试做得更好是零点,那么为什么要使用任何 API 调用呢?

private int m_conTimer;

void OneSecondThreadCallback() {
    if (++m_conTimer >= TIMEOUT_VALUE)
        // Connection timed out. React accordingly.
}
于 2013-06-13T00:01:32.863 回答
3

有几个很好的理由不用DateTime.Now于此目的

  • 正如您所指出的,存在性能问题。这主要是因为它在内部必须转换为本地时区。

  • 由于是当地时区的“现在”,不适合比较或计算。这是因为随着夏令时转换的发生,许多时区每年有两次不连续性。如果您要与上次活动时间进行比较,那么当时钟向前滚动时,您将有一个额外的小时。当它们回滚时,您可能会离开 0 到 60 分钟(具体取决于您进行比较的确切时间)。

  • 它的精度比Stopwatchor低得多Environment.TickCount。虽然DateTime能够表示非常少量的时间,但系统时钟本身只能精确到大约 10 到 30 毫秒。(但也许这对您的用例并不那么重要。)

UsingDateTime.UtcNow确实解决了前两点,但不是第三点。为此,您需要完全远离DateTime

此外 - 还有“时钟漂移”的概念。时钟可能会在较长时间内减速或加速。如果操作系统有时间同步源,它实际上可以调整一个滴答中实际经过的时间量以进行补偿。您可以在本文中阅读更多内容(参见“时间调整”部分)。

您可能还对ClockRes实用程序感兴趣,该实用程序可用于为您提供有关系统计时器分辨率的信息。

但是- 您的用例可能最好不要使用任何这些技术。您说您有兴趣在超时后取消活动。您还说您有一个单独的线程定期检查超时期限是否已过。所有这些都是不必要的。相反,只需使用一个Timer类。

请注意,.Net 中提供了三种不同的计时器。你可能想要一个System.Threading.Timer

// start the timer, callback in 10000 milliseconds, and don't fire more than once
var timer = new Timer(TimerCallback, null, 10000, Timeout.Infinite);

// to reset the timer when you receive data
timer.Change(10000, Timeout.Infinite);

// to stop the timer completely
timer.Change(Timeout.Infinite, Timeout.Infinite);

// and in your callback
private void TimerCallback(object state)
{
    // disconnect, or do whatever you want
}

如果可能的话,您会从多个线程重置计时器,那么您应该使用System.Timers.Timer- 这是线程安全的。

// to set up the timer
var timer = new Timer(10000) {AutoReset = false};
timer.Elapsed += TimerOnElapsed;

// to start the timer running
timer.Start();

// to reset the timer
timer.Stop();
timer.Start();

// and the callback
private void TimerOnElapsed(object sender, ElapsedEventArgs args)
{
    // disconnect, or do whatever you want
}
于 2013-06-13T00:31:38.270 回答
2

我是否过于担心 Environment.TickCount 和 Stopwatch.GetTimestamp() 函数中的低可能性问题?

可能,是的。

我更喜欢这个Stopwatch.GetTimestamp()选项。如果您的系统上可用,这将使用高性能计时器,DateTime.Ticks如果这不是一个选项,则回退到。因此,在给定硬件上可用时,您将获得最佳时间,并且在高性能计时器不可用时作为后备的不错选择。

虽然Environment.TickCount可能是一个有趣的选项,但如果您的机器可能运行超过 24.9 天,您将需要处理溢出的情况。

于 2013-06-12T23:57:30.873 回答