NTP時間同步伺服器(時鐘同步)工作原理介紹

ahjzdz發表於2021-05-06

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部落格 ” ,連結:http://blog.itpub.net/69969420/viewspace-2771041/,如需轉載,請註明出處,否則將追究法律責任。

相關文章