基于c/c++实现linux/windows跨平台获取ntp网络时间戳

目录

使用场景

在项目中,有时需要根据时钟同步做一些操作,例如网络请求、多设备视频同步播放等。

在多设备同步播放视频的环境中,我们可以定时请求网络时间,并根据每次请求的耗时取均值做一下滤波,这样就能使得我们的各设备同步播放视频更加准确。

c/c++源码

废话不多说,我们直接上源码,新建文件并命名为ntp_client.cpp,然后将下方源码拷贝到文件里。

cpp 复制代码
#include <cstdint>
#include <cstring>
#include <string>
#include <iostream>

#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netdb.h>
#endif


// NTP 客户端返回码
#define NTP_OK             0
#define NTP_ERR_WSA_INIT   -1
#define NTP_ERR_SOCKET     -2
#define NTP_ERR_INVALID_IP -3
#define NTP_ERR_SENDTO     -4
#define NTP_ERR_RECV       -5
#define NTP_ERR_DNS        -6

/**
 * @brief 设置请求超时
 * @param sock 套接字fd
 * @param timeout_ms 超时时间(单位:毫秒)
 */
static void SetSocketTimeout(int sock, int timeout_ms) {
#ifdef _WIN32
    DWORD timeout = (DWORD)timeout_ms;
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&timeout, sizeof(timeout));
#else
    struct timeval tv {};
    tv.tv_sec = timeout_ms / 1000;
    tv.tv_usec = (timeout_ms % 1000) * 1000;
    setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
#endif
}

/**
* @brief 关闭套接字
* @param sock 套接字fd
*/
static void CloseSocket(int sock) {
#ifdef _WIN32
    closesocket(sock);
#else
    close(sock);
#endif
}

// NTP 报文结构体 (SNTPv4 RFC 4330)
struct NtpPacket {
    uint8_t li_vn_mode = 0x1B;      // 00 110 011: 无警告、NTPv4、客户端模式
    uint8_t stratum = 0;            // 服务器层级
    uint8_t poll = 0;               // 轮询间隔
    uint8_t precision = 0;          // 时钟精度

    uint32_t root_delay = 0;        // 根延迟
    uint32_t root_dispersion = 0;   // 误差范围
    uint32_t ref_id = 0;            // 参考ID

    uint32_t ref_ts_sec = 0;        // 参考时间戳(秒)
    uint32_t ref_ts_frac = 0;       // 参考时间戳(小数秒)

    uint32_t orig_ts_sec = 0;       // 客户端发起请求时间(秒)
    uint32_t orig_ts_frac = 0;      // 客户端发起请求时间(小数秒)

    uint32_t recv_ts_sec = 0;       // 服务器接收请求时间(秒)
    uint32_t recv_ts_frac = 0;      // 服务器接收请求时间(小数秒)

    uint32_t trans_ts_sec = 0;      // 服务器回复时间(秒)【核心字段】
    uint32_t trans_ts_frac = 0;     // 服务器回复时间(小数秒)
};

/**
* @brief 获取 NTP 时间戳(毫秒级)
* @param ntp_time_ms 出参毫秒级ntp时间
* @param server ntp服务器地址
* @param timeout_ms 请求超时时间(单位:毫秒)
* @return NTP_OK 获取成功;其他 获取失败
*/
static int GetNtpTimestamp(int64_t& ntp_time_ms, const std::string& server = "ntp.aliyun.com", int timeout_ms = 2000)
{
    ntp_time_ms = 0;

#ifdef _WIN32
    WSADATA wsaData;
    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
        return NTP_ERR_WSA_INIT;
    }
