使用 std::chrono 的 NTP 时间戳

发布于 2025-01-14 07:23:26 字数 3611 浏览 1 评论 0原文

我试图在 C++ 中使用 NTP 时间戳(包括 NTP 纪元)表示 <代码>std::chrono。因此,我决定使用 64 位无符号 int (unsigned long long) 作为刻度,并将其除以最低 28 位表示秒的一小部分(接受 4 位的截断)与原始标准时间戳相比),接下来的 32 位代表纪元的秒数​​,最高 4 位代表纪元。这意味着每个刻度需要 1 / (2^28 - 1) 秒。

我现在有以下简单的实现:

#include <chrono>

/**
 * Implements a custom C++11 clock starting at 1 Jan 1900 UTC with a tick duration of 2^(-28) seconds.
 */
class NTPClock
{

public:

    static constexpr bool is_steady = false;
    static constexpr unsigned int era_bits = 4;                                     // epoch uses 4 bits
    static constexpr unsigned int fractional_bits = 32-era_bits;                    // fraction uses 28 bits
    static constexpr unsigned int seconds_bits = 32;                                // second uses 32 bits

    using duration = std::chrono::duration<unsigned long long, std::ratio<1, (1<<fractional_bits)-1>>;
    using rep = typename duration::rep;
    using period = typename  duration::period;
    using time_point = std::chrono::time_point<NTPClock>;

    /**
     * Return the current time of this. Note that the implementation is based on the assumption
     * that the system clock starts at 1 Jan 1970, which is not defined with C++11 but seems to be a
     * standard in most compilers.
     * 
     * @return The current time as represented by an NTP timestamp
     */
    static time_point now() noexcept
    {
        return time_point
        (
            std::chrono::duration_cast<duration>(std::chrono::system_clock::now().time_since_epoch())
                + std::chrono::duration_cast<duration>(std::chrono::hours(24*25567))   // 25567 days have passed between 1 Jan 1900 and 1 Jan 1970
        );
    };
}

不幸的是,一个简单的测试表明这不能按预期工作:

#include <chrono>
#include <iostream>

#include <catch2/catch.hpp>
#include "NTPClock.h"

using namespace std::chrono;

TEST_CASE("NTPClock_now")
{
    auto ntp_dur = NTPClock::now().time_since_epoch();
    auto sys_dur = system_clock::now().time_since_epoch();
    std::cout << duration_cast<hours>(ntp_dur) << std::endl;
    std::cout << ntp_dur << std::endl;
    std::cout << duration_cast<hours>(sys_dur) << std::endl;
    std::cout << sys_dur << std::endl;
    REQUIRE(duration_cast<hours>(ntp_dur)-duration_cast<hours>(sys_dur) == hours(24*25567));
}

输出:

613612h
592974797620267184[1/268435455]s
457599h
16473577714886015[1/10000000]s

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PackageTest.exe is a Catch v2.11.1 host application.
Run with -? for options

-------------------------------------------------------------------------------
NTPClock_now
-------------------------------------------------------------------------------
D:\Repos\...\TestNTPClock.cpp(10)
...............................................................................

D:\Repos\...\TestNTPClock.cpp(18): FAILED:
  REQUIRE( duration_cast<hours>(ntp_dur)-duration_cast<hours>(sys_dur) == hours(24*25567) )
with expansion:
  156013h == 613608h

===============================================================================
test cases: 1 | 1 failed
assertions: 1 | 1 failed

我还在 NTPClock::now 中删除了 25567 天的偏移量,断言相等但没有成功。我不确定这里出了什么问题。有人可以帮忙吗?

I'm trying to represent NTP timestamps (including the NTP epoch) in C++ using std::chrono. Therefore, I decided to use a 64-bit unsigned int (unsigned long long) for the ticks and divide it such that the lowest 28-bit represent the fraction of a second (accepting trunction of 4 bits in comparison to the original standard timestamps), the next 32-bit represent the seconds of an epoch and the highest 4-bit represent the epoch. This means that every tick takes 1 / (2^28 - 1) seconds.

I now have the following simple implementation:

#include <chrono>

/**
 * Implements a custom C++11 clock starting at 1 Jan 1900 UTC with a tick duration of 2^(-28) seconds.
 */
class NTPClock
{

public:

    static constexpr bool is_steady = false;
    static constexpr unsigned int era_bits = 4;                                     // epoch uses 4 bits
    static constexpr unsigned int fractional_bits = 32-era_bits;                    // fraction uses 28 bits
    static constexpr unsigned int seconds_bits = 32;                                // second uses 32 bits

    using duration = std::chrono::duration<unsigned long long, std::ratio<1, (1<<fractional_bits)-1>>;
    using rep = typename duration::rep;
    using period = typename  duration::period;
    using time_point = std::chrono::time_point<NTPClock>;

    /**
     * Return the current time of this. Note that the implementation is based on the assumption
     * that the system clock starts at 1 Jan 1970, which is not defined with C++11 but seems to be a
     * standard in most compilers.
     * 
     * @return The current time as represented by an NTP timestamp
     */
    static time_point now() noexcept
    {
        return time_point
        (
            std::chrono::duration_cast<duration>(std::chrono::system_clock::now().time_since_epoch())
                + std::chrono::duration_cast<duration>(std::chrono::hours(24*25567))   // 25567 days have passed between 1 Jan 1900 and 1 Jan 1970
        );
    };
}

Unfortunately, a simple test reveals this does not work as expected:

#include <chrono>
#include <iostream>

#include <catch2/catch.hpp>
#include "NTPClock.h"

using namespace std::chrono;

