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 個位元組。
協定格式
封包字段
字段名 | 長 度 | 含義 |
---|---|---|
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位址。
實驗流程如下:
- 擷取NTP伺服器位址資訊;
- 建立socket;
- 建構ntp協定包;
- 向NTP伺服器發送請求協定包;
- 從NTP伺服器接收協定包
- 更新目前系統時間;
- 具體代碼如下:
/* 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