最近接手的一個客戶端項目,需要獲取網絡時間戳進行超時判斷,想到了使用NTP協議來實現。
在網上參看了不少博文,大多數人提供的C/C++代碼過於雜亂,不宜在正式項目中使用(拒絕野代碼)。
在此我重寫了一遍,並在 Windows 與 Linux 兩大平台上測試通過。
NTP 工作原理
NTP的基本工作原理如圖所示(Device A 相當於客戶端,Device B 相當於 NTP 服務端)。Device A 和 Device B 通過網絡相連,它們都有自己獨立的系統時鍾,需要通過NTP實現各自系統時鍾的自動同步。為便於理解,作如下假設:
- 在Device A和Device B的系統時鍾同步之前,Device A的時鍾設定為10:00:00am,Device B的時鍾設定為11:00:00am。
- Device B作為NTP時間服務器,即Device A將使自己的時鍾與Device B的時鍾同步。
- NTP報文在Device A和Device B之間單向傳輸所需要的時間為1秒。
圖 NTP基本原理圖
獲取網絡時間戳的工作過程如下:
- Device A發送一個NTP報文給Device B,該報文帶有它離開Device A時的時間戳,該時間戳為10:00:00am(T1)。
- 當此NTP報文到達Device B時,Device B加上自己的時間戳,該時間戳為11:00:01am(T2)。
- 當此NTP報文離開Device B時,Device B再加上自己的時間戳,該時間戳為11:00:02am(T3)。
- 當Device A接收到該響應報文時,Device A的本地時間為10:00:03am(T4)。
至此,Device A已經擁有足夠的信息來計算兩個重要的參數:
- NTP報文的往返時延 Delay =(T4 - T1) - (T3 - T2) = 2秒。
- Device A 相對於 Device B 的時間差 Offset = ((T2 - T1) + (T3 - T4)) / 2 = 1小時。
- Device A 同步到 Device B 的時間戳 T = T4 + ((T2 - T1) + (T3 - T4)) / 2 。
這樣,Device A就能夠根據這些信息來設定自己的時鍾,使之與Device B的時鍾同步。
以上內容只是對NTP工作原理的一個粗略描述,詳細內容請參閱RFC 1305。
NTP 報文格式
NTP有兩種不同類型的報文,一種是時鍾同步報文,另一種是控制報文。控制報文僅用於需要網絡管理的場合,它對於時鍾同步功能來說並不是必需的,這里不做介紹。時鍾同步報文封裝在UDP報文中,其格式如圖所示。
圖 時鍾同步報文格式
主要字段的解釋如下:
- LI(Leap Indicator,閏秒提示):長度為2比特,值為“11”時表示告警狀態,時鍾未被同步。為其他值時NTP本身不做處理。
- VN(Version Number,版本號):長度為3比特,表示NTP的版本號,目前的最新版本為4。
- Mode:長度為3比特,表示NTP的工作模式。不同的值所表示的含義分別是:
0 未定義;
1 表示主動對等體模式;
2 表示被動對等體模式;
3 表示客戶模式;
4 表示服務器模式;
5 表示廣播模式或組播模式;
6 表示此報文為NTP控制報文;
7 預留給內部使用。
- Stratum:系統時鍾的層數,取值范圍為1~16,它定義了時鍾的准確度。層數為1的時鍾准確度最高,准確度從1到16依次遞減,層數為16的時鍾處於未同步狀態。
- Poll:輪詢時間,即兩個連續NTP報文之間的時間間隔。
- Precision:系統時鍾的精度。
- Root Delay:本地到主參考時鍾源的往返時間。
- Root Dispersion:系統時鍾相對於主參考時鍾的最大誤差。
- Reference Identifier:參考時鍾源的標識。
- Reference Timestamp:系統時鍾最后一次被設定或更新的時間。
- Originate Timestamp:NTP請求報文離開發送端時發送端的本地時間。
- Receive Timestamp:NTP請求報文到達接收端時接收端的本地時間。
- Transmit Timestamp:應答報文離開應答者時應答者的本地時間。
- Authenticator:驗證信息。
常用的 NTP 服務器
1.cn.pool.ntp.org
2.cn.pool.ntp.org
3.cn.pool.ntp.org
0.cn.pool.ntp.org
cn.pool.ntp.org
tw.pool.ntp.org
0.tw.pool.ntp.org
1.tw.pool.ntp.org
2.tw.pool.ntp.org
3.tw.pool.ntp.org
pool.ntp.org
time.windows.com
time.nist.gov
time-nw.nist.gov
asia.pool.ntp.org
europe.pool.ntp.org
oceania.pool.ntp.org
north-america.pool.ntp.org
south-america.pool.ntp.org
africa.pool.ntp.org
ca.pool.ntp.org
uk.pool.ntp.org
us.pool.ntp.org
au.pool.ntp.org
C/C++代碼實現的主要流程
- NTP通信相關的數據結構體
/**
* @struct x_ntp_timestamp_t
* @brief NTP 時間戳。
*/
typedef struct x_ntp_timestamp_t
{
x_uint32_t xut_seconds; ///< 從 1900年至今所經過的秒數
x_uint32_t xut_fraction; ///< 小數部份,單位是微秒數的4294.967296( = 2^32 / 10^6 )倍
} x_ntp_timestamp_t;
/**
* @enum em_ntp_mode_t
* @brief NTP工作模式的相關枚舉值。
*/
typedef enum em_ntp_mode_t
{
ntp_mode_unknow = 0, ///< 未定義
ntp_mode_initiative = 1, ///< 主動對等體模式
ntp_mode_passive = 2, ///< 被動對等體模式
ntp_mode_client = 3, ///< 客戶端模式
ntp_mode_server = 4, ///< 服務器模式
ntp_mode_broadcast = 5, ///< 廣播模式或組播模式
ntp_mode_control = 6, ///< 報文為 NTP 控制報文
ntp_mode_reserved = 7, ///< 預留給內部使用
} em_ntp_mode_t;
/**
* @struct x_ntp_packet_t
* @brief NTP 報文格式。
*/
typedef struct x_ntp_packet_t
{
x_uchar_t xct_li_ver_mode; ///< 2 bits,飛躍指示器;3 bits,版本號;3 bits,NTP工作 模式(參看 em_ntp_mode_t 相關枚舉值)
x_uchar_t xct_stratum ; ///< 系統時鍾的層數,取值范圍為1~16,它定義了時鍾的准確 度。層數為1的時鍾准確度最高,准確度從1到16依次遞減,層數為16的時鍾處於未同步狀態,不能作為參考時鍾
x_uchar_t xct_poll ; ///< 輪詢時間,即兩個連續NTP報文之間的時間間隔
x_uchar_t xct_percision ; ///< 系統時鍾的精度
x_uint32_t xut_root_delay ; ///< 本地到主參考時鍾源的往返時間
x_uint32_t xut_root_dispersion; ///< 系統時鍾相對於主參考時鍾的最大誤差
x_uint32_t xut_ref_indentifier; ///< 參考時鍾源的標識
x_ntp_timestamp_t xtmst_reference; ///< 系統時鍾最后一次被設定或更新的時間
x_ntp_timestamp_t xtmst_originate; ///< NTP請求報文離開發送端時發送端的本地時間
x_ntp_timestamp_t xtmst_receive ; ///< NTP請求報文到達接收端時接收端的本地時間
x_ntp_timestamp_t xtmst_transmit ; ///< 應答報文離開應答者時應答者的本地時間
} x_ntp_packet_t;
- NTP請求的操作流程
/**********************************************************/
/**
* @brief 向 NTP 服務器發送 NTP 請求,獲取相關計算所需的時間戳(T1、T2、T3、T4如下所訴)。
* <pre>
* 1. 客戶端 發送一個NTP報文給 服務端,該報文帶有它離開 客戶端 時的時間戳,該時間戳為 T1。
* 2. 當此NTP報文到達 服務端 時,服務端 加上自己的時間戳,該時間戳為 T2。
* 3. 當此NTP報文離開 服務端 時,服務端 再加上自己的時間戳,該時間戳為 T3。
* 4. 當 客戶端 接收到該應答報文時,客戶端 的本地時間戳,該時間戳為 T4。
* </pre>
*
* @param [in ] xszt_host : NTP 服務器的 IP(四段式 IP 地址)。
* @param [in ] xut_port : NTP 服務器的 端口號(可取默認的端口號 NTP_PORT : 123)。
* @param [in ] xut_tmout : 超時時間(單位 毫秒)。
* @param [out] xit_tmlst : 操作成功返回的相關計算所需的時間戳(T1、T2、T3、T4)。
*
* @return x_int32_t
* - 成功,返回 0;
* - 失敗,返回 錯誤碼。
*/
static x_int32_t ntp_get_time_values(x_cstring_t xszt_host, x_uint16_t xut_port, x_uint32_t xut_tmout, x_int64_t xit_tmlst[4])
{
x_int32_t xit_err = -1;
x_sockfd_t xfdt_sockfd = X_INVALID_SOCKFD;
x_ntp_packet_t xnpt_buffer;
x_ntp_timeval_t xtm_value;
x_int32_t xit_addrlen = sizeof(struct sockaddr_in);
struct sockaddr_in skaddr_host;
do
{
//======================================
if ((X_NULL == xszt_host) || (xut_tmout <= 0) || (X_NULL == xit_tmlst))
{
break;
}
//======================================
xfdt_sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (X_INVALID_SOCKFD == xfdt_sockfd)
{
break;
}
// 設置 發送/接收 超時時間
#ifdef _WIN32
setsockopt(xfdt_sockfd, SOL_SOCKET, SO_SNDTIMEO, (x_char_t *)&xut_tmout, sizeof(x_uint32_t));
setsockopt(xfdt_sockfd, SOL_SOCKET, SO_RCVTIMEO, (x_char_t *)&xut_tmout, sizeof(x_uint32_t));
#else // !_WIN32
xtm_value.tv_sec = (x_long_t)((xut_tmout / 1000));
xtm_value.tv_usec = (x_long_t)((xut_tmout % 1000) * 1000);
setsockopt(xfdt_sockfd, SOL_SOCKET, SO_SNDTIMEO, (x_char_t *)&xtm_value, sizeof(x_ntp_timeval_t));
setsockopt(xfdt_sockfd, SOL_SOCKET, SO_RCVTIMEO, (x_char_t *)&xtm_value, sizeof(x_ntp_timeval_t));
#endif // _WIN32
// 服務端主機地址
memset(&skaddr_host, 0, sizeof(struct sockaddr_in));
skaddr_host.sin_family = AF_INET;
skaddr_host.sin_port = htons(xut_port);
inet_pton(AF_INET, xszt_host, &skaddr_host.sin_addr.s_addr);
//======================================
// 初始化請求數據包
ntp_init_request_packet(&xnpt_buffer);
// NTP請求報文離開發送端時發送端的本地時間
ntp_gettimeofday(&xtm_value);
ntp_timeval_to_timestamp(&xnpt_buffer.xtmst_originate, &xtm_value);
// T1
xit_tmlst[0] = (x_int64_t)ntp_timeval_ns100(&xtm_value);
// 轉成網絡字節序
ntp_hton_packet(&xnpt_buffer);
// 投遞請求
xit_err = sendto(xfdt_sockfd,
(x_char_t *)&xnpt_buffer,
sizeof(x_ntp_packet_t),
0,
(sockaddr *)&skaddr_host,
sizeof(struct sockaddr_in));
if (xit_err < 0)
{
xit_err = ntp_sockfd_lasterror();
continue;
}
//======================================
memset(&xnpt_buffer, 0, sizeof(x_ntp_packet_t));
// 接收應答
xit_err = recvfrom(xfdt_sockfd,
(x_char_t *)&xnpt_buffer,
sizeof(x_ntp_packet_t),
0,
(sockaddr *)&skaddr_host,
(socklen_t *)&xit_addrlen);
if (xit_err < 0)
{
xit_err = ntp_sockfd_lasterror();
break;
}
if (sizeof(x_ntp_packet_t) != xit_err)
{
xit_err = -1;
break;
}
// T4
xit_tmlst[3] = (x_int64_t)ntp_gettimevalue();
// 轉成主機字節序
ntp_ntoh_packet(&xnpt_buffer);
xit_tmlst[1] = (x_int64_t)ntp_timestamp_ns100(&xnpt_buffer.xtmst_receive ); // T2
xit_tmlst[2] = (x_int64_t)ntp_timestamp_ns100(&xnpt_buffer.xtmst_transmit); // T3
//======================================
xit_err = 0;
} while (0);
if (X_INVALID_SOCKFD != xfdt_sockfd)
{
ntp_sockfd_close(xfdt_sockfd);
xfdt_sockfd = X_INVALID_SOCKFD;
}
return xit_err;
}
- 項目中實際使用到的接口
/**********************************************************/
/**
* @brief 向 NTP 服務器發送 NTP 請求,獲取服務器時間戳。
*
* @param [in ] xszt_host : NTP 服務器的 IP(四段式 IP 地址) 或 域名(如 3.cn.pool.ntp.org)。
* @param [in ] xut_port : NTP 服務器的 端口號(可取默認的端口號 NTP_PORT : 123)。
* @param [in ] xut_tmout : 網絡請求的超時時間(單位為毫秒)。
* @param [out] xut_timev : 操作成功返回的應答時間值(以 100納秒 為單位,1970年1月1日到現在的時間)。
*
* @return x_int32_t
* - 成功,返回 0;
* - 失敗,返回 錯誤碼。
*/
x_int32_t ntp_get_time(x_cstring_t xszt_host, x_uint16_t xut_port, x_uint32_t xut_tmout, x_uint64_t * xut_timev);
其中,返回的時間戳值 xut_timev
由計算公式 T = T4 + ((T2 - T1) + (T3 - T4)) / 2;
所得。若要將該時間戳值轉換為其他易描述(或實際應用)的數據信息,調用 ntp_tmctxt_bv(xut_timev, &xtm_context)
接口即可,詳細說明參考如下所列代碼:
// VxNtpHelper.h
/**
* @struct x_ntp_time_context_t
* @brief 時間描述信息結構體。
*/
typedef struct x_ntp_time_context_t
{
x_uint32_t xut_year : 16; ///< 年
x_uint32_t xut_month : 6; ///< 月
x_uint32_t xut_day : 6; ///< 日
x_uint32_t xut_week : 4; ///< 周幾
x_uint32_t xut_hour : 6; ///< 時
x_uint32_t xut_minute : 6; ///< 分
x_uint32_t xut_second : 6; ///< 秒
x_uint32_t xut_msec : 14; ///< 毫秒
} x_ntp_time_context_t;
/**********************************************************/
/**
* @brief 轉換(以 100納秒 為單位的)時間值(1970年1月1日到現在的時間)
* 為具體的時間描述信息(即 x_ntp_time_context_t)。
*
* @param [in ] xut_time : 時間值(1970年1月1日到現在的時間)。
* @param [out] xtm_context : 操作成功返回的時間描述信息。
*
* @return x_bool_t
* - 成功,返回 X_TRUE;
* - 失敗,返回 X_FALSE。
*/
x_bool_t ntp_tmctxt_bv(x_uint64_t xut_time, x_ntp_time_context_t * xtm_context);
也可通過如下方式轉換為 timeval 信息:
struct timeval tv;
tv.tv_sec = (long)((xut_timev / 10000000LL));
tv.tv_usec = (long)((xut_timev % 10000000LL) / 10);
源碼下載
至Github下載:https://github.com/Gaaagaa/ntp_client
參看資料
- NTP工作原理:http://ntp.neu.edu.cn/archives/92
- NTP的報文格式:http://ntp.neu.edu.cn/archives/95