创建时间: 2017-09-11
最后修改时间: 2017-09-11因个人水平有限,文章中存在不足,错误之处,还望指正
实验环境
Linux 2.6.32
gcc version 4.4.6 20120305 (Red Hat 4.4.6-4) (GCC)
本NTP客户端实现是基于NTPv3单播模式来实现的,其中参考了SNTP的实现(SNTP为NTP的简化版)。
要完成客户端的开发需要准备一些知识,比如NTP工作模式,NTP报文格式等等,大体了解完这些后才能更好地掌握整个开发过程。
NTP(Network Time Protocol),网络时间协议,应用于分布式时间服务器和客户端之间,实现客户端和服务器之间的时间同步,从而使网络内所有设备的时间基本保持一致。NTP工作于UDP的123端口。
0 2 5 8 16 24 32
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|LI | VN |Mode | Stratum | Poll | Precision |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Root Delay |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Root Dispersion |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reference Identifier |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Reference Timestamp (64) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Originate Timestamp (64) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Receive Timestamp (64) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Transmit Timestamp (64) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Authentication (optional) (64) |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
NTPv3报文格式
Leap Indicator(LI)
闰秒指示符,这是一个2位的代码,用于警示在当天的最后一分钟里插入或删除的闰秒。取值如下:
0 无预告
1 最近一分钟有61秒
2 最近一分钟有59秒
3 警告状态(时钟未同步)
Version Number(VN)
版本号,这是一个3位的整数,用于表示NTP的版本。
Mode
模式,这是一个3位的整数,表示模式,值定义如下:
0 保留
1 对称主动
2 对称被动
3 客户端
4 服务器端
5 广播
6 为NTP控制控制消息
7 为自用保留
Stratum
本地时钟层级,这是一个八位无符号整数,表示本地时钟的层级,其值定义如下:
0 未定义或难以获得
1 主要参考(如无线电时钟钟,校正的院子时钟)
2-255 第二参考(通过NTP或SNTP)
Poll
轮询间隔,这是一个8位有符号整数,用于表示连续消息之间的最大间隔,以最接近2的N次幂来表示。如值为6表示2^6=64。
Precision
本地时钟精度精度,这是一个8位有符号整数,用于表示本地时钟精度,以最接近2的N次幂来表示。
Root Delay
这是一个32位有符号定点数,表示主要参考源的总往返时延,以秒为单位。该变量可以为正值和负值,具体取决于时间精度和偏移。
Root Dispersion
这是一个32位有符号定点数,表示相对于主参考源的最大误差,以秒为单位,在15和16位之间。通常在该字段中出现的值范围为0到几百毫秒
Reference Identifier
这是一个标识特定参考源的32位位串。在NTP版本3或版本4层级0或层级1服务器的情况下,这是一个4字符ASCII字符串,左对齐并且以0填充到32位。在NTP版本3辅助服务器中,这是参考源的32位IPv4地址。
Reference Timestamp
这是以64位时间戳格式表示的上次设置或更正的本地时钟时间。
Original Timestamp
这是以64位时间戳格式表示的请求离开客户端的时间。
Receive Timestamp
这是以64位时间戳格式表示的请求到达服务器端的时间。
Transmit Timestamp
这是以64位时间戳格式表示的应答离开服务器端的时间。
Authentication
认证信息。
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Integer Part |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Fraction Part |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
NTP时间戳格式
NTP时间戳使用的是自1970-01-01所经过的秒数(单位为秒),它分为整数部分和小数部分。NTP时间戳整数部分与ICMP时间戳消息所使用的时间戳格式整数部分一致,但小数部分却是不同的。
在64位的NTP时间戳中,前32位为整数部分,后32位为小数部分,其转换如下:
frac * 1e6 / (2<<32)
= frac / 4294.967296
根据以上转换规则可得NTP时间戳所能表示的最小精度为1 / 4294.967,296 = 0.2328307e-9,约等为0.232纳秒。
在32位的NTP时间戳中,前16位表示整数部分,后16位为小数部分,其转换与上面的类似:
frac * 1e6 / (2<<16)
= frac * 15.2587890625
关于往返时延和本地时钟偏移的计算
为了计算相对于服务器的往返时延d和本地时钟偏移t,客户端根据客户端时钟设置请求中的发送时间戳。服务器将该字段复制到应答中的起始时间戳(Originate Timestamp),并根据服务器时钟设置接收时间戳(Receive Timestamp)和传送时间戳(Transmit Timestamp)。
当接收到服务器应答时,客户端根据NTP时间戳格式的时钟确定目的时间戳变量为到达时间。以下总结了四个时间戳:
Originate Timestamp T1 客户端发送时间请求的时间
Receive Timestamp T2 服务器收到时间请求的时间
Transmit Timestamp T3 服务器发送时间回复的时间
Destination Timestamp T4 客户端收到时间回复的时间
往返时延d和本地时钟偏移t定义为:
d = (T4 - T1) - (T2 - T3)
t = ((T2 - T1) + (T3 - T4)) / 2
为了方便对NTP报文进行操作,自定义了ntphdr结构体,如下:
/* 32位时间戳 */
struct s_fixedpt {
uint16_t intpart;
uint16_t fracpart;
};
/* 64位时间戳 */
struct l_fixedpt {
uint32_t intpart;
uint32_t fracpart;
};
struct ntphdr {
#if __BYTE_ORDER == __BID_ENDIAN
unsigned int ntp_li:2;
unsigned int ntp_vn:3;
unsigned int ntp_mode:3;
#endif
#if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned int ntp_mode:3;
unsigned int ntp_vn:3;
unsigned int ntp_li:2;
#endif
uint8_t ntp_stratum;
uint8_t ntp_poll;
int8_t ntp_precision;
struct s_fixedpt ntp_rtdelay;
struct s_fixedpt ntp_rtdispersion;
uint32_t ntp_refid;
struct l_fixedpt ntp_refts;
struct l_fixedpt ntp_orits;
struct l_fixedpt ntp_recvts;
struct l_fixedpt ntp_transts;
};
ntphdr结构体定义比较特殊,由于按位定义了字段,所以需要解决字节序的问题。可以使用宏__BYTE_ORDER(在头文件
为了便于对NTP时间戳的操作,定义了以下宏:
/*
* 自1900-01-01到1970-01-01所经过的秒数
*/
#define JAN_1970 0x83aa7e80
/*
* 用于64位NTP时间戳,即小数部分为32位
* NTP_CONV_FRAC32(x)将x转换为NTP时间戳中的小数部分值;
* NTP_REVE_FRAC32(x)则相反,将NTP时间戳小数部分值x解析成具体值。
*/
#define NTP_CONV_FRAC32(x) (uint64_t) ((x) * ((uint64_t)1<<32))
#define NTP_REVE_FRAC32(x) ((double) ((double) (x) / ((uint64_t)1<<32)))
/*
* 用于32位NTP时间戳,即小数部分为16位
* NTP_CONV_FRAC16(x)将x转换为NTP时间戳中的小数部分值;
* NTP_REVE_FRAC16(x)则相反,将NTP时间戳小数部分值x解析成具体值。
*/
#define NTP_CONV_FRAC16(x) (uint32_t) ((x) * ((uint32_t)1<<16))
#define NTP_REVE_FRAC16(x) ((double)((double) (x) / ((uint32_t)1<<16)))
/*
* timeval结构中tv_usec字段和NTP时间戳小数部分互转
*/
#define USEC2FRAC(x) ((uint32_t) NTP_CONV_FRAC32( (x) / 1000000.0 ))
#define FRAC2USEC(x) ((uint32_t) NTP_REVE_FRAC32( (x) * 1000000.0 ))
/*
* 将l_fixedpt结构(NTP64位时间戳)换算成自1970-01-01所经过的秒数。
* 该l_fixedpt结构的字段均为网络字节序。
*/
#define NTP_LFIXED2DOUBLE(x) ((double) ( ntohl(((struct l_fixedpt *) (x))->intpart) - JAN_1970 + FRAC2USEC(ntohl(((struct l_fixedpt *) (x))->fracpart)) / 1000000.0 ))
上面的宏定义可以简化,但是为了能更明白其中的所以然,这里就保留原始的过程不做简化处理。
函数主体:
int main(int argc, char *argv[])
{
char buf[BUFSIZE];
size_t nbytes;
int sockfd, maxfd1;
struct sockaddr_in servaddr;
fd_set readfds;
struct timeval timeout, recvtv, tv;
double offset;
if (argc != 2) {
usage();
exit(-1);
}
//构建服务器地址套接字结构
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(NTP_PORT);
servaddr.sin_addr.s_addr = inet_host(argv[1]);
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket error");
exit(-1);
}
if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(struct sockaddr)) != 0) {
perror("connect error");
exit(-1);
}
//构建并发送NTP请求报文
nbytes = BUFSIZE;
if (get_ntp_packet(buf, &nbytes) != 0) {
fprintf(stderr, "construct ntp request error \n");
exit(-1);
}
send(sockfd, buf, nbytes, 0);
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
maxfd1 = sockfd + 1;
//设置select超时时间
timeout.tv_sec = TIMEOUT;
timeout.tv_usec = 0;
if (select(maxfd1, &readfds, NULL, NULL, &timeout) > 0) {
if (FD_ISSET(sockfd, &readfds)) {
if ((nbytes = recv(sockfd, buf, BUFSIZE, 0)) < 0) {
perror("recv error");
exit(-1);
}
//计算客户端时间与服务器端时间偏移量
gettimeofday(&recvtv, NULL);
offset = get_offset((struct ntphdr *) buf, &recvtv);
//更新系统时间
gettimeofday(&tv, NULL);
tv.tv_sec += (int) offset;
tv.tv_usec += offset - (int) offset;
if (settimeofday(&tv, NULL) != 0) {
perror("settimeofday error");
exit(-1);
}
printf("%s \n", ctime((time_t *) &tv.tv_sec));
}
}
//操作完成后别玩了关闭套接字
close(sockfd);
return 0;
}
NTP请求报文构建函数:
/*
* 构建一个NTP请求报文
* c参数buf指向存放NTP报文的缓冲区;
* size为值-结果参数,传入是为缓冲区长度,返回时为NTP报文长度
*/
int get_ntp_packet(void *buf, size_t *size)
{
struct ntphdr *ntp;
struct timeval tv;
if (!size || *sizereturn -1;
memset(buf, 0, *size);
ntp = (struct ntphdr *) buf;
ntp->ntp_li = NTP_LI;
ntp->ntp_vn = NTP_VN;
ntp->ntp_mode = NTP_MODE;
ntp->ntp_stratum = NTP_STRATUM;
ntp->ntp_poll = NTP_POLL;
ntp->ntp_precision = NTP_PRECISION;
gettimeofday(&tv, NULL);
ntp->ntp_transts.intpart = htonl(tv.tv_sec + JAN_1970);
ntp->ntp_transts.fracpart = htonl(USEC2FRAC(tv.tv_usec));
*size = NTP_HLEN;
return 0;
}
往返时延和时间偏移量计算函数:
/*
* 获取客户端与服务器之间的往返时延
* 参数ntp指向服务器应答所在的缓冲区;
* 参数recvtv指向收到服务器应答的本地时间;
*/
double get_rrt(const struct ntphdr *ntp, const struct timeval *recvtv)
{
double t1, t2, t3, t4;
t1 = NTP_LFIXED2DOUBLE(&ntp->ntp_orits);
t2 = NTP_LFIXED2DOUBLE(&ntp->ntp_recvts);
t3 = NTP_LFIXED2DOUBLE(&ntp->ntp_transts);
t4 = recvtv->tv_sec + recvtv->tv_usec / 1000000.0;
return (t4 - t1) - (t3 - t2);
}
/*
* 获取客户端与服务器的时间偏移量
* 参数ntp指向服务器应答所在的缓冲区;
* 参数recvtv指向收到服务器应答的本地时间;
*/
double get_offset(const struct ntphdr *ntp, const struct timeval *recvtv)
{
double t1, t2, t3, t4;
t1 = NTP_LFIXED2DOUBLE(&ntp->ntp_orits);
t2 = NTP_LFIXED2DOUBLE(&ntp->ntp_recvts);
t3 = NTP_LFIXED2DOUBLE(&ntp->ntp_transts);
t4 = recvtv->tv_sec + recvtv->tv_usec / 1000000.0;
return ((t2 - t1) + (t3 - t4)) / 2;
}
/* ntpclient.c */
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define VERSION_3 3
#define VERSION_4 4
#define MODE_CLIENT 3
#define MODE_SERVER 4
#define NTP_LI 0
#define NTP_VN VERSION_3
#define NTP_MODE MODE_CLIENT
#define NTP_STRATUM 0
#define NTP_POLL 4
#define NTP_PRECISION -6
#define NTP_HLEN 48
#define NTP_PORT 123
#define NTP_SERVER "182.92.12.11"
#define TIMEOUT 10
#define BUFSIZE 1500
#define JAN_1970 0x83aa7e80
#define NTP_CONV_FRAC32(x) (uint64_t) ((x) * ((uint64_t)1<<32))
#define NTP_REVE_FRAC32(x) ((double) ((double) (x) / ((uint64_t)1<<32)))
#define NTP_CONV_FRAC16(x) (uint32_t) ((x) * ((uint32_t)1<<16))
#define NTP_REVE_FRAC16(x) ((double)((double) (x) / ((uint32_t)1<<16)))
#define USEC2FRAC(x) ((uint32_t) NTP_CONV_FRAC32( (x) / 1000000.0 ))
#define FRAC2USEC(x) ((uint32_t) NTP_REVE_FRAC32( (x) * 1000000.0 ))
#define NTP_LFIXED2DOUBLE(x) ((double) ( ntohl(((struct l_fixedpt *) (x))->intpart) - JAN_1970 + FRAC2USEC(ntohl(((struct l_fixedpt *) (x))->fracpart)) / 1000000.0 ))
struct s_fixedpt {
uint16_t intpart;
uint16_t fracpart;
};
struct l_fixedpt {
uint32_t intpart;
uint32_t fracpart;
};
struct ntphdr {
#if __BYTE_ORDER == __BID_ENDIAN
unsigned int ntp_li:2;
unsigned int ntp_vn:3;
unsigned int ntp_mode:3;
#endif
#if __BYTE_ORDER == __LITTLE_ENDIAN
unsigned int ntp_mode:3;
unsigned int ntp_vn:3;
unsigned int ntp_li:2;
#endif
uint8_t ntp_stratum;
uint8_t ntp_poll;
int8_t ntp_precision;
struct s_fixedpt ntp_rtdelay;
struct s_fixedpt ntp_rtdispersion;
uint32_t ntp_refid;
struct l_fixedpt ntp_refts;
struct l_fixedpt ntp_orits;
struct l_fixedpt ntp_recvts;
struct l_fixedpt ntp_transts;
};
in_addr_t inet_host(const char *host)
{
in_addr_t saddr;
struct hostent *hostent;
if ((saddr = inet_addr(host)) == INADDR_NONE) {
if ((hostent = gethostbyname(host)) == NULL)
return INADDR_NONE;
memmove(&saddr, hostent->h_addr, hostent->h_length);
}
return saddr;
}
int get_ntp_packet(void *buf, size_t *size)
{
struct ntphdr *ntp;
struct timeval tv;
if (!size || *sizereturn -1;
memset(buf, 0, *size);
ntp = (struct ntphdr *) buf;
ntp->ntp_li = NTP_LI;
ntp->ntp_vn = NTP_VN;
ntp->ntp_mode = NTP_MODE;
ntp->ntp_stratum = NTP_STRATUM;
ntp->ntp_poll = NTP_POLL;
ntp->ntp_precision = NTP_PRECISION;
gettimeofday(&tv, NULL);
ntp->ntp_transts.intpart = htonl(tv.tv_sec + JAN_1970);
ntp->ntp_transts.fracpart = htonl(USEC2FRAC(tv.tv_usec));
*size = NTP_HLEN;
return 0;
}
void print_ntp(struct ntphdr *ntp)
{
time_t time;
printf("LI:\t%d \n", ntp->ntp_li);
printf("VN:\t%d \n", ntp->ntp_vn);
printf("Mode:\t%d \n", ntp->ntp_mode);
printf("Stratum:\t%d \n", ntp->ntp_stratum);
printf("Poll:\t%d \n", ntp->ntp_poll);
printf("precision:\t%d \n", ntp->ntp_precision);
printf("Route delay:\t %lf \n",
ntohs(ntp->ntp_rtdelay.intpart) + NTP_REVE_FRAC16(ntohs(ntp->ntp_rtdelay.fracpart)));
printf("Route Dispersion:\t%lf \n",
ntohs(ntp->ntp_rtdispersion.intpart) + NTP_REVE_FRAC16(ntohs(ntp->ntp_rtdispersion.fracpart)));
printf("Referencd ID:\t %d \n", ntohl(ntp->ntp_refid));
time = ntohl(ntp->ntp_refts.intpart) - JAN_1970;
printf("Reference:\t%d %ld %s \n",
ntohl(ntp->ntp_refts.intpart) - JAN_1970,
FRAC2USEC(ntohl(ntp->ntp_refts.fracpart)),
ctime(&time));
time = ntohl(ntp->ntp_orits.intpart) - JAN_1970;
printf("Originate:\t%d %d frac=%ld (%s) \n",
ntohl(ntp->ntp_orits.intpart) - JAN_1970,
FRAC2USEC(ntohl(ntp->ntp_orits.fracpart)),
ntohl(ntp->ntp_orits.fracpart),
ctime(&time) );
time = ntohl(ntp->ntp_recvts.intpart) - JAN_1970;
printf("Receive:\t%d %d (%s) \n",
ntohl(ntp->ntp_recvts.intpart) - JAN_1970,
FRAC2USEC(ntohl(ntp->ntp_recvts.fracpart)),
ctime(&time) );
time = ntohl(ntp->ntp_transts.intpart) - JAN_1970;
printf("Transmit:\t%d %d (%s) \n",
ntohl(ntp->ntp_transts.intpart) - JAN_1970,
FRAC2USEC(ntohl(ntp->ntp_transts.fracpart)),
ctime(&time) );
}
double get_rrt(const struct ntphdr *ntp, const struct timeval *recvtv)
{
double t1, t2, t3, t4;
t1 = NTP_LFIXED2DOUBLE(&ntp->ntp_orits);
t2 = NTP_LFIXED2DOUBLE(&ntp->ntp_recvts);
t3 = NTP_LFIXED2DOUBLE(&ntp->ntp_transts);
t4 = recvtv->tv_sec + recvtv->tv_usec / 1000000.0;
return (t4 - t1) - (t3 - t2);
}
double get_offset(const struct ntphdr *ntp, const struct timeval *recvtv)
{
double t1, t2, t3, t4;
t1 = NTP_LFIXED2DOUBLE(&ntp->ntp_orits);
t2 = NTP_LFIXED2DOUBLE(&ntp->ntp_recvts);
t3 = NTP_LFIXED2DOUBLE(&ntp->ntp_transts);
t4 = recvtv->tv_sec + recvtv->tv_usec / 1000000.0;
return ((t2 - t1) + (t3 - t4)) / 2;
}
void usage(void)
{
fprintf(stderr,
"Usage : ntpclient"
" destination"
"\n"
);
}
int main(int argc, char *argv[])
{
char buf[BUFSIZE];
size_t nbytes;
int sockfd, maxfd1;
struct sockaddr_in servaddr;
fd_set readfds;
struct timeval timeout, recvtv, tv;
double offset;
if (argc != 2) {
usage();
exit(-1);
}
servaddr.sin_family = AF_INET;
servaddr.sin_port = htons(NTP_PORT);
servaddr.sin_addr.s_addr = inet_host(argv[1]);
if ((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0) {
perror("socket error");
exit(-1);
}
if (connect(sockfd, (struct sockaddr *) &servaddr, sizeof(struct sockaddr)) != 0) {
perror("connect error");
exit(-1);
}
nbytes = BUFSIZE;
if (get_ntp_packet(buf, &nbytes) != 0) {
fprintf(stderr, "construct ntp request error \n");
exit(-1);
}
send(sockfd, buf, nbytes, 0);
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
maxfd1 = sockfd + 1;
timeout.tv_sec = TIMEOUT;
timeout.tv_usec = 0;
if (select(maxfd1, &readfds, NULL, NULL, &timeout) > 0) {
if (FD_ISSET(sockfd, &readfds)) {
if ((nbytes = recv(sockfd, buf, BUFSIZE, 0)) < 0) {
perror("recv error");
exit(-1);
}
gettimeofday(&recvtv, NULL);
offset = get_offset((struct ntphdr *) buf, &recvtv);
gettimeofday(&tv, NULL);
tv.tv_sec += (int) offset;
tv.tv_usec += offset - (int) offset;
if (settimeofday(&tv, NULL) != 0) {
perror("settimeofday error");
exit(-1);
}
printf("%s \n", ctime((time_t *) &tv.tv_sec));
}
}
close(sockfd);
return 0;
}
在整个开发过程中遇到了大大小小的问题,故对主要的问题做以下总结:
NTP时间戳是相对于1900-01-01的时间,且小数部分转换与一般时间戳转换不一样,具体为 frac * 1e6 / (2<
按位定义结构体的成员时需注意字节序问题,通过宏__BYTE_ORDER来判断(在头文件
《UNIX网络高级编程 卷1:套接字联网API》
RFC 1305 Network Time Protocol (Version 3) Specification, Implementation and Analysis
RFC 4330 Simple Network Time Protocol (SNTP) Version 4 for IPv4, IPv6 and OSI