不能直接用 std::chrono 或系统时钟获取 ntp 时间,因其仅提供本地单调/稳态时间,不联网且无法自动修正漂移;ntp 需手动实现 udp 报文交互、时间戳解析与 rfc 5905 偏移计算。

为什么不能直接用 std::chrono 或系统时钟获取 NTP 时间
因为 std::chrono 只提供本地单调/稳态时钟,不接触网络;gettimeofday 或 clock_gettime 返回的是本机当前时间,哪怕它已经漂移了 5 分钟也不会自动修正。NTP 同步本质是发 UDP 包给服务器、解析响应里的 transmit_timestamp 和 originate_timestamp,再结合往返延迟做偏移估算——这一步必须自己实现或调用底层协议逻辑。
ntpdate 已弃用,但它的核心逻辑仍可复用
现代 Linux 发行版里 ntpdate 被标记为 deprecated,不是因为它错了,而是它不支持持续守时(比如 drift 补偿、时钟平滑)。但它发包和解析的流程干净直接,适合抄逻辑:
- 构造一个 48 字节的 NTP 报文,重点填
li_vn_mode = 0b00100011(version=4, mode=3/client) - 清空
stratum到root_delay等字段,只设transmit_timestamp为当前时间(注意:需转为 NTP 时间戳格式:自 1900-01-01 起的秒数) - UDP 目标端口固定为
123,服务器如pool.ntp.org或time.google.com - 收到响应后,检查
li_vn_mode是否为0b01000100(version=4, mode=4/server),再提取四个时间戳字段
手动计算时间偏移时最容易漏掉的三个校正项
NTP 客户端不直接取 receive_timestamp - transmit_timestamp,必须用 RFC 5905 定义的公式:offset = ((t1 - t2) + (t3 - t4)) / 2,其中:
-
t1= 客户端发送请求时的本地时间(originate_timestamp) -
t2= 服务端收到请求时的本地时间(receive_timestamp) -
t3= 服务端发送响应时的本地时间(transmit_timestamp) -
t4= 客户端收到响应时的本地时间(当前clock_gettime(CLOCK_MONOTONIC)) - 所有时间戳都需从 NTP 格式(1900 年起点)转成 Unix 时间戳(1970 年起点),差值是 2208988800 秒
- 务必用
CLOCK_MONOTONIC测t1和t4,避免系统时间被中途修改干扰延迟计算
别碰 settimeofday,改系统时钟要 root 权限且风险高
绝大多数场景下,你真正需要的不是“把系统时间设成 NTP 时间”,而是“知道当前本地时间比 NTP 快/慢多少”。所以建议只返回 offset 值(单位微秒),由上层决定是否调用 adjtimex 做渐进式调整,或仅用于日志对齐、采样校准等轻量用途:
立即学习“C++免费学习笔记(深入)”;
-
settimeofday需要CAP_SYS_TIME或 root,普通进程基本失败 - 硬跳变时间会破坏
epoll_wait超时、std::this_thread::sleep_for等依赖系统时钟的行为 - 真要同步,应走
systemd-timesyncd或chrony的 socket 接口,而非自己发 UDP 包后强行写内核时钟
实际跑通的关键在于:先用 nc -u -w1 pool.ntp.org 123 确认 UDP 端口可达;再用 tcpdump -i any port 123 抓包验证报文结构;最后比对 ntpq -p 输出的 offset 值是否与你算出来的一致——差值在 ±5ms 内就算握手成功。NTP 协议本身简单,难的是时间戳精度控制和系统时钟行为的理解。










