首页 > 人工智能 > 人工智能 > NTP时间同步服务器(时钟同步)工作原理介绍
NTP时间同步服务器(时钟同步)工作原理介绍
NTP时间同步服务器(时钟同步)工作原理介绍
1、网络时间协议( NTP )是一种通过因特网服务于计算机时钟的同步时间协议。它提供了一种同步时间机制,能在庞大而复杂多样的因特网中用光速调整时间分配。它使用的是可返回时间设计方案,其特点是:时间服务器是一种分布式子网,能自我组织操作、分层管理配置,经过有线或无线方式同步逻辑时钟达到国家标准时间。此外,通过本地路由选择运算法则及时间后台程序,服务器可以重新分配标准时间。
NTP 的校时涉及三个概念 — 时间偏差、时间延迟及差量,它们与指定参考时钟都是相关联的。时钟偏差表示本地时钟与参考时钟之间的偏差数;时间延迟表示在指定时间内由一方发送消息到另一方接收到消息间的延时时间;差量表示了相对于参考时钟本地时钟的最大偏差错误。因为大多数主机时间服务器通过其它对等时间服务器达到同步,所以这三个参量都有两个组成部分:其一是由对等决定的部分,这部分是相对于原始标准时间的参考来源而言;其二是由主机衡量的部分,这部分是相对于对等而言。每一部分在协议中都是独立维持的,从而可以使错误控制和子网本身的管理操作变得容易。它们不仅提供了偏移和延迟的精密测量,而且提供了明确的最大错误范围,这样用户接口不但可以决定时间,而且可以决定时间的准确度。
2. NTP 协议包结构:
进行网络协议实现时最重要的是了解协议数据格式。除了可扩展部分,基本的 NTP 数据包有 48 个字节,其中 NTP 包头 16 字节,时间戳 32 个字节。其协议格式下表所示。
|
2 |
5 |
8 |
16 |
24 |
32bit |
|
LI ( 2 ) |
VN ( 3 ) |
Mode ( 3 ) |
Stratum ( 8 ) |
Poll ( 8 ) |
Precision ( 8 ) |
|
Root Delay | |||||
|
Root Dispersion | |||||
|
Reference Identifier | |||||
|
Reference timestamp ( 64 ) | |||||
|
Originate Timestamp ( 64 ) | |||||
|
Receive Timestamp ( 64 ) | |||||
|
Transmit Timestamp ( 64 ) | |||||
|
Key Identifier ( optional )( 32 ) | |||||
|
Message digest ( optional )( 128 ) | |||||
· LI :跳跃指示器,警告在当月最后一天的最终时刻插入的迫近闺秒(闺秒)。
· VN :版本号。
· Mode :模式。该字段包括以下值:0-预留;1-对称行为;3-客户机;4-服务器;5-广播;6-NTP 控制信息
· Stratum :对本地时钟级别的整体识别。
· Poll :有符号整数表示连续信息间的最大间隔。
· Precision :有符号整数表示本地时钟精确度。
· Root Delay :有符号固定点序号表示主要参考源的总延迟,很短时间内的位15到16间的分段点。
· Root Dispersion :无符号固定点序号表示相对于主要参考源的正常差错,很短时间内的位15到16间的分段点。
· Reference Identifier :识别特殊参考源。
· Originate Timestamp :这是向服务器请求分离客户机的时间,采用64位时标(Timestamp)格式。
· Receive Timestamp :这是向服务器请求到达服务器的时间,采用64位时标(Timestamp)格式。
· Transmit Timestamp :这是向客户机答复分离服务器的时间,采用64位时标(Timestamp)格式。
Authenticator (Optional):当实现了 NTP 认证模式,主要标识符和信息数字域就包括已定义的信息认证代码(MAC)信息。
3. Daemon 进程概念:
Daemon 是长时间运行的进程,通常在系统启动后就运行,在系统关闭时才结束。一般说Daemon程序在后台运行,是因为它没有控制终端,无法和前台的用户交互。Daemon程序一般都作为服务程序使用,等待客户端程序与它通信。我们也把运行的Daemon程序称作守护进程。
比如,我们的网络服务程序,可以在完成创建套接口,绑定套接口,设置套接口为监听模式后,变成守护进程进入后台执行而不占用控制终端,这是网络服务程序的常用模式。Linux下的网络服务程序,如samba、FTP、Telnet一般都是由守护进程(Daemon)来实现的。Linux的守护进程一般都命名为*d的形式,如httpd,telnetd等等。守护进程一旦脱离了终端,退出就成了问题。可以使用命令:ps aux|grep *,其中*号为进程名,找到相应进程的ID,再使用命令:kill -SIGTERM ID,终止它。
4. Daemon 程序编写:
编写Daemon程序有一些基本的规则,以避免不必要的麻烦。
(1) 首先是程序运行后调用fork,并让父进程退出。子进程获得一个新的进程ID,但 继承了父进程的进程组ID。
(2) 调用setsid创建一个新的session,使自己成为新session和新进程组的leader,并使进程没有控制终端(tty)。
(3) 改变当前工作目录至根目录,以免影响可加载文件系统。或者也可以改变到某些特定的目录。
(4) 设置文件创建mask为0,避免创建文件时权限的影响。
(5) 关闭不需要的打开文件描述符。因为Daemon程序在后台执行,不需要于终端交互,通常就关闭STDIN、STDOUT和STDERR。其它根据实际情况处理。
另一个问题是Daemon程序不能和终端交互,也就无法使用printf方法输出信息了。我们可以使用syslog机制来实现信息的输出,方便程序的调试。当然,你也可以把这些信息输出到自己的日志文件中查看。
下面给出一段Daemon程序的例子:
|
#include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <stdlib.h> #include <stdio.h> #include <syslog.h> #include <signal.h>
int daemon_init(void) { pid_t pid; if((pid = fork()) < 0) return(-1); else if(pid != 0) exit(0); /* parent exit */ /* child continues */ setsid(); /* become session leader */ chdir("/"); /* change working directory */ umask(0); /* clear file mode creation mask */ close(0); /* close stdin */ close(1); /* close stdout */ close(2); /* close stderr */ return(0); }
void sig_term(int signo) { if(signo == SIGTERM) //catched signal sent by kill(1) command { syslog(LOG_INFO, "program terminated."); closelog(); exit(0); } }
int main(void) { daemon_init(); openlog("daemontest", LOG_PID, LOG_USER); syslog(LOG_INFO, "program started."); signal(SIGTERM, sig_term); /* arrange to catch the signal */ while(1) { sleep(1); /* put your main program here */ } return(0); } |
1. src 子目录:
实验代码stdinc.h,这是程序使用到的标准头文件的一个集合,这样做不仅可以减少编码工作量,也不至于因为修改了头文件的内容而减慢编译的速度。
|
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdarg.h> #include <unistd.h> #include <netinet/in.h> #include <sys/socket.h> #include <sys/types.h> #include <arpa/inet.h> #include <netdb.h> #include <sys/time.h> #include <time.h> #include <sys/select.h> #include <stdbool.h> #include <signal.h> #include <sys/param.h> #include <sys/stat.h> #include <fcntl.h> |
实验代码def.h,这里主要是一些宏定义和结构类型定义。另外,调试宏函数PDEBUG的定义方式值得大家注意。
|
#ifndef __DEF_H__ #define __DEF_H__
#define CMD_NAME "ntpclient" //ntp 时间从年开始,本地时间从年开始,这是两者之间的差值 #define JAN_1970 0x83aa7e80 //3600s*24h*(365days*70years+17days) //x*10^(-6)*2^32 微妙数转 NtpTime 结构的 fraction 部分 #define NTPFRAC(x) (4294 * (x) + ((1981 * (x)) >> 11)) //NTPFRAC 的逆运算 #define USEC(x) (((x) >> 12) - 759 * ((((x) >> 10) + 32768) >> 16))
#define DEF_NTP_SERVER "210.72.145.44" // 国家授时中心 ip //#define DEF_NTP_SERVER "stdtime.gov.hk" // 香港标准时间 //#define DEF_NTP_SERVER "pool.ntp.org" //ntp 官方时间 #define DEF_NTP_PORT 123 #define DEF_PSEC 10 #define DEF_PMIN 0 #define DEF_PHOUR 0 #define DEF_TIMEOUT 10 #define DEF_LOGEN 1 #define DEF_LOGPATH "/tmp/" CMD_NAME ".log"
#define CONF_PATH "/etc/" CMD_NAME "/" CMD_NAME ".conf" #define DAE_PID_PATH "/var/run/" CMD_NAME ".pid" #define INIT_PATH "/etc/init.d/" CMD_NAME
//ntp 时间戳结构 typedef struct { unsigned int integer; unsigned int fraction; } NtpTime;
// 校准信息结构 typedef struct { struct timeval dlytime; struct timeval offtime; struct timeval newtime; } NtpServResp;
//ntp 客户端配置结构,对应 ntpclient.conf 中各项 typedef struct { char servaddr[256]; unsigned int port; int psec; int pmin; int phour; int timeout; bool logen; char logpath[256]; } NtpConfig;
#ifndef DEBUG #define PDEBUG(fmt, args...)/ do {} while (0) #else #define PDEBUG(fmt, args...)/ printf( "[%s:%d]" fmt, __func__, __LINE__, # #args) #endif
#endif |
实验代码ntpclient.c,这是核心代码,在代码开头使用了extern申明了外部定义的函数,主要是日志、交互运行和后台运行的相关函数。前面提过,在项目代码中还使用select函数提供端口监控和超时控制的功能。在学习源代码之前,你可以通过下面的表格了解这个函数的用法。
|
|
select |
|
函数功能 |
发送通过远端主机指定套接字数据 |
|
头文件 |
#include < select .h> #include < time .h> #include <sys/types.h> #include <unistd.h> |
|
函数原型 |
int select(int nfds, fd_set *readfds, fd_set *writefds, f d_set *exceptfds, struct timeval *timeout); |
|
参数说明 |
nfds :监控的文件描述符集中最大的文件描述符值 +1 。
select()
函数的接口主要是建立在一种叫
'fd_set'
类型的基础上。它
('fd_set')
是一组文件描述符
(fd)
的集合。由于
fd_set
类型的长度在不同平台上不同,因此应该用一组标准的宏定义来处理此类变量:
readfds :可读文件描述符集,监控该集合直到其中有元素可读或超时。 writefds :可写文件描述符集,监控该集合直到其中有元素可写或超时。 exceptfds :异常文件描述符集,监控该集合直到其中有元素发生异常或超时。 timeout :等待超时阀值。 NULL 指针代表无限等待,否则是指向 timeval 结构的指针,代表最长等待时间。 ( 如果其中 tv_sec 和 tv_usec 都等于 0, 则文件描述符的状态不被影响,但函数并不挂起 ) |
|
返回值 |
成功返回 就绪的 fd 数 , 超时 返回 0 , 错误返回 -1 。 |
|
范例 |
见项目代码 |
|
#include "stdinc.h" #include "def.h"
extern int log_record( char *record, ...); extern void log_dlytime( struct timeval dly); extern void log_offtime( struct timeval off); extern void log_newtime( struct timeval new ); extern void sig_log_term( int signo);
extern int get_cfg_from_menu(); extern void init_daemon( void ); extern int record_pid_to_file( const char *pidfn); extern int get_cfg_from_file( const char *cfn);
// 配置信息变量 NtpConfig NtpCfg;
/* * 构造并发送 ntp 协议包 * */ void send_packet( int fd) { unsigned int data[12]; int ret; struct timeval now;
#define LI 0 // 协议头中的元素 #define VN 3 // 版本 #define MODE 3 // 模式 : 客户端请求 #define STRATUM 0 #define POLL 4 // 连续信息间的最大间隔 #define PREC -6 // 本地时钟精度 if ( sizeof (data) != 48) { printf( "data size error!/n" ); exit(1); } memset(( char *)data, 0, sizeof (data)); data[0] = htonl((LI << 30) | (VN << 27) | (MODE << 24) | (STRATUM << 16) | (POLL << 8) | (PREC & 0xff)); data[1] = htonl(1 << 16); data[2] = htonl(1 << 16); // 获得本地时间 gettimeofday(&now, NULL);
data[10] = htonl(now.tv_sec + JAN_1970); data[11] = htonl(NTPFRAC(now.tv_usec)); PDEBUG( "tratime.fraction=%d/n" , data[11]); ret = send(fd, data, 48, 0); PDEBUG( "send packet to ntp server, ret: %d/n" , ret); }
/* * 获得并解析 ntp 协议包 * @sock -- 与时间服务器通讯的套接字 * @resp -- 从服务器应答中提取的有用信息 * */ bool get_server_time( int sock, NtpServResp *resp) { int ret; unsigned int data[12]; NtpTime oritime, rectime, tratime, destime; struct timeval offtime, dlytime; struct timeval now;
bzero(data, sizeof (data)); ret = recvfrom (sock, data, sizeof (data), 0, NULL, 0); if (ret == -1) { PDEBUG( "recvfrom was failed!/n" ); log_record( "recvfrom was failed! 被迫终止 !/n" ); exit(1); } else if (ret == 0) { PDEBUG( "recvfrom receive 0!/n" ); return false ; }
gettimeofday(&now, NULL); destime.integer = now.tv_sec + JAN_1970; destime.fraction = NTPFRAC (now.tv_usec);
#define DATA(i) ntohl((( unsigned int *)data)[i]) oritime.integer = DATA(6); oritime.fraction = DATA(7); rectime.integer = DATA(8); rectime.fraction = DATA(9); tratime.integer = DATA(10); tratime.fraction = DATA(11); #undef DATA // 与 send_packet 中发送的 tratime.faction 一致 PDEBUG( "oritime.faction=%d/n" ,htonl(oritime.fraction));
//Originate Timestamp T1 客户端发送请求的时间 //Receive Timestamp T2 服务器接收请求的时间 //Transmit Timestamp T3 服务器答复时间 //Destination Timestamp T4 客户端接收答复的时间 // 网络延时 d 和服务器与客户端的时差 t //d = (T2 - T1) + (T4 - T3); t = [(T2 - T1) + (T3 - T4)] / 2; #define MKSEC(ntpt) ((ntpt).integer - JAN_1970) #define MKUSEC(ntpt) (USEC((ntpt).fraction)) #define TTLUSEC(sec,usec) (( long long )(sec)*1000000 + (usec)) #define GETSEC(us) ((us)/1000000) #define GETUSEC(us) ((us)%1000000)
long long orius, recus, traus, desus, offus, dlyus;
orius = TTLUSEC(MKSEC(oritime), MKUSEC(oritime)); recus = TTLUSEC(MKSEC(rectime), MKUSEC(rectime)); traus = TTLUSEC(MKSEC(tratime), MKUSEC(tratime)); desus = TTLUSEC(now.tv_sec, now.tv_usec);
offus = ((recus - orius) + (traus - desus))/2; dlyus = (recus - orius) + (desus - traus);
offtime.tv_sec = GETSEC(offus); offtime.tv_usec = GETUSEC(offus); dlytime.tv_sec = GETSEC(dlyus); dlytime.tv_usec = GETUSEC(dlyus);
struct timeval new ;
// 粗略校时 //new.tv_sec = tratime.integer - JAN_1970; //new.tv_usec = USEC(tratime.fraction); // 精确校时 new .tv_sec = destime.integer - JAN_1970 + offtime.tv_sec; new .tv_usec = USEC(destime.fraction) + offtime.tv_usec;
resp->newtime = new ; resp->dlytime = dlytime; resp->offtime = offtime;
return true ; }
/* * 更新本地时间 * @newtime -- 要新的时间 * */ int mod_localtime( struct timeval newtime) { // 只有 root 用户拥有修改时间的权限 if (getuid() != 0 &&geteuid () != 0) { log_record( " 不是 root 用户,无法进行时间校准,被迫终止 !/n" ); exit(1); } if (settimeofday(&newtime, NULL) == -1) { log_record( " 设置时间失败 !/n" ); return -1; } else { log_record( " 设置时间成功 !/n" ); } return 0; }
/* * 连接时间服务器 * */ int ntp_conn_server( const char *servname, int port) { int sock;
int addr_len = sizeof ( struct sockaddr_in); struct sockaddr_in addr_src; // 本地 socket <netinet/in.h> struct sockaddr_in addr_dst; // 服务器 socket
//UDP 数据报套接字 sock = socket(PF_INET, SOCK_DGRAM, 0); if (sock == -1) { log_record( " 套接字创建失败,被迫终止 ! /n" ); exit(1); } memset(&addr_src, 0, addr_len); addr_src.sin_family = AF_INET; addr_src.sin_port = htons(0); addr_src.sin_addr.s_addr = htonl(INADDR_ANY); //<arpa/inet.h> // 绑定本地地址 if (-1 == bind(sock, ( struct sockaddr *) &addr_src, addr_len)) { log_record( " 绑定失败,被迫终止 !/n" ); exit (1); } memset(&addr_dst, 0, addr_len); addr_dst.sin_family = AF_INET; addr_dst.sin_port = htons(port);
struct hostent *host = gethostbyname(servname); //<netdb.h> if (host == NULL) { log_record( " 主机名获取错误,被迫终止 !/n" ); exit (1); } memcpy (&(addr_dst.sin_addr.s_addr), host->h_addr_list[0], 4); PDEBUG( "Connecting to NTP_SERVER: %s ip: %s port: %d.../n" , servname, inet_ntoa(addr_dst.sin_addr), port);
if (-1 == connect(sock, ( struct sockaddr *) &addr_dst, addr_len)) { log_record( " 连接服务器失败,被迫终止 !/n" ); exit (1); }
return sock; }
/* * 装入默认的配置 * */ void load_default_cfg() { strcpy(NtpCfg.servaddr, DEF_NTP_SERVER); NtpCfg.port = DEF_NTP_PORT; NtpCfg.psec = DEF_PSEC; NtpCfg.pmin = DEF_PMIN; NtpCfg.phour = DEF_PHOUR; NtpCfg.timeout = DEF_TIMEOUT; NtpCfg.logen = DEF_LOGEN; strcpy(NtpCfg.logpath, DEF_LOGPATH); }
/* * 初始化 ntp 客户端程序 * */ int ntpclient_init( int argc, char **argv) { int ret; bzero(&NtpCfg, sizeof (NtpCfg)); if (!access(DAE_PID_PATH, F_OK)) { PDEBUG(DAE_PID_PATH " 已经存在,服务正在后台运行 !/n" ); exit(1); } // 装入默认配置 load_default_cfg();
if (1 == argc || !strcmp(argv[1], "-D" )) // 后台运行 { // 从配置文件获取配置 get_cfg_from_file(CONF_PATH);
init_daemon(); // 初始化为 Daemon ret= record_pid_to_file(DAE_PID_PATH); // 记录 pid if (ret==-1) { PDEBUG(DAE_PID_PATH " 创建失败 !/n" ); }
log_record( "NTP 服务开始在后台为您校准时间 !/n" );
} else if (2 == argc && !strcmp(argv[1], "-i" )) // 交互式运行 { while ((ret=get_cfg_from_menu())==0); if (ret==-1) { exit(1); } log_record( "NTP 服务开始在终端为您校准时间 !/n" ); } else { printf( "/n 用法 : %s -i/-D/n" " Or: %s/n" " -i, 以交互方式在终端运行 /n" " -D, 以守护进程方式运行 ( 默认 )/n/n" " 守护进程 PID 文件 : " DAE_PID_PATH "/n" " 配置文件 : " CONF_PATH "/n" " 启动脚本 : " INIT_PATH "/n" " 日志文件 : %s/n/n" , CMD_NAME, CMD_NAME, NtpCfg.logpath); exit(1); }
// 保证日志存在 if (NtpCfg.logen) { int fd; if ((fd=open(NtpCfg.logpath, O_RDONLY|O_CREAT))==-1) { PDEBUG( " 日志打开失败,将关闭日志标志 !/n" ); } close(fd); } return 0; }
/* * 程序入口 * */ int main( int argc, char **argv) { int sock; int ret; NtpServResp response; struct timeval timeout; //<sys/time.h> // 初始化 ntpclient ntpclient_init(argc, argv); // 注册 signal 处理函数 signal(SIGTERM, sig_log_term); signal(SIGINT, sig_log_term);
// 连接 ntp 服务器 sock = ntp_conn_server(NtpCfg.servaddr, NtpCfg.port);
// 发送 ntp 包 send_packet(sock); while (1) { fd_set fds_read; FD_ZERO(&fds_read); FD_SET(sock, &fds_read);
timeout.tv_sec = NtpCfg.timeout; timeout.tv_usec = 0;
ret = select(sock + 1, &fds_read, NULL, NULL, &timeout); if (ret == -1) { log_record( "select 函数出错,被迫终止 !/n" ); exit(0); } if (ret == 0 || !FD_ISSET (sock, &fds_read)) { log_record( " 等待服务器响应超时,重发请求 !/n" ); // 向服务器发送数据 send_packet(sock); continue ; }
if ( false == get_server_time(sock, &response)) { continue ; } mod_localtime(response.newtime);
log_offtime(response.offtime); log_dlytime(response.dlytime); log_newtime(response.newtime); // 间隔指定时间校准一次 sleep(NtpCfg.phour*3600+NtpCfg.pmin*60+NtpCfg.psec); // 发送 ntp 包 send_packet(sock); }
close(sock); exit(0); }
|
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/69969420/viewspace-2771041/ ,如需转载,请注明出处,否则将追究法律责任。