Time Shield Library
C++ library for working with time
Loading...
Searching...
No Matches
ntp_client.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: MIT
2#pragma once
3#ifndef _TIME_SHIELD_NTP_CLIENT_HPP_INCLUDED
4#define _TIME_SHIELD_NTP_CLIENT_HPP_INCLUDED
5
14
15#include "config.hpp"
16
17#if TIME_SHIELD_ENABLE_NTP_CLIENT
18
19#include "time_utils.hpp"
20
21#include <atomic>
22#include <chrono>
23#include <cstdint>
24#include <cstring>
25#include <functional>
26#include <mutex>
27#include <stdexcept>
28#include <string>
29
30#if TIME_SHIELD_PLATFORM_WINDOWS
31
33
34namespace time_shield {
35
38 class NtpClient {
39 public:
41 NtpClient(std::string server = "pool.ntp.org", int port = 123)
42 : m_host(std::move(server)), m_port(port) {
44 }
45
48 bool query() {
50 if (!WsaGuard::instance().success()) {
51 m_is_success = false;
52 throw std::runtime_error("WSAStartup failed with error: " + std::to_string(WsaGuard::instance().ret_code()));
53 }
54
55 SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
56 if (sock == INVALID_SOCKET) {
57 m_is_success = false;
58 return false;
59 }
60
61 struct sockaddr_in addr{};
62 addr.sin_family = AF_INET;
63 addr.sin_port = htons(static_cast<u_short>(m_port));
64
65 addrinfo hints{}, *res = nullptr;
66 hints.ai_family = AF_INET; // IPv4
67 hints.ai_socktype = SOCK_DGRAM;
68 hints.ai_protocol = IPPROTO_UDP;
69
70 if (getaddrinfo(m_host.c_str(), nullptr, &hints, &res) != 0 || !res) {
71 last_error_code_slot() = WSAGetLastError();
72 closesocket(sock);
73 m_is_success = false;
74 return false;
75 }
76
77 sockaddr_in* resolved = reinterpret_cast<sockaddr_in*>(res->ai_addr);
78 addr.sin_addr = resolved->sin_addr;
79 freeaddrinfo(res);
80
81 ntp_packet pkt;
82 fill_packet(pkt);
83
84 int timeout_ms = 5000;
85 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast<const char*>(&timeout_ms), sizeof(timeout_ms));
86 if (sendto(sock, reinterpret_cast<const char*>(&pkt), sizeof(pkt), 0,
87 reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) {
88 last_error_code_slot() = WSAGetLastError();
89 closesocket(sock);
90 m_is_success = false;
91 return false;
92 }
93
94 sockaddr_in from;
95 int from_len = sizeof(from);
96 if (recvfrom(sock, reinterpret_cast<char*>(&pkt), sizeof(pkt), 0,
97 reinterpret_cast<sockaddr*>(&from), &from_len) < 0) {
98 last_error_code_slot() = WSAGetLastError();
99 closesocket(sock);
100 m_is_success = false;
101 return false;
102 }
103
104 closesocket(sock);
105
106 int64_t result_offset;
107 if (parse_packet(pkt, result_offset)) {
108 m_offset_us = result_offset;
109 m_is_success = true;
110 return true;
111 }
112
113 m_is_success = false;
114 return false;
115 }
116
119 bool success() const noexcept {
120 return m_is_success.load();
121 }
122
124 int64_t get_offset_us() const noexcept {
125 return m_offset_us;
126 }
127
130 int64_t get_utc_time_us() const noexcept {
131 const int64_t offset = m_offset_us.load();
132 return now_realtime_us() + offset;
133 }
134
137 int64_t get_utc_time_ms() const noexcept {
138 return get_utc_time_us() / 1000;
139 }
140
143 time_t get_utc_time() const noexcept {
144 return static_cast<time_t>(get_utc_time_us() / 1000000);
145 }
146
148 int get_last_error_code() const noexcept {
149 return last_error_code_slot();
150 }
151
152 private:
153 static constexpr int64_t NTP_TIMESTAMP_DELTA = 2208988800ll;
154
157 struct ntp_packet {
158 uint8_t li_vn_mode; // Eight bits. li, vn, and mode.
159 // li. Two bits. Leap indicator.
160 // vn. Three bits. Version number of the protocol.
161 // mode. Three bits. Client will pick mode 3 for client.
162 uint8_t stratum; // Eight bits. Stratum level of the local clock.
163 uint8_t poll; // Eight bits. Maximum interval between successive messages.
164 uint8_t precision; // Eight bits. Precision of the local clock.
165 uint32_t root_delay; // 32 bits. Total round trip delay time.
166 uint32_t root_dispersion; // 32 bits. Max error aloud from primary clock source.
167 uint32_t ref_id; // 32 bits. Reference clock identifier.
168 uint32_t ref_ts_sec; // 32 bits. Reference time-stamp seconds.
169 uint32_t ref_ts_frac; // 32 bits. Reference time-stamp fraction of a second.
170 uint32_t orig_ts_sec; // 32 bits. Originate time-stamp seconds.
171 uint32_t orig_ts_frac; // 32 bits. Originate time-stamp fraction of a second.
172 uint32_t recv_ts_sec; // 32 bits. Received time-stamp seconds.
173 uint32_t recv_ts_frac; // 32 bits. Received time-stamp fraction of a second.
174 uint32_t tx_ts_sec; // 32 bits and the most important field the client cares about. Transmit time-stamp seconds.
175 uint32_t tx_ts_frac; // 32 bits. Transmit time-stamp fraction of a second.
176 };
177
178 std::string m_host;
179 int m_port = 123;
180 std::atomic<int64_t> m_offset_us{0};
181 std::atomic<bool> m_is_success{false};
182
183 static int& last_error_code_slot() noexcept {
184 static thread_local int value = 0;
185 return value;
186 }
187
188 static bool get_now_us_u64(uint64_t& out) noexcept {
189 const int64_t v = time_shield::now_realtime_us();
190 if (v < 0) return false;
191 out = static_cast<uint64_t>(v);
192 return true;
193 }
194
195 static uint64_t ntp_frac_to_us(uint32_t frac_net) noexcept {
196 // NTP frac: unsigned 32-bit fraction of second with denominator 2^32.
197 const uint64_t frac = static_cast<uint64_t>(ntohl(frac_net));
198 return (frac * 1000000ULL) >> 32; // / 2^32
199 }
200
201 static bool ntp_ts_to_unix_us(uint32_t sec_net, uint32_t frac_net, uint64_t& out_us) noexcept {
202 const int64_t sec = static_cast<int64_t>(ntohl(sec_net)) - NTP_TIMESTAMP_DELTA;
203 if (sec < 0) return false; // время до 1970 — не ожидается, но защитимся
204 out_us = static_cast<uint64_t>(sec) * 1000000ULL + ntp_frac_to_us(frac_net);
205 return true;
206 }
207
209 void fill_packet(ntp_packet& pkt) const {
210 std::memset(&pkt, 0, sizeof(pkt));
211 pkt.li_vn_mode = (0 << 6) | (3 << 3) | 3; // LI=0, VN=3, Mode=3 (client)
212
213 uint64_t now_us = 0;
214 if (!get_now_us_u64(now_us)) {
215 pkt.tx_ts_sec = 0;
216 pkt.tx_ts_frac = 0;
217 return;
218 }
219
220 const uint64_t sec = now_us / 1000000 + static_cast<uint64_t>(NTP_TIMESTAMP_DELTA);
221 const uint64_t frac = ((now_us % 1000000) * 0x100000000ULL) / 1000000;
222
223 pkt.tx_ts_sec = htonl(static_cast<uint32_t>(sec));
224 pkt.tx_ts_frac = htonl(static_cast<uint32_t>(frac));
225 }
226
228 bool parse_packet(const ntp_packet& pkt, int64_t& result_offset_us) const {
229 uint64_t arrival_us = 0;
230 if (!get_now_us_u64(arrival_us)) return false;
231
232 uint64_t originate_us = 0, receive_us = 0, transmit_us = 0;
233
234 if (!ntp_ts_to_unix_us(pkt.orig_ts_sec, pkt.orig_ts_frac, originate_us)) return false;
235 if (!ntp_ts_to_unix_us(pkt.recv_ts_sec, pkt.recv_ts_frac, receive_us)) return false;
236 if (!ntp_ts_to_unix_us(pkt.tx_ts_sec, pkt.tx_ts_frac, transmit_us)) return false;
237
238 // RFC 5905: offset = ((t2 - t1) + (t3 - t4)) / 2
239 const int64_t t1 = static_cast<int64_t>(originate_us);
240 const int64_t t2 = static_cast<int64_t>(receive_us);
241 const int64_t t3 = static_cast<int64_t>(transmit_us);
242 const int64_t t4 = static_cast<int64_t>(arrival_us);
243
244 result_offset_us = ((t2 - t1) + (t3 - t4)) / 2;
245 return true;
246 }
247
248 };
249
250} // namespace time_shield
251
252#elif TIME_SHIELD_PLATFORM_UNIX
253
254#include <arpa/inet.h>
255#include <netdb.h>
256#include <netinet/in.h>
257#include <sys/socket.h>
258#include <unistd.h>
259#include <cerrno>
260
261namespace time_shield {
262
265 class NtpClient {
266 public:
268 NtpClient(std::string server = "pool.ntp.org", int port = 123)
269 : m_host(std::move(server)), m_port(port) {
271 }
272
275 bool query() {
277
278 const int sock = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
279 if (sock < 0) {
280 last_error_code_slot() = errno;
281 m_is_success = false;
282 return false;
283 }
284
285 addrinfo hints{}, *res = nullptr;
286 hints.ai_family = AF_INET;
287 hints.ai_socktype = SOCK_DGRAM;
288 hints.ai_protocol = IPPROTO_UDP;
289
290 const int resolve_code = getaddrinfo(m_host.c_str(), nullptr, &hints, &res);
291 if (resolve_code != 0 || !res) {
292 last_error_code_slot() = (resolve_code != 0) ? resolve_code : errno;
293 ::close(sock);
294 m_is_success = false;
295 return false;
296 }
297
298 sockaddr_in addr{};
299 addr.sin_family = AF_INET;
300 addr.sin_port = htons(static_cast<uint16_t>(m_port));
301 addr.sin_addr = reinterpret_cast<sockaddr_in*>(res->ai_addr)->sin_addr;
302 freeaddrinfo(res);
303
304 ntp_packet pkt{};
305 fill_packet(pkt);
306
307 timeval timeout{};
308 timeout.tv_sec = 5;
309 timeout.tv_usec = 0;
310 setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
311
312 const ssize_t sent = ::sendto(sock, &pkt, sizeof(pkt), 0, reinterpret_cast<sockaddr*>(&addr), sizeof(addr));
313 if (sent < 0) {
314 last_error_code_slot() = errno;
315 ::close(sock);
316 m_is_success = false;
317 return false;
318 }
319
320 sockaddr_in from{};
321 socklen_t from_len = sizeof(from);
322 const ssize_t received = ::recvfrom(sock, &pkt, sizeof(pkt), 0, reinterpret_cast<sockaddr*>(&from), &from_len);
323 if (received < 0) {
324 last_error_code_slot() = errno;
325 ::close(sock);
326 m_is_success = false;
327 return false;
328 }
329
330 ::close(sock);
331
332 int64_t result_offset = 0;
333 if (parse_packet(pkt, result_offset)) {
334 m_offset_us = result_offset;
335 m_is_success = true;
336 return true;
337 }
338
339 m_is_success = false;
340 return false;
341 }
342
345 bool success() const noexcept {
346 return m_is_success.load();
347 }
348
350 int64_t get_offset_us() const noexcept {
351 return m_offset_us;
352 }
353
356 int64_t get_utc_time_us() const noexcept {
357 uint64_t system_time_us = 0;
358 if (!get_now_us_u64(system_time_us)) return 0;
359 return static_cast<int64_t>(system_time_us) + m_offset_us.load();
360 }
361
364 int64_t get_utc_time_ms() const noexcept {
365 return get_utc_time_us() / 1000;
366 }
367
370 time_t get_utc_time() const noexcept {
371 return static_cast<time_t>(get_utc_time_us() / 1000000);
372 }
373
375 int get_last_error_code() const noexcept {
376 return last_error_code_slot();
377 }
378
379 private:
380 static constexpr int64_t NTP_TIMESTAMP_DELTA = 2208988800ll;
381
384 struct ntp_packet {
385 uint8_t li_vn_mode; // Eight bits. li, vn, and mode.
386 // li. Two bits. Leap indicator.
387 // vn. Three bits. Version number of the protocol.
388 // mode. Three bits. Client will pick mode 3 for client.
389 uint8_t stratum; // Eight bits. Stratum level of the local clock.
390 uint8_t poll; // Eight bits. Maximum interval between successive messages.
391 uint8_t precision; // Eight bits. Precision of the local clock.
392 uint32_t root_delay; // 32 bits. Total round trip delay time.
393 uint32_t root_dispersion; // 32 bits. Max error aloud from primary clock source.
394 uint32_t ref_id; // 32 bits. Reference clock identifier.
395 uint32_t ref_ts_sec; // 32 bits. Reference time-stamp seconds.
396 uint32_t ref_ts_frac; // 32 bits. Reference time-stamp fraction of a second.
397 uint32_t orig_ts_sec; // 32 bits. Originate time-stamp seconds.
398 uint32_t orig_ts_frac; // 32 bits. Originate time-stamp fraction of a second.
399 uint32_t recv_ts_sec; // 32 bits. Received time-stamp seconds.
400 uint32_t recv_ts_frac; // 32 bits. Received time-stamp fraction of a second.
401 uint32_t tx_ts_sec; // 32 bits and the most important field the client cares about. Transmit time-stamp seconds.
402 uint32_t tx_ts_frac; // 32 bits. Transmit time-stamp fraction of a second.
403 };
404
405 std::string m_host;
406 int m_port = 123;
407 std::atomic<int64_t> m_offset_us{0};
408 std::atomic<bool> m_is_success{false};
409
410 static int& last_error_code_slot() noexcept {
411 static thread_local int value = 0;
412 return value;
413 }
414
415 static bool get_now_us_u64(uint64_t& out) noexcept {
416 const int64_t v = time_shield::now_realtime_us();
417 if (v < 0) return false;
418 out = static_cast<uint64_t>(v);
419 return true;
420 }
421
422 static uint64_t ntp_frac_to_us(uint32_t frac_net) noexcept {
423 // NTP frac: unsigned 32-bit fraction of second with denominator 2^32.
424 const uint64_t frac = static_cast<uint64_t>(ntohl(frac_net));
425 return (frac * 1000000ULL) >> 32; // / 2^32
426 }
427
428 static bool ntp_ts_to_unix_us(uint32_t sec_net, uint32_t frac_net, uint64_t& out_us) noexcept {
429 const int64_t sec = static_cast<int64_t>(ntohl(sec_net)) - NTP_TIMESTAMP_DELTA;
430 if (sec < 0) return false;
431 out_us = static_cast<uint64_t>(sec) * 1000000ULL + ntp_frac_to_us(frac_net);
432 return true;
433 }
434
436 void fill_packet(ntp_packet& pkt) const {
437 std::memset(&pkt, 0, sizeof(pkt));
438 pkt.li_vn_mode = (0 << 6) | (3 << 3) | 3; // LI=0, VN=3, Mode=3 (client)
439
440 uint64_t now_us = 0;
441 if (!get_now_us_u64(now_us)) {
442 pkt.tx_ts_sec = 0;
443 pkt.tx_ts_frac = 0;
444 return;
445 }
446
447 const uint64_t sec = now_us / 1000000 + static_cast<uint64_t>(NTP_TIMESTAMP_DELTA);
448 const uint64_t frac = ((now_us % 1000000) * 0x100000000ULL) / 1000000;
449
450 pkt.tx_ts_sec = htonl(static_cast<uint32_t>(sec));
451 pkt.tx_ts_frac = htonl(static_cast<uint32_t>(frac));
452 }
453
455 bool parse_packet(const ntp_packet& pkt, int64_t& result_offset_us) const {
456 uint64_t arrival_us = 0;
457 if (!get_now_us_u64(arrival_us)) return false;
458
459 uint64_t originate_us = 0, receive_us = 0, transmit_us = 0;
460
461 if (!ntp_ts_to_unix_us(pkt.orig_ts_sec, pkt.orig_ts_frac, originate_us)) return false;
462 if (!ntp_ts_to_unix_us(pkt.recv_ts_sec, pkt.recv_ts_frac, receive_us)) return false;
463 if (!ntp_ts_to_unix_us(pkt.tx_ts_sec, pkt.tx_ts_frac, transmit_us)) return false;
464
465 // RFC 5905: offset = ((t2 - t1) + (t3 - t4)) / 2
466 const int64_t t1 = static_cast<int64_t>(originate_us);
467 const int64_t t2 = static_cast<int64_t>(receive_us);
468 const int64_t t3 = static_cast<int64_t>(transmit_us);
469 const int64_t t4 = static_cast<int64_t>(arrival_us);
470
471 result_offset_us = ((t2 - t1) + (t3 - t4)) / 2;
472 return true;
473 }
474
475 };
476
477} // namespace time_shield
478
479#else // TIME_SHIELD_ENABLE_NTP_CLIENT but unsupported platform
480
481# warning "NtpClient is unsupported on this platform."
482
483namespace time_shield {
484
486 class NtpClient {
487 public:
489 static_assert(sizeof(void*) == 0, "time_shield::NtpClient is unsupported on this platform.");
490 }
491 };
492
493} // namespace time_shield
494
495#endif // TIME_SHIELD_PLATFORM_WINDOWS/UNIX
496
497#else // !TIME_SHIELD_ENABLE_NTP_CLIENT
498
499# warning "NtpClient is disabled or unsupported on this platform."
500
501namespace time_shield {
502
504 class NtpClient {
505 public:
507 static_assert(sizeof(void*) == 0, "time_shield::NtpClient is disabled by configuration.");
508 }
509 };
510
511} // namespace time_shield
512
513#endif // TIME_SHIELD_ENABLE_NTP_CLIENT
514
515#endif // _TIME_SHIELD_NTP_CLIENT_HPP_INCLUDED
Windows implementation of the NTP client for measuring time offset.
void fill_packet(ntp_packet &pkt) const
Converts local time to NTP timestamp format.
static bool get_now_us_u64(uint64_t &out) noexcept
bool success() const noexcept
Returns whether the last NTP query was successful.
NtpClient(std::string server="pool.ntp.org", int port=123)
Constructs NTP client with specified host and port.
static uint64_t ntp_frac_to_us(uint32_t frac_net) noexcept
bool parse_packet(const ntp_packet &pkt, int64_t &result_offset_us) const
Parses response and calculates offset.
std::atomic< bool > m_is_success
int64_t get_utc_time_us() const noexcept
Returns current UTC time in microseconds based on last NTP offset.
static constexpr int64_t NTP_TIMESTAMP_DELTA
Seconds between 1900 and 1970 epochs.
time_t get_utc_time() const noexcept
Returns current UTC time as time_t (seconds since Unix epoch).
int64_t get_offset_us() const noexcept
Returns the last measured offset in microseconds.
int64_t get_utc_time_ms() const noexcept
Returns current UTC time in milliseconds based on last NTP offset.
int get_last_error_code() const noexcept
Returns last WinSock error code (if any).
static bool ntp_ts_to_unix_us(uint32_t sec_net, uint32_t frac_net, uint64_t &out_us) noexcept
std::atomic< int64_t > m_offset_us
static int & last_error_code_slot() noexcept
bool query()
Queries the NTP server and updates the local offset.
static const WsaGuard & instance()
Returns the singleton instance, initializing WSA if needed.
Definition wsa_guard.hpp:31
Configuration macros for the library.
int64_t now_realtime_us()
Get current real time in microseconds using a platform-specific method.
Main namespace for the Time Shield library.
Структура пакета NTP Total: 384 bits or 48 bytes.
Header file with time-related utility functions.
Singleton guard for WinSock initialization.