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部落格 ” ,連結:http://blog.itpub.net/69969420/viewspace-2771041/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- NTP時間同步伺服器(區域網時鐘同步)解決方案伺服器
- NTP時間同步
- 使用NTP原理進行時間同步
- Linux使用ntp時間伺服器同步時間Linux伺服器
- 時間同步協議NTP - 原理&實踐協議
- 論企業生產網NTP時鐘同步(網路時間同步服務)
- NTP校時(時間同步伺服器)IPC網路攝像機時鐘問題排查伺服器
- Ubuntu的NTP同步時鐘設定方法Ubuntu
- Centos下部署NTP時間伺服器同步環境CentOS伺服器
- CentOS7使用NTP搭建時間同步伺服器CentOS伺服器
- 網路時間協議介紹以及伺服器同步網路時間協議伺服器
- 電力GPS北斗衛星時鐘(NTP時鐘伺服器)同步技術淺談伺服器
- Linux叢集環境下NTP伺服器時間同步Linux伺服器
- LINUX 解決時間同步問題(NTP)Linux
- Ubuntu 時間不準,怎麼設定NTP時間同步?Ubuntu
- NTP網路時間伺服器原理及功能介紹(京準電子)伺服器
- 北斗GPS衛星同步時鐘(NTP時鐘伺服器)在通訊系統中應用伺服器
- Oracle叢集(RAC)時間同步(ntp和CTSS)Oracle
- Windows ntp時間同步設定(bat指令碼)WindowsBAT指令碼
- Linux系統時間同步方法小結(NTP)Linux
- Linux的時鐘及常用的時間同步伺服器地址Linux伺服器
- Linux Centos7 同步伺服器時鐘為北京時間LinuxCentOS伺服器
- ChatDBA | OceanBase NTP 時鐘不同步的問題排查?
- 如何讓主控制域與NTP時間同步伺服器通訊起來伺服器
- 時間伺服器-NTP伺服器
- 新基建如何構造精準時鐘同步(NTP網路授時)體系
- NTP時間同步伺服器(北斗授時裝置)應用農產品安全追溯系統伺服器
- 同步vmware時間
- centos:時間同步CentOS
- chrony時間同步
- chrony 時間同步
- win10時間同步失敗怎麼辦 win10時間自動同步出錯解決方法介紹Win10
- 北斗GPS同步時鐘(授時系統)技術原理詳解
- .NET實現獲取NTP伺服器時間並同步(附帶Windows系統啟用NTP服務功能)伺服器Windows
- Linux ntpdate同步時間Linux
- linux 配置ntp時間伺服器Linux伺服器
- gPTP時間同步(時鐘同步)協議對智慧駕駛車載網路的重要性GPT協議
- 伺服器時間同步引發的"慘案"伺服器