天天看點

利用NTP協定擷取并更新系統時間

Network Time Protocol(NTP)協定是用來使計算機時間同步化的一種協定,它可以使計算機對其伺服器或時鐘 源(如石英鐘,GPS 等)做同步化,它可以提供高精确度的時間校正(LAN 上與标準時間差小于 1 毫秒,WAN 上幾十毫秒),且可用加密确認的方式來防止惡毒的協定攻擊。

NTP 提供準确時間,首先要有準确的時間來源,這一時間應該是國際标準時間 UTC。 NTP 獲得 UTC 的 時間來源可以是原子鐘、天文台、衛星,也可以從 Internet 上擷取。這樣就有了準确而可靠的時間源。時間是按 NTP 伺服器的等級傳播。按照距離外部 UTC 源的遠近将所有伺服器歸入不同的 Stratun(層)中。 Stratum-1 在頂層,有外部 UTC 接入,而 Stratum-2 則從 Stratum-1 擷取時間,Stratum-3 從 Stratum-2 擷取 時間,以此類推,但 Stratum 層的總數限制在 15 以内。所有這些伺服器在邏輯上形成階梯式的架構并互相 連接配接,而 Stratum-1 的時間伺服器是整個系統的基礎。

進行網絡協定實作時最重要的是了解協定資料格式。NTP 資料包有 48 個位元組,其中 NTP 標頭 16 位元組, 時間戳 32 個位元組。

協定格式

利用NTP協定擷取并更新系統時間

封包字段

字段名 長 度 含義
LI(Leap Indicator) 2比特 這是一個兩位的代碼,表示在NTP時間标尺中将要插入的下一跳情況。值為“11”時表示告警狀态,警告在當月最後一天的最終時刻插入的迫近閨秒(閨秒),此時時鐘不能被同步。
VN(Version Number) 3比特 NTP的版本号。
Mode 3比特 NTP的工作模式。不同值表示的含義如下:0:reserved,保留。1:symmetric active,主動對等體模式。2:symmetric passive,被動對等體模式。3:client,客戶模式。4:server,伺服器模式。5:broadcast,廣播模式。6:reserved for NTP control messages,NTP控制封包。7:reserved for private use,内部使用預留。在主/被動對稱模式中,有一對 一的連接配接,雙方均可同步對方或被對方同步,先發出申請建立連接配接的一方工作在主動模式下,另一方工作在被動模式下; 客戶/伺服器模式與主/被動模式基本相同,惟一差別在于客戶方可被伺服器同步,但伺服器不能 被客戶同步;在廣播模式中,有一對多的連接配接,伺服器不論客戶工作在何種模式下,都會主動發 出時間資訊,客戶根據此資訊調整自己的時間。
Stratum 8比特 表示本地時鐘的層級數,定義了時鐘的準确度。層數為1的時鐘準确度最高,從1到15依次遞減。
Poll Interval 8比特 輪詢時間,即發送封包的最小間隔時間。
Precision 8比特 時鐘的精度。
Root Delay 32比特 到主參考時鐘的總往返延遲時間,它是有 15~16 位小數部分的符号定點小數。
Root Dispersion 32比特 本地時鐘相對于主參考時鐘的最大誤差,它是有 15~16 位小數部分的無符号定點小 數。
Reference Identifier 32比特 辨別特定參考時鐘。
Reference Timestamp 64比特 本地時鐘最後一次被設定或更新的時間,采用 64 位時标格式。如果值為0表示本地時鐘從未被同步過。
Originate Timestamp 64比特 NTP封包離開源端時的本地時間。
Receive Timestamp 64比特 NTP封包到達目的端的本地時間。
Transmit Timestamp 64比特 目的端應答封包離開伺服器端的本地時間。
Authenticator 96比特 (可選)驗證資訊,當實作了 NTP 認證模式時,主要辨別符和資訊數字域就包括已定義的 資訊認證代碼(MAC)資訊。

示例

由于 NTP 協定中涉及比較多的時間相關的操作,為了簡化實作過程,在本實驗中,僅要求實作 NTP 協定 用戶端部分的網絡通信子產品,也就是構造 NTP 協定字段進行發送和接收,最後與時間相關的操作不需進行 處理。NTP 協定是作為 OSI 參考模型的高層協定比較适合采用 UDP 傳輸協定進行資料傳輸,專用端口号 為 123。在實驗中,因為國家授時中心伺服器經常難以連接配接,是以使用cn.pool.ntp.org(IP 位址為 162.159.200.123)作為 NTP(網絡時間)伺服器。

指令行輸入”nslookup 域名“就可以檢視對應的IP位址。