#endif

    int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
    if (sock < 0) {
#ifdef _WIN32
        WSACleanup();
#endif
        return NTP_ERR_SOCKET;
    }

    SetSocketTimeout(sock, timeout_ms);

    // 域名解析
    struct addrinfo hints {};
    hints.ai_family = AF_INET;       // IPv4 only
    hints.ai_socktype = SOCK_DGRAM;
    hints.ai_protocol = IPPROTO_UDP;

    struct addrinfo* result = nullptr;
    int ret = getaddrinfo(server.c_str(), "123", &hints, &result);
    if (ret != 0 || result == nullptr) {
        CloseSocket(sock);
#ifdef _WIN32
        WSACleanup();
#endif
        return NTP_ERR_DNS;
    }

    // 直接使用解析后的地址
    struct sockaddr_in addr {};
    memcpy(&addr, result->ai_addr, result->ai_addrlen);
    freeaddrinfo(result);  // 必须释放

    // NTP 报文 (SNTPv4)
    struct NtpPacket pkt{};

    if (sendto(sock, (const char*)&pkt, sizeof(pkt), 0, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        CloseSocket(sock);
#ifdef _WIN32
        WSACleanup();
#endif
        return NTP_ERR_SENDTO;
    }

    socklen_t addr_len = sizeof(addr);
    if (recvfrom(sock, (char*)&pkt, sizeof(pkt), 0, (struct sockaddr*)&addr, &addr_len) < 0) {
        CloseSocket(sock);
#ifdef _WIN32
        WSACleanup();
#endif
        return NTP_ERR_RECV;
    }

    // ========================
    // 关键:秒 + 小数 → 毫秒级时间戳
    // ========================
    uint64_t sec = ntohl(pkt.trans_ts_sec);
    uint64_t frac = ntohl(pkt.trans_ts_frac);

    const int64_t NTP_OFFSET = 2208988800LL;
    int64_t unix_sec = (int64_t)sec - NTP_OFFSET;
    int64_t ms = (int64_t)((double)frac * 1000.0 / 4294967296.0);

    ntp_time_ms = unix_sec * 1000LL + ms;

    CloseSocket(sock);
#ifdef _WIN32
    WSACleanup();
#endif

    return NTP_OK;
}

int main(int argc, char* argv[])
{
    std::string ntp_servive{ "ntp.aliyun.com" };
    if (2 == argc) {
        ntp_servive = argv[1];
    }
    std::cout << "server address:" << ntp_servive << std::endl;

    int64_t time_now = 0;
    int ret = GetNtpTimestamp(time_now, ntp_servive, 2000);

    if (ret == NTP_OK) {
        std::cout << "Get Unix timestamp:" << time_now << std::endl;
    } else {
        std::cout << "Failed to get timestamp,error code:" << ret << std::endl;
    }

    return 0;
}

结果验证

windows编译命令

powershell 复制代码
g++ -std=c++11 ntp_client.cpp -o ntp_time -lws2_32

linux编译命令

powershell 复制代码
g++ -std=c++11 ntp_client.cpp -o ntp_time

输出结果

bash 复制代码
$ ./ntp_client.exe
Get Unix timestamp:1776244593733
相关推荐
博客18001 天前
酷宝的使用方法,超好用的免费界面库,C++、MFC可用
c++·mfc·界面库·库来帮·酷宝
郝学胜_神的一滴1 天前
CMake 026:属性体系精讲、四大作用域全解 & 实战代码落地
c++·cmake
Sokach10151 天前
Linux Shell 脚本从零到能用:一个新手的一天学习总结
linux
AlfredZhao2 天前
Docker 容器时区不对,`timedatectl` 不存在怎么办?
linux·timezone
众少成多积小致巨2 天前
JNI (Java Native Interface) 技术手册中文参考指南
android·java·c++
zzzzzz3103 天前
9K Star 炸裂开源!这个 C 语言写的代码知识图谱,把 Linux 内核索引压缩到了 3 分钟
linux·服务器·sql
XIAOHEZIcode3 天前
Linux系统鼠标偏移常见原因以及修复方案
linux·运维·游戏
A小辣椒5 天前
TShark:Wireshark CLI 功能
linux
A小辣椒5 天前
TShark:基础知识
linux
AlfredZhao5 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci