基于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
相关推荐
LuminousCPP21 分钟前
C 语言动态内存管理全解析:从基础函数到柔性数组与内存分区
c语言·经验分享·笔记·学习·柔性数组
小杍随笔30 分钟前
【WordPress 核心表】
linux·运维·服务器
TEC_INO33 分钟前
Linux_55:RV1126的VENC模块讲解
linux·网络·人工智能
mounter62534 分钟前
比 veth 更强、为 eBPF 而生:深度解析 Linux netkit 虚拟网卡驱动
linux·ebpf·kernel·netkit
用户23678298016836 分钟前
Linux du 命令深度解析:从磁盘占用统计到目录空间分析
linux
Lazionr42 分钟前
【栈与队列经典OJ】
c语言·数据结构
charlie11451419144 分钟前
基于开源项目的现代C++工程实践——OnceCallback 前置知识(下):C++20/23 高级特性
c++·开源·c++20
H Journey1 小时前
网络编程:Linux下高性能TCP网络服务器(代码完整版)多线程版本
linux·服务器·网络
蜡笔小马1 小时前
04.C++设计模式-桥接模式
c++·设计模式·桥接模式
码云骑士1 小时前
jwt入门介绍
linux·运维·数据库