本帖最后由 WSGustin 于 2018-3-28 20:45 编辑
第二十七章 NTP27.1 NTP例程概述 NTP例程主要实现W7500EVB从授时服务器获取准确时间的功能。使用该例程前,先简单了解下NTP协议。 27.2 NTP协议简介网络时间协议(Network Time Protocol,简称NTP)最早是由美国Delaware大学Mills教授设计实现的,它是用来使计算机时间同步化的一种协议,可以使计算机对其服务器或时钟源(如原子钟、GPS卫星等国际标准时间)做同步化,能够提供高精准度的时间校正,它由时间协议、ICMP时间戳消息及IP时间戳选项发展而来,是OSI参考模型的高层协议,它使用UTC作为时间标准,是基于无连接的IP 协议和UDP协议的应用层协议,使用层次式时间分布模型,所能取得的准确度依赖于本地时钟硬件的精确度和对设备及进程延迟的严格控制。 在配置时,NTP可以利用冗余服务器和多条网络路径来获得时间的高准确性和高可靠性。实际应用中,又有确保秒级精度的简单的网络时间协议(SimpleNetworkTimeProtocol,SNTP)。NTP拥有专用源端口和目标端口123。 国内外常用的NTP服务器有: 中国国家授时中心(陕西西安)210.72.145.44 上海交通大学网络中心 202.120.2.101 北京邮电大学 202.112.10.60 日本福冈大学 133.100.11.8 NTP采用层次式时间分布模型。网络体系结构主要包括主时间服务器、从时间服务器、客户机和各节点之间的传输路径。主时间服务器与高精度时间源进行同步,为其他节点提供时间服务。各客户端从时间服务器经由主服务器获得时间同步。正常情况下,节点(包括时间服务器和客户机)只用最可靠、最准确的服务器及传输路径进行同步,所以通常的同步路径为一个层次结构。其中,主时间服务器位于根节点,其他从时间服务器随同步精度增加而位于靠近叶子节点的层上,主机和学校服务器处于叶子节点。NTP将传输路径分为主动同步路径和备份同步路径,两者都同时进行时间信息包的传输,但节点只用主动同步路径数据进行同步处理。 本地时钟进程:处理由修正模块得出的偏移量并且用NTP中专用算法对本地时钟的相位和频率进行调节。 传送进程:由和每个远端实体对应的不同定时器触发,用以从数据库中收集信息,并向远端实体发送NTP消息。每个消息包括发送时的本地时间戳,前一次收到的时间戳,还有用来判断同步网络层次结构及管理连接的信息。 接收进程:接收NTP消息,计算出远端时钟和本地时钟之间的偏移量。 修正模块:处理与各个远端实体之间的偏移量,并用NTP中的一个算法选择最佳的一个。 本地时钟进程:处理由修正模块得出的偏移量并且用NTP中专用算法对本地时钟进行调节。 图27.2.1 客户/服务器模式的一个实现模型 27.3 NTP协议工作原理 NTP的基本工作原理如下图27.3.1所示。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秒。 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接收到该响应报文时,DeviceA的本地时间为10:00:03am(T4)。至此,Device A已经拥有足够的信息来计算两个重要的参数: NTP报文的往返时延Delay=(T4-T1)-(T3-T2)=2秒。 Device A相对Device B的时间差offset=((T2-T1)+(T3-T4))/2=1小时。 图27.3.1 NTP工作原理图
27.4报文格式 NTP有两种不同类型的报文,一种是时钟同步报文,另一种是控制报文。NTP基于UDP报文进行传输,使用的UDP端口号为123;时钟同步报文封装在UDP报文中,其格式如图27.4.1所示。 图27.4.1 NTP报文格式 主要字段的解释如下: LI(Leap Indicator):长度为2比特,值为“11”时表示告警状态,时钟未被同步。为其他值时NTP本身不做处理。 VN(Version Number):长度为3比特,表示NTP的版本号,目前的最新版本为3。 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:验证信息。 27.5 NTP例程解析本示例主要讲解的是W7500EVB从NTP服务器获取时间信息的过程,这只是一个简单的NTP协议的实现和演示,其中并没有包括时钟同步的问题,也就是说忽略了NTP数据帧的发送/接收时的传输时间。有兴趣的读者可以自行添加时钟同步的功能。 主函数中初始化与DHCP例程相同,这里就不再赘述。主函数中重要的是调用ntpclient_init()和do_ntp_client()两个函数。前者初始化NTP报文,后者完成与NTP服务器的交互过程。首先来看一下ntpformat结构体,它封装了NTP报文,里面的内容与上面介绍的NTP报文是一一对应的。 - typedef struct _ntpformat
- {
- uint8_t dstaddr[4]; /* destination (local) address */
- char version; /* version number */
- char leap; /* leap indicator */
- char mode; /* mode */
- char stratum; /* stratum */
- char poll; /* poll interval */
- s_char precision; /* precision */
- tdist rootdelay; /* root delay */
- tdist rootdisp; /* root dispersion */
- char refid; /* reference ID */
- tstamp reftime; /* reference time */
- tstamp org; /* origin timestamp */
- tstamp rec; /* receive timestamp */
- tstamp xmt; /* transmit timestamp */
- } ntpformat;
复制代码ntpclient_init()函数如下: - void ntpclient_init(void)
- {
- uint8_t flag;
- NTPformat.dstaddr[0] = NTP_Server_IP[0];
- NTPformat.dstaddr[1] = NTP_Server_IP[1];
- NTPformat.dstaddr[2] = NTP_Server_IP[2];
- NTPformat.dstaddr[3] = NTP_Server_IP[3];
- /*NTPformat.dstaddr[0] = ip[0];
- NTPformat.dstaddr[1] = ip[1];
- NTPformat.dstaddr[2] = ip[2];
- NTPformat.dstaddr[3] = ip[3];*/
- NTPformat.leap = 0; /* leap indicator */
- NTPformat.version = 4; /* version number */
- NTPformat.mode = 3; /* mode */
- NTPformat.stratum = 0; /* stratum */
- NTPformat.poll = 0; /* poll interval */
- NTPformat.precision = 0; /* precision */
- NTPformat.rootdelay = 0; /* root delay */
- NTPformat.rootdisp = 0; /* root dispersion */
- NTPformat.refid = 0; /* reference ID */
- NTPformat.reftime = 0; /* reference time */
- NTPformat.org = 0; /* origin timestamp */
- NTPformat.rec = 0; /* receive timestamp */
- NTPformat.xmt = 1; /* transmit timestamp */
- flag = (NTPformat.leap<<6)+(NTPformat.version<<3)+NTPformat.mode; //one byte Flag
- memcpy(NTP_Message,(void const*)(&flag),1);
- }
复制代码 NTPformat是一个ntpformat类型的变量,ntpclient_init()函数对其进行初始化。需要注意的是,在结构体定义中version、leap、mode均为char类型,各自占8位,而在实际的报文中它们合起来占8位,所以需要通过函数中的位移操作将这三个变量合成一个8位变量flag。以上代码就是实现这个功能。 由于本程序只是实现从服务器获取时间,并未涉及时钟同步的问题,所以后面的字段都不需要用到,全部初始化为0,为了简化程序,NTP_Message中也仅仅包含flag中的内容。 下面分析do_ntp_client();函数。 - 1. uint8_t NTP_Server_IP[4]={202, 112, 10, 60};
- 2. uint8_t NTP_Retry_Cnt=0; /*请求重传次数*/
- 3. void do_ntp_client(void)
- 4. {
- 5. if(Total_Seconds) /*已获取时间则不再执行NPT程序*/
- 6. return;
- 7. else
- 8. {
- 9. uint16_t len;
- 10. uint8_t * data_buf = BUFPUB;
- 11. uint32_t destip = 0;
- 12. uint16_t destport;
- 13. uint16_t startindex = 40; /*回复包中时间数据首地址*/
- 14. switch(getSn_SR(SOCK_NTP))
- 15. {
- 16. case SOCK_UDP: /*UDP模式开启*/
- 17. if(Total_Seconds>0) return; /*已获取时间则不再执行NPT程序*/
- 18. if(NTP_Retry_Cnt<100)
- 19. {
- 20. if(NTP_Retry_Cnt==0)//首先发送请求,无需等待。
- 21. {
- 22. sendto(SOCK_NTP,NTP_Message, sizeof(NTP_Message), NTP_Server_IP, NTP_Port);
- 23. NTP_Retry_Cnt++;
- 24. NTP_Timeouttimer_Start=1;
- 25. ntptimer=0;
- 26. }
- 27. else
- 28. {
- 29. if(ntptimer>2) /*3秒请求一次*/
- 30. {
- 31. sendto(SOCK_NTP,NTP_Message,sizeof(NTP_Message),NTP_Server_IP, NTP_Port);
- 32. if(ConfigMsg.debug) printf("ntp retry: %d\r\n", NTP_Retry_Cnt); /*打印请求次数*/
- 33. NTP_Retry_Cnt++;
- 34. ntptimer=0;
- 35. }
- 36. }
- 37. }
- 38. if ((len = getSn_RX_RSR(SOCK_NTP)) > 0)
- 39. {
- 40. if (len > TX_RX_MAX_BUF_SIZE) len = TX_RX_MAX_BUF_SIZE; /*接收NTP服务器回复数据*/
- 41. recvfrom(SOCK_NTP, data_buf, len, (uint8_t*)&destip, &destport); /*从NTP服务器获取时间*/
- 42. get_seconds_from_ntp_server(data_buf,startindex);
- 43. printf("%d-%02d-%02d %02d:%02d:%02d\r\n",
- 44. (ConfigMsg.date.year[0]<<8)+ConfigMsg.date.year[1],
- 45. ConfigMsg.date.month,
- 46. ConfigMsg.date.day,
- 47. ConfigMsg.date.hour,
- 48. ConfigMsg.date.minute,
- 49. ConfigMsg.date.second);
- 50. NTP_Retry_Cnt=0;
- 51. }
- 52. else /*NTP请求失败*/
- 53. {
- 54. NTP_Retry_Cnt=0;
- 55. if(ConfigMsg.debug) printf("ntp retry failed!\r\n");
- 56. }
- 57. break;
- 58. case SOCK_CLOSED:
- 59. socket(SOCK_NTP,Sn_MR_UDP,NTP_Port,0x01);//标志位flag不能为0,否则有错误
- 60. break;
- 61. }
- 62. }
- 63. }
复制代码
第11~12行是一个if判断,total_seconds变量存储从服务器获取的时间,初始化为0,这里判断当不为0时函数返回,即如果已经获取到时间就不会反复获取。第20行开始进入状态机模式,第22行,当socket模式为UDP时,25行到41行则是授时服务器发送请求,且定时器定期将状态位ntptimer加1。42行到56行则判断如果从服务器获取到时间数据则将其存入nowdate结构体中并打印出来。51行~54行则是一开始的socket初始化。 至此,NTP例程讲解完毕。编译烧录后结果如图27.5.1。成功从授时服务器获取到时间。 图27.5.1:NTP例程打印结果
|