目录
使用场景
在项目中,有时需要根据时钟同步做一些操作,例如网络请求、多设备视频同步播放等。
在多设备同步播放视频的环境中,我们可以定时请求网络时间,并根据每次请求的耗时取均值做一下滤波,这样就能使得我们的各设备同步播放视频更加准确。
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