ITPub博客

首页 > 人工智能 > 人工智能 > NTP时间同步服务器(时钟同步)工作原理介绍

NTP时间同步服务器(时钟同步)工作原理介绍

原创 人工智能 作者:ahjzdz 时间:2021-05-06 17:09:01 0 删除 编辑

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   类型的长度在不同平台上不同,因此应该用一组标准的宏定义来处理此类变量:    
   
       fd_set set; 
   
       FD_ZERO(&set); /*  将   set   清零     */  
   
       FD_SET(fd, &set); /*  将   fd   加入   set */  
   
       FD_CLR(fd, &set); /*  将   fd   从   set   中清除     */  
   
       FD_ISSET(fd, &set); /*  如果   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/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论
安徽京准电子科技公司 微信电话同步13295517758

注册时间:2020-04-15

  • 博文量
    53
  • 访问量
    50729