基于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
相关推荐
左手厨刀右手茼蒿2 小时前
Linux 内核中的进程管理:从创建到终止
linux·嵌入式·系统内核
geinvse_seg2 小时前
中小团队如何低成本搭建项目管理系统?基于 Ubuntu 的 Dootask 私有化部署实战
linux·运维·ubuntu
CSCN新手听安2 小时前
【linux】高级IO,以ET模式运行的epoll版本的TCP服务器实现reactor反应堆
linux·运维·服务器·c++·高级io·epoll·reactor反应堆
丶伯爵式2 小时前
Ubuntu 24.04 更换国内软件源指南 | 2026年3月26日
linux·运维·ubuntu·国内源·升级
左手厨刀右手茼蒿2 小时前
Linux 内核中的 DMA 管理:从缓冲区到传输
linux·嵌入式·系统内核
Java后端的Ai之路2 小时前
Linux端口进程查找与终止教程
linux·运维·服务器
松☆3 小时前
C++ 算法竞赛题解:P13569 [CCPC 2024 重庆站] osu!mania —— 浮点数精度陷阱与 `eps` 的深度解析
开发语言·c++·算法
(Charon)3 小时前
【C++/Qt】C++/Qt 实现 TCP Server:支持启动监听、消息收发、日志保存
c++·qt·tcp/ip
爱编码的小八嘎4 小时前
C语言完美演绎8-10
c语言