TEST_CASE("NTPClock_now")
{
    auto ntp_dur = NTPClock::now().time_since_epoch();
    auto sys_dur = system_clock::now().time_since_epoch();
    std::cout << duration_cast<hours>(ntp_dur) << std::endl;
    std::cout << ntp_dur << std::endl;
    std::cout << duration_cast<hours>(sys_dur) << std::endl;
    std::cout << sys_dur << std::endl;
    REQUIRE(duration_cast<hours>(ntp_dur)-duration_cast<hours>(sys_dur) == hours(24*25567));
}

Output:

613612h
592974797620267184[1/268435455]s
457599h
16473577714886015[1/10000000]s

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PackageTest.exe is a Catch v2.11.1 host application.
Run with -? for options

-------------------------------------------------------------------------------
NTPClock_now
-------------------------------------------------------------------------------
D:\Repos\...\TestNTPClock.cpp(10)
...............................................................................

D:\Repos\...\TestNTPClock.cpp(18): FAILED:
  REQUIRE( duration_cast<hours>(ntp_dur)-duration_cast<hours>(sys_dur) == hours(24*25567) )
with expansion:
  156013h == 613608h

===============================================================================
test cases: 1 | 1 failed
assertions: 1 | 1 failed

I also removed the offset of 25567 days in NTPClock::now asserting equality without success. I'm not sure what is going wrong here. Can anybody help?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

夏末 2025-01-21 07:23:26

遗憾的是,您的刻度周期:1/268'435'455 非常好,而且在使用您所需的转化次数时也无法大幅减少分数(即在 system_clock::durationNTPClock::duration 之间,这会导致 unsigned long long 的内部溢出。 NTPClock::rep

例如,在 Windows 上,system_clock 时钟周期为 1/10,000,000 秒。 now() 的值约为 1.6 x 1016 要将其转换为 NTPClock::duration,您必须进行计算。 1.6 x 1016 乘以 53,687,091/2,000,000 第一步是该值乘以转换系数的分子,约为 8 x 1023,它溢出了unsigned long long

有几种方法可以克服这种溢出,并且两者都涉及至少使用具有更大范围的中间表示。人们可以使用 128 位整数类型,但我不认为它可以在 Windows 上使用,除非是通过第三方库。 long double 是另一种选择。这可能看起来像:

static time_point now() noexcept
{
    using imd = std::chrono::duration<long double, period>;
    return time_point
    (
        std::chrono::duration_cast<duration>(imd{std::chrono::system_clock::now().time_since_epoch()
            + std::chrono::hours(24*25567)})
    );
};

也就是说,执行偏移移位而不进行转换(system_clock::duration 单位),然后将其转换为具有 imd 的中间表示形式>long double rep,以及与 NTPClock 相同的period。这将使用 long double 计算 1.6 x 1016 乘以 53,687,091/2,000,000。然后最后duration_castNTPClock::duration。最终的duration_cast 最终除了将 long double 转换为 unsigned long long 之外什么也不做,因为转换因子只是 1/1

完成同样事情的另一种方法是:

static time_point now() noexcept
{
    return time_point
    (
        std::chrono::duration_cast<duration>(std::chrono::system_clock::now().time_since_epoch()
            + std::chrono::hours(24*25567)*1.0L)
    );
};

这利用了这样一个事实,即您可以将任何 duration 乘以 1,但使用备用单位,结果将具有 >rep 与两个参数的 common_type ,但在其他方面具有相同的值。即 std::chrono::hours(24*25567)*1.0L 是一个基于 long double小时long double 执行其余的计算,直到 duration_cast 将其返回到 NTPClock::duration

第二种方式编写起来更简单,但代码审查者可能不理解 *1.0L 的意义,至少在它成为更常见的习惯用法之前是这样。

Your tick period: 1/268'435'455 is unfortunately both extremely fine and also doesn't lend itself to much of a reduced fraction when your desired conversions are used (i.e. between system_clock::duration and NTPClock::duration. This is leading to internal overflow of your unsigned long long NTPClock::rep.

For example, on Windows the system_clock tick period is 1/10,000,000 seconds. The current value of now() is around 1.6 x 1016. To convert this to NTPClock::duration you have to compute 1.6 x 1016 times 53,687,091/2,000,000. The first step in that is the value times the numerator of the conversion factor which is about 8 x 1023, which overflows unsigned long long.

There's a couple of ways to overcome this overflow, and both involve using at least an intermediate representation with a larger range. One could use a 128 bit integral type, but I don't believe that is available on Windows, except perhaps by a 3rd party library. long double is another option. This might look like:

static time_point now() noexcept
{
    using imd = std::chrono::duration<long double, period>;
    return time_point
    (
        std::chrono::duration_cast<duration>(imd{std::chrono::system_clock::now().time_since_epoch()
            + std::chrono::hours(24*25567)})
    );
};

That is, perform the offset shift with no conversion (system_clock::duration units), then convert that to the intermediate representation imd which has a long double rep, and the same period as NTPClock. This will use long double to compute 1.6 x 1016 times 53,687,091/2,000,000. Then finally duration_cast that to NTPClock::duration. This final duration_cast will end up doing nothing but casting long double to unsigned long long as the conversion factor is simply 1/1.

Another way to accomplish the same thing is:

static time_point now() noexcept
{
    return time_point
    (
        std::chrono::duration_cast<duration>(std::chrono::system_clock::now().time_since_epoch()
            + std::chrono::hours(24*25567)*1.0L)
    );
};

This takes advantage of the fact that you can multiply any duration by 1, but with alternate units and the result will have a rep with the common_type of the two arguments, but otherwise have the same value. I.e. std::chrono::hours(24*25567)*1.0L is a long double-based hours. And that long double carries through the rest of the computation until the duration_cast brings it back to NTPClock::duration.

This second way is simpler to write, but code reviewers may not understand the significance of the *1.0L, at least until it becomes a more common idiom.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文