實驗流程如下:

  1. 擷取NTP伺服器位址資訊;
  2. 建立socket;
  3. 建構ntp協定包;
  4. 向NTP伺服器發送請求協定包;
  5. 從NTP伺服器接收協定包
  6. 更新目前系統時間;
  • 具體代碼如下:
/* ntp.c */ 
#include <sys/socket.h> 
#include <sys/wait.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <errno.h> 
#include <string.h> 
#include <sys/un.h> 
#include <time.h> 
#include <sys/ioctl.h> 
#include <unistd.h> 
#include <netinet/in.h> 
#include <string.h> 
#include <netdb.h> 

#define NTP_PORT 123 /*NTP 專用端口号字元串*/ 
#define TIME_PORT 37 /* TIME/UDP 端口号 */ 
#define NTP_SERVER_IP "210.72.145.44" /*國家授時中心 IP*/ 
#define NTP_PORT_STR "123" /*NTP 專用端口号字元串*/ 
#define NTPV1 "NTP/V1" /*協定及其版本号*/ 
#define NTPV2 "NTP/V2" 
#define NTPV3 "NTP/V3" 
#define NTPV4 "NTP/V4" 
#define TIME "TIME/UDP" 
#define NTP_PCK_LEN 48 
#define LI 0 
#define VN 3 
#define MODE 3 
#define STRATUM 0 
#define POLL 4 
#define PREC -6 
#define JAN_1970 0x83aa7e80 /* 1900 年~1970 年之間的時間秒數 */ 
#define NTPFRAC(x) (4294 * (x) + ((1981 * (x)) >> 11)) 
#define USEC(x) (((x) >> 12) - 759 * ((((x) >> 10) + 32768) >> 16)) 

typedef struct _ntp_time 
{ 
    unsigned int coarse; 
    unsigned int fine; 
} ntp_time; 

struct ntp_packet 
{ 
    unsigned char leap_ver_mode; 
    unsigned char startum; 
    char poll; 
    char precision; 
    int root_delay; 
    int root_dispersion; 
    int reference_identifier; 
    ntp_time reference_timestamp; 
    ntp_time originage_timestamp; 
    ntp_time receive_timestamp; 
    ntp_time transmit_timestamp; 
}; 

char protocol[32]; 


/*建構 NTP 協定包*/ 
int construct_packet(char *packet) 
{ 
    char version = 1; 
    long tmp_wrd; 
    int port; 
    time_t timer; 
    strcpy(protocol, NTPV3); 
    /*判斷協定版本*/ 
    if(!strcmp(protocol, NTPV1) || !strcmp(protocol, NTPV2) ||\
       !strcmp(protocol, NTPV3)||!strcmp(protocol, NTPV4)) 
    { 
        memset(packet, 0, NTP_PCK_LEN); 
        port = NTP_PORT; 
        /*設定 16 位元組的標頭*/ 
        version = protocol[6] - 0x30; 
        tmp_wrd = htonl((LI << 30)|(version << 27) 
                        |(MODE << 24)|(STRATUM << 16)|(POLL << 8)|(PREC & 0xff)); 
        memcpy(packet, &tmp_wrd, sizeof(tmp_wrd)); 

        /*設定 Root Delay、Root Dispersion 和 Reference Indentifier */ 
        tmp_wrd = htonl(1<<16); 
        memcpy(&packet[4], &tmp_wrd, sizeof(tmp_wrd)); 
        memcpy(&packet[8], &tmp_wrd, sizeof(tmp_wrd)); 
        /*設定 Timestamp 部分*/ 
        time(&timer); 
        /*設定 Transmit Timestamp coarse*/ 
        tmp_wrd = htonl(JAN_1970 + (long)timer); 
        memcpy(&packet[40], &tmp_wrd, sizeof(tmp_wrd)); 
        /*設定 Transmit Timestamp fine*/ 
        tmp_wrd = htonl((long)NTPFRAC(timer)); 
        memcpy(&packet[44], &tmp_wrd, sizeof(tmp_wrd)); 
        return NTP_PCK_LEN; 
    } 
    else if (!strcmp(protocol, TIME))/* "TIME/UDP" */ 
    { 
        port = TIME_PORT; 
        memset(packet, 0, 4); 
        return 4; 
    } 
    return 0; 
} 


