简介:这份资源是一套基于C语言实现的SNTP服务器程序源码,面向需要为局域网设备提供时间同步服务的开发者与网络运维人员。SNTP是NTP的简化版本,适合对性能要求不高但仍需保证时钟一致性的场景,如分布式计算、日志记录与金融交易系统。压缩包共6个文件,包含3个.c源文件、2个.h头文件及1个Makefile,整体仅4KB,结构精简,便于快速阅读与二次开发。源码中可看到时间同步算法、UDP网络接口、配置管理、日志记录与安全机制等核心模块的实现思路,读者可据此理解SNTP请求响应流程与时间戳校准逻辑,并直接编译运行搭建简易时间服务器。目前已有188人学习下载,适合作为网络编程入门或时间同步协议学习的参考实例。
1. 从一份 ntp.rar 说起:SNTP 服务器程序到底能解决什么
手里这份ntp.rar解压出来只有七个文件:main.c、main.h、ntp.c、ntp.h、time.c、Makefile,外加一个说明。没有 CMake,没有第三方依赖,也没有一堆看不懂的构建脚本。第一次看到这种结构,我下意识觉得是个教学 demo,但把ntp.c翻完之后发现,它把 SNTP 服务器该有的骨架都写全了:UDP 123 端口监听、请求报文解析、时间戳回填、响应发送,一条链路是通的。
它解决的问题很具体:局域网里有一堆设备时间各走各的,日志对不上、定时任务错乱、证书校验因为时间偏差直接失败。你不需要去外网找公共时间源,也不需要装 ntpd 那一整套重量级服务,编译运行这个程序,它自己就成了一台 SNTP 服务器,其他设备指向它就能把时间拉齐。适合谁?手上有一台常年开机的 Linux 小主机、树莓派或者工控机,想用最小成本把内网时间统一起来的人。如果你要的是毫秒级、带复杂时钟滤波和冗余源仲裁的方案,这份代码不够用,但作为内网时间基准,它够。
2. 拆开 ntp.rar:七个文件里各管什么
2.1 文件职责与调用关系
先把文件清单摆出来,别急着编译,搞清楚每个文件在链路里的位置,后面改代码才不会迷路。
| 文件 | 职责 | 关键内容 |
|---|---|---|
main.c | 程序入口 | 参数解析、socket 初始化、主循环 |
main.h | 入口声明 | 函数原型、全局宏 |
ntp.c | 协议核心 | 报文结构体、收发逻辑、时间戳填充 |
ntp.h | 协议声明 | NTP 报文格式定义、常量 |
time.c | 时间处理 | 系统时间读取、NTP 时间戳转换 |
Makefile | 构建 | 编译规则、目标文件、链接 |
调用关系是线性的:main.c启动后调用ntp.c里的初始化函数创建 UDP socket,绑定 123 端口,然后进入recvfrom阻塞等待。收到报文后交给ntp.c的解析函数判断是不是合法的 SNTP 请求,如果是,调用time.c取当前系统时间,转换成 NTP 时间戳格式,填进响应报文,再sendto回去。main.h和ntp.h负责把这些函数的声明和结构体定义串起来,Makefile把.c编译成.o再链接成可执行文件。
这种结构的优点是改起来直观。比如你想加一个日志功能,在ntp.c的响应发送前插几行就行;想换时间源,改time.c里的取时函数即可。缺点是所有逻辑揉在三个.c里,规模再大就得拆模块了。
2.2 NTP 报文格式与 SNTP 的取舍
SNTP 和 NTP 用的是同一套报文格式,区别在于 SNTP 不实现 NTP 的时钟滤波、时钟选择和聚类算法。对于服务器端来说,这意味着它收到请求后不需要维护复杂的对等体状态,直接回当前时间就行。
NTP 报文固定 48 字节,头部结构如下:
| 字段 | 偏移 | 长度 | 说明 |
|---|---|---|---|
| LI/VN/Mode | 0 | 1 字节 | 前 2 位闰秒指示,中间 3 位版本号,后 3 位模式 |
| Stratum | 1 | 1 字节 | 时钟层级,1 表示一级,2 表示二级 |
| Poll | 2 | 1 字节 | 轮询间隔,服务器端通常忽略 |
| Precision | 3 | 1 字节 | 时钟精度,有符号整数 |
| Root Delay | 4 | 4 字节 | 到参考源的往返延迟 |
| Root Dispersion | 8 | 4 字节 | 到参考源的离散度 |
| Reference ID | 12 | 4 字节 | 参考源标识 |
| Reference Timestamp | 16 | 8 字节 | 上次校准时间 |
| Originate Timestamp | 24 | 8 字节 | 客户端发送时间(回显) |
| Receive Timestamp | 32 | 8 字节 | 服务器收到时间 |
| Transmit Timestamp | 40 | 8 字节 | 服务器发送时间 |
服务器端最核心的操作是:把客户端请求里的 Transmit Timestamp 复制到响应的 Originate Timestamp 字段,然后填入自己的 Receive Timestamp 和 Transmit Timestamp。客户端拿到这三个时间戳就能算出往返延迟和时钟偏差。
Mode 字段在请求里是 3(客户端模式),服务器响应时设为 4(服务器模式)。Stratum 字段填 2 到 15 之间的值,表示你离一级时间源有多远。如果你这台服务器本身是从别处同步来的,Stratum 就填上游的层级加一;如果它直接用本地时钟,填 1 也行,但精度上不诚实。
2.3 编译与首次运行
拿到源码第一步是编译。Makefile里一般会写CC、CFLAGS和TARGET,直接make就行。
# 解压后进入目录 unzip ntp.rar -d ntp_src cd ntp_src # 查看 Makefile 内容,确认编译器与目标名 cat Makefile # 编译 make # 如果报错找不到头文件,检查是否缺少标准库 # 常见缺失:sys/socket.h、arpa/inet.h、netinet/in.h编译通过后会生成一个可执行文件,名字看Makefile里的TARGET定义,常见的是ntp或sntp_server。运行需要绑定 123 端口,普通用户没有权限,必须用 root 或者给二进制文件加CAP_NET_BIND_SERVICE能力。
# 直接以 root 运行 sudo ./ntp # 或者赋予绑定低端口能力,之后普通用户也能跑 sudo setcap cap_net_bind_service=+ep ./ntp ./ntp程序启动后不会有花哨的输出,一般只打印一行监听地址和端口。这时候它已经在 UDP 123 上等着了。你可以在另一台机器上用ntpdate或者chrony的chronyc去指它,也可以在本机用sntp命令测试。
# 本机测试,假设服务器 IP 是 192.168.1.10 sntp -d 192.168.1.10 # 或者用 ntpdate(注意 ntpdate 已逐渐被 chrony 替代) sudo ntpdate -d 192.168.1.10如果返回了时间并且偏差在合理范围内,说明服务器端逻辑是通的。如果超时,先检查防火墙和端口监听状态。
3. 让 SNTP 服务器跑在正确的时间上:时间戳转换与系统时钟
3.1 NTP 时间戳与 Unix 时间的换算
NTP 时间戳是一个 64 位无符号定点数:高 32 位是秒,低 32 位是秒的小数部分。它的纪元从 1900 年 1 月 1 日 00:00:00 UTC 开始,而 Unix 时间从 1970 年 1 月 1 日开始。两者相差 2208988800 秒。
time.c里最关键的函数就是做这个转换。常见写法是:
#include <stdint.h> #include <sys/time.h> #include <time.h> #define NTP_EPOCH_OFFSET 2208988800UL // 将 Unix 时间转换为 NTP 时间戳(64 位) uint64_t unix_to_ntp(struct timeval *tv) { uint64_t seconds = (uint64_t)(tv->tv_sec + NTP_EPOCH_OFFSET); // 微秒转成 32 位小数部分:微秒数 * 2^32 / 1e6 uint64_t fraction = (uint64_t)((double)tv->tv_usec * 4294967296.0 / 1000000.0); return (seconds << 32) | (fraction & 0xFFFFFFFF); } // 从系统时钟取当前时间并直接返回 NTP 时间戳 uint64_t get_ntp_timestamp(void) { struct timeval tv; gettimeofday(&tv, NULL); return unix_to_ntp(&tv); }这里有两个容易翻车的点。第一,tv_usec是微秒,范围 0 到 999999,乘以 2^32 再除以 10^6 得到小数部分,这个浮点运算在 32 位平台上可能有精度损失,但对 SNTP 来说足够。第二,seconds << 32在 32 位系统上如果uint64_t定义不当会溢出,确保包含<stdint.h>并且用uint64_t类型。
3.2 系统时钟的读取与精度边界
gettimeofday的精度取决于内核的时钟中断频率,通常是微秒级,但实际分辨率可能在 1 到 10 毫秒之间。对于内网时间同步,这个精度完全够用。如果你想要更高精度,可以用clock_gettime(CLOCK_REALTIME, ...),它返回struct timespec,纳秒级字段。
#include <time.h> uint64_t get_ntp_timestamp_precise(void) { struct timespec ts; clock_gettime(CLOCK_REALTIME, &ts); uint64_t seconds = (uint64_t)(ts.tv_sec + NTP_EPOCH_OFFSET); uint64_t fraction = (uint64_t)((double)ts.tv_nsec * 4294967296.0 / 1000000000.0); return (seconds << 32) | (fraction & 0xFFFFFFFF); }但要注意,CLOCK_REALTIME受系统时间调整影响,如果系统正在被 NTP 同步,取到的时间可能在两次调用之间跳变。SNTP 服务器本身不应该频繁调整系统时钟,它只是把当前系统时间报告出去。所以更稳妥的做法是:这台服务器自己先用 chrony 或 systemd-timesyncd 跟上游对好时,然后它再作为二级服务器对外服务。
3.3 响应报文的填充顺序
收到请求后,填充响应的顺序不能乱。先把请求里的 Transmit Timestamp 读出来存好,然后取当前时间填 Receive Timestamp,再取一次当前时间填 Transmit Timestamp。两次取时间之间间隔越短越好,否则 Receive 和 Transmit 的差值会偏大,客户端算出来的延迟就不准。
// 假设 req 是收到的请求报文,resp 是要发送的响应 void fill_response(ntp_packet *req, ntp_packet *resp) { // 1. 复制请求的 transmit timestamp 到响应的 originate timestamp resp->originate_timestamp = req->transmit_timestamp; // 2. 取当前时间填 receive timestamp resp->receive_timestamp = get_ntp_timestamp(); // 3. 设置头部字段 resp->li_vn_mode = (0 << 6) | (4 << 3) | 4; // LI=0, VN=4, Mode=4 resp->stratum = 2; resp->poll = req->poll; resp->precision = 0xFA; // -6,表示约 1 微秒精度 // 4. 取当前时间填 transmit timestamp resp->transmit_timestamp = get_ntp_timestamp(); }li_vn_mode这个字节的位操作是血泪经验:LI 占高 2 位,VN 占中间 3 位,Mode 占低 3 位。写反了客户端会直接丢弃报文。precision字段是有符号 8 位整数,0xFA 对应 -6,意思是时钟精度约 2^-6 秒,也就是 15.6 毫秒。填 0xEC(-20)表示微秒级,但实际达不到就别乱填。
4. 避坑与排查:SNTP 服务器最常见的五个翻车现场
4.1 端口 123 绑定失败
现象:程序启动直接报bind: Permission denied或者Address already in use。
原因:123 是特权端口,非 root 用户没有绑定权限;另一种可能是系统里已经有 ntpd、chronyd 或 systemd-timesyncd 占着这个端口。
解决:先用sudo ss -ulnp | grep 123看谁在占用。如果是系统自带的时间服务,停掉它:sudo systemctl stop chronyd或sudo systemctl stop systemd-timesyncd。如果不想停,就把你的 SNTP 服务器改到其他端口,但客户端也得跟着改,比较麻烦。推荐做法是停掉冲突服务,让这份代码独占 123。
4.2 客户端收不到响应
现象:服务器端recvfrom有返回,但客户端一直超时。
原因:防火墙拦了 UDP 123 的入站或出站。很多人只记得开入站,忘了出站规则。另外,如果服务器有多张网卡,recvfrom可能绑在了错误的接口上。
解决:检查iptables或firewalld规则。临时放行:sudo iptables -I INPUT -p udp --dport 123 -j ACCEPT。如果是 firewalld:sudo firewall-cmd --add-port=123/udp。同时确认程序绑定的是INADDR_ANY还是特定 IP,绑定INADDR_ANY才能在所有网卡上监听。
4.3 时间偏差越同步越大
现象:客户端同步后时间反而偏了几秒甚至几分钟。
原因:服务器本身的系统时间就是错的。SNTP 服务器不修正自己的时间,它只是把当前系统时间报告出去。如果这台机器的时间没跟上游对过,它就是一个错误的基准。
解决:在运行 SNTP 服务器之前,先用 chrony 或 ntpdate 把本机时间校准。校准完成后,再启动这个程序。如果你希望它自动跟上游同步,那需要的是完整的 NTP 实现,不是这份简化代码。
4.4 报文长度不对导致解析失败
现象:服务器收到请求但判断为非法报文,直接丢弃。
原因:NTP 报文固定 48 字节,但有些客户端会发送带认证字段的扩展报文,长度超过 48。如果代码里写死了recvfrom的缓冲区大小或者校验了len != 48,就会误判。
解决:接收缓冲区至少给 1024 字节,解析时只读前 48 字节,忽略后面的扩展字段。校验时判断len >= 48而不是len == 48。
4.5 长时间运行后时间戳溢出
现象:服务器跑了一段时间后,客户端同步出来的时间跳到 2036 年附近。
原因:NTP 时间戳的秒字段是 32 位无符号整数,从 1900 年算起,到 2036 年 2 月 7 日会溢出回绕。虽然现在离 2036 年还有距离,但如果代码里用了有符号 32 位来存秒数,溢出会更早出现。
解决:确保秒数字段用uint32_t或uint64_t。在转换函数里不要用int存 NTP 秒数。如果你在 2036 年之前看到时间跳变,先检查类型定义。
5. 进阶用法:把这份代码改造成可用的内网时间基准
5.1 增加日志与监控
原始代码大概率没有日志输出,排错全靠猜。加日志最省事的方式是在响应发送前后各打一行,记录客户端 IP、请求模式和发送状态。
#include <stdio.h> #include <arpa/inet.h> void log_request(struct sockaddr_in *client, ntp_packet *req) { char ip[INET_ADDRSTRLEN]; inet_ntop(AF_INET, &(client->sin_addr), ip, INET_ADDRSTRLEN); int mode = req->li_vn_mode & 0x07; int version = (req->li_vn_mode >> 3) & 0x07; fprintf(stderr, "[SNTP] client=%s:%d mode=%d version=%d\n", ip, ntohs(client->sin_port), mode, version); }把log_request插在recvfrom成功之后、解析之前。日志走stderr,不干扰标准输出,方便用systemd收集。如果请求量不大,这样打没问题;如果每秒几百个请求,就得考虑限流或者只记录异常。
5.2 用 systemd 托管并限制权限
直接sudo ./ntp跑在终端里,一关终端就没了。用 systemd 托管,顺便把权限收紧。
# /etc/systemd/system/sntp-server.service [Unit] Description=Simple SNTP Server After=network.target [Service] Type=simple ExecStart=/usr/local/bin/ntp Restart=on-failure RestartSec=5 User=nobody Group=nogroup AmbientCapabilities=CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_BIND_SERVICE NoNewPrivileges=true ProtectSystem=strict ProtectHome=true [Install] WantedBy=multi-user.target关键在AmbientCapabilities和CapabilityBoundingSet:只给绑定低端口的能力,其他权限全部剥掉。User=nobody让进程以非特权用户运行,即使被攻破也拿不到系统控制权。ProtectSystem=strict把整个文件系统挂成只读,程序只能写/dev和临时目录。
改完之后sudo systemctl daemon-reload,然后sudo systemctl enable --now sntp-server。用systemctl status sntp-server看运行状态,用journalctl -u sntp-server -f看实时日志。
5.3 验证同步效果与精度边界
部署完之后得验证。最直接的方法是在客户端上用chronyc看偏差。
# 在客户端上临时指定这台服务器 sudo chronyd -q 'server 192.168.1.10 iburst' # 或者用 sntp 查询 sntp 192.168.1.10 # 查看当前同步状态 chronyc trackingchronyc tracking会输出System time、Last offset、RMS offset等字段。Last offset是最近一次同步的偏差,RMS offset是长期偏差的均方根。对于内网 SNTP,Last offset在几毫秒到几十毫秒之间算正常。如果超过 100 毫秒,检查网络抖动和服务器负载。
精度边界方面,这份代码没有实现 NTP 的时钟滤波和频率补偿,所以它只能做到“报告当前系统时间”,做不到“平滑调整客户端时钟”。客户端如果用ntpdate这种一次性同步工具,时间会跳变;用 chrony 或 ntpd 的iburst模式,它们会自己做滤波和渐进调整。所以服务器端简单没关系,客户端端的算法会兜底。
从那以后我每次部署内网时间服务,都强制走一遍:先校准本机时间,再启动 SNTP 服务器,然后用 systemd 限制权限,最后在客户端上用chronyc tracking确认偏差在可接受范围内。这套流程跑下来,基本不会再出现时间越同步越乱的情况。希望帮到你。
本文还有配套的精品资源,点击获取