目录
使用场景
在某些严格要求时间同步很精准的项目中,获取网络ntp时间的时间延时比较大,做滤波处理可能效果也不理想。因此可以搭建一个本地ntp服务器,这样可以大大缩短网络链路,使得ntp时间更加精准。
c/c++源码
废话不多说,我们直接上源码,新建文件并命名为ntp_server.cpp,然后将下方源码拷贝到文件里。
cpp
#include <iostream>
#include <cstdio>
#include <cstring>
#include <ctime>
#include <stdint.h>
// 跨平台套接字头文件与库
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
typedef int socklen_t;
#define close closesocket
#else
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SOCKET int
#define INVALID_SOCKET -1
#endif
// NTP 版本3 数据包结构 (RFC 1305)
#pragma pack(push, 1)
struct NtpPacket {
// LI(2位) + Version(3位) + Mode(3位)
uint8_t li_vn_mode;
uint8_t stratum; // 层级
uint8_t poll; // 轮询间隔
uint8_t precision; // 精度
uint32_t rootDelay; // 根延迟
uint32_t rootDispersion; // 根离散度
uint32_t refId; // 参考ID
uint32_t refTimestampSec; // 参考时间戳(秒)
uint32_t refTimestampFrac; // 参考时间戳(小数)
uint32_t origTimestampSec; // 原始时间戳(秒)
uint32_t origTimestampFrac; // 原始时间戳(小数)
uint32_t recvTimestampSec; // 接收时间戳(秒)
uint32_t recvTimestampFrac; // 接收时间戳(小数)
uint32_t transTimestampSec; // 发送时间戳(秒)
uint32_t transTimestampFrac;// 发送时间戳(小数)
};
#pragma pack(pop)
// NTP 时间基准: 1900-01-01 到 1970-01-01 的秒数
const uint64_t NTP_TIMESTAMP_DELTA = 2208988800ULL;
// 跨平台初始化网络库
static bool InitSocket() {
#ifdef _WIN32
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
std::cerr << "Winsock initialization failed" << std::endl;
return false;
}
#endif
return true;
}
// 跨平台清理网络库
static void CleanupSocket() {
#ifdef _WIN32
WSACleanup();
#endif
}
// 获取当前系统时间转换为 NTP 时间戳
static void GetNtpTime(uint32_t& sec, uint32_t& frac) {
// 获取 Unix 时间 (1970-01-01 起的秒)
time_t unixTime = time(nullptr);
uint64_t ntpSec = (uint64_t)unixTime + NTP_TIMESTAMP_DELTA;
sec = htonl((uint32_t)ntpSec);
// 小数部分: 简单填充 0 (生产环境可使用高精度时钟)
frac = htonl(0);
}
int main() {
if (!InitSocket()) {
return -1;
}
// 创建 UDP 套接字
SOCKET serverFd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if (serverFd == INVALID_SOCKET) {
std::cerr << "Failed to create socket" << std::endl;
CleanupSocket();
return -1;
}
// 绑定 0.0.0.0:123 (NTP 默认端口)
sockaddr_in serverAddr{};
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(123);
if (bind(serverFd, (sockaddr*)&serverAddr, sizeof(serverAddr)) < 0) {
std::cerr << "Failed to bind port 123! (Administrator/root privileges required)" << std::endl;
close(serverFd);
CleanupSocket();
return -1;
}
std::cout << "NTP server started successfully, listening on 0.0.0.0:123" << std::endl;
std::cout << "Waiting for client requests..." << std::endl;
// 循环接收请求并应答
sockaddr_in clientAddr{};
socklen_t clientLen = sizeof(clientAddr);
NtpPacket packet{};
while (true) {
memset(&packet, 0, sizeof(packet));
// 接收 NTP 请求
ssize_t recvLen = recvfrom(
serverFd,
(char*)&packet,
sizeof(packet),
0,
(sockaddr*)&clientAddr,
&clientLen
);
if (recvLen < 0) continue;
// 打印客户端信息
char clientIp[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &clientAddr.sin_addr, clientIp, INET_ADDRSTRLEN);
std::cout << "Received NTP request from " << clientIp << std::endl;
// 构造 NTP 应答
// LI=0, VN=3 (NTPv3), Mode=4 (Server)
packet.li_vn_mode = (0 << 6) | (3 << 3) | 4;
packet.stratum = 1; // 层级 1 (参考本地时钟)
packet.poll = 6;
packet.precision = 0xFA;
// 填充时间戳
GetNtpTime(packet.refTimestampSec, packet.refTimestampFrac);
GetNtpTime(packet.recvTimestampSec, packet.recvTimestampFrac);
GetNtpTime(packet.transTimestampSec, packet.transTimestampFrac);
// 发送应答
sendto(
serverFd,
(const char*)&packet,
sizeof(packet),
0,
(sockaddr*)&clientAddr,
clientLen
);
}
// 理论上不会执行到这里
close(serverFd);
CleanupSocket();
return 0;
}
结果验证
windows编译命令
shell
g++ -std=c++11 ntp_server.cpp -o ntp_server -lws2_32
linux编译命令
shell
g++ -std=c++11 ntp_server.cpp -o ntp_server
服务器输出结果
shell
.\ntp_server.exe
NTP server started successfully, listening on 0.0.0.0:123
Waiting for client requests...
Received NTP request from 192.168.0.149
客户端输出结果
客户端实现可参考基于c/c++实现linux/windows跨平台获取ntp网络时间戳
shell
.\ntp_client.exe 192.168.0.149
server address:192.168.0.149
Get Unix timestamp:1776611741000