前言
NTP(Network Time Protocol)网络时间协议基于UDP,用于网络时间同步的协议,使网络中的计算机时钟同步到UTC,再配合各个时区的偏移调整就能实现精准同步对时功能。提供NTP对时的服务器有很多,比如微软的NTP对时服务器,利用NTP服务器提供的对时功能,可以使我们的设备时钟系统能够正确运行。
NTP报文格式
NTP报文格式如上图所示,它的字段含义参考如下:
- LI 闰秒标识器,占用2个bit
- VN 版本号,占用3个bits,表示NTP的版本号,现在为3
- Mode 模式,占用3个bits,表示模式
- stratum(层),占用8个bits
- Poll 测试间隔,占用8个bits,表示连续信息之间的最大间隔
- Precision 精度,占用8个bits,,表示本地时钟精度
- Root Delay根时延,占用8个bits,表示在主参考源之间往返的总共时延
- Root Dispersion根离散,占用8个bits,表示在主参考源有关的名义错误
- Reference Identifier参考时钟标识符,占用8个bits,用来标识特殊的参考源
- 参考时间戳,64bits时间戳,本地时钟被修改的最新时间。
- 原始时间戳,客户端发送的时间,64bits。
- 接受时间戳,服务端接受到的时间,64bits。
- 传送时间戳,服务端送出应答的时间,64bits。
- 认证符(可选项)
抛开复杂的协议报文,我们来理解一下NTP客户端与服务器的交互过程,进而理解参考时间戳、原始时间戳、接受时间戳、传送时间戳的关系。如图,客户端和服务端都有一个时间轴,分别代表着各自系统的时间,当客户端想要同步服务端的时间时,客户端会构造一个NTP协议包发送到NTP服务端,客户端会记下此时发送的时间t0,经过一段网络延时传输后,服务器在t1时刻收到数据包,经过一段时间处理后在t2时刻向客户端返回数据包,再经过一段网络延时传输后客户端在t3时刻收到NTP服务器数据包。特别声明,t0和t3是客户端时间系统的时间、t1和t2是NTP服务端时间系统的时间,它们是有区别的。对于时间要求不那么精准设备,直接使用NTP服务器返回t2时间也没有太大影响。但是作为一个标准的通信协议,它是精益求精且容不得过多误差的,于是必须计算上网络的传输延时。客户端与服务端的时间系统的偏移定义为θ、网络的往返延迟定义为δ,基于此,可以对t2进行精确的修正,已达到相关精度要求,它们的计算公式如下:
式中:
t0是请求数据包传输的客户端时间戳
t1是请求数据包回复的服务器时间戳
t2是响应数据包传输的服务器时间戳
t3是响应数据包回复的客户端时间戳
对此,我们只需将NTP服务端返回的时间t2加上网络延时δ的一半就可以了(t2+δ/2)。
NTP请求样例
-
#include <sys/types.h>
-
#include <sys/stat.h>
-
#include <sys/ioctl.h>
-
#include <sys/socket.h>
-
#include <sys/wait.h>
-
#include <sys/time.h>
-
#include <netdb.h>
-
#include <netinet/in.h>
-
#include <arpa/inet.h>
-
#include <unistd.h>
-
#include <stdio.h>
-
#include <stdlib.h>
-
#include <string.h>
-
#include <pthread.h>
-
#include <dirent.h>
-
#include <time.h>
-
#include <fcntl.h>
-
#include <errno.h>
-
-
#define debugprintf 1
-
#ifdef debugprintf
-
#define debugpri(mesg, args...) fprintf(stderr, "[NetRate print:%s:%d:] " mesg "\n", __FILE__, __LINE__, ##args)
-
#else
-
#define debugpri(mesg, args...)
-
#endif
-
-
#define JAN_1970 0x83aa7e80
-
#define NTPFRAC(x) (4294 * (x) + ((1981 * (x))>>11))
-
#define USEC(x) (((x) >> 12) - 759 * ((((x) >> 10) + 32768) >> 16))
-
#define Data(i) ntohl(((unsigned int *)data)[i])
-
#define LI 0
-
#define VN 3
-
#define MODE 3
-
#define STRATUM 0
-
#define POLL 4
-
#define PREC -6
-
struct NtpTime
-
{
-
unsigned
int coarse;
-
unsigned
int fine;
-
};
-
-
void sendPacket(int fd)
-
{
-
unsigned
int data[
12];
-
struct timeval now;
-
-
if (
sizeof(data) !=
48)
-
{
-
fprintf(
stderr,
"size error\n");
-
return;
-
}
-
-
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));
-
send(fd, data,
48,
0);
-
}
-
//获取NTP服务器返回的时间
-
void getNewTime(unsigned int *data,struct timeval *ptimeval)
-
{
-
struct NtpTime trantime;
-
trantime.coarse = Data(
10);
-
trantime.fine = Data(
11);
-
-
ptimeval->tv_sec = trantime.coarse - JAN_1970;
-
ptimeval->tv_usec = USEC(trantime.fine);
-
}
-
-
int getNtpTime(struct hostent* phost,struct timeval *ptimeval)
-
{
-
if(phost ==
NULL)
-
{
-
debugpri(
"err:host is null!\n");
-
return
-1;
-
}
-
int sockfd;
-
struct sockaddr_in addr_src,addr_dst;
-
fd_set fds;
-
int ret;
-
int recv_len;
-
unsigned
int buf[
12];
-
memset(buf,
0,
sizeof(buf));
-
int addr_len;
-
int count =
0;
-
-
struct timeval timeout;
-
-
addr_len =
sizeof(struct sockaddr_in);
-
-
memset(&addr_src,
0, addr_len);
-
addr_src.sin_family = AF_INET;
-
addr_src.sin_addr.s_addr = htonl(INADDR_ANY);
-
addr_src.sin_port = htons(
0);
-
-
memset(&addr_dst,
0, addr_len);
-
addr_dst.sin_family = AF_INET;
-
memcpy(&(addr_dst.sin_addr.s_addr), phost->h_addr_list[
0],
4);
-
addr_dst.sin_port = htons(
123);
//ntp默认端口123
-
-
if(
-1==(sockfd = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP)))
//创建UDP socket
-
{
-
debugpri(
"create socket error!\n");
-
return
-1;
-
}
-
-
ret = bind(sockfd, (struct sockaddr*)&addr_src, addr_len);
//bind
-
if(
-1==ret)
-
{
-
debugpri(
"bind error!\n");
-
close(sockfd);
-
return
-1;
-
}
-
-
ret = connect(sockfd, (struct sockaddr*)&addr_dst, addr_len);
//连接NTP服务器
-
if(
-1==ret)
-
{
-
debugpri(
"connect error!\n");
-
close(sockfd);
-
return
-1;
-
}
-
sendPacket(sockfd);
//发送请求包
-
while (count <
50)
//轮询请求
-
{
-
FD_ZERO(&fds);
-
FD_SET(sockfd, &fds);
-
-
timeout.tv_sec =
0;
-
timeout.tv_usec =
100000;
-
ret = select(sockfd +
1, &fds,
NULL,
NULL, &timeout);
-
if (
0 == ret)
-
{
-
count++;
-
debugpri(
"ret == 0\n");
-
sendPacket(sockfd);
-
usleep(
100*
1000);
-
continue;
-
}
-
if(FD_ISSET(sockfd, &fds))
-
{
-
recv_len = recvfrom(sockfd, buf,
sizeof(buf),
0, (struct sockaddr *)&addr_dst, (
socklen_t*)&addr_len);
-
if(
-1==recv_len)
-
{
-
debugpri(
"recvfrom error\n");
-
close(sockfd);
-
return
-1;
-
}
-
else
if(recv_len >
0)
-
{
-
debugpri(
"receiv data\n");
-
getNewTime(buf,ptimeval);
-
debugpri(
"sec = %d usec = %d",ptimeval->tv_sec ,ptimeval->tv_usec);
//打印输出NTP服务器返回的时间
-
break;
-
}
-
}
-
else
-
{
-
debugpri(
"count %d \n",count);
-
usleep(
50*
1000);
-
count ++;
-
}
-
}
-
if(count >=
50)
-
{
-
debugpri(
"getNewTime timeout fail \n");
-
close(sockfd);
-
return
-1;
-
}
-
close(sockfd);
-
return
0;
-
}
-
-
int main(int argc, char** argv)
-
{
-
struct timeval TimeSet;
-
static
struct hostent *host = NULL;
-
-
host = gethostbyname(argv[
1]);
-
memset(&TimeSet ,
0 ,
sizeof(TimeSet));
-
getNtpTime(host,&TimeSet);
-
return
0;
-
-
}
常用的NTP服务端站点
time.windows.com
time.nist.gov
s1a.time.edu.cn
s1c.time.edu.cn
time-nw.nist.gov
time-a.nist.gov
time-b.nist.gov
s1b.time.edu.cn
nist1.aol-ca.truetime.com
总结:
NTP协议作为常用的通信协议,各种参考资料齐全,本文做一次梳理,以加深对NTP协议的理解。参考:
https://zh.m.wikipedia.org/wiki/%E7%B6%B2%E8%B7%AF%E6%99%82%E9%96%93%E5%8D%94%E5%AE%9A
https://wenku.baidu.com/view/4ab65c3ec850ad02de80418e.html