使用 std::chrono 的 NTP 时间戳

NTP timestamps using std::chrono

我正在尝试使用 std::chrono 在 C++ 中表示 NTP timestamps(包括 NTP 纪元)。因此,我决定对刻度使用 64 位无符号整数 (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 天的偏移量,断言平等但没有成功。我不确定这里出了什么问题。有人可以帮忙吗?

您的报价周期: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 101653,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 单位),然后将其转换为具有 long double rep 的中间表示 imdperiodNTPClock 相同。这将使用 long double 计算 1.6 x 101653,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,但使用交替单位,结果将具有 repcommon_type的两个参数,但在其他方面具有相同的值。 IE。 std::chrono::hours(24*25567)*1.0L 是基于 long doublehourslong double 进行剩余的计算,直到 duration_cast 将其带回 NTPClock::duration

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