/*擷取 NTP 時間*/ 
int get_ntp_time(int sk, struct addrinfo *addr, struct ntp_packet *ret_time) 
{ 
    fd_set pending_data; 
    struct timeval block_time; 
    char data[NTP_PCK_LEN * 8]; 
    int packet_len, data_len = addr->ai_addrlen, count = 0, result, i, re; 
    if (!(packet_len = construct_packet(data))) 
    { 
        return 0; 
    } 
    /*用戶端給伺服器端發送 NTP 協定資料包*/ 
    if ((result = sendto(sk, data, 
                         packet_len, 0, addr->ai_addr, data_len)) < 0) 
    { 
        perror("sendto"); 
        return 0; 
    } 

    /*調用 select()函數,并設定逾時時間為 1s*/ 
    FD_ZERO(&pending_data); 
    FD_SET(sk, &pending_data); 
    block_time.tv_sec=10; 
    block_time.tv_usec=0;
    if (select(sk + 1, &pending_data, NULL, NULL, &block_time) > 0) 
    { 
        /*接收伺服器端的資訊*/ 
        if ((count = recvfrom(sk, data, 
                              NTP_PCK_LEN * 8, 0, addr->ai_addr, &data_len)) < 0) 
        { 
            perror("recvfrom"); 
            return 0; 
        } 

        if (protocol == TIME) 
        { 
            memcpy(&ret_time->transmit_timestamp, data, 4); 
            return 1; 
        } 
        else if (count < NTP_PCK_LEN) 
        { 
            return 0; 
        } 
        /* 設定接收 NTP 包的資料結構 */ 
        ret_time->leap_ver_mode = ntohl(data[0]); 
        ret_time->startum = ntohl(data[1]); 
        ret_time->poll = ntohl(data[2]); 
        ret_time->precision = ntohl(data[3]); 
        ret_time->root_delay = ntohl(*(int*)&(data[4])); 
        ret_time->root_dispersion = ntohl(*(int*)&(data[8])); 
        ret_time->reference_identifier = ntohl(*(int*)&(data[12])); 
        ret_time->reference_timestamp.coarse = ntohl (*(int*)&(data[16])); 
        ret_time->reference_timestamp.fine = ntohl(*(int*)&(data[20])); 
        ret_time->originage_timestamp.coarse = ntohl(*(int*)&(data[24])); 
        ret_time->originage_timestamp.fine = ntohl(*(int*)&(data[28])); 
        ret_time->receive_timestamp.coarse = ntohl(*(int*)&(data[32])); 
        ret_time->receive_timestamp.fine = ntohl(*(int*)&(data[36])); 
        ret_time->transmit_timestamp.coarse = ntohl(*(int*)&(data[40])); 
        ret_time->transmit_timestamp.fine = ntohl(*(int*)&(data[44])); 
        return 1; 
    } /* end of if select */ 
    return 0; 
} 


/* 修改本地時間 */ 
int set_local_time(struct ntp_packet * pnew_time_packet) 
{ 
 struct timeval tv; 
 tv.tv_sec = pnew_time_packet->transmit_timestamp.coarse - JAN_1970; 
 tv.tv_usec = USEC(pnew_time_packet->transmit_timestamp.fine); 
 return settimeofday(&tv, NULL); 
} 


int main() 
{ 
    int sockfd, rc; 
    struct addrinfo hints, *res = NULL; 
    struct ntp_packet new_time_packet; 
    memset(&hints, 0, sizeof(hints)); 
    hints.ai_family = AF_UNSPEC; 
    hints.ai_socktype = SOCK_DGRAM; 
    hints.ai_protocol = IPPROTO_UDP; 
    /*調用 getaddrinfo()函數,擷取位址資訊*/ 
    rc = getaddrinfo(NTP_SERVER_IP, NTP_PORT_STR, &hints, &res);
    if (rc != 0) 
    { 
        perror("getaddrinfo"); 
        return 1; 
    } 
    /* 建立套接字 */ 
    sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
    if (sockfd <0 ) 
    { 
        perror("socket"); 
        return 1; 
    } 
    /*調用取得 NTP 時間的函數*/ 
    if (get_ntp_time(sockfd, res, &new_time_packet))
    { 
        /*調整本地時間*/ 
        if (!set_local_time(&new_time_packet)) 
        { 
            printf("NTP client success!\n"); 
        } 
    } 
    close(sockfd); 
    return 0; 
} 

           
  • 注意:因為更改系統時間需要根使用者權限,是以在運作時需要在前面添加sudo
  • 為了更好地觀察程式的效果,先用 date 指令修改一下系統時間,再運作執行個體程式。運作完了之後再檢視系 統時間,可以發現已經恢複準确的系統時間了。具體運作結果如下所示:
    [email protected]:~/letcode/socket$ date			//原來系統時間
    Sun Feb 12 22:32:58 CST 2023
    [email protected]:~/letcode/socket$ date			//故意更改後的系統時間
    Mon Feb 13 22:33:48 CST 2023
    [email protected]:~/letcode/socket$ sudo ./ntp   //運作程式,擷取并重設系統時間
    [sudo] leon 的密碼: 
    Get time from NTP success!
    NTP client success!
    [email protected]:~/letcode/socket$ date			//驗證系統時間是否正确設定
    Sun Feb 12 22:34:15 CST 2023
               

繼續閱讀