NTP协议详解

前言

NTP(Network Time Protocol)网络时间协议基于UDP,用于网络时间同步的协议,使网络中的计算机时钟同步到UTC,再配合各个时区的偏移调整就能实现精准同步对时功能。提供NTP对时的服务器有很多,比如微软的NTP对时服务器,利用NTP服务器提供的对时功能,可以使我们的设备时钟系统能够正确运行。

NTP报文格式

NTP报文格式如上图所示,它的字段含义参考如下:

  1. LI 闰秒标识器,占用2个bit
  2. VN 版本号,占用3个bits,表示NTP的版本号,现在为3
  3. Mode 模式,占用3个bits,表示模式
  4. stratum(层),占用8个bits
  5. Poll 测试间隔,占用8个bits,表示连续信息之间的最大间隔
  6. Precision 精度,占用8个bits,,表示本地时钟精度
  7. Root Delay根时延,占用8个bits,表示在主参考源之间往返的总共时延
  8. Root Dispersion根离散,占用8个bits,表示在主参考源有关的名义错误
  9. Reference Identifier参考时钟标识符,占用8个bits,用来标识特殊的参考源    
  10. 参考时间戳,64bits时间戳,本地时钟被修改的最新时间。
  11. 原始时间戳,客户端发送的时间,64bits。
  12. 接受时间戳,服务端接受到的时间,64bits。
  13. 传送时间戳,服务端送出应答的时间,64bits。
  14. 认证符(可选项)

抛开复杂的协议报文,我们来理解一下NTP客户端与服务器的交互过程,进而理解参考时间戳、原始时间戳、接受时间戳、传送时间戳的关系。如图,客户端和服务端都有一个时间轴,分别代表着各自系统的时间,当客户端想要同步服务端的时间时,客户端会构造一个NTP协议包发送到NTP服务端,客户端会记下此时发送的时间t0,经过一段网络延时传输后,服务器在t1时刻收到数据包,经过一段时间处理后在t2时刻向客户端返回数据包,再经过一段网络延时传输后客户端在t3时刻收到NTP服务器数据包。特别声明,t0和t3是客户端时间系统的时间、t1和t2是NTP服务端时间系统的时间,它们是有区别的。对于时间要求不那么精准设备,直接使用NTP服务器返回t2时间也没有太大影响。但是作为一个标准的通信协议,它是精益求精且容不得过多误差的,于是必须计算上网络的传输延时。客户端与服务端的时间系统的偏移定义为θ、网络的往返延迟定义为δ,基于此,可以对t2进行精确的修正,已达到相关精度要求,它们的计算公式如下:

式中:

t0是请求数据包传输的客户端时间戳

t1是请求数据包回复的服务器时间戳

t2是响应数据包传输的服务器时间戳

t3是响应数据包回复的客户端时间戳

对此,我们只需将NTP服务端返回的时间t2减去网络延时δ就可以了。

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

猜你喜欢

转载自blog.csdn.net/dosthing/article/details/81588219
NTP
今日